게임 분석, 리뷰, 소개, 개발 전문 블로그

오늘 공부할 내용

이동 목적지(타일)을 선택하면 최상의 경로를 찾아주는 것을 구현할 겁니다.

원문은 아래 링크를 참조하세요.

http://theliquidfire.com/2015/06/08/tactics-rpg-path-finding/


Directions

Assets\Scripts의 하위 폴더로 Enums 를 만듭니다.

Enums 폴더에 C# 스크립트 Directions.cs 를 만들고 아래 코드를 작성해주세요.

1
2
3
4
5
6
7
8
9
using UnityEngine;
using System.Collections;
public enum Directions
{
    North,
    East,
    South,
    West
}
cs

방향을 나타내는 Enum 입니다. 이것의 사용 용도는 아래에서 설명하겠습니다.

 


Directions Extensions

Assets\Scripts\ 폴더의 하위폴더로 Extensions 를 만듭니다.

Extentions 폴더에 C# 스크립트 DirectionsExtenstions.cs 만들고 아래 코드를 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;
 
public static class DirectionsExtensions
{
    // 타겟과의 방향에 따라 Directions의 Enum 값이 리턴된다.
    public static Directions GetDirection(this Tile t1, Tile t2)
    {
        if (t1.pos.y < t2.pos.y)
            return Directions.North;
        if (t1.pos.x < t2.pos.x)
            return Directions.East;
        if (t1.pos.y > t2.pos.y)
            return Directions.South;
        return Directions.West;
    }
 
    // 방향을 오일러 각도로 반환한다.
    public static Vector3 ToEuler(this Directions d)
    {
        return new Vector3(0, (int)d * 900);
    }
}
 
cs

#코드 설명

Static은 스택 영역에 올라가서 다른 클래스에서 쉽게 접근할 수 있습니다.

 

◆ GetDirection(this Tile t1, Tile t2)

타겟을 바라보는 방향에 따라 Directions의 Enum 값을 반환합니다.

여기서 this Tile 은 GetDirection을 호출한 개체가 this Tile 로 전달된다. 

 

◆ ToEuler(this Directions d)

Directions의 Enum은 방향에 따라 0~3의 값을 반환합니다. 반환된 0~3의 값에 90 도를 곱해서 Y 값에 입력하고 x, z는 0을 입력해서 오일러 각도를 구하는 함수입니다.

* 오일러라는 것은 회전하는 물체를 수학적(x, y, z)으로 표현하는 방법입니다.

초록색 선이 Y 값입니다.

빨간색 선은 X 값입니다.

파란색 선은 Z 값입니다.

카메라 뷰는 위에서 내려다 보는 각도입니다.

Y값에 따라 타일의 바라보는 방향이 결정됩니다.

이제 우리는 Tile1 에서 Tile2 를 바라보는 방향과 그 방향의 오일러 각도를 구할 수 있습니다.

 


Tile
 Assets\Scripts\View Model Component\Tile.cs 를 아래와 같이 수정합니다.

1
public GameObject content;
cs

전역 변수를 추가합니다.

 

1
2
[HideInInspector] public Tile prev;
[HideInInspector] public int distance;
cs

경로 찾기에서 사용할 전역 변수 두개를 선언합니다.

HideInInspector 는 Inspector 뷰에서 숨기겠다는 의미입니다.

 


Unit

Assets\Scripts\View Model Component 에 C# 스크립트 Unit.cs 만듭니다.

Assets\Prefabs\Hero 와 Monster에게 Unit.cs를 컴포넌트로 추가하고 아래 코드를 작성합니다.

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
using UnityEngine;
using System.Collections;
public class Unit : MonoBehaviour
{
    public Tile tile { get; protected set; }
    public Directions dir;
    public void Place(Tile target)
    {
        // 이전에 선택한 tile이 null로 초기화 안되었다면
        if (tile != null && tile.content == gameObject)
            tile.content = null;
 
        tile = target;
 
        if (target != null)
            target.content = gameObject;
    }
 
    // 해당 게임 오브젝트의 Position 과 EulerAngles 값을 변경합니다.
    public void Match()
    {
        transform.localPosition = tile.center;
 
        // Vector3(x, y, z)로 Rotation을 구하는 겁니다.
        transform.localEulerAngles = dir.ToEuler();
    }
}
cs

#코드 설명

◆ public void Place(Tile target)

Unit(몬스터 또는 히어로)가 머물고 있는 타일을 변경할 때 호출하는 함수입니다.

Unit 이동, 죽음 등의 이유로 이 함수가 호출될 것으로 예상됩니다.

 


Board

보드에서 길 찾기의 핵심 로직들이 들어갑니다.

이번에는 코드를 보여드리기 전에 먼저 길찾기 로직에 대해서 설명하겠습니다.

이동할 대상(Player, Monster)를 선택합니다.

해당 대상이 있는 타일이 시작 타일이 됩니다.

시작 타일의 Tile.distance는 0 이고 Tile.prev는 null 입니다.


시작 타일의 주변 타일을 참조합니다.

주변 타일과 시작타일의 거리는 1칸이므로 

주변 타일의 Tile.distance는 1이 들어갑니다.

주변 타일의 Tile.prev 에 시작 타일이 들어갑니다.


빨간색 타일이 이동 가능 범위 내에 있으면

다음 검사 대기열에 주변타일을 넣습니다.

 

초록 타일의 주변 타일을 검사하기 위해 참조시킵니다.

동그라미에 대각선이 그려진 아이콘은 이미 검사했으니 이번 대기열 검사에서 건너띄는 타일을 의미합니다.

위 4번의 초록타일의 주변 타일을 다음 검사 대기열에 등록했으니, 검사가 완료된 지역으로 표시합니다.

그 다음 순번의 타일을 검사를 하면서 주변타일1의 주변 타일2을 참조시킵니다.

1~4 과정이 계속 반복되는겁니다.

시작 타일의 주변 타일을 위에서 부터 시계방향으로 검사하면서 주변 타일을 참조해서 다음 검사 대기열에 넣는 과정입니다.


 

 6번과 동일


 

시작 타일의 주변타일1에 대한 검사가 완료되었습니다.

이제 주변타일1의 주변타일2을 검사할 차례입니다.

주변 타일2의 Tile.distance는 2이 들어갑니다.

(시작타일과의 거리)

주변 타일2의 Tile.prev 에 해당 주변타일1이 들어갑니다.

 

주변타일2 를 검사하면서 주변타일3을 참조시킵니다.

해당 개체(Hero, Monster)의 이동 범위 내의 모든 타일을 검색하는 방식입니다.

 

로직 설명은 끝났습니다. 아래 코드를 Board.cs에 추가해주세요.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
using UnityEngine;
using System.Collections.Generic;
using System;
 
public class Board : MonoBehaviour
{
    [SerializeField] GameObject tilePrefab;
    public Dictionary<Point, Tile> tiles = new Dictionary<Point, Tile>();
 
    // 선택/미선택에 따른 타일 색상
    Color selectedTileColor = new Color(0111);
    Color defaultTileColor = new Color(1111);
 
    // 현재 검사할 타일의 주변 타일을 참조할 때
    // 사용하는 변수
    Point[] dirs = new Point[4]
    {
        new Point(01),
        new Point(0-1),
        new Point(10),
        new Point(-10)
    };
 
 
    public void Load(LevelData data)
    {
        for (int i = 0; i < data.tiles.Count; ++i)
        {
            GameObject instance = Instantiate(tilePrefab) as GameObject;
            Tile t = instance.GetComponent<Tile>();
            t.Load(data.tiles[i]);
            tiles.Add(t.pos, t);
        }
    }
 
    public List<Tile> Search(Tile start, Func<Tile, Tile, bool> addTile)
    {
        List<Tile> retValue = new List<Tile>();
        retValue.Add(start);
        
        ClearSearch();     // 검사전에 전체 타일의 Prev, distance 초기화
        Queue<Tile> checkNext = new Queue<Tile>();
        Queue<Tile> checkNow = new Queue<Tile>();
        
        start.distance = 0;
        checkNow.Enqueue(start);
 
        // 검사 시작
        while (checkNow.Count > 0)
        {
            Tile t = checkNow.Dequeue();
            for (int i = 0; i < 4++i)
            {
                Tile next = GetTile(t.pos + dirs[i]);
                if (next == null || next.distance <= t.distance + 1)
                    continue;
 
                // 이동 가능 거리 내에 있는 타일인지 검사.
                if (addTile(t, next))
                {
                    // 가능한 타일이면 다음 검사 대기열에 추가.
                    next.distance = t.distance + 1;
                    next.prev = t;
                    checkNext.Enqueue(next);
                    retValue.Add(next);
                }
            }
 
            // 현재 검사할 대기열이 종료되면
            // 다음 검사할 대기열로 교체한다. 
            // While 이 계속 돌아간다.
            if (checkNow.Count == 0)
                SwapReference(ref checkNow, ref checkNext);
        }
 
 
        // 검사 범위 내 타일들을 반환한다.
        return retValue;
    }
    
    // 해당 좌표에 타일이 있는지 검사
    public Tile GetTile(Point p)
    {
        return tiles.ContainsKey(p) ? tiles[p] : null;
    }
 
    // 검사하기 전에 타일들의 prev, distance를 초기화
    void ClearSearch()
    {
        foreach (Tile t in tiles.Values)
        {
            t.prev = null;
            t.distance = int.MaxValue;
        }
    }
 
    // 다음 대기열로 교체
    void SwapReference(ref Queue<Tile> a, ref Queue<Tile> b)
    {
        Queue<Tile> temp = a;
        a = b;
        b = temp;
    }
 
    public void SelectTiles(List<Tile> tiles)
    {
        for (int i = tiles.Count - 1; i >= 0--i)
        {
            Renderer tileRender = tiles[i].GetComponent<Renderer>();
            tileRender.material.SetColor("_Color", selectedTileColor);
        }
    }
 
    public void DeSelectTiles(List<Tile> tiles)
    {
        for (int i = tiles.Count - 1; i >= 0--i)
        {
            Renderer tileRender = tiles[i].GetComponent<Renderer>();
            tileRender.material.SetColor("_Color", defaultTileColor);
        }
    }
}
 
cs

#코드 설명

내용이 너무 많아서.. 주석으로 설명한 내용은 생략했습니다.

◆ 코드 흐름

이동 가능 범위를 표시할 때

Search() 가 가장 먼저 호출됩니다.

Search() 에서 타일 초기화 -> 타일 검사 -> 이동 범위 내 존재하는 타일들을 반환합니다.

 

◆ public List<Tile> Search(Tile start, Func<Tile, Tile, bool> addTile)

타일을 검사하는 함수입니다.

매게 변수로 시작타일과 Func타입의 addTile을 받습니다.

Func 생소하실 분이 많으실 거에요. 저도 생소합니다.

Func 에 등록된 함수들을 매게변수로 받겠다는 의미입니다.

Func는 최대 16개의 함수를 등록시킬 수 있다고 합니다. 자세한 설명은 아래 링크를 참고하세요.

http://www.csharpstudy.com/Tip/Tip-Func.aspx

 

◆ Queue<Tile> checkNext = new Queue<Tile>();

Queue 자료구조는 입력된 순서대로 처리해야 하는 상황에 사용하면 최적화에 도움이 됩니다.

Queue은 맨뒤에 데이터를 계속 추가하고 맨 앞의 Head 에서만 데이터를 읽습니다. http://www.csharpstudy.com/DS/queue.aspx

 

◆ Tile t = checkNow.Dequeue();

 While문의 첫줄의 코드입니다.

Dequeue();는 Queue타입에서 맨앞 번호의 데이터를 반환하고 삭제합니다.

즉, While문이 반복 될 때마다 checkNow의 데이터는 점점 삭제되는 겁니다.

 

◆ for (int i = 0; i < 4; ++i)

While 안의 for 문에서 현재 검사중인 타일의 주변 타일 정보를 참조합니다.

 

◆ Tile next = GetTile(t.pos + dirs[i]);

GetTile() 에서는 매개변수로 받은 좌표에 타일이 존재 여부를  반환합니다.

 

◆ if (next == null || next.distance <= t.distance + 1)

null 일 경우는 타일이 없는 경우입니다.

next.distance 가 시작타일의 distance 보다 작거나 같다는 것은 이미 검사가 완료된 타일이라는 의미입니다. (* 전체 타일을 초기화할 때 distance 값을 최대값으로 넣습니다. )

 

◆ if (addTile(t, next))

델리게이트에 등록된 함수를 호출합니다.

addTile() 에 등록되는 함수는 아직 만들어지지 않은 Movement.ExpandSearch() 입니다.

 Movement.ExpandSearch() 는 이동하는 개체 (Hero 또는 Monster) 의 이동거리 안의 타일인지를 체크해서 bool 값으로 반환합니다.

 

◆ return retValue;

이동 범위 내에의 모든 타일을 retValue 에 담아서 반환합니다.

 


Movement

Assets\Script\View Model Componot 의 하위폴더로 Movement 폴더를 만듭니다.

Movement 폴더에 Movement.cs 를 만들고 아래 코드를 작성합니다.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
public abstract class Movement : MonoBehaviour
{
    public int range;           //이동 범위
    public int jumpHeight;      //점프 높이
    protected Unit unit;        //이동하는 개체(Monster or Hero)
    protected Transform jumper; 
 
    protected virtual void Awake()
    {
        unit = GetComponent<Unit>();
        jumper = transform.Find("Jumper");
    }
    
    public virtual List<Tile> GetTilesInRange(Board board)
    {
        List<Tile> retValue = board.Search(unit.tile, ExpandSearch);
        Filter(retValue);
 
        // 이동 범위 내 이동할 수 있는 타일들을 반환
        return retValue;
    }
 
    // Movement를 상속받는 클래스에서는
    // 해당 개체의 이동에 대해 강제적으로 정의하도록
    // 추상화 함수를 만들었습니다.
    public abstract IEnumerator Traverse(Tile tile);
 
    // 회전을 담당하는 함수
    protected virtual IEnumerator Turn(Directions dir)
    {
        // 다른 강좌에서 했던 내용입니다.
        // 차후 해당 강좌도 블로그에서 설명하겠습니다.
        // 각도 회전 등을 반환합니다.
        TransformLocalEulerTweener t = (TransformLocalEulerTweener)transform.RotateToLocal
            (
                dir.ToEuler(), 
                0.25f,
                EasingEquations.EaseInOutQuad
            );
 
        // 북쪽과 서쪽 사이를 회전할 때는 장치가 가장 효율적인 방법으로 회전하는 것처럼 보이도록
        // 예외를 만들어야 한다. (0 과 360도는 동일하게 본다.)
        if (Mathf.Approximately(t.startValue.y, 0f) && Mathf.Approximately(t.endValue.y, 270f))
        {
            t.startValue = new Vector3(t.startValue.x, 360f, t.startValue.z);
        }
           
        else if (Mathf.Approximately(t.startValue.y, 270&& Mathf.Approximately(t.endValue.y, 0))
        {
            t.endValue = new Vector3(t.startValue.x, 360f, t.startValue.z);
        }
           
        unit.dir = dir;
 
        while (t != null) yield return null;     
    }
 
 
    // 이동 범위 안의 타일인지 체크
    protected virtual bool ExpandSearch(Tile from, Tile to)
    {
        return (from.distance + 1<= range;
    }
 
    // 몬스터가 있거나 영웅이 있는 타일은 제외시킨다.
    protected virtual void Filter(List<Tile> tiles)
    {
        for (int i = tiles.Count - 1; i >= 0--i)
            if (tiles[i].content != null)
                tiles.RemoveAt(i);
    }
 
}
cs

#코드 설명

내용이 길어 주석으로 설명한 내용은 생략합니다.

쉬운 내용도 생략했습니다.

◆ 코드 흐름

유닛이 선택되면 GetTilesInRange() 가 호출되어 Filter() 에서 추가로 이동할 수 없는 타일이 제외되고 남은 타일을 반환합니다.

Traverse() 가 호출되어 이동이 시작되고 이동 중에 Traverse() 에서  Turn을 호출해서 유닛을 이동하는 방향으로 회전시킵니다.

 

◆ public virtual List<Tile> GetTilesInRange(Board board)

해당 함수를 호출하면 해당 Unit 이 이동할 수 있는 범위 내의 타일들을 List<Tile> 로 반환합니다.

이동 범위 내 타일 중에 Unit 이 있는 타일과 이동 불가능한 타일을 제외한 타일을 반환합니다.

 

◆ List<Tile> retValue = board.Search(unit.tile, ExpandSearch);

ExpandSearch은 해당 타일이 이동할 수 있는 타일인지 아닌지를 Bool 값으로 반환하는 함수로 board.Search에 매게변수로 전달하면서 Func<Tile, Tile, bool> 델리게이트에 등록됩니다.

 

◆ protected virtual IEnumerator Turn(Directions dir)

이 함수는 캐릭터의 회전을 담당하며, 회전하는 동안 다른 로직도 처리해야 하므로 코루틴으로 만든겁니다. 다른 강좌에서 설명한 코드가 사용되었기 때문에 상세한 설명은 다른 강좌에서 진행하겠습니다.

* PS *

RotateToLocal는 static으로 만든 사용자 정의 함수입니다.

마지막에 while (t != null) yield return null; 코드가 들어간 이유는 다른 강좌에서 만든 코드에서 캐릭터의 회전을 매프레임마다 갱신시켜주기 때문에 해당 코루틴의 반복은 중지시킨겁니다.

 


Walk Movement

Assets\Script\View Model Componot\Movement 에 C#스크립트 WalkMovement.cs 를 만들고 아래 코드를 작성해주세요.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class WalkMovement : Movement
{
    protected override bool ExpandSearch(Tile from, Tile to)
    {
        // 점프 높이보다 두 타일 사이의 높이가 높으면 건너띕니다.
        if ((Mathf.Abs(from.height - to.height) > jumpHeight))
            return false;
 
        // 타일에 다른 대상이 있으면 건너 뛴다.
        if (to.content != null)
            return false;
 
        // base.ExpandSearch에서는 이동거리를 체크한다.
        return base.ExpandSearch(from, to);
    }
 
    public override IEnumerator Traverse(Tile tile)
    {
        // 유닛이 머물고 있는 타일 정보를 이동하려는 위치로 갱신한다.
        unit.Place(tile);
        
 
        List<Tile> targets = new List<Tile>();
        while (tile != null)
        {
            // targets의 0번에 tile을 추가한다.
            // targets에 있던 데이터들은 한칸씩 뒤로 밀린다.
            targets.Insert(0, tile);
            tile = tile.prev;
        }
 
 
        // 연속해서 각 지점으로 이동.
        for (int i = 1; i < targets.Count; ++i)
        {
            // targets[target.count-1] 이 최종 목적지이다.
            Tile from = targets[i - 1];
            Tile to = targets[i];
 
            // from 이 to를 바라보는 방향을 enum 값으로 반환한다.
            Directions dir = from.GetDirection(to);
 
            if (unit.dir != dir)
            {
                // unit가 바라보는 방향을 dir 회전시킨다.
                yield return StartCoroutine(Turn(dir));
            }
                
            if (from.height == to.height)
            {
                // 높이가 같으면 걷고
                yield return StartCoroutine(Walk(to));
            }
                
            else
            {
                // 높이가 다르면 뛴다.
                yield return StartCoroutine(Jump(to));
            }
        }
        yield return null;
    }
 
    IEnumerator Walk(Tile target)
    {
        Tweener tweener = transform.MoveTo(target.center, 0.5f, EasingEquations.Linear);
        while (tweener != null)
            yield return null;
    }
    IEnumerator Jump(Tile to)
    {
        Tweener tweener = transform.MoveTo(to.center, 0.5f, EasingEquations.Linear);
 
        Vector3 stepHeightVec = new Vector3(0, Tile.stepHeight * 2f, 0);
        float destinationTweener = tweener.easingControl.duration / 2f;
 
        Tweener t2 
        = jumper.MoveToLocal(stepHeightVec, destinationTweener, EasingEquations.EaseOutQuad);
        t2.easingControl.loopCount = 1;
        t2.easingControl.loopType = EasingControl.LoopType.PingPong;
 
        while (tweener != null)
            yield return null;
    }
}
 
cs

#코드 설명

Movement를 상속받았습니다.

IEnumerator Walk(Tile target) 와 IEnumerator Jump(Tile to) 는 다른 강좌에서 진행한 코드가 추가되었습니다. 여기서 설명하기엔 주제가 벗어나니 차후에 강좌를 추가해서 설명하겠습니다. 개체의 이동에 대한 내용들입니다.

 


Fly Move

Assets\Script\View Model Componot\Movement 에 C# 스크립트 FlyMovement.cs 만들고 아래 코드를 작성합니다.

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
36
37
38
39
40
41
42
43
44
45
46
47
using UnityEngine;
using System.Collections;
public class FlyMovement : Movement
{
    public override IEnumerator Traverse(Tile tile)
    {
        // 시작 타일과 대상 타일의 거리를 저장합니다.
        float xMathfPow = Mathf.Pow(tile.pos.x - unit.tile.pos.x, 2);
        float yMathfPow = Mathf.Pow(tile.pos.y - unit.tile.pos.y, 2);
 
        float dist = Mathf.Sqrt(xMathfPow + yMathfPow);
        unit.Place(tile);
 
        // 지상 타일과 부딪치지 않을 정도의 높이를 지정
        float y = Tile.stepHeight * 10;
        float duration = (y - jumper.position.y) * 0.5f;
        Vector3 moveToPosition = new Vector3(0, y, 0);
        
        Tweener tweener 
            = jumper.MoveToLocal(moveToPosition, duration, EasingEquations.EaseInOutQuad);
 
        while (tweener != null) yield return null;
 
        // 날아가는 방향을 바라보게 만든다.
        Directions dir;
        Vector3 toTile = (tile.center - transform.position);

        // 각도가 많이 꺽인 쪽이 이동하는 방향이다. (생각해보면 그렇다.)

        if (Mathf.Abs(toTile.x) > Mathf.Abs(toTile.z))
            dir = toTile.x > 0 ? Directions.East : Directions.West;
        else
            dir = toTile.z > 0 ? Directions.North : Directions.South;
        yield return StartCoroutine(Turn(dir));
 
 
        // 이동시킨다.
        duration = dist * 0.5f;
        tweener = transform.MoveTo(tile.center, duration, EasingEquations.EaseInOutQuad);
        while (tweener != null) yield return null;
        
        // 착륙
        duration = (y - tile.center.y) * 0.5f;
        tweener = jumper.MoveToLocal(Vector3.zero, 0.5f, EasingEquations.EaseInOutQuad);
        while (tweener != null)
            yield return null;
    }
}
 

cs

#코드 설명

다른 강좌에서 만든 스크립트를 재활용한건 설명에서 제외하겠습니다.

Mathf.Pow 는 제곱, Mathf.Sqrt 는 루트를 하는 함수입니다.

두 점 사이의 거리를 구하는 공식입니다.

MoveTo 어쩌고 관련은 전부 다른 강좌에서 Static 으로 만든 함수들입니다.

 


Teleport Movement

Assets\Script\View Model Componot\Movement 에 C# 스크립트 TeleportMovement를 만들고 아래 내용을 입력하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEngine;
using System.Collections;
public class TeleportMovement : Movement
{
    public override IEnumerator Traverse(Tile tile)
    {
        // 목적지를 해당 Unit 머물고 있는 타일로 갱신
        unit.Place(tile);
 
        // 순간이동할 때 회전과 스케일 값 변화로
        // 연출을 하네요.
        Tweener spin = jumper.RotateToLocal(new Vector3(03600), 0.5f, EasingEquations.EaseInOutQuad);
        spin.easingControl.loopCount = 1;
        spin.easingControl.loopType = EasingControl.LoopType.PingPong;
        Tweener shrink = transform.ScaleTo(Vector3.zero, 0.5f, EasingEquations.EaseInBack);
 
        while (shrink != null) yield return null;
 
        // 목적지로 이동 완료.
        transform.position = tile.center;
        Tweener grow = transform.ScaleTo(Vector3.one, 0.5f, EasingEquations.EaseOutBack);
        while (grow != null) yield return null;
    }
}
cs

#코드 설명

목적지로 바로 이동하는 코드이며, 순간이동하면서 연출을 진행합니다.

연출 관련 코드는 다른 강좌에서 진행한 스크립트라서 설명하지 않습니다.

 


Battle Controller

Assets\Scripts\Controller\BattleController 에 아래 변수를 전역변수로 추가합니다. 테스트를 위한 임시 변수입니다.

1
2
3
4
    // 임시 코드
    public GameObject heroPrefab;
    public Unit currentUnit;
    public Tile currentTile { get { return board.GetTile(pos); } }
cs

 

유니티 에디터에서 Hierarchy뷰의 BattleController를 선택하고 Inspector 뷰에서 BattleController.HeroPrefab에 Assets\Prefab\Hero를 연결합니다.

 


InitBattleState

Assets\Scripts\Controller\BattleController\Battle States\InitBattleState.cs 에 아래 코드를 추가합니다. 영웅을 소환하고 영웅의 이동방식을 설정하는 임시 코드입니다.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using UnityEngine;
using System.Collections;
public class InitBattleState : BattleState
{
    public override void Enter()
    {
        base.Enter();
        StartCoroutine(Init());
    }
    IEnumerator Init()
    {
        // 타일맵을 로드한다.
        board.Load(levelData);
 
        // 현재 선택된 타일인디게이터(게임오브젝트) 의 좌표를 설정한다.
        Point p = new Point((int)levelData.tiles[0].x, (int)levelData.tiles[0].z);
        SelectTile(p);
 
        // 임시 코드(영웅을 소환)
        SpawnTestUnits(); 
 
        yield return null;
 
        // 현재 상태를 SelectUnitState로 변경한다.
        owner.ChangeState<SelectUnitState>();
    }
 
    // 임시 함수
    void SpawnTestUnits()
    {
        System.Type[] components 
            = new System.Type[] 
            { typeof(WalkMovement), typeof(FlyMovement), typeof(TeleportMovement) };
 
 
        for (int i = 0; i < 3++i)
        {
            // 영웅을 생성하고
            GameObject instance = Instantiate(owner.heroPrefab) as GameObject;
 
            // 해당 영웅의 시작 좌표를 부여하고.
            Point p = new Point((int)levelData.tiles[i].x, (int)levelData.tiles[i].z);
            Unit unit = instance.GetComponent<Unit>();
            unit.Place(board.GetTile(p));
            unit.Match();
 
            // 영웅의 이동 방식을 넣고
            Movement m = instance.AddComponent(components[i]) as Movement;
 
            // 이동범위와 점프 높이를 설정한다.
            m.range = 5;
            m.jumpHeight = 1;
        }
    }
}
cs

 


Select Unit State

선택한 대상을 움직일 수 있도록 만드는 임시 클래스를 만들겁니다. 실제 게임에서는 턴마다 진행되도록 구현될 예정입니다.

Assets\Scripts\Controller\BattleController\Battle States에 C# 스크립트 SelectUnitState.cs를 만들고 아래 코드를 작성하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using System.Collections;
public class SelectUnitState : BattleState
{
    protected override void OnMove(object sender, InfoEventArgs<Point> e)
    {
        SelectTile(e.info + pos);
    }
 
    protected override void OnFire(object sender, InfoEventArgs<int> e)
    {
        GameObject content = owner.currentTile.content;
        if (content != null)
        {
            owner.currentUnit = content.GetComponent<Unit>();
            owner.ChangeState<MoveTargetState>();
        }
    }
}
cs

#코드 설명

마우스를 클릭하면 현재 TileSelectIndicator가 있는 타일에 Unit이 있는지 체크하고 있다면 해당 Unit을 BattleController.currentUnit 로 저장하고 MoveTargetState 로 상태를 변경합니다.

 


Move Target State

Assets\Scripts\Controller\BattleController\Battle States\MoveTargetState의 내용을 수정합니다.

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
36
37
38
39
40
41
42
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
public class MoveTargetState : BattleState
{
    List<Tile> tiles;
 
    public override void Enter()
    {
        base.Enter();
        
        // 사용자가 Unit을 선택하면 MoveTargetState 상태가 되어
        // 이동 가능한 타일들의 색상을 변경한다.
        Movement mover = owner.currentUnit.GetComponent<Movement>();
        tiles = mover.GetTilesInRange(board);
        board.SelectTiles(tiles);
    }
 
    public override void Exit()
    {
        base.Exit();
 
        // MoveTargetState 상태가 종료되면
        // 변경된 타일들의 색상을 원래대로 변경한다.
        board.DeSelectTiles(tiles);
        tiles = null;
    }
 
 
    protected override void OnMove(object sender, InfoEventArgs<Point> e)
    {
        SelectTile(e.info + pos);
    }
 
    protected override void OnFire(object sender, InfoEventArgs<int> e)
    {
        // 클릭한 타일로 이동시킵니다.
        if (tiles.Contains(owner.currentTile))
            owner.ChangeState<MoveSequenceState>();
    }
}
cs

#코드 설명

이동 가능한 영역의 색상이 변경된 상태에서 마우스를 클릭하면 MoveSequenceState 상태로 변경되어 이동이 시작됩니다. 

 


Move Sequence State

Assets\Scripts\Controller\BattleController\Battle States에 C# 스크립트 MoveSequenceState.cs를 만들고 아래 코드를 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using System.Collections;
public class MoveSequenceState : BattleState
{
    public override void Enter()
    {
        base.Enter();
        StartCoroutine("Sequence");
    }
 
    IEnumerator Sequence()
    {
        Movement m = owner.currentUnit.GetComponent<Movement>();
 
        // 해당 유닛의 Move 방식에 따라 이동시킵니다.
        yield return StartCoroutine(m.Traverse(owner.currentTile));
 
        // 이동이 완료되면 SelectUnitState 상태로 변경합니다.
        owner.ChangeState<SelectUnitState>();
    }
}
cs

 


테스트

방향키로 인디케이터를 이동 시킨 후 마우스 클릭 또는 좌측 컨트롤키를 누르면 이동이 시작됩니다.

 


작성 후기

솔직히 말씀드리면 이번 강좌는 제가 가진 지식의 한계를 뛰어넘어야만 가능한 강자였습니다.

내용을 이해하고 블로그에 담는데에 10시간 걸린 듯 싶습니다.

원문이 영어로 된 텍스트로 이미지 몇장없이 설명하는 방식이라 내용이 헷갈려서 더욱 힘들었네요.

(추측하는건데.. 원문 작성자도 적다가 나중에 좀 지친거같습니다. 설명이 점점 빈약해지는듯한..)

 

이번 강좌를 마무리하고 나니 제 자신의 한계를 넘어 초사이언이 된 기분이 듭니다.

더불어 영어실력도 늘었네요 하하하하핳......

이 강좌를 소개해준 간디님과 이 강좌의 원문을 만들어주신 LiguidFire님 감사합니다.

좀더 힘내서 마무리 짓겠습니다.

 

이번 강좌의 아쉬운 점은

제가 얻은 지식만큼 표현하지 못한 것이 너무 아쉽습니다. 

지식의 한계를 벗어난 모르는 내용을 이해하면서 동시에 해당 내용을 설명하는 글을 적는게

꽤 난이도가 높네요.

원문의 라이센스를 보니 큰 범위에서 사용해도 되더군요..

나중에 마이크 생기면 유투브로 상세히 설명해드릴게요.