게임 개발에서는 다양한 객체와 데이터를 다루어야 하는데요.
이 때, 데이터를 효율적으로 관리하기 위해 컬렉션을 사용하는 것이 매우 유용합니다.
컬렉션은 데이터의 집합을 구조화하고 관리할 수 있는 도구를 제공합니다.
이번에는 C#에서 컬렉션의 정의와 종류에 대해 알아보고, 이를 어떻게 활용할 수 있는지 구체적인 예시를 통해 학습해보겠습니다.
컬렉션이란?
컬렉션은 여러 개의 데이터를 하나의 객체로 묶어서 관리할 수 있는 데이터 구조입니다.
컬렉션을 적절하게 사용하면 개발에서 효율성과 성능을 크게 향상시킬 수 있습니다.
이는 동적으로 데이터를 추가하거나 제거할 수 있으며, 데이터에 대한 다양한 조작할 수 있는 방법을 제공하는데요.
각 컬렉션의 특성에 따라 적절히 선택하여 선택하면 코드의 효율성과 가독성을 높일 수 있습니다.
그럼 컬렉션에는 어떤 종류가 있고 어떻게 사용하는지 한 번 알아봅시다.
컬렉션의 종류와 사용 예시
1. 배열 (Array)
배열은 고정된 크기의 동일한 데이터 타입의 데이터 집합을 저장하는 구조입니다.
배열의 크기는 생성 시에 결정되며, 이후에는 변경할 수 없습니다.
배열은 메모리 상에 연속적으로 저장되며, 인덱스를 사용해 요소에 접근할 수 있습니다.
배열의 크기는 고정되어 있어 추가적인 공간이 필요할 경우, 새로운 배열을 생성해야합니다.
•
배열 사용법
// 배열 선언 및 초기화
int[] numbers = new int[5]; // 크기가 5인 정수 배열 선언
numbers[0] = 10; // 배열의 첫 번째 요소에 값 할당, 배열은 0이 첫 번째 요소
C#
복사
위의 코드는 크기가 5인 정수 배열을 선언하고 첫 번째 요소에 10이라는 값을 할당한 코드입니다.
배열의 크기가 5라면 [0],[1],[2],[3],[4] 이렇게 5개의 요소를 가지게 됩니다.
그럼 만약 모든 요소를 채우지 않으면 어떻게 될까요?
배열의 크기를 지정하고 모든 요소를 초기화하지 않으면, 초기화되지 않은 요소들은 해당 타입의 기본값을 가지게 됩니다.
•
배열의 기본값
◦
참조 타입: null
◦
정수형 (int, long, short 등): 0
◦
실수형 (float, double 등): 0.0
◦
bool 타입: false
◦
char 타입: '\0' (null 문자)
그럼 이제 사용 예시를 보며 장단점을 알아보겠습니다.
public class Enemy
{
public string Name { get; set; }
public int Health { get; set; }
}
class Program
{
static void Main()
{
// 고정된 위치에 배치된 5명의 적
Enemy[] enemies = new Enemy[5];
enemies[0] = new Enemy { Name = "Goblin", Level = 10 };
enemies[1] = new Enemy { Name = "Orc", Level = 20 };
// 나머지 요소는 기본값 (null)로 초기화됨
// 배열의 요소를 순회하면서 출력
foreach (var enemy in enemies)
{
if (enemy != null)
{
Console.WriteLine($"{enemy.Level} 레벨의 {enemy.Name} 이(가) 등장했습니다.");
}
}
}
}
C#
복사
•
배열의 장단점
◦
장점 : 인덱스를 사용하여 빠르게 접근할 수 있으며, 메모리 사용이 효율적입니다.
◦
단점 : 크기가 고정되어 있어 동적 데이터 관리에 적합하지 않습니다.
그렇다면 어떤 경우에 배열을 사용하는 것이 유용할까요?
•
고정된 크기의 데이터가 필요한 경우 : 예를 들어 여러분이 게임을 하다가 몬스터를 잡는다고 가정합시다.
그러면 몬스터의 개체 수는 정해져있을 것이고, 지정된 리스폰 위치에서 생성될 것입니다.
이처럼 게임에서 한번에 처리해야하는 고정된 개수의 데이터를 저장할 때 유용합니다.
2. 리스트 (LIST<T>)
리스트란 크기가 동적으로 조절되는 데이터 집합을 저장하는 구조입니다.
배열과 달리 크기를 동적으로 조절할 수 있으며, 다양한 메서드를 제공하여 데이터를 쉽게 추가, 삭제, 검색할 수 있습니다.
LIST는 내부적으로 배열을 사용하지만, 크기가 자동으로 조절되므로 데이터를 추가할 때마다 새로운 배열을 생성하지 않아도 됩니다.
데이터를 추가하거나 삭제할 때 유연하게 대처할 수 있습니다.
•
리스트 사용법
// 리스트 선언 및 초기화
List<int> numbers = new List<int>(); // 정수 리스트 선언
numbers.Add(10); // 리스트에 값 추가
C#
복사
위의 코드는 정수형 리스트를 만들고, 리스트의 첫 번째 요소에 10의 값을 할당한 것입니다.
또한 리스트는 다양한 메서드를 제공하는데요.
리스트에는 어떤 메서드가 있는지 설명해드리겠습니다.
1.
Add(T item) : 리스트의 끝에 요소를 추가합니다.
List<string> fruits = new List<string>();
fruits.Add("Apple");
fruits.Add("Banana");
C#
복사
2.
Remove(T item) : 리스트에서 첫 번째로 일치하는 요소를 제거합니다.
요소가 여러 번 존재하면 첫 번째 요소만 제거됩니다.
//문자열 리스트를 선언하고 리스트에 Apple, Banana, Cherry 할당
List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };
fruits.Remove("Banana"); // "Banana"를 제거합니다.
C#
복사
3.
Insert(int index, T item) : 지정된 인덱스 위치에 요소를 삽입합니다.
기존의 요소들은 한 칸씩 뒤로 밀립니다.
List<string> fruits = new List<string> { "Apple", "Cherry" };
fruits.Insert(1, "Banana"); // 인덱스 1에 "Banana"를 삽입합니다.
C#
복사
4.
Clear() : 리스트의 모든 요소를 제거합니다. 리스트의 크기는 0이 됩니다.
List<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };
fruits.Clear(); // 리스트를 비웁니다.
C#
복사
리스트는 배열과 달리 크기가 동적으로 변한다고 했죠?
밑의 코드를 통해 크기가 동적으로 변하는 것을 보여드리겠습니다.
using System;
using System.Collections.Generic;
public class Item
{
public string Name { get; set; }
}
class Program
{
static void Main()
{
// 빈 리스트 선언
List<Item> inventory = new List<Item>();
// 리스트의 크기를 확인
Console.WriteLine($"Inventory count: {inventory.Count}");
// 리스트에 요소를 추가
inventory.Add(new Item { Name = "Sword" });
inventory.Add(new Item { Name = "Shield" });
// 리스트의 크기를 확인
Console.WriteLine($"Inventory count after adding items: {inventory.Count}");
// 리스트의 첫 번째 요소 출력
if (inventory.Count > 0) // 리스트에 요소가 있는지 확인
{
Item firstItem = inventory[0]; // 첫 번째 요소 접근
Console.WriteLine($"First item: {firstItem.Name}");
}
else
{
Console.WriteLine("The inventory is empty.");
}
}
}
C#
복사
//출력 결과
Inventory count: 0
Inventory count after adding items: 2
First item: Sword
C#
복사
위의 코드를 실행하면 처음에는 인벤토리의 크기는 0이었지만, Sword와 Shield를 추가하여 크기가 2가 된 것을 볼 수 있습니다.
또한 배열처럼 inventory[0]을 출력하면 리스트의 첫 번째 요소가 출력됩니다.
•
리스트의 장단점
◦
장점 : 데이터의 크기를 동적으로 조절할 수 있으며, 다양한 메서드를 통해 데이터 조작이 용이합니다.
◦
단점 : 배열보다 메모리 사용이 비효율적일 수 있습니다.
그렇다면 리스트는 어떤 상황에 사용하는 것이 유용할까요?
•
동적으로 크기가 변경되는 데이터가 필요한 경우 : 여러분이 게임을 하다보면 다양한 아이템을 획득하고 인벤토리에 그 아이템이 추가되고 필요없는 아이템은 버릴 것입니다.
이럴 경우 리스트는 Add, Remove, Insert, Clear 등 다양한 메서드를 제공하여 아이템을 효율적으로 관리할 수 있습니다.
3. 딕셔너리 (Dictionary<TKey, TValue>)
딕셔너리란 키와 값을 쌍으로 저장하는 데이터 구조입니다.
각 키는 유일해야하며, 이를 통해 값을 빠르게 검색할 수 있습니다.
딕셔너리는 해시 테이블을 기반으로 하며, 키를 통해 값을 빠르게 검색할 수 있습니다. 키와 값은 특정 타입으로 정의됩니다.
•
딕셔너리 사용법
1.
딕셔너리 선언 및 초기화
딕셔너리를 선언할 때는 키와 값의 데이터 타입을 명시합니다. 초기화는 new Dictionary<TKey, TValue>()를 사용합니다.
// 문자열을 키로 하고 정수를 값을 가지는 딕셔너리 선언 및 초기화
Dictionary<string, int> ages = new Dictionary<string, int>();
C#
복사
2.
값 추가 및 업데이트
Add 메서드 또는 인덱서를 사용하여 키-값 쌍을 추가하거나 업데이트할 수 있습니다. 키가 이미 존재하는 경우 Add는 예외를 발생시키지만, 인덱서를 사용하면 값을 덮어쓸 수 있습니다.
// Add 메서드를 사용하여 키-값 쌍 추가
ages.Add("Alice", 25);
// 인덱서를 사용하여 키-값 쌍 추가 또는 업데이트
ages["Bob"] = 30; // 키 "Bob"이 존재하지 않으면 추가, 존재하면 값 업데이트
C#
복사
3.
값 조회
TryGetValue 메서드를 사용하면 키가 존재하는지 확인하고 값을 안전하게 조회할 수 있습니다.
//Alice 키를 찾고, 해당 키에 대응하는 값을 반환
if (ages.TryGetValue("Alice", out int age))
{
Console.WriteLine($"Alice's age is {age}");
}
else
{
Console.WriteLine("Key not found.");
}
C#
복사
4.
값 삭제
Remove 메서드를 사용하여 특정 키-값 쌍을 삭제할 수 있습니다.
ages.Remove("Bob"); // 키 "Bob"과 관련된 쌍을 삭제
C#
복사
5.
딕셔너리 순회
foreach 루프를 사용하여 딕셔너리의 모든 키-값 쌍을 순회할 수 있습니다.
foreach (var kvp in ages)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}//kvp : KeyValuePair의 약자. 키-값 쌍을 나타냄.
C#
복사
6.
딕셔너리의 키와 값에 접근
Keys와 Values 속성을 사용하여 딕셔너리의 키와 값 컬렉션에 접근할 수 있습니다.
// 모든 키 출력
foreach (var key in ages.Keys)
{
Console.WriteLine($"Key: {key}");
}
// 모든 값 출력
foreach (var value in ages.Values)
{
Console.WriteLine($"Value: {value}");
}
C#
복사
7.
딕셔너리의 크기 확인
Count 속성을 사용하여 딕셔너리에 저장된 키-값 쌍의 개수를 확인할 수 있습니다.
Console.WriteLine($"Number of elements in dictionary: {ages.Count}");
C#
복사
•
딕셔너리의 장단점
◦
장점 : 빠른 검색 속도를 제공하며, 키를 통해 데이터를 쉽게 접근할 수 있습니다.
◦
단점 : 메모리 사용량이 증가할 수 있으며, 키의 유일성을 보장해야합니다.
딕셔너리의 장단점을 알아보았으니, 사용 예시를 보며 어떤 상황에서 딕셔너리를 사용하는 것이 유용한지 알아보겠습니다.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 사용자 ID를 키로 하고, 닉네임을 값으로 가지는 딕셔너리 선언 및 초기화
Dictionary<int, string> userNicknames = new Dictionary<int, string>
{
{ 1001, "Alice" },
{ 1002, "Bob" },
{ 1003, "Charlie" }
};
// 검색할 ID를 지정
int searchId = 1002;
// ID로 닉네임 찾기
if (userNicknames.TryGetValue(searchId, out string nickname))
{
Console.WriteLine($"User with ID {searchId} has the nickname: {nickname}");
}
else
{
Console.WriteLine($"No user found with ID {searchId}");
}
}
}
C#
복사
•
특정 키에 대한 데이터 접근이 필요할 경우 : 여러분이 게임을 하면서 캐릭터 고유의 ID를 검색하여 닉네임을 찾거나 캐릭터의 정보를 조회한다고 가정합시다.
이런 경우 딕셔너리는 키를 통해 값을 빠르게 검색할 수 있어, 특정 키에 대한 데이터 접근이 필요할 때 유용합니다.
4. 큐 (Queue<T>)
큐는 데이터가 FIFO(First-In-First-Out) 방식으로 처리되는 구조입니다.
FIFO는 선입선출이라고도 하는데 가장 먼저 추가된 데이터가 가장 먼저 제거되는 구조입니다.
이는 주로 데이터를 순서대로 처리할 때 유용하며 Enqueue와 Dequeue 메서드를 통해 데이터를 추가하고 제거합니다.
•
큐 사용 방법
// 큐 선언 및 초기화
Queue<int> numbers = new Queue<int>(); // 정수 큐 선언
numbers.Enqueue(10); // 큐에 값 추가
int firstNumber = numbers.Dequeue(); // 큐에서 값 제거 및 반환
C#
복사
Enqueue() 메서드를 이용하여 큐에 값을 추가하고 Dequeue() 메서드를 이용하여 큐에서 값을 제거합니다.
•
큐의 장단점
◦
장점 : FIFO 방식으로 데이터 처리 시 유용하며, 순서대로 처리할 수 있습니다.
◦
단점 : 특정 데이터를 무작위로 접근하거나 제거하기 어렵습니다.
그럼 활용 예제를 보며 어떤 경우 큐를 사용하는 것이 유용한지 알아보겠습니다.
using System;
using System.Collections.Generic;
class Program
{
// 공격 단계를 나타내는 열거형 정의
enum Attack
{
FirstAttack,
SecondAttack,
FinalAttack
}
static void Main()
{
// 3단계 공격 큐 선언
Queue<AttackStage> attackQueue = new Queue<Attack>();
// 큐에 3단계 공격 추가
attackQueue.Enqueue(Attack.FirstAttack);
attackQueue.Enqueue(Attack.SecondAttack);
attackQueue.Enqueue(Attack.FinalAttack);
// 공격 처리
while (attackQueue.Count > 0)
{
AttackStage stage = attackQueue.Dequeue();
// 공격 단계에 따라 처리 (구현 생략)
Console.WriteLine($"Processing {stage} attack.");
}
}
}
C#
복사
•
데이터의 순서가 중요할 때 : 위의 코드는 여러분의 캐릭터가 공격을 했을 때, 3단 공격하는 것을 구현한 코드입니다.
이는 큐를 사용하여 3단계 공격을 순서대로 처리합니다.
큐의 FIFO 특성을 활용하여 공격 단계를 정확한 순서로 실행할 수 있습니다.
이처럼 게임에서 복잡한 공격 패턴이나 여러 단계를 순차적으로 처리할 때 큐를 사용하면 깔끔하게 구현이 가능합니다.
5. 스택 (Stack<T>)
스택은 데이터가 LIFO(Last-In-First-Out) 방식으로 처리되는 구조입니다.
LIFO는 후입선출이라고도 하며, 이는 가장 마지막에 추가된 데이터가 가장 먼저 제거되는 방식입니다.
주로 최근에 추가된 데이터를 먼저 처리할 때 유용하며, Push와 Pop 메서드를 통해 데이터를 추가하고 제거합니다.
•
스택 사용 방법
// 스택 선언 및 초기화
Stack<int> numbers = new Stack<int>(); // 정수 스택 선언
numbers.Push(10); // 스택에 값 추가
int lastNumber = numbers.Pop(); // 스택에서 값 제거 및 반환
C#
복사
Push() 메서드를 이용해 스택에 값을 추가하고 Pop() 메서드를 이용해 스택에서 값을 제거합니다.
•
스택의 장단점
◦
장점 : LIFO 방식으로 데이터를 처리하기 때문에 최근에 추가된 데이터를 우선적으로 처리할 수 있습니다.
◦
단점 : 특정 데이터를 무작위로 접근하거나 제거하기 어렵습니다.
그럼 스택의 활용 예시를 보며 어떤 경우에 사용하는 것이 유용한지 알아보겠습니다.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// 상태 스택 선언
Stack<string> gameStateStack = new Stack<string>();
// 게임 상태 저장
SaveState("Level1", gameStateStack);
SaveState("Level2", gameStateStack);
SaveState("Level3", gameStateStack);
// 상태 복원
RestoreState(gameStateStack); // 출력: Restoring state: Level3
RestoreState(gameStateStack); // 출력: Restoring state: Level2
}
static void SaveState(string state, Stack<string> stateStack)
{
Console.WriteLine($"Saving state: {state}");
stateStack.Push(state);
}
static void RestoreState(Stack<string> stateStack)
{
if (stateStack.Count > 0)
{
string restoredState = stateStack.Pop();
Console.WriteLine($"Restoring state: {restoredState}");
}
else
{
Console.WriteLine("No state to restore.");
}
}
}
C#
복사
•
최근의 추가된 데이터를 먼저 처리할 때 : 만약 여러분이 게임을 하면서 게임 상태를 저장해둔다고 가정하겠습니다. 그런 경우 가장 마지막에 저장해둔 데이터가 스택에 쌓이고 여러분이 게임을 진행하다가 저장해둔 시점으로 돌아갈 때 유용하게 사용할 수 있습니다. 또한 전에 했던 작업으로 되돌리는 Undo 등의 기능도 스택을 이용한 것이라고 할 수 있습니다.
결론
컬렉션에 대해 잘 이해하셨나요?
각각의 컬렉션 타입은 특정 상황에서 장점이 있으며, 올바르게 활용하면 효율적이고 관리하기 쉬운 코드를 작성할 수 있습니다.
적절한 컬렉션을 선택하는 것은 게임의 성능과 유지 보수성에 큰 영향을 미칩니다.
따라서, 여러분이 컬렉션에 대해 정확히 이해하고 상황에 맞는 컬렉션을 활용하여 게임 개발에 더욱 도움이 되었으면 좋겠습니다.