✅ 오늘의 학습 목표
1. 맵 최적화 생각해보기
- Linked List (연결 리스트)
- Graph (그래프)
1. 맵 최적화
저번 시간에는 렌더링만 구분해서 오브젝트가 그려지도록 하였는데
이번에는 오브젝트 생성 자체를 조절해주려고 한다.

예를들어 플레이어가 3번방에 위치하고 있을 때
플레이어 시야에서는 보이지 않는 1번, 4번방을 계속 생성해두면 불필요한 메모리를 잡아먹기 때문에
플레이어 위치에 따라 방을 활성/비활성화 하는 로직을 만들어 줄 것이다.


우선 각 방에 설치된 Light, Environment 들을 각 Room 오브젝트의 자식으로 배치해준 후 Prefab화 해주었다.
1. 자료구조
플레이어의 위치에 따라 연결된 방만 생성되는 로직을 만들려면
단순한 배열(List)보다 더 복잡한 그래프(graph) 구조의 이해가 필요하다.

🚨 연결 리스트 (Linked List)
하나의 데이터가 다음 데이터를 가리키는 자료구조

각각의 칸(노드)이 자신 다음 칸을 알고 있어서 차례차례 이동할 수 있는 구조인데
이런 구조는 순서가 정해져 있고 앞뒤로만 이동하는 상황에 적합하다.
예를 들어, 방이 한 줄로만 이어지는 게임이라면 연결 리스트로도 충분히 로직을 구현할 수 있다.
하지만 우리 게임처럼
- 2번 방에서 3번, 4번 방으로 이동할 수도 있고
- 3번 방에서 다시 2번, 5번 방으로 이동할 수도 있다면
이런 분기 구조는 연결 리스트로 표현할 수 없다.
즉, 연결 리스트는 일렬로만 이어진 구조에는 적합하지만 여러 갈래로 나뉘는 복잡한 맵을 표현하기에는 한계가 있다.
🚨 스택과 큐
[Stack (스택)]
스택은 한쪽 끝에서만 데이터를 넣고 빼는 구조로 LIFO (Last In, First Out) 라고도 부른다.
- Push : 스택의 위쪽(top)에 데이터를 삽입
- Pop : 스택의 위쪽(top)에서 데이터를 꺼내기
[Queue (큐)]
큐는 스택과 반대로 먼저 들어온 것이 먼저 나가는 구조이다. (FIFO (First In, First Out) 방식)
- Enqueue : 큐의 뒤쪽(rear)에 데이터를 삽입
- Dequeue : 큐의 앞쪽(front)에서 데이터를 꺼내기
🚨 그래프 (Graph)
그래서 우리는 그래프를 활용해줄 것이다.
그래프는 연결리스트보다 훨씬 자유로운 구조라고 생각하면 편하다.
여러개의 방(노드)가 있고
그 방들이 여러 방향으로 연결(링크)되어 있을 수 있다.
[딕셔너리로 그래프 표현하기]
Dictionary<int, List<int>> roomGraph = new Dictionary<int, List<int>>()
{
{1, new List<int>{2}},
{2, new List<int>{3,4}},
{3, new List<int>{2,5}},
// ...
};
그래프는 딕셔너리로 표현이 가능하다.
여기서 Key는 방 번호, Value는 그 방에서 갈 수 있는 다른 방들의 목록이다.
int currentRoom = 1;
foreach (int nextRoom in roomGraph[currentRoom])
{
SpawnRoom(nextRoom); // 연결된 방만 생성
}
플레이어가 1번방에 있을 때 roomGraph[1]을 확인하여 2번방이 연결되어 있다는 것을 알 수 있다.
이런 방식으로 모든 방을 한 번에 생성하지 않고 플레이어가 이동할 때마다 필요한 방만 불러올 수 있다.
🚨 탐색 알고리즘
그래프를 만들었다면 이제 남은 건 탐색이다.
탐색이란 간단히 말해 “그래프 안에서 이동하면서 원하는 노드를 찾는 과정”이다.
이 탐색에도 여러 방법이 있는데 그중 대표적인 두 가지가 깊이 우선 탐색(DFS) 과 너비 우선 탐색(BFS) 이다.
| 알고리즘 탐색 | 방식 | 예시 |
| DFS (Depth-First Search) | 한 길로 끝까지 들어가다 막히면 돌아옴 | 경로 탐색, 순환 감지 |
| BFS (Breadth-First Search) | 가까운 노드부터 차례대로 탐색 | 최단 거리 탐색, AI 이동 |
2. 코드
using System.Collections.Generic;
using UnityEngine;
public class Room
{
public int Id { get; private set; } // Room ID
public List<Room> Neighbors { get; private set; } // 연결된 룸
public GameObject roomInstance; // 씬에 생성된 룸 프리팹
public Room (int id)
{
roomInstance = null;
Id = id;
Neighbors = new List<Room> ();
}
public void AddNeighbor(Room room)
{
if (!Neighbors.Contains(room))
{
Neighbors.Add(room);
room.AddNeighbor(this);
}
}
public void RemoveNeighbor(Room room)
{
if (Neighbors.Contains(room))
{
Neighbors.Remove(room);
room.Neighbors.Remove(this);
}
}
}
Room 클래스는 방 하나를 표현하는 클래스이다.
외부 스크립트에서 방 번호와 이웃을 읽을 수 있도록 캡슐화 해주었고
생성자를 통해 Room이 만들어질 때 id와 이웃 룸을 담을 리스트를 만들어주었다.
Add / Remove 메서드를 통해서 주변 이웃 룸을 연결/해제 해줄 수 있도록 구현하였고
room.AddNeighbor(this)로 양방향 연결까지 처리해 주었다.
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class RoomPrefabInfo
{
public int Id;
public GameObject Prefab;
}
public class RoomManager : MonoBehaviour
{
[SerializeField] private List<RoomPrefabInfo> roomPrefabs;
private Dictionary<int, Room> rooms;
private Room currentRoom;
private static RoomManager _instance;
public static RoomManager Instance
{
get
{
if (_instance == null)
{
_instance = FindFirstObjectByType<RoomManager>();
if (_instance == null)
{
// TODO : 오류 처리
}
}
return _instance;
}
}
void Awake()
{
Room room0 = new Room(0);
Room room1 = new Room(1);
Room room2 = new Room(2);
Room room3 = new Room(3);
Room room4 = new Room(4);
room0.AddNeighbor(room1);
room1.AddNeighbor(room2);
room1.AddNeighbor(room3);
room2.AddNeighbor(room1);
room2.AddNeighbor(room4);
room3.AddNeighbor(room1);
room3.AddNeighbor(room4);
room4.AddNeighbor(room2);
room4.AddNeighbor(room3);
rooms = new Dictionary<int, Room>
{
{0, room0},
{1, room1},
{2, room2},
{3, room3},
{4, room4},
};
}
}
이제 모든 Room들을 생성하고 관리하는 스크립트를 작성해주었다.
방에 맞는 프리팹을 연결해주기 위해 구조체 클래스를 생성해주었고
Room들은 id로 빠르게 찾아가기 위해 딕셔너리(Map) 형태로 저장해주었다.
이 외 수업시간에 한 작업
Enemy 배치 / Bake 처리
