Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(100일차) - [3D 게임] 게임 최적화를 위한 자료구조 학습

by 독기품은토끼 2025. 10. 20.
✅ 오늘의 학습 목표
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 처리