본문 바로가기

Game/Unity & C#

6장 모노를 이용한 개발

6장 모노를 이용한 개발

 

1. 리스트와 컬렉션

- 모노 프레임워크는 데이터 목록을 유지할 수 있도록 여러 클래스들을 제공한다. 세 가지 메인 클래스로 List, Stack, Dictionary가 있다. 이 클래스들은 각각 특정 목적에 유용한 것들이다.

 

1) List 클래스

- 정렬되지 않은 상태로 하나의 데이터 형식을 가지는 연속적인 항목들의 목록이 필요할 때, 이 목록이 저장된 데이터의 크기에 맞게 늘어나거나 줄어드는 것을 원한다면 List 클래스가 적합하다. List는 아이템을 추가/삭제하고 저장된 모든 항목들을 순차적으로 순회해야 할 필요가 있을 때 특히 적합하다. 추가로, List 오브젝트는 유니티의 오브젝트 인스펙터에서 편집할 수도 있다.

 

- List 클래스는 항목들을 개별적으로 혹은 한거번에 제거하는 몇 가지 메소드를 제공하는데, 이 메소드들은 리스트를 순회하는 반복문의 바깥에서 사용하도록 고안된 것이다.

 

 

2) Dictionary 클래스

- Dictionary 클래스는 C++의 std::map 클래스와 유사하다. 이 클래스는 단순한 항목들의 목록 이상의 기능을 원할 때 특히 유용하다. 키 값에 따른 특정 원소를 검색해 즉시 접근해야 할 때는 Dictionary 클래스가 필수다. 리스트 안의 각 아이템은 키나 ID에 대응하는 다른 모든 항목으로부터 구별되는 해당 항목만의 고유한 식별자를 지정해줘야 한다. Dictionary 클래스는 하나의 키에 의해 이런 항목에 바로 접근할 수 있도록 해준다.

 

 

3) Stack 클래스

- 스택은 후입선출(LIFO) 모델에 기반한 특별한 종류의 리스트다. 쌓아 올리는 개념이다.

 

 

 

2. IEnumerable과 IEnumerator

- 보통 List, Dictionary, Stack 등 데이터 컬렉션을 이용한 작업을 할 때, 리스트의 전체 혹은 일부 항목에 대한 일정 범위에 대해 순회하며 반복 작업하기를 원할 것이다. 대부분의 경우에 항목들을 앞에서부터 순서대로 순회하도록 하길 원하겠지만, 앞에서 봤듯이 거꾸로 순회하는 것이 적합할 때도 있다. 표준 반복문을 이용해 항목을 차례로 순회할 수 있지만, 이 방법이 다소 성가신 경우에 IEnumerable과 IEnumerator 인터페이스가 도움을 줄 수 있다.
for 반복문을 사용하면 두 가지 성가신 부분이 있다. 첫째로 정수형 반복자 변수를 선언해야하고, 반복자 스스로 배열의 한계를 넘어가지 않는다는 것을 보장할 수 없다(out of bound). 이 문제는 foreach 반복문을 이용해서 어느 정도 고칠 수 있다.

foreach 반복문은 더 단순하고 가독성을 위해 선호되지만 눈에 보이는 것 이상의 장점이 있다. foreach 반복문은 IEnumerable 인터페이스를 구현하는 클래스와만 함께 동작한다. 그리고 배열 형식이 아닌 오브젝트의 그룹들을 순회할 수 있다는 장점이 있다.

 

 

3. 문자열과 정규식

- 텍스트 데이터를 다루는 것은 여러 가지 이유에서 중요하다(자막, 게임 내 문자열, 현지화 기능-다중 언어). 유니티에서 텍스트 에셋이란 유니티 프로젝트 안에 포함된 모든 텍스트 파일을 말하는 것으로서 여러 줄로 된 문자열의 경우(각 줄은 개행 문자 \n으로 구분한다)에도 각각의 에셋을 하나의 긴 문자열로 취급한다. 코드에서 이런 문자열을 처리해야 한다면 보통 여러 가지 방법이 주어진다. 일반적인 내용이지만 중요한 문자열 조작에 대해 살펴보자.

 

