발전하는 춘배
[원고, C++/OOP] 로그라이크 게임 만들어보기 7 - 카메라 : 카메라와 렌더링(출력) 아키텍처, AI 에이전트를 이용한 디버깅 본문
[원고, C++/OOP] 로그라이크 게임 만들어보기 7 - 카메라 : 카메라와 렌더링(출력) 아키텍처, AI 에이전트를 이용한 디버깅
춘배0 2026. 2. 14. 23:53저번에 여러 방들을 템플릿화해서 맵에 랜덤으로 가져다 쓰는 식으로 해보고 싶다고 했었는데 일단 미루기로 했다.
당장 해보고 싶은게 생겼다. 카메라 느낌이다.
넓은 맵을 한번에 다 출력하니 뭔가 그 게임 느낌이 안 난다. 플레이어 주변으로 필요한 부분만 조금 확대(는 안 되겠지만) 집중시켜 출력하면 좋을 것 같다.
1. 아키텍처 구상
어떻게 해야 할까?
여태까지의 깨달음을 바탕으로 생각해본다.
1. 알고 있는 것.
"카메라가 알고 있는 것은?" - 출력해야할 맵의 범위
2. 생명 주기
"월드가 리셋되면 카메라가 없어진다"와, "게임이 없어지면 카메라가 없어진다"
둘 중 후자가 맞는 것 같다는 느낌이 든다. 즉, Camera는 World가 가지는 요소라기보다는 Game이 가지는 게 맞다.
여기까지 고려했을 때 일단 Camera 객체는 Game이 가지는 게 맞는 건 알겠다. World는 Camera를 알면 안된다.
좀 더 어려운 고민이 생겼다. 출력은 어디에서 하는가?
(지금) 맵을 출력하는 건 World다. Entity(World가 알고 있음)와 Tile(Map이 알고 있음 ) 정보를 취합해서 World가 출력하기 때문이다. 근데 카메라를 도입한다고 해보자. 1번에서 카메라는 출력할 맵의 범위를 알고 있다고 했다. 2번에서 World는 카메라를 모른다고 했다. 그러면 이제 게임의 현재 상황을 콘솔에 출력하는 건 World가 아니라, Game이 되어야 한다. World의 정보(엔티티 + 타일맵)와 카메라의 정보(출력할 범위)를 취합해서 출력해야 하기 때문이다.
이런 식으로 사고의 흐름을 가져가면 구조 짜기 좋구나 깨닫는다. 연습 많이 된다.
결론적으로
Game
- World
- Camera
- 출력 로직
이런 모양으로 가보자.
class Camera {
private:
int centerX;
int centerY;
int width;
int height;
public:
Camera(int centerX, int centerY, int width, int height);
// GETTERS
int getCenterX();
int getCenterY();
int getWidth();
int getHeight();
int getLeftX();
int getRightX();
int getTopY();
int getBottomY();
// SETTERS
void setCenterX(int centerX);
void setCenterY(int centerY);
void setWidth(int width);
void setHeight(int height);
void setCenter(int centerX, int centerY);
// METHODS
void move(int dx, int dy);
void zoom(int dw, int dh);
};
여기서 문제.
Camera에 void follow(Entity* entity) 메서드를 추가해도 될까?
이걸 추가하려면 카메라는 엔티티도 알아야 하고, 해당 엔티티를 중심으로 카메라를 옮겼을 때 맵 밖으로 카메라가 삐져나가지는 않는지 검사도 해야 하므로 월드도 알아야 한다.
즉, 아까 구상했던
Game
- World (Entity + Map)
- Camera
- 출력 로직
이 구조에서 형제 관계에 있는 요소들을 알아야 한다는 건데, 지금까지의 경험으로 봤을 때 이런 구조는 좋지 않다.
그러므로 과감히 Camera::follow는 포기하고 게임으로 올려준다.
까지가 나의 생각.
GPT형님의 조언을 구해봤더니 나의 고민에서 Camera가 World나 Entity를 의존하지 않게 한 건 좋으니 그대로 유지하면서, 구현상 더 깔끔한 방식을 제안해주셨다.
void follow(Entity* entity) 가 아니라
void follow(int targetX, int targetY, int worldWidth, int worldHeight) 로 구현하는 것이다.
이러면 entity를 가지고 World의 데이터를 받아 "모든 걸" 카메라가 하는 것이 아니라,
단순히 "데이터"들을 전달받아서 계산기로써의 역할만 수행하는 것이 가능하다.
void Camera::follow(int targetX, int targetY, int worldWidth, int worldHeight) {
if (worldWidth <= width) {
centerX = worldWidth / 2;
width = worldWidth;
} else {
centerX = std::clamp(targetX, width / 2, worldWidth - width / 2);
}
if (worldHeight <= height) {
centerY = worldHeight / 2;
height = worldHeight;
} else {
centerY = std::clamp(targetY, height / 2, worldHeight - height / 2);
}
}
코드는 GPT형님의 도움을 받았다.
clamp는 값을 해당 범위 안에 가두는 함수라고 한다.
앞서 말한 이유로 World가 이제 더 이상 출력을 전담하지 않으므로
World의 drawCurrentMap() 대신
drawCurrentMap(int startX, int startY, int endX, int entY)를 만들어준다.
반복문 범위만 수정해주면 되므로 간단한 작업이다.
그러고 나서 게임에서 호출해주면 된다.
void Game::run() {
isRunning = true;
world->populateMap("map1", 50, 50);
player= world->getPlayer();
camera->follow(player->getX(), player->getY(), world->getCurrentMap()->getWidth(), world->getCurrentMap()->getHeight());
while (isRunning) {
render();
handleInput();
if (world->isPlayerDead()) {
std::cout << "You died!\n";
isRunning = false;
break;
}
camera->follow(player->getX(), player->getY(), world->getCurrentMap()->getWidth(), world->getCurrentMap()->getHeight());
}
}
void Game::render() {
world->drawCurrentMap(camera->getLeftX(), camera->getTopY(), camera->getRightX(), camera->getBottomY());
}
이거 run()도 좀 함수로 빼서 예쁘게 만들고 싶은데 다음에..
테스트:

