1. 목적

출발점에서 목적지까지 길 찾기를 하고자 할때 중간 중간 장애물을 피해가며 목적지까지 도달하는 알고리즘이 필요해졌다. 물론 길 찾기를 하는동안 목적지까지 도달 할 수 없는 경우도 판별이 가능할 것이다.

2. 개요

이 알고리즘의 아이디어는 어떠한 노드에 대하여 시작점으로부터 온 거리(g)와 앞으로 목적지까지 가는데 남은 거리(h : heuristic)를 더한 값(f)이 가장 적은 노드를 선택하여 목적지 까지 탐색하는 것이다.

여기서 중요한것은 목적지까지 남은 거리(h)를 적절하게 추정하여 설정하는 것이다.

그림1. 가장 작은 f값을 선택해가며 진행한다

그리고 두개의 목록으로 노드들을 관리해야 하는데 그것이 열린목록(Open list)과 닫힌목록(Close list)이다.

열린목록은 현재 조사중인 노드에 인접한 리스트로 최단거리로써 선택 가능성이 열려있는 노드들의 집합이다.

닫힌목록은 조사가 끝난 노드들의 집합으로 다시 조사할 필요가 없다.

 

3. 설계

각 노드는 위에서 언급한 g, h, f의 값과 자신의 노드id, 어디로부터 왔는지 부모노드의 id를 저장한다.

열린목록은 일반적인 list를 이용하는 것이 아닌 가장 최선의 방법일 가능성이 높은 즉, f의 값이 가장 작은 노드를 먼저 검색할 것이기 때문에 priority queue를 사용한다.

닫힌목록은 일반적인 queue로 생성한다.

전체적인 흐름은 시작노드를 열린목록에 넣는것으로 시작한다.

그리고 다음을 반복한다.

1. 열린목록의 노드를 꺼내어 접근할 수 있는 노드를 모두 검사한다.

2-1. 해당 노드가 닫힌목록에 있는 노드라면 무시한다.

2-2. 해당 노드가 열린목록에 있는 노드라면 f값을 비교해 더 작은 노드의 정보로 갱신한다.

2-3. 해당 노드가 열린목록 혹은 닫힌목록에 있지 않는 노드라면 열린목록에 넣는다.

3. 1번에서 꺼낸 노드는 닫힌목록으로 이동한다.

4. 목표 노드가 열린목록에 들어왔다면 중지한다.

나머지는 목표 노드로부터 부모노드를 찾아 시작노드까지 조사해 길을 완성한다.

만약 목표 노드가 들어오기전에 열린 목록이 먼저 비었다면 길이 없다는 뜻이다.

 

4. 예시

다음 예는 대각 이동 불가, h는 목표까지의 x의 차이와 y의 차이를 더한 값, 열린목록에서 같은 f값이 들어왔다면 먼저 들어온 노드에 우선권등 조건을 부여하였다.

그림2. a*알고리즘의 예시

 

소스코드 : https://github.com/puze/AStarAlgorithm/blob/main/AStarAlgorithm.cs

출처 : aidev.co.kr/game/501

 

게임 인공지능 - A* 알고리즘을 사용한 길찾기

  소스코드 및 실행 : 첨부파일       A* 알고리즘의 개요   A*(에이 스타) 알고리즘은 1968년에 만들어진 것으로, 탐색을 수행하는데 있어 매우 효과적인 알고리즘이며 다양한 종류의 문제들을

aidev.co.kr

en.wikipedia.org/wiki/A*_search_algorithm

 

A* search algorithm - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Algorithm used for pathfinding and graph traversal A* (pronounced "A-star") is a graph traversal and path search algorithm, which is often used in many fields of computer science due t

en.wikipedia.org

 

1. 문제점

StartCoroutine과 StopCoroutine을 사용하기위해 현재 사용하는 코루틴을 IEnumerator변수에 저장 후 StopCoroutine을 호출 하였지만 코루틴이 멈추지 않고 실행되는 문제가 발견되었다.

2. 원인

코루틴을 호출하는 클래스가 모노비헤이비어를 상속받지않아 다른 모노비헤이비어를 상속받은 클래스에서 StartCoroutine을 대리 호출해주는 방식을 사용하였다.

하지만 StartCoroutine을 호출하는 모노비헤이비어와 StopCoroutine을 호출 하는 모노비헤이비어가 달랐고 이 때문에 코루틴이 멈추지 않고 실행된것이다.