1) 널, 빈 문자열, 여백

- 문자열의 유효성을 검사하는 일반적인 방법은 먼저 문자열이 null인지 살펴본 후, (null이 아니면) 문자열의 길이를 검사한다. 문자열의 길이가 0이면 문자열은 빈 상태이므로 null이 아닐지라도 유효한 것이 아니다.
또한, 문자열 전체가 공백으로 구성될 가능성을 없애고 싶을 수 있다. null이 아닌 문자열이면서 여백으로만 채워진 경우에는 처리할 것이 아무것도 없는 문자열이지만 실제로는 길이가 0이 아니기 때문이다. 닷넷의 문자열 클래스에는 IsNullOrWhiteSpace라는 이름의 일체형 메소드를 통해 편의를 제공하고 있다. 하지만 이 메소드는 닷넷 4.5에서 소개된 기능이며, 모노는 아직 이 버전을 지원하지 않고 있다. 동일한 동작을 위해 다음과 같이 직접 구현할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEngine;
using System.Collections;
 
// null 및 여백 관련 기능을 추가하기 위한 기존 클래스 확장
public static class StringExtensions {
    public static bool IsNullOrWhitespace(this string fString)
    {
        return fString == null || fString.Trim().Length == 0;
    }
}
 
public class StringOps : MonoBehaviour
{
    // 문자열의 유효성을 검사한다
    public bool IsValid(string fString)
    {
        // null이나 여백이 존재하는지 검사한다
        if (fString.IsNullOrWhitespace())
        {
            return false;
        }
        else
        {
            // 추가적인 검사를 한다
            return true;
        }
        
    }
}
 

 

2) 문자열 비교

- 보통 두 문자열이 동일한지를 판별하는 식으로 == 연산자를 사용하는데, 더 나은 성능을 위해 String.Equals 메소드를 사용하는 것을 추천한다. 이 메소드에는 여러 가지 버전이 있는데 모두 다른 연산 비용이 든다. 일반적으로 StringComparison 파라미터를 포함하는 버전을 택하게 될 것이다. 먼저 첫 번째로 단순한 비교 방식이다.

1
2
3
4
5
6
// 문자열 비고
public bool IsSame(string fStr1, string fStr2)
{
    // 대소문자를 무시한다
    return string.Equals(fStr1, fStr2, System.StringComparison.CurrentCultureIgnoreCase);
}
 

같은 두 문자열의 동일성을 비교하는 빠르고 일반적인 다른 방법은 문자열 해시(Hash)를 이용하는 것이다. 이 방법은 문자열을 고유한 정수 값으로 변환해 이 값들을 대신 비교한다.

1
2
3
4
5
6
7
8
// 해시를 이용해 문자열 비교
public bool StringHashCompare(string fStr1, string fStr2)
{
    int fHash1 = Animator.StringToHash(fStr1);
    int fHash2 = Animator.StringToHash(fSTr2);
 
    return fHash1 == fHash2;
}
 

하지만 때로는 일치하는지만 비교해보는 것을 원하지 않을 수도 있다. 어떤 문자열이 알파벳 순으로 우선인지 정하길 원할 수 있다. 예를 들어 딕셔너리 내에 두 문자열이 알파벳 순으로 나열되어 있을 때, 한 문자열이 다른 문자열 앞에 오도록 하는 경우가 있다. String.Compare 함수를 이용하면 이렇게 만들 수 있다. StringComparison 형식의 파라미터를 사용하는 버전을 사용해야 한다. 이 함수는 fStr1이 fStr2 앞에 오게 되는 경우 -1을, 반대의 경우에는 1을, 문자열이 동일하면 0을 반환한다.

