발전하는 춘배

[원고, C++/OOP] 로그라이크 게임 만들어보기 2 - Map 구현하기 1 : World 클래스 도입 및 책임 부여, unordered_map의 rehash, unique_ptr 본문

.원고

[원고, C++/OOP] 로그라이크 게임 만들어보기 2 - Map 구현하기 1 : World 클래스 도입 및 책임 부여, unordered_map의 rehash, unique_ptr

춘배0 2026. 2. 8. 12:42

1. Map의 고유성과 World 도입

만들다 보니 또 뭔가 부딪혀서 생각이 복잡해졌다.

한 게임에서 Map이 고유하다면 static으로 만들면 될 거 같은데..

이러면 맵을 이동했다가 다시 돌아오고 이런 건 어떡하지

어 근데 맵을 이동한다는 거 자체가 맵이 고유하지 않다는 말인디

아니 그럼 main에서 map1 map2 이런식으로 여러 맵을 다 만들어야 되나?

근데 뭔가 이건 좀 안예쁜거같은디

그래서 GPT형님한테 또 물어봤다 어떡해야할까요 형님

 

결론부터 말하면
👉 Map은 instance로 두되, “전역 싱글 Map”이 아니라 World / Game이 관리하게 하는 구조가 제일 깔끔해.

 

아하 그렇구나

하긴 어떤 시점에 플레이어가 존재하는 맵은 하나이지만, 넓게 봤을 때 '던전 1층', '던전 2층', '보스방', '상점방' 이런 건 싹다 다른 map instance인 게 자명하다.

그래서 gpt형님이 추천해 준 구조는 이렇다.

 

  • Map : 순수한 “지도 데이터”
  • World (또는 Game) : Map들을 소유 & 관리
  • Player : “현재 어떤 Map에 있는지”만 안다

이에 따라 World 클래스를 만들어봤다.

class World {
  protected:
    std::unordered_map<std::string, Map> maps;
    Map *currentMap;

  public:
    World();

    // GETTERS
    Map *getCurrentMap();
};

 

근데 GPT형님이 이런 구조는 "곧 터진다"고 말씀하셨다.

무슨 소리일까?

 

2. unordered_map

내부적으로 이렇게 생겼다

[ bucket 0 ] -> (key, value)
[ bucket 1 ] -> 비어있음
[ bucket 2 ] -> (key, value)
[ bucket 3 ] -> (key, value)

이때 버킷 개수는 고정된 게 아니라서, 

load_factor = (원소 개수) / (bucket 개수)

데이터를 계속 삽입하다가 이 값이 대충 어느 임계값에 다다르면 버킷 개수를 늘려주는 작업을 하는데 이때 key값을 새롭게 rehash해버리기 때문에 데이터의 주소가 바뀌어버린다.

(생각해보니 데이터베이스 수업에서 비슷한 걸 배웠던 기억이 새록새록 난다. key 해시값의 앞 2자리까지 사용해서 버킷 4개에 넣고 다 차면 앞 3자리까지해서 버킷 8개에 넣고... 그랬던 것 같아서 gpt한테 물어봤더니 내가 기억하는 건 "확장 해싱(extendible hashing)"이고 unordered_map은 버킷사이즈가 고정되어 있다가 확장이 필요할 때 사이즈를 늘리고 모든 데이터를 다시 해시하는 일반적인 해시 테이블 구조라 비슷하긴 한데 다른 거라고.. 아하! 적당히 이해했으니 넘어가기로 한다.)

 

아무튼 원래 World 구현에서 currentMap은 unordered_map 내부에서 그 맵의 주소를 가리키고 있을 텐데... rehash가 발생하면서 그 맵의 실제 주소가 달라지면 currentMap은 이상한 곳을 가리키고 있는 것이다.

그럼 어떻게 해결하는가?

3. unique_ptr

https://modoocode.com/229

 

씹어먹는 C ++ - <13 - 1. 객체의 유일한 소유권 - unique_ptr>

모두의 코드 씹어먹는 C ++ - <13 - 1. 객체의 유일한 소유권 - unique_ptr> 작성일 : 2018-09-18 이 글은 73902 번 읽혔습니다. 이번 강좌에서는 C++ 의 RAII 패턴unique_ptr안녕하세요 여러분! 지난번 강좌에서 다

modoocode.com

 