3. 해결방법

결국 StartCoroutine과 StopCoroutine을 호출하는 모노비헤이비어를 일치 시켜주는 것 으로 해결이 되었다.

플랫폼 대응을 위해 유니티 버전을 2018.3.5f1에서 2018.4.2f1로 버전업을 하였는데 그 뒤로 비주얼스튜디오를 유니티에 붙이면 멈춤 현상이 일어났다. 당연히 브레이크 포인트에 걸리지 않은 상태였고 Attach를 해제하면 정상작동을 하였다.

원인은 유니티의 오래된 버그라고한다.

해결방법은 비주얼스튜디오에 남아있는 모든 중단점을 삭제한 후 다시 Attach하게되면 정상작동하게된다. 이 후 브레이크포인트를 걸고 작업 후 다시 Attach를 해도 문제없이 작동한다.

출처 : https://forum.unity.com/threads/unity-freeze-when-connecting-vs-debugger.529863/

 

Unity Freeze when connecting VS Debugger

Hey Guys. Since three days, i have a really annoying issue: When i connect the Visual Studio 2017 Debugger to my Unity Installation (Unity 4.x), the...

forum.unity.com

 

1. List<T>

C#을 하면서 가장 많이 사용하는 클래스라고 생각된다.

List라고한다면 Array List와 Linked List가 떠오르는데 정답은 Array List이다.

Remarks
The List class is the generic equivalent of the ArrayList class. It implements the IList generic interface by using an array whose size is dynamically increased as required.

You can add items to a List by using the Add or AddRange methods.

The List class uses both an equality comparer and an ordering comparer.

ArrayList와 다른점은 ArrayList는 Object를 저장하여 어떤 자료형이라도 저장 할 수 있지만 박싱과 언박싱이라는 큰 성능상의 문제가 있다. 이것을 해결한 것이 템플릿을 이용한 List이다.

처음부터 자료형을 정하기 때문에 ArryList와 비교해 유연하지 못하지만, 박싱과 언박싱을 피할수 있다. 이 제네릭 클래스 List의 강력한 기능 중 하나가 Random Access이다. 

Random Access란?
데이터가 저장되어있는 메모리에 특정 인덱스로 접근 하는 것을 뜻한다. 대표적으로 Array가 있다.
Sequential access란?
데이터에 접근할 시 Root인덱스부터 순차적으로 검색하여 데이터에 접근 하는것을 뜻한다. 대표적으로 Linked list가 있다.

그럼 대표적인 오퍼레이션을 살펴보자.

1. Add()

작업속도는 O(1)

특정 상황에따라 추가적인 작업을 한다.

기본적으로 데이터 저장공간 Array이므로 저장공간에 한계가 있다. 이 저장공간이 가득찰때마다 2배씩 늘린 저장공간을 다시생성해 copy하게된다. 이 경우엔 O(n)이 소요된다. 저장공간을 늘리는 작업은 코스트가 꽤 높지만 이 알고리즘은 꽤 효율적으로 작동한다고 한다.

2. Remove()

작업속도는 O(n)

특정 자료가 삭제될시 배열에 빈공간이 남게된다. 물론 이것을 그냥 방치하면 여러가지 논리적 오류가 발생하기 쉽게된다. 그러므로 삭제된 데이터의 뒷부분을 모두 한칸씩 앞으로 당겨오는 작업이 필요로 한다.

3. Search

- [] : 작업속도는 O(1)

위에서 설명한 랜덤 엑세스가 가능하므로 특정 인덱스의 데이터를 손쉽게 접근할수 있다.

- IndexOf() : 하지만 인덱스가 아닌 데이터의 내용으로 검색할 시 O(n)이 필요로 하게된다.

 

4. Clear()

작업속도는 O(n)

List를 순회하며 데이터를 삭제한다. 이 메소드로는 List의 용량까지 초기화 되지는 않는다.

2. Dictionary<TKey, TValue>

해시테이블로 구현되어있지만 키 충돌을 허용하지 않는다.

이 해쉬테이블은 List로

1. Add()

일반적인 경우엔 O(1)이지만 Dictionary도 마찬가지로 해시 테이블의 용량을 늘리는 작업을 할 시 O(n)이 걸린다.

2. Remove()

