이번 글에서는 구현한 자동 맵 생성(MapGenerate) 로직과, 생성된 맵 구조에 맞춰 서브 레벨을 동적으로 로드하는 레벨 스트리밍 구조를 정리해 보겠습니다. 특히 어떤 알고리즘으로 방(Room) 그래프를 만들었는지, 그 그래프를 어떻게 실제 월드 좌표에 배치했는지, 그리고 ACMRoom과 스트리밍 레벨을 어떻게 연결했는지 위주로 기술해 보겠습니다.

데이터 설정은 UCMRoomDataDefinition을 기반으로 결정됩니다. 이 곳에서 넓이, 방의 갯수, 방 종류 등을 셋팅합니다. 또한, 맵 생성 알고리즘과 시작 지점 선택 알고리즘도 컴포넌트화시켜 기획 담당자가 쉽게 변경할 수 있도록 설계했습니다.
맵 생성의 진입점
UCMMapGenerateLogicBaseComponent::ExecuteMapGenerationLogicInSeedUCMRoomDataDefinition* InMapDataTMap<FCMRoomPosition, TObjectPtr<ACMRoom>>& OutRoomMapGenerateRoom에서 방 그래프 및 방 액터 생성PlaceWallsAndEntrances에서 각 방의 벽과 입구(Entrance) 스폰핵심 데이터 구조
FCMRoomPositionOutRoomMapFCMRoomPosition → ACMRoom* 매핑전체 알고리즘 성격
주요 의도
UCMRoomDataDefinition에 너비/높이/최대 방 수, 보물 방 수, 보스 방 수를 정의MinX, MaxX, MinY, MaxY 계산시작점 설정
Start = FCMRoomPosition(0, 0)OutRoomMap.Add(Start, nullptr)로 먼저 좌표만 점유방향 정의
DirOffset[4] 배열을 사용해 FCMRoomPosition 델타 계산확장 로직
FRandomStream(시드 고정 랜덤)으로 후보 방향 중 하나를 선택Next를 계산하고, OutRoomMap.Add(Next, nullptr)로 점유Adjacency에 Curr <-> Next 양방향으로 추가ParentDirIndex를 기록해 나중에 트리 구조 복원에 사용결과
OutRoomMap.Keys()가 전체 방 좌표 집합Adjacency가 그래프의 엣지 집합거리 계산
Distance 계산Adjacency를 그대로 사용해 그래프 탐색리프 노드 추출
거리 기반 정렬
Distance 내림차순으로 정렬보스/보물 방 배치
DesiredBoss 개수만큼 선택BossPositions 집합에 좌표 저장DesiredTreasure개를 TreasurePositions 집합으로 선택방 크기 측정 방법
ACMRoom은 UCMRoomBoundsBox 컴포넌트를 통해 실제 방의 월드 상 가로, 세로 크기를 노출GetRoomWidth, GetRoomHeight에서 박스 익스텐트를 2배한 값으로 길이 산출대표 간격 계산
SpacingX, SpacingY를 결정좌표 → 월드 위치 매핑
FVector Location(Pos.X * SpacingX, Pos.Y * SpacingY, 0.f)룸 타입 판정
BossPositions에 포함되면 보스 방TreasurePositions에 포함되면 보물 방클래스 선택 로직
UCMRoomDataDefinition에 각 타입별 클래스 배열 보유FRandomStream으로 해당 배열에서 하나를 랜덤 선택스폰
World->SpawnActor<ACMRoom>(ClassToSpawn, Location, Rotation, SpawnParams)OutRoomMap[Pos]를 실제 포인터로 덮어써서 좌표와 액터 인스턴스를 연결DirIndexFromDelta(DX, DY)
OppositeDir(Dir)
맵 생성 후, 모든 방에 대해 인접 좌표 리스트를 순회
인접 좌표에 해당하는 ACMRoom*를 찾아 상호 연결
Room->SetConnectedRoom(Dir, NeighborRoom)NeighborRoom->SetConnectedRoom(OppositeDir(Dir), Room)디버그 로그
PlaceWallsAndEntrances(OutRoomMap) 단계
ACMRoom에 대해 GetConnectedRooms()로 상하좌우 연결 상태 조회ACMRoom::SpawnBorderElement
USceneComponent(North/South/East/West EntrancePoint)를 기준으로 좌표와 회전값 계산RoomEntranceClass 또는 RoomWallClass를 사용해 액터 스폰ACMRoom에 추가된 프로퍼티
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Streaming") FName StreamingLevelName;역할
L_StreamingTest1, L_StreamingTest2 같은 이름을 방마다 다르게 설정해 사용Getter/Setter
FName GetStreamingLevelName() const로 외부에서 조회void SetStreamingLevelName(FName InLevelName)로 런타임 중 변경 가능진입점
void ACMRoom::StreamingLevelStreamIn()처리 흐름
World와 StreamingLevelName 유효성 검사StreamingLevelName을 레벨 애셋 이름으로 보고, 경로 문자열 생성StreamingLevelName = "L_StreamingTest1"LevelPath = "/Game/MainStage/L_StreamingTest1"Room의 월드 위치를 가져와 스트리밍 레벨의 기준 위치로 사용RoomLocation = GetActorLocation()ULevelStreamingDynamic::LoadLevelInstance 호출WorldLevelPath (패키지 경로)RoomLocationFRotator::ZeroRotatorbSuccess (성공 여부)결과
StreamingLevelName을 사용하면, 같은 레벨이 여러 위치에 여러 인스턴스로 생성되는 효과성공/실패 로그
StreamingLevelStreamIn: Failed to dynamically load level '...' for Room ...StreamingLevelStreamIn: Dynamically loaded level '...' at Room ... Location=(x, y, z)Room 생성 시점
UCMMapGenerateLogicBaseComponent::GenerateRoom 안에서 방 액터가 스폰됩니다스트리밍 호출 시점
ACMRoom::ExcuteSpawnRoom에서StreamingLevelStreamIn();SpawnBorderElement(DirectionIndex, bIsEntrance);활용 아이디어

이번 글에서는 적용한 맵 생성과 레벨 스트리밍 구조를 정리해 보았습니다. 격자 기반 DFS 스패닝 트리 알고리즘을 이용해 항상 연결된 방 그래프를 만들고, 시작점에서의 거리 정보를 활용해 보스 방과 보물 방을 자연스럽게 배치하였습니다. 이후 방의 크기를 기반으로 월드 좌표를 계산해 액터를 스폰하고, 인접 정보에 따라 입구와 벽을 자동으로 구성하는 과정을 정리해 보았습니다.
또한 각 방에 스트리밍 레벨 이름을 직접 바인딩할 수 있는 구조를 만들고, ULevelStreamingDynamic::LoadLevelInstance를 통해 방 위치에 맞춰 서브 레벨을 동적으로 로드하는 방식을 도입하였습니다. 이를 통해 동일한 레벨 자산을 여러 방에서 재사용하거나, 방 타입에 따라 서로 다른 레벨 레이아웃을 바인딩하는 등 확장성이 높은 구조를 갖추게 되었습니다.