이번 글에서는 Map Generator를 구현하면서, 방(Room) 경계에 배치되는 Entrance(문/통로)들이 비정상적으로 여러 개씩 생성되거나, 잘못된 방향에 배치되는 문제를 어떻게 추적하고 해결했는지 정리해 보겠습니다.
증상 요약
관련 주요 컴포넌트
UCMMapGenerateLogicBaseComponent::GenerateRoom()FCMRoomPosition)를 생성OutRoomMap: TMap<FCMRoomPosition, ACMRoom*> 에 방 스폰 결과 저장UCMMapGenerateLogicBaseComponent::PlaceWallsAndEntrances()OutRoomMap을 순회하면서 각 방향에 Wall / Entrance 스폰ACMRoom::SpawnBorderElement()RoomBorderActors[4] 에 캐시이 세 곳의 로직이 서로 어떻게 엮이는지 이해하는 것이 문제 해결의 핵심이었습니다.
GenerateRoom 내부 정의
DirOffset[4] = {(0,1), (1,0), (0,-1), (-1,0)}DirIndexFromDelta(DX, DY) → 0:Up, 1:Left, 2:Down, 3:RightOppositeDir(Dir) → 0↔2, 1↔3PlaceWallsAndEntrances에서 사용하던 배열 (초기 상태)
DirectionX[4] = {1, 0, -1, 0}; // 우, 하, 좌, 상DirectionY[4] = {0, -1, 0, 1}; // 우, 하, 좌, 상이로 인한 문제
FCMRoomPosition)와 그래프(Adjacency, ConnectedRooms)는 상/좌/하/우 기준으로 잘 생성됨조치
UCMMapGenerateLogicBaseComponent 헤더에서 DirectionX/DirectionY 를 다음과 같이 수정:DirectionX[4] = { 0, -1, 0, 1}; // 상, 좌, 하, 우DirectionY[4] = { 1, 0, -1, 0}; // 상, 좌, 하, 우DirOffsetDirIndexFromDeltaOppositeDirDirectionX/DirectionY 및 ACMRoom::ConnectedRooms[4] / RoomBorderActors[4]교훈
원래 의도
ACMRoom::SpawnBorderElement(int32 DirectionIndex, bool bIsEntrance) 는 같은 방향으로 여러 번 호출되더라도 한 번만 SpawnActor 하고, 이후에는 캐싱된 액터 포인터를 재사용해야 했습니다.if (RoomBorderActors[DirectionIndex]) return RoomBorderActors[DirectionIndex];문제 구간
if (ConnectedRooms[DirectionIndex + 2 % 4])ConnectedRooms[DirectionIndex + 2 % 4]->RoomBorderActors[DirectionIndex] = SpawnedActor;버그 1: 연산자 우선순위 실수
DirectionIndex + 2 % 4 는 DirectionIndex + (2 % 4) 로 계산됨 → DirectionIndex + 2(DirectionIndex + 2) % 4 로 0↔2, 1↔3 매핑이었지만, 실제로는ConnectedRooms 배열 범위를 벗어나 잘못된 메모리를 읽거나 쓰게 되고, RoomBorderActors 중복 체크가 붕괴될 위험이 생겼습니다.버그 2: 반대편 룸에 잘못된 인덱스로 저장
ConnectedRooms[DirectionIndex + 2 % 4]->RoomBorderActors[DirectionIndex] = SpawnedActor;DirectionIndex 를 사용하고 있었습니다.결과
RoomBorderActors[DirectionIndex] 또는 이웃 룸의 RoomBorderActors 가 꼬이면서,SpawnActor 가 재호출되어 Entrance가 계속 쌓이는 현상으로 이어졌습니다.수정 포인트
const int32 OppDir = (DirectionIndex + 2) % 4;RoomBorderActors[DirectionIndex] = SpawnedActor;ConnectedRooms[OppDir]->RoomBorderActors[OppDir] = SpawnedActor;핵심 아이디어
RoomBorderActors[해당 방향] 에서 동일한 액터 포인터를 가지도록 설계SpawnBorderElement 를 호출해도,OutRoomMap 에 담긴 (FCMRoomPosition → ACMRoom*) 정보를 다시 순회하면서OutRoomMap.Find(NeighborPos) 를 호출ConnectedRooms 로 다시 묶어 주도록 후처리FCMRoomPosition 기반 인접성에만 의존하게 되어, 트리 구조에 의해 왜곡되지 않음
이번 트러블슈팅을 통해, 단순히 "Entrance가 여러 개 생긴다"는 증상을 해결하는 데에도 여러 층위의 문제가 겹쳐 있을 수 있음을 체감했습니다. 방향 인덱스와 좌표계 정의가 조금만 어긋나도 전체 구조가 틀어지고, 작은 연산자 우선순위 실수 하나가 메모리 오염과 중복 스폰으로 이어질 수 있음을 경험했습니다.
앞으로는 방향/인덱스/좌표 체계를 프로젝트 초기에 명확히 문서화하고, 경계(Entrance/Wall)와 같은 공유 리소스는 "한 번만 스폰, 양쪽에서 공유"라는 설계를 기본 원칙으로 삼으려 합니다.