List와 Dictionary는 게임에서 가장 많이 사용되는 컬렉션입니다. 많이 사용된다는 것은 최적화에도 영향을 많이 미친다고 볼 수 있습니다. 제가 게임을 제작할 당시에도 데이터를 사용할 때 두개의 용도를 정확하게 썼던 게임과 아닌 게임의 프레임 저하 현상이 확연하게 차이 났던 경험을 겪었습니다. 경험을 토대로 두 컬렉션을 사용하는 방법을 소개하겠습니다.
목차
[List를 알아보자]
[무엇인가?]
데이터를 저장하는 자료구조 이며, 데이터가 순차적으로 저장되는 연속적인 특성을 가집니다.
연속적이라는 것은 데이터가 서로 연결되어 있어서 다음 데이터의 주소지를 쉽게 찾아갈 수 있다는 뜻입니다.
[특징]
다음 데이터의 주소지를 알고 있기 때문에 반복문을 사용하기에 적합합니다. 또한 동적으로 크기를 조절 할 수 있기 때문에 미리 크기를 지정하지 않아도 되는 편리함이 있습니다. 데이터의 추가와 삭제 이외에도 정렬이나 원하는 데이터를 찾는 기능 등이 있어 편리합니다. 그리고 LINQ와 사용하게 되면 간결한 코드 구성으로 데이터를 관리 할 수 있다는 장점과 상황에 따라서는 최적화에 유리하다는 장점이 있습니다.
[단점]
동적으로 크기를 조절할 때 성능의 저하가 발생합니다. 예를 들어서 크기를 100으로 사용하고 있다면 101개가 되는 시점에 크기를 늘린 후에 현재 까지 저장된 데이터를 복사해서 가져오기 때문입니다.
이런 특징을 가지는 이유는 내부적으로 배열의 형태를 띄기 때문입니다. 그렇기에 중간의 삽입을 하거나 삭제를 하는 것도 비효율 적입니다. 또한 원하는 데이터를 찾으려면 순차적으로 탐색을 해야 하기 때문에 만약 1000개의 데이터를 가지고 있다면 최악의 경우 1000개를 다 탐색해야 합니다. 한가지 더 알아야 할 점은 선언된 List가 많을 수록 메모리 낭비를 할 가능성이 있습니다. 내부적으로 배열로 구성돼 있다는 뜻은 정해진 공간이 있다는 뜻이고 공간을 쓰지 않아도 할당은 되어 있다는 뜻이기 때문입니다.
[잘못된 사용의 예시]
가장 중요한 것은 잘못된 사용을 피하는 것 입니다. 예를 들어서 특정 유저의 데이터를 가져와서 대전하는 게임에서 사용한다면 상황에 따라서 심각한 성능 저하가 발생 할 수 있습니다. 10만명의 유저를 가지고 있는 게임이라면 10만개의 데이터를 탐색 해야 하기 때문 이죠.
[언제 사용해야 할까?]
유니티의 인스펙터 창과 호환이 되며, 편리하다는 장점, 탐색이 불리하다는 단점 등을 생각 하셔서 사용하시면 됩니다. 예를 들어 10개의 데이터 밖에 없다면 특정 데이터를 찾는데도 그리 시간이 안 걸리기에 컬렉션 중 에서 다루기 편리한 리스트를 사용 하시면 됩니다. 아래는 특정 오브젝트를 찾는 예시코드 입니다.
using UnityEngine;
using System.Collections.Generic;
public class ObjectManager : MonoBehaviour
{
private List<GameObject> objectList = new List<GameObject>();
private void Start()
{
objectList.Add(GameObject.Find("Player"));
objectList.Add(GameObject.Find("Enemy1"));
objectList.Add(GameObject.Find("Enemy2"));
objectList.Add(GameObject.Find("Item"));
PrintObjectList();
}
private void PrintObjectList()
{
Debug.Log("오브젝트 목록:");
foreach (GameObject obj in objectList)
{
Debug.Log("- " + obj.name);
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
// List.Find를 사용하여 특정 조건을 만족하는 오브젝트 검색
GameObject enemy = objectList.Find(obj => obj.name.Contains("Enemy"));
if (enemy != null)
{
Debug.Log("적 오브젝트: " + enemy.name);
}
else
{
Debug.Log("적 오브젝트를 찾지 못했습니다.");
}
}
}
}
Add를 이용하여 오브젝트를 삽입하고, 특정 오브젝트를 찾는 Find를 이용하여 Enemy를 찾는 코드 입니다. 이 코드에서 Find의 부분을 LINQ로 변환해 보겠습니다.
// 성능을 위해서 enemies를 캐싱
List<GameObject> enemies;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
// LINQ를 사용하여 특정 조건을 만족하는 오브젝트 검색
enemies = objectList.Where(obj => obj.name.Contains("Enemy")).ToList();
Debug.Log("적 오브젝트 목록:");
foreach (GameObject enemy in enemies)
{
Debug.Log("- " + enemy.name);
}
}
}
Where을 사용하여 Enemy를 탐색하는 것으로 변환했습니다. 여기서 굳이 Find대신 Where을 쓰는 이유가 궁금 하실 수 있습니다. 만약 게임 내 오브젝트가 10000개라면 Find를 사용할 때마다 10000개의 오브젝트를 탐색 해야 합니다. 하지만 이를 사용하여 Enemy들을 캐싱 해 놓으면 단 한번만 10000개의 오브젝트를 탐색하면 됩니다. 이런 경우 성능 적으로 이득을 볼 수 있습니다.
[Dictionary를 알아보자]
[무엇인가?]
키와 값을 가지고 있으며, 키를 이용하여 값을 찾을 수 있는 특성을 가지고 있습니다.
[특징]
키는 중복 될 수 없으며, 특정한 키를 가진 값을 찾는 것에 특화되어 있습니다. 동적으로 크기가 조절 되며 마찬가지로 제네릭 클래스로 제공 되기에 자료형의 제한이 없습니다. 또한 내부적으로 해시 테이블로 구성되어 있어서 연속적이지 않은 구조입니다.
[단점]
연속적이지 않은 구조를 가지고 있어서 반복 문 사용에 적합하지 않습니다. 또한 유니티 인스펙터 창과 호환 되지 않습니다.
[잘못된 사용의 예시]
퀘스트의 id를 키로 사용하고, 값으로 데이터를 저장하여 사용 할 때 id가 아닌 다른 값으로 찾을 때 반복문을 사용할 경우 잘못된 사용입니다. 퀘스트가 10개 정도라면 상관 없겠지만 게임의 특성상 퀘스트의 데이터는 크기 때문에 주의해서 사용해야 합니다.
[언제 사용해야 할까?]
위에서 말했던 특정 id로 퀘스트를 찾을 때도 유용하게 사용되고, 특정 유저를 찾을 때도 유용하게 사용됩니다. 처음 사용 시 키와 데이터를 설정하는 번거로움이 있지만 데이터가 많아 질수록 장점이 크게 발휘되는 컬렉션 입니다.
using UnityEngine;
using System.Collections.Generic;
public class QuestManager : MonoBehaviour
{
// Quest 클래스 정의
public class Quest
{
public string title;
public string description;
public Quest(string title, string description)
{
this.title = title;
this.description = description;
}
}
private Dictionary<int, Quest> questDictionary = new Dictionary<int, Quest>();
private void Start()
{
// 퀘스트 정보를 Dictionary에 추가
questDictionary.Add(1, new Quest("첫 번째 퀘스트", "마을 주변의 몬스터를 처치하세요."));
questDictionary.Add(2, new Quest("두 번째 퀘스트", "물고기를 5마리 낚아 오세요."));
questDictionary.Add(3, new Quest("세 번째 퀘스트", "보물을 찾아서 가져오세요."));
// Dictionary 내용 출력
PrintQuestInfo(2);
}
private void PrintQuestInfo(int questId)
{
if (questDictionary.ContainsKey(questId))
{
Quest quest = questDictionary[questId];
Debug.Log("퀘스트 제목: " + quest.title);
Debug.Log("퀘스트 내용: " + quest.description);
}
else
{
Debug.Log("해당 ID의 퀘스트가 없습니다.");
}
}
}
[마무리 하며]
간혹 자료구조를 깊이 이해하지 않고 사용하는 경우가 많은데, 취미의 목적으로 게임을 만드시는 거라면 크게 상관 없습니다. 하지만 상용화의 목적이나 회사를 취업하고 싶은 목적이 있으시다면 자료구조는 기본입니다. 사용 하는 것에 대해서 정해진 답은 없습니다. 하지만 장점을 이해하고 상황에 따라서 사용한다면 품질 높은 프로그램을 만드는데 큰 기여를 하기에 시간을 투자해서 공부하시길 바랍니다. 저 같은 경우에는 관련 이론을 공부하고 게임들의 콘텐츠가 어떤 자료구조들로 구성 돼 있을지 정리 했습니다. 만약 게임의 보스전이 있다면, 보스의 데이터는 어떤 자료구조로 저장 돼 있을 지, 내 캐릭터의 아이템은 어떤 구조로 저장 돼 있을 지 등을 생각하여 의문 점이 드는 것은 직접 검색하여 찾아 봤던 기억이 납니다.