std::unordered_map<std::string, std::unique_ptr<Map>> maps;

maps를 이렇게 바꿔준다. 즉 value에 Map자체를 넣는 게 아니라 unique_ptr이란 걸 넣어준다.

gpt에 따르면 “어떤 객체를 딱 한 곳에서만 소유하는 스마트 포인터”로, 자동으로 delete되고 복사는 불가하며 이동만 가능한 RAII의 정수(精髓)란다.

여기서 "소유"라는 건, 다른 포인터로 그 객체를 가리킬 순 있지만 소멸시키거나 할 수 없다는 걸 의미한다.

즉 unique_ptr을 쓰면 delete를 직접 해줄 필요가 없어 메모리 누수를 예방할 수 있고, 한 객체를 두 번 delete하는 double free 버도 예방 가능하다.

 

생성:

auto map = std::make_unique<Map>(50, 50);

이때 map의 실제 type은 Map이 아니라 std::unique_ptr<Map>이다. (당연)

 

접근:

map->printMap();

 

raw 포인터 얻기:

Map* ptr = map.get();

이때 ptr은 그 map에 대한 소유권이 없다.

 

이동:

std::unique_ptr<Map> b = std::move(a);
// a == nullptr

참고로 auto b = a; 처럼 복사를 시도하려고 하면 컴파일러한테 혼난다.

이동 후에 원래 a는 nullptr가 되어 실제 객체의 소유권은 "unique"하게 존재할 수 있게 된다.

 

그래서 결론적으로 World를 이렇게 바꿔준다.

class World {
  protected:
    std::unordered_map<std::string, std::unique_ptr<Map>> maps;
    Map* currentMap;

  public:
    World();

    // GETTERS
    Map *getCurrentMap();
};

이때 currentMap은 그대로 raw pointer로 나둬도 된다. 어차피 currentMap은 map을 소유용이 아니라 지금 플레이어가 존재하는 로딩된 맵을 "가리키기"만 하면 되기 때문이다.

4. Map 생성 책임을 World로 모으기

World 클래스를 도입해서 map들 관리를 맡긴 이상, main에서 map을 만들려고 하니 뭔가 기분이 좋지 않다.

Map의 생성 책임을 World에게 부여해보자.

물론 스스로 "Map은 World를 통해서만 만들자"고 결심(?)하면 되지만 작심삼일이란 말이 괜히 있는 게 아니다. 컴파일러에게 내가 실수든 고의든 다른 곳에서 Map을 생성하는 걸 막아달라고 부탁해보자.

방법: Map 생성자를 public이 아닌 곳에 넣는다.

class Map  {
friend class World;

protected:
  ...
  Map(int width, int height);

public:
  ...
};

이렇게 하면 friend로 지정된 World만 Map 생성자를 호출할 수 있게 된다.

이제 외부에서는 Map 자체를 생성하는 게 차단되었으니 World에 요청해서 Map을 생성할 수 있도록 World에 API를 추가해주어야 한다.

void World::createMap(const std::string &id, int width, int height) {
  if (maps.find(id) != maps.end()) return;
  maps[id] = std::make_unique<Map>(width, height);
}

5. Map과 World의 책임 정의

이제 고민해볼 게 맵 내부에 엔티티나 벽이나 뭐 땅이나 이런 요소들을 채워 넣어야 하는데 이건 또 누구의 책임으로 해야할까?
1. "Map이 몬스터와 벽과 나무를 생성해서 스스로 채워 넣는다"
2. "World가 몬스터와 벽과 나무를 생성해서 Map에 채워 넣는다"

고 생각해봤을 때, 1은 뭔가뭔가다. 몬스터와 벽과 나무는 Map이 생성하면 안될 것 같다는 느낌이 빡 든다. 게임 규칙에 따라 맵이 생성될텐데,. Map이 그 규칙을 알고 적용하기엔 너무 말단 클래스라는 느낌이 든다.

결론적으로 Map과 World의 책임을 다음과 같이 나눈다.

Map:

  • 타일 정보 보관 (벽, 땅, 나무 / 엔티티)
  • 특정 좌표가 막혔는지
  • 특정 좌표에 엔티티가 있는지
  • 화면 출력

World:

 

  • 맵 생성
  • 맵 초기화 (벽, 땅 채우기)
  • 플레이어 배치
  • 몬스터 배치
  • 맵 전환

 

반응형