이 강좌는 2024년 부산 콘텐츠 아카데미 퍼스널 트레이닝에서 강의한 원고로, Unity 실습 프로젝트 하나로 C# 기초 문법부터 게임 구조까지 배웁니다. 수업마다 클래스 하나를 만들고, 그 클래스만 갈아 끼우면 같은 화면에서 다른 수업이 실행됩니다. 첫 편에서는 이 실습 프로젝트의 틀을 보고, 이름 목록을 담는 그릇을 배열에서 List 까지 바꿔 봅니다.
2024년 부산 콘텐츠 아카데미 퍼스널 트레이닝 수업에서 MyLesson Unity 실습 프로젝트의 Strategy_01_Array.cs 로 가르친 내용입니다. 수업은 다섯 단계로 나뉘고, 단계마다 코드가 조금씩 바뀝니다.
수업 하나가 클래스 하나
실습 프로젝트의 씬에는 Program 오브젝트가 하나 있습니다. Inspector 의 classPath 칸에 Strategy_01_Array 처럼 수업 클래스 이름을 적고 실행하면, 그 수업의 코드가 돌아갑니다. 수업 클래스는 모두 Strategy 라는 약속(인터페이스)을 지킵니다.
public interface Strategy
{
public void Run();
public void Update();
}약속은 두 가지입니다. 처음 한 번 부르는 Run() 과 매 프레임 부르는 Update() 입니다. 이 약속만 지키면 StrategyRunner 는 어떤 수업이든 똑같이 실행합니다.
이렇게 실행할 대상을 같은 모양의 클래스로 만들어 바꿔 끼우는 구조를 전략 패턴(Strategy Pattern)이라고 부릅니다. 패턴 이름은 나중에 다시 나오니, 지금은 "갈아 끼우는 구조" 정도로 기억해 두면 됩니다.
이름으로 클래스를 만드는 방법
Program 의 Start() 는 classPath 에 적힌 문자열로 클래스를 찾아 실행합니다. 이름 앞에 Strategy_ 를 빼먹었으면 붙여 줍니다.
private void Start()
{
runner = new StrategyRunner();
var findedStrategy = CreateStrategyFromClassName
(classPath.Contains("strategy_", StringComparison.OrdinalIgnoreCase) ? classPath : "Strategy_" + classPath);
if (findedStrategy != null)
{
runner.SetStrategy(findedStrategy);
runner.Run();
}
}private Strategy CreateStrategyFromClassName(string className)
{
Type type = Type.GetType(className);
if (type != null)
{
object instance = Activator.CreateInstance(type);
if (instance is Strategy myClassInstance)
{
return (Strategy)instance;
}
}
Program.WriteLine("Type not found.");
return null;
}| 코드 | 하는 일 |
|---|---|
Type.GetType(className) | 문자열 이름으로 타입(클래스 정보)을 찾습니다. 없으면 null 입니다. |
Activator.CreateInstance(type) | 그 타입의 객체를 만듭니다. new 를 문자열로 하는 셈입니다. |
instance is Strategy | 만든 객체가 Strategy 약속을 지키는지 확인합니다. |
코드를 고치지 않고 Inspector 의 글자만 바꿔 수업을 고를 수 있는 이유가 여기에 있습니다. 첫 수업은 화면에 한 줄을 찍는 Strategy_Hello 입니다.
public class Strategy_Hello : Strategy
{
public void Run()
{
// CTRL + R > G
Program.WriteLine("Hello World!");
}
public void Update()
{
}
}Unity 의 Console 창 대신 게임 화면의 글 상자에 결과를 찍는 도우미입니다. 수업이 진행되면서 목록을 찍는 WriteList, 경로로 오브젝트를 찾는 FindObject 같은 도우미가 하나씩 늘어납니다.
단계는 컴파일 기호로 나눈다
수업 하나는 여러 단계로 진행됩니다. 단계마다 파일을 따로 만들지 않고, 한 파일 안에 #if 로 단계를 나눠 둡니다. Unity 의 Project Settings → Player → Scripting Define Symbols 에 ARRAY_STEP2 같은 기호를 더하면 그 단계의 코드가 컴파일됩니다.
- ARRAY_STEP2 없음1단계 코드
- ARRAY_STEP2 추가2단계 코드
- ARRAY_STEP3 추가3단계 코드
- … STEP5마지막 단계
기호는 쌓아 가며 켭니다. 3단계를 보려면 STEP2 와 STEP3 를 함께 켭니다. 그래서 코드 안의 #if 가 양파처럼 겹쳐 있습니다.
배열로 이름 목록 만들기
첫 단계에서는 게임 캐릭터 이름 네 개를 문자열 배열에 담아 찍습니다.
static string[] GenerateNames()
{
string[] names = new string[4];
names[0] = "Periona";
names[1] = "Leon";
names[2] = "Heloon";
names[3] = "Darkan";
return names;
}
static void PrintNames(string[] names)
{
foreach (string name in names)
{
Program.WriteLine("PrintNames: " + name);
}
}new string[4] 는 칸이 네 개인 배열을 만듭니다. 배열은 만들 때 크기가 정해지고, 나중에 늘릴 수 없습니다. 그리고 PrintNames 는 string[] 만 받습니다. 이름이 아니라 숫자 목록을 찍고 싶으면 함수를 또 만들어야 합니다.
2단계에서는 찍는 일을 Program 의 도우미로 옮기면서, 어떤 타입의 배열이든 받도록 바꿉니다.
public static void WriteList<T>(T[] array)
{
foreach (T name in array)
{
WriteLine(name.ToString());
}
}WriteList<T> 의 T 는 "아직 정하지 않은 타입" 입니다. 부르는 쪽이 string[] 을 넘기면 T 는 string 이 되고, int[] 를 넘기면 int 가 됩니다. 이것이 다음 편에서 자세히 볼 제네릭입니다.
그릇을 바꿔도 받는 쪽은 그대로
3단계부터는 크기가 자동으로 늘어나는 그릇을 차례로 써 봅니다.
static ArrayList GenerateNames()
{
ArrayList names = new ArrayList();
names.Add("S3: Periona");
names.Add("S3: Leon");
names.Add("S3: Heloon");
names.Add("S3: Darkan");
return names;
}static StringCollection GenerateNames()
{
StringCollection names = new StringCollection();
names.Add("S4: Periona");
names.Add("S4: Leon");
names.Add("S4: Heloon");
names.Add("S4: Darkan");
return names;
}static List<string> GenerateNames()
{
List<string> names = new List<string>();
names.Add("S5: Periona");
names.Add("S5: Leon");
names.Add("S5: Heloon");
names.Add("S5: Darkan");
return names;
}| 그릇 | 크기 | 담는 것 | 꺼낼 때 |
|---|---|---|---|
string[] | 고정 | string 만 | 그대로 string |
ArrayList | 자동으로 늘어남 | 무엇이든(object) | 형 변환이 필요하고, 틀린 타입이 섞여도 컴파일러가 모름 |
StringCollection | 자동으로 늘어남 | string 만 | 그대로 string, 하지만 string 전용 클래스 |
List<string> | 자동으로 늘어남 | string 만 | 그대로 string. 다른 타입은 List<int> 처럼 쓰면 됨 |
그릇이 네 번 바뀌는 동안 Run() 은 거의 바뀌지 않습니다. 비결은 받는 변수의 타입입니다.
public void Run()
{
IEnumerable names = GenerateNames();
Program.WriteList(names);
}배열, ArrayList, StringCollection, List 는 모두 IEnumerable 을 구현합니다. "foreach 로 하나씩 꺼낼 수 있다" 는 약속입니다. 받는 쪽이 이 약속 하나만 요구하면, 그릇의 종류가 바뀌어도 코드를 고칠 필요가 없습니다.
public static void WriteList<T>(T array) where T : IEnumerable
{
foreach (var name in array)
{
WriteLine(name.ToString());
}
}ArrayList 와 StringCollection 은 제네릭이 나오기 전의 그릇입니다. 새 코드에서는 크기가 정해진 목록이면 배열, 늘어나는 목록이면 List<T> 를 씁니다. 수업에서 옛 그릇을 거쳐 가는 이유는 List 가 무엇을 해결했는지 보기 위해서입니다.
돌아보며
이번 편에서 얻은 것
- 수업을 갈아 끼우는 구조: 같은 약속(Strategy)을 지키는 클래스는 서로 바꿔 끼울 수 있다
- 이름 문자열로 클래스를 만드는 리플렉션(Type.GetType, Activator)
- 받는 쪽이 IEnumerable 만 요구하면 그릇이 바뀌어도 코드가 그대로다
다음에 풀 질문
- WriteList<T> 의 T 는 정확히 무엇이고, where 는 무엇을 막는가
- ArrayList 에 int 를 넣으면 object 로 바뀌는데, 그때 무슨 일이 일어나는가(박싱)
정리
- 약속이 같으면 바꿔 끼울 수 있습니다. 수업 클래스는 모두 Strategy 의 Run · Update 를 지키고, Program 은 classPath 의 이름으로 수업을 고릅니다.
- 단계는 #if 기호로 나눕니다. Scripting Define Symbols 에 ARRAY_STEP2 같은 기호를 쌓아 켜면 그 단계의 코드가 컴파일됩니다.
- 그릇보다 약속에 기대세요. 배열에서 List 까지 그릇이 바뀌어도, IEnumerable 로 받는 코드는 고칠 필요가 없습니다.
다음 편에서는 WriteList<T> 에 나온 제네릭과, 타입을 값처럼 다루는 typeof 를 봅니다.