1
2
3
4
5
6
7
8
// 비교 정렬
public int StringOrder(string fStr1, string fStr2)
{
    // 대소문자를 무시한다
 
    return string.Compare(fStr1, fStr2, System.StringComparison.CurrentCultureIgnoreCase);
}
 

두 문자열이 동일한 경우에 String.Compare가 0을 반환하긴 하지만, 이 함수를 절대 동일성 검사용으로 사용하지는 말자. 이 경우 String.Equals 함수나 해시를 이용하는 것이 String.Compare을 이용하는 것보다 훨씬 빠르게 작동한다.

 

3) 문자열 서식 지정

4) 문자열 순회

5) 문자열 생성

 

6) 문자열 찾기

- 텍스트 에셋과 같은 파일에서 여러 줄의 텍스트를 읽어 처리할 때, 큰 문자열 안에서 작은 문자열이 처음 발견되는 지점을 찾아야 할 경우가 있다. 이럴 때에는 IndexOf 메소드를 이용하면 이렇게 문자열을 찾을 수 있다. 문자열을 찾은 경우, 이 함수는 큰 문자열 안에서 찾은 단어의 첫 번째 글자 위치를 양수인 정수로 반환한다. 찾지 못한 경우엔 -1을 반환한다.

 

 

7) 정규식

- 때때로, 엄청 큰 문자열에서 복잡한 검색이 필요한 경우가 있다. 이를테면 a로 시작해서 t로 끝나는 모든 단어를 문자열의 처음부터 찾는 것 같은 작업이 이런 경우에 해당한다. 이럴 때 검색으로 찾은 결과를 배열에 담으면 좋을 것이다. 정규식(Regex)을 이용하면 이런 것이 가능해진다. 정규식은 규격에 맞춘 특수한 문법을 통해 검색 패턴을 지정한다. 예를 들어, 문자열 [dw]ay는 'd나 w로 시작하고 ay로 끝나는 모든 단어 검색'을 의미한다.

 

 

8) 가변 개수 파라미터

- String.Format 함수와 같이 겉으로 보기에 개수의 제한 없이 파라미터를 계속 받을 수 있는 함수들을 닷넷과 모노를 살펴보면서 여러 번 봤을 것이다. 서식을 지정한 문자열에 여러 개의 파라미터를 삽입할 때는 String.Format을 이용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
public int Sum(params int[] fNumbers)
{
    int fAnswer = 0;
 
    for(int i=0; i<fNumbers.Length; i++)
    {
        fAnswer += fNumbers[i];
    }
    
    return Answer;
}
 

 

 

9) 통합 언어 쿼리

- 게임은 문자열뿐 아니라 오브젝트, 데이터베이스, 테이블, 문서 등 여기서 모두 나열하기 어려울 만큼 수없이 많은 데이터를 통해 작동한다. 하지만 이러한 광범위하고 다양한 데이터를 필요에 따라 작은 주제로 살펴볼 수 있도록 걸러내야 할 필요가 늘 생기기 마련이다. 예를 들어 씬의 모든 마법사 오브젝트를 담은 배열(혹은 리스트)이 주어졌을 때, 체력이 50퍼센트 이하이고 방어 포인트가 5 미만인 마법사만 보도록 결과를 제한하기를 원할 수 있다. 플레이어를 다시 공격하기 전에 가까운 물약을 찾아 체력을 회복하기 위해 떼로 도망치는 동작을 만들어내려 할 때 아마도 이런 처리가 필요할 수 있을 것이다. 이런 시나리오에서 통합 언어 쿼리(LINQ)라는 기술이 어떻게 도움이 될 수 있는지 살펴보자.

 

- LINQ는 데이터베이스와 XML 문서에 쿼리하는 것처럼 배열과 오브젝트를 포함한 데이터 세트에 쿼리를 실행하기 위한 고수준의 특화된 언어다. 쿼리는 LINQ에 의해 데이터 세트에 이용할 수 있는 적합한 언어(이를테면 데이터베이스용 SQL)로 자동 변환된다.

 

 