작업속도는 O(1)에 가깝다.

단지 해시테이블에 있는 값을 찾아가 데이터를 삭제하면 되기 때문에 매우 빠르게 작동한다.

 

3. Search 

- [] 및 ContainsKey(TKey)

작업속도는 O(1)에 가깝다.

사실은 이 Dictionary를 구성하고 있는 해시 알고리즘에 영향을 받는다.

- ContainsValue(TValue)

작업속도는 O(n)이다.

Dictionary를 순회하여 찾고자 하는 데이터와 비교하게 된다.

4. Clear

작업속도는 O(n)이다.

Dictionary를 순회하며 모든 데이터를 삭제하게된다.

 

 

출처 : https://docs.microsoft.com/en-us/dotnet/api/System.Collections.Generic.List-1?view=netframework-4.8

이미 사용되고 있는 변수명이 바뀌거나 함수가 변경되었을 때 많은 오류가 발생하게되고 하나하나 수정해야 한다. 그럴때 사용하면 좋을 단축키를 찾아보았다. 커서가 자동으로 오류가 발생한 지역으로 이동하게 된다. (물론 컴파일 에러만 해당한다)

1. 문제점

TextMeshPro에서 사용할 마테리얼과 쉐이더가 에디터 상에서는 문제없이 표시되지만 마테리얼과 쉐이더를 전부 에셋 번들화 하고 게임 중 로드 후 사용할 시 기본적으로 제공하는 TextMeshPro/Distance Field쉐이더의 Underlay, Glow 효과가 적용되지 않았다. (Lighting은 사용하지 않았지만 같은 현상이 있을 것으로 예상됨)

TextMeshPro 컴포넌트를 사용 할 시 기본적으로 선택되는 쉐이더 프로퍼티

2. 원인 찾기

한가지 의심스러웠던 부분은 Face속성과 Outline속성은 문제없이 작동한다는 것이다.

뭔가 저 체크박스가 있는 속성은 에셋 번들화 될 시 제대로 듣질 않는 것처럼 보였다.

그래서 스크립트상에서 강제로 저 속성들을 활성화하기 위해 쉐이더의 속성들을 살펴보았다.

TextMeshPro/Distance Field Shader코드

GLOW의 속성을 찾아보았는데 인스펙터에 보이는 각 변수들은 존재하였지만 내가 찾던 저 체크박스를 관리하는 변수는 보이질 않았다. 

TextMeshPro/Distance Field Shader코드

하지만 #pragma shader_feature키워드를 사용해 define처럼 사용하는 코드가 보였다.

바로 유니티 메뉴얼에서 shader_feature라고 검색해보았다.

Difference between shader_feature and multi_compile
#pragma shader_feature is very similar to #pragma multi_compile, the only difference is that unused variants of shader_feature shaders will not be included into game build. So shader_feature makes most sense for keywords that will be set on the materials, while multi_compile for keywords that will be set from code globally.
Additionally, it has a shorthand notation with just one keyword:
#pragma shader_feature FANCY_STUFF
Which is just a shortcut for #pragma shader_feature _ FANCY_STUFF, i.e. it expands into two shader variants (first one without the define; second one with it).

눈에 띌 만한 내용이 있었다. 바로 사용하지 않은 쉐이더 베리언트는 빌드에 포함하지 않는다는 내용.

씬, 쉐이더, 마테리얼 전부 에셋번들화 되어있어 해당 효과를 사용하는지 하지 않는지 판별할 수 없고 포함하지 않은 것처럼 보인다. 아마 저 코드 덕분(?)에 내가 원하는 glow 같은 효과들이 빌드에 포함되지 않은 것 같다. 

 

3. 해결책

가장 좋은 해결책은 저 효과들을 사용한다고 유니티에게 인식시키고 빌드에 포함시켜 게임내에서 쉐이더를 정상적으로 컴파일을 하는 것일 것이다. 하지만 전부 에셋 번들화 되어있는 지금 딱히 방법이 떠오르지 않아 shader_feature 키워드를 전부 벗겨 강제로 빌드에 포함시키는 방법을 사용하기로 했다. 결과적으론 해결.. 하였지만 모든 텍스트에 glow효과가 컴파일될 것이고 결과적으로 성능을 저해할 것이다.

 

4. 여담 

위에서 하나 궁금증으로 남아있던 것은 체크박스는 도대체 뭐란 말인가였다.