캡처 그림판 노가다가 더 힘들었단 사실
확실히 전체 맵에서 플레이어만 슬슬 움직이는 것보다 카메라가 움직이는 게 훨씬 게임 느낌이 산다.
gif 만드는 게 너무 오래걸렸다. 나중에 단축키 누르면 지정범위 연속으로 캡처해서 gif 변환까지 해주는 프로그램도 만들어야겠다.
2. AI 에이전트의 활용
일단 이번 프로젝트는 OOP에 따른 아키텍처 설계 연습에 초점이 맞추어져 있어서 사용을 안 하긴 했는데
지금 사용하고 있는 웹 IDE인 replit에는 중요한 좋은 기능이 있다. 바로 AI 에이전트이다.
공부용 프로젝트이므로 사용을 자제한다 쳐도 사용하면 정말 좋을 때가 있는데, 바로 의도와 다른 프로그램 동작(=버그)을 해결하는 것이다.
카메라에 따른 렌더링을 구현을 마치고 플레이해보았는데 플레이어가 오른쪽으로 쭉 갈 때 어느 순간부터 출력되는 맵의 크기가 한 칸 줄어드는 문제가 생겼다. 왜 그러냐 에이전트에게 물어보았더니 지 혼자 내 코드를 막 뜯어보더니 수정까지 해서 결국 버그를 해결하였다.

원인은
Camera의 메서드 중
getRightX(), getBottomY() 얘네를
return centerX + width / 2; 이런 식으로 만들어 둔 데 있었다.
사실 답을 알고 나니 납득이 금방 가는 당연한 결과였으나, 코드의 볼륨이 '나름' 커진 상태에서 출력이 한 칸 줄어든 데의 원인을 직접 찾는 건 여간 귀찮스러운 일이 아니다. 즉 내가 여태 써 둔 소스코드를 일일히 뜯어보는 지루한 일을 내가 직접 하는 게 아니라 에이전트에게 딸깍 맡겨 해결할 수 있다는 게 AI 에이전트의 장점 중 하나가 아닐까 생각해본다.