10) LINQ와 정규식

- LINQ를 꼭 분리해서 사용할 필요는 없다. 예를 들어, 정규식과 함께 사용해 큰 문자열로부터 특정 문자열 패턴을 뽑아내어 일치하는 결과를 탐색 가능한 배열로 변환할 수도 있따. 이 방법을 사용하면 쉼표로 구분된 값 형식의 파일(CSV 파일)을 처리할 때 특히 유용하다.
(CSV 파일이란, 텍스트 파일 안에 각각의 항목이 쉼표로 구분되어 있는 파일을 말한다)

LINQ와 정규식을 함께 사용하면 이런 파일에서 각각의 값을 읽어 매우 쉽고 빠르게 고유한 배열 항목으로 집어넣을 수 있다.

 

- 새로운 유닛을 사람 이름으로 생성해야 하는 RTS 게임을 예로 들어보자. CSV 형식을 이용해 남자와 여자 두 그룹으로 나뉜 이름이 저장되어 있다. 캐릭터를 생성할 때, 이 캐릭터는 남자나 여자가 될 수 있으며 다음의 예제 코드와 같이 CSV 파일에서 읽은 적합한 이름을 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System.Collections;
using System.Linq;
using UnityEngine;
 
public class LINQCSV : MonoBehaviour
{
    void Start()
    {
        // 여자 이름을 생성한다
        // 정규식 검색 패턴
        // 'female:'을 접두어로 가지는 모든 이름을 찾되 접두어를 포함시키지 않는다
        string search = @"(?<=\bfemale:)\w+\b";
 
        // CSV 데이터 - 캐릭터 이름들
        string CSVData = "male:john,male:tom,male:bob,female:betty,female:jessica,male:drink";
 
        // 정규식을 수행해 여자 접두어를 가진 모든 이름을 접두어를 제외하고 얻는다
        string[] FemaleNames = (from Match m in Regex.Matches(CSVData, search) select m.Groups[0].Value).ToArray():
 
        // 결과에 포함된 모든 여자 이름을 출력한다
        foreach(string S in FemaleNames)
        {
            Debug.Log(S);
        }
 
        // 컬렉션에서 임의의 여자 이름을 뽑는다
        string RandomFemaleName = FemaleNames[Random.Range(0FemaleNames.Length)];
    }
}
 
 

 

 

4. 텍스트 데이터 에셋 다루기

- 지금까지 다룬 예제들을 통해 문자열 오브젝트에 직접 담긴 텍스트를 살펴봤다. 이에 더해 유니티에서는 텍스트 파일을 다루는 것도 가능하다. 다시 말해, 외부에서 텍스트를 불러오는 것도 가능하다. 이렇게 하는 방법에 대해 살펴보자.

 

1) 텍스트 에셋: 정적 로딩

- 첫 번째 방법은 텍스트 파일을 유니티 프로젝트로 드래그앤드롭해 임포트하는 것이다. 그후에 코드에서 'TextAsset' 오브젝트로 접근이 가능하다.

 

2) 텍스트 에셋: 로컬 파일 로딩

- 텍스트 데이터를 로컬 하드 드라이브와 같이 프로젝트 바깥에서 로딩하기 위한 다른 방법을 살펴보자. 스크립트에서 동적으로 텍스트 데이터를 불러오는 방법으로, 씬 시작 시점에 불러올 필요가 없고 필요한 시점에 코드를 실행하면 된다. 긴 텍스트 파일을 불러올 때는 무거운 처리가 필요하므로 지연시간을 중요하게 고려해야 한다.

 

3) 텍스트 에셋: INI 파일 로딩

- 많은 텍스트 파일 형식 중 INI 파일도 불러올 수 있다. INI 파일을 불러오기 위한 이상적인 데이터 구조는 키-값 쌍 구조를 반영하는 딕셔너리다. 따라서 INI 파일을 딕셔너리에 불러오면 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 기본적인 INI 파일을 딕셔너리로 읽어오는 함수
public static Dictionary<stringstring> ReadINIFile(string Filename)
{
    // 파일이 시스템에 존재하지 않는다면, null을 반환한다
    if(!File.Exists(Filename))
    {
        return null;
    }
 
    // 새 딕셔너리 생성
    Dictionary<stringstring> INIFile = new Dictionary<stringstring>();
 
    // 새 스트림 리더 생성
    using (StreamReader SR = new StreamReader(Filename))
    {
        // 현재 줄을 담을 문자열
        string Line;
 
        // 유효한 줄을 계속 읽어나간다
        while(!string.IsNullOrEmpty(Line = SR.ReadLine()))
        {
            // 줄 앞뒤의 여백을 제거한다
            Line.Trim();
 
            // 현재 줄의 키=값 사이를 분리한다
            string[] Parts = Line.Split(new char[] {'='});
 
            // 딕셔너리에 추가한다
            INIFile.Add(Parts[0].Trim(), Parts[1].Trim());
        }
    }
 
    // 딕셔너리를 반환한다
    return INIFile;
}
r
 

이 함수에서 반환된 딕셔너리는 INI 파일의 구조와 일치하게 된다. 따라서 INI 파일의 키에 따른 값에 접근할 때 Value = MyDictionary["Key"];와 같은 방식으로 접근할 수 있게 된다. 다음 에제 코드처럼 foreach 안에서 딕셔너리의 모든 키와 값 항목을 순회해 나열하는 것도 가능하다.

1
2
3
4
5
6
7
8
9
// INI 파일로부터 딕셔너리 생성하기
Dictionary<stringstring> DB = ReadINIFile(@"c:\myfile.ini");
 
// 딕셔너리 안의 모든 항목 나열하기
foreach(KeyValuePair<stringstring> ENtry in DB)
{
    // 각각의 키와 값에 대해 반복
    Debug.Log("Key: " + Entry.Key + " Value: " + Entry.Value);
}
 
 

 

4) 텍스트 에셋: CSV 파일 로딩

- 앞에서 남녀 캐릭터 이름을 모두 가지고 있는 CSV 파일의 처리 방법에 대해 살펴봤다. 이번엔 쉼표로 각각 구분된 문자열이 담겨 있는 CSV 파일을 디스크에서 문자열 배열로 불러오는 방법을 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// CSV 파일을 문자열 배열로 불러오는 함수
public static string[] LoadFromCSV(string Filename)
{
    // 파일이 시스템에 존재하지 않는다면, null을 반환한다
    if (!File.Exists(Filename))
    {
        return null;
    }
 
    // 모든 텍스트를 읽어온다
    string AllText = File.ReadAllText(Filename);
 
    // 문자열 배열을 반환한다
    return AllText.Split(new char[] {','});
}
 

 

5) 텍스트 에셋: 웹 로딩

- 멀티플레이어 게임을 만든다면 웹과 공유하는 플레이어나 게임 데이터에 접근해야 할 때가 있다. 패스워드의 해시를 온라인으로 검증한다거나 웹페이지의 구성 요소를 처리하기 위해 접근하려 할 때는 다음 예제 코드처럼 온라인으로 텍스트 데이터를 받기 위한 WWW 클래스가 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
// 웹의 텍스트를 문자열로 불러온다
public IEnumerator GetTextFromURL(string URL)
{
    // 새 WWW 오브젝트를 생성한다
    WWW TXTSource = new WWW(URL);
 
    // 데이터를 불러오길 기다린다
    yield return TXTSource;
 
    // 텍스트 데이터를 얻는다
    string ReturnedText = TXTSource.text;
}