일단 인스펙터상에서 보이는 UI들이 기본적인 쉐이더 프로퍼티가 아니니 어디선가 Editor를 수정한 것으로 보인다.

그래서 찾아보았다. 바로 이것.

Package폴더에있는 Shader Editor코드

체크박스의 변수로 보이는 bool변수도 존재하고 ShaderFeature라는 익숙한 타입도 보인다

Package폴더에있는 Shader Editor코드 

아마 위의 코드에서 쉐이더에 키워드를 전달해 define을 하는 흐름인 것 같다.

상속의 교과서 유닛에 대해 구현해보려고 한다.

 

일단 구체적인 사양이 정해지지 않아 내 상상을 좀 가미해야 하는데 만약 사양이 다르게 된다면 그때 수정하는 것으로 하고 일반적인 상황을 상상하여 구현해보도록 하자.

 

상속에 필요한 키워드(예약어)는 abstract, virtual, override, static 등 하나하나 알아보자.

 

1. abstract(추상적인)

메소드앞에 사용할 수 있으며 이 키워드를 사용할 경우 클래스도 abstract키워드를 붙여야한다.
이 예약어를 붙일 경우 뜻 그대로 추상적인 클래스가 되며 이 클래스로는 선언을 하지 못하게된다.

이 클래스를 상속받게 되면 무엇을 구현해야 하는지 알 수 있는 인터페이스 역할을 한다.

(참고로 클래스와 인터페이스의 다른 점은 단 하나의 클래스만 상속받을 수 있으며, 여러개의 인터페이스를 상속 받을 수 있다)

추상 함수는 추상 클래스 내에서 구현할 수 없으며 자식 클래스에서 그 내용을 반드시 구현(override)해야 한다.

당연히 그 내용을 자식 클래스에서 접근해야 하므로 접근자를 private으로 선언할 수 없다. (public과 protected만 가능)

2. virtual (가상의)

abstract와 비교해보자.
비슷한점은 상속받은 클래스 역시 이 함수를 재구현(override) 할수있다.
다른점은 이 함수자체를 해당 클래스에서 반드시 구현을 해야한다. 그리고 해당 클래스에 추상함수가 없이 가상함수만 있다면 클래스앞에 abstract나 virtual키워드를 붙일 필요가없다. 일반적인 클래스에서 가상함수를 선언 할수 있다. 
이 클래스를 상속받은 클래스는 이 가상함수가 이미 부모 클래스에서 구현되어 있으므로 추상함수와 다르게 구현하지 않아도되고 물론 구현해도된다!
만약 자식 클래스에서 이 함수를 구현할때 부모클래스의 함수도 이용하고싶다면 base키워드를 사용하면된다.

마찬가지로 그 내용을 자식 클래스에서 접근해야하므로 접근자를 private으로 선언 할수 없다. (public과 protected만 가능)

3. override (덮어 씌우다)

위의 예약어로 생성한 추상함수나 가상함수를 실제로 구현하는 키워드이다. 부모가 선언한 추상함수를 구현하지 않으면 에러가 나므로 깜빡하고 구현을 안하고 넘어갈 일은 없다. 그러니 특정 클래스를 상속 받는 클래스라면 해당하는 함수들은 구현이 되어있다고 상정후 코딩을 해도된다. 
가상함수를 오버라이딩한다면 부모가 구현한 함수는 사라지고 해당 클래스의 함수만 작동하게되는데 여기서 base키워드를 사용해 부모가 구현한 가상함수를 호출 할 수있게된다.

4. static (정적인)

조금 뜬금 없긴 하지만 클래스간의 상속관계에서 자주보여서 넣어봤다.
이 키워드는 변수앞에 사용하면 단 메모리상에서 사라지지 않는 단 하나의 변수가 만들어진다.
일반적인 변수는 상속을 받으면 변수가 갖는 공간만큼을 해당클래스에서 다시 확보하게 된다.
Unit 클래스의 hp를 상속받은 휴먼과 플랜트는 각각 각자의 hp를 갖게 되는 것이다.
하지만 static으로 선언한 변수는 해당 변수를 가지고있는 클래스들이 모두 한곳의 변수를 바라보게된다.
어떤 한 클래스가 static변수를 수정하게 된다면 모든 클래스의 변수들이 영향을 받게된다.
꽤 위험해보이고 객체지향성을 무너뜨린다. 멀티 스레드 환경에서 락이나 세마포어를 열심히 공부했던 것도 떠오른다. 심지어 메모리상에서 사라지지도 않아 권장 하지 않는다. 
하지만 이게 또 꽤 편리할 때가 있다. 지금당장 떠오르진 않지만 어쨋든 알아 두는걸로!
 
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
public abstract class Unit
{
    protected static int playerIndex = 0;
    protected int hp;
    protected abstract void InitUnit();
    protected virtual void Attack() { }
}
 
public class Human : Unit
{
    // 반드시 구현해야함
    protected override void InitUnit()
    {
         hp = 10;
    }
 
    // 선택적 구현
    protected override void Attack()
    {
        // 부모클래스의 함수 실행
        base.Attack();
    }
}
 
public class Plant : Unit {
    protected override void InitUnit()
    {
        hp = 5;
    }
}
cs

 

맨땅에서 게임 개발을 하기위해 가장 힘든 점은 아마 같이 작업할 팀원을 구하는 것이 아닐까 싶다...

그래도 언제 공중분해 될지 모르지만 성격좋은 사람들과 함께 게임을 제작하기로 했다. 

그것도 일본인 디자이너와 홍콩인 기획자 이렇게 셋이서 말이다. 완전 글로벌...

모두 돈없는 젊은이들이라 최대한 돈이 안드는 방향으로 구축하기로했다.


공동 작업 공간

일단 서버로 이용 할 수있는 컴퓨터가 없다. 하지만 모두의 작업을 공유할 수 있는 공간이 필요하므로 이것저것 알아보았다. 

기본적인 자료공유(회의록이나 참고자료등)는 프로그래머가 아니더라도 쉬운 작업환경에 무료 공간도 넉넉한 구글 드라이브 (무료 15GB)를 사용하기로 했다. 

그리고 가장 중요한 버전관리는 두말할 것 없는 강력한 git을 사용하기로 하고 레포지토리에 대해 고민해봤다.

 

1. GitHub

github은 뭐 워낙 유명하니 제일먼저 찾아보았다. 대학생때도 써보았고 지금도 github의 많은 오픈소스들을 이용하고있으니!

그런데 큰 문제점이있었다. 무료 저장소 공간이 1기가밖에 안된다... 소스파일만 관리하면 문제없겠지만 디자인도 데이터도 버전관리한다면 확실히 부족하겠지... 라고 생각하며 일단 킵해놓고 다른 방법을 찾아보았다.


2. 구글 드라이브 위에 레포지토리를 올려 사용

구글 드라이브의 설치형 어플리케이션을 이용해 드라이브 폴더를 원격 저장소 처럼 이용하는 방법이다. 이미 구글 드라이브는 확보했고 더이상의 서버가 필요하지 않기에 잠깐 생각해 보았지만 왠지이거 꺼림칙했다. 혼자 이용한다면 문제는 없을 것 같은데 원격 저장소(로컬에 있는 구글 드라이브 이지만)에 push를 한 후 바로 적용 되는 것이 아닌 구글 드라이브에서 한번더 동기화를 하기 때문이다... 충돌 처리를 어떻게 해주는지 까지는 찾아 보지 않았지만 일반적이지 않은 방법 같기에 다른 방법을 알아보았다.

3. AWS + 설치형git

만약에 nas가 있었다면 서버위에 설치형 git을 사용 하는 방법도 있었을 것이다. 하지만 서버를 빌려 설치형git을 운용해보는건 어떨까? 마침 설치형git은 리눅스상에서 밖에 돌아가지 않는듯 하기도 하고. 그리고 처음엔 좀 번거로워도 나중에 어디가서 어필도 할 수 있을 것 같다. (자랑용) 하지만 조금 복잡한 요금때문에 일단 킵!


4. GitLab

엄청 괜찮은 저장소를 찾았다! 무료 용량도 무려 10GB에 private으로 프로젝트를 진행하는것도 가능하다.리소스를 전부 버전관리하는것은 힘들어보이지만 큰 이미지라던가는 따로 구글드라이브로 공유하면 어찌어찌 될것같은 느낌이 들었다. 조금 서버가 불안정하다는 이야기도 있었지만 지금 그런거 가릴때가 아니기 때문에 이걸로 결정.


+ Recent posts