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

오늘 공부할 내용

모든 상태를 처리할 상태머신을 만들겁니다.

처음에는 초기화를 담당하는 상태를 만들고 입력 컨트롤러의 이벤트를 사용하여 타일 선택 표시기를 보드 주위로 움직일 수 있는 다른 상태를 추가 할 것입니다.

PS) Inscope RPG 강좌의 상태머신 강좌 내용을 정리해놨습니다. 여기서 소개하는 방식과 다르니, 그것도 함께 참고해서 보시면 더 좋을 듯 싶습니다.

http://mrbinggrae.tistory.com/entry/InScope-RPG-30-Enemy-States

 


State.cs 생성 및 작성

Assets\Scripts\Common 에 State Machine 폴더를 추가하세요. 해당 폴더에 C# 스크립트 State.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
using UnityEngine;
using System.Collections;
 
public abstract class State : MonoBehaviour
{
 
    // 상태가 시작될 때 호출한다.
    public virtual void Enter()
    {
        AddListeners();
    }
 
    // 상태가 종료될 때 호출한다.
    public virtual void Exit()
    {
        RemoveListeners();
    }
 
    // 안전하게 Listener를 제거하기 위한 용도.
    protected virtual void OnDestroy()
    {
        RemoveListeners();
    }
 
 
    // 이벤트핸들러에 이벤트를 추가한다.
    protected virtual void AddListeners()
    {
 
    }
 
    // 이벤트핸들러에 이벤트를 제거한다.
    protected virtual void RemoveListeners()
    {
 
    }
}
 
cs
#코드설명

다른 클래스에서 상속받기 위해 만든 클래스입니다. 골격같은 존재로 추상화 클래스입니다.

상태가 변경될 때 이벤트 핸들러에 이벤트를 등록시키거나, 해제하는 함수들이 있습니다.


StateMachine.cs 생성 및 작성

Assets\Scripts\Common\State Machine 에 C# 스크립트 StateMachine.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
using UnityEngine;
using System.Collections;
public class StateMachine : MonoBehaviour
{
    // 현재 타입을 저장하거나, 불러올 때 호출되는
    // _currentState 의 속성
    public virtual State CurrentState
    {
        get { return _currentState; }
        set { Transition(value); }
    }
    protected State _currentState;
 
    protected bool _inTransition;
 
    // 변경하려는 상태가 해당 게임오브젝트에 컴포넌트로 있는지 체크한다.
    public virtual T GetState<T>() where T : State
    {
        T target = GetComponent<T>();
 
        // 변경하려는 State가 게임오브젝트 없으면
        // 추가시킨다.
        if (target == null)
            target = gameObject.AddComponent<T>();
 
        return target;
    }
 
 
    // 상태를 변경시킬 때 호출된다.
    public virtual void ChangeState<T>() where T : State
    {
        CurrentState = GetState<T>();
    }
 
 
    protected virtual void Transition(State value)
    {
        // 현재 상태와 변경하려는 상태가 같은지 확인
        // 또는 상태가 변경 중인지 확인.
        if (_currentState == value || _inTransition) return;
        _inTransition = true;
 
 
        // 상태를 변경할 때 현재 상태의 State.Exit()를 호출한다.
        if (_currentState != null)  _currentState.Exit();
 
        // 현재 상태를 변경하고
        _currentState = value;
 
        // 변경된 상태의 State.Enter()를 호출한다.
        if (_currentState != null)  _currentState.Enter();
 
        // 변경이 완료되면 _inTransition false로 변경한다.
        _inTransition = false;
    }
}
cs
 

#코드 설명

상태를 변경될 때 아래의 순서로 실행됩니다.

1) ChangeState()가 호출되고

2) GetState() 에서 변경하려는 State를 상속받은 클래스가 게임오브젝트에 컴포넌트로 추가되어있는지 확인합니다. 없으면 추가한 뒤 해당 State를 반환합니다.

3) CurrentState 속성에서 GetState() 에서 반환된 State를 매개변수로 Transition(State value) 를 호출합니다.

4) Transition(State value) 에서 현재 상태의 Exit 를 호출시키고, 변경될 상태의 Enter()를 호출시켜 이벤트헨들러에 이벤트 등록/해제 등의 처리를 합니다.


Battle Controller

Assets\Scripts\Controller 에 Battle States폴더를 만들고 C# 스크립트 Battle Controller.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 BattleController : StateMachine
{
    // 메인카메라의 컴포넌트
    // 카메라가 tileSelectionIndicator를 따라다니게 한다.
    public CameraRig cameraRig;
 
    // LevelData의 정보를 토대로 타일맵을 불러오는 클래스
    public Board board;
 
    // 타일맵의 타일들의 좌표 정보가 저장된 LeveData
    public LevelData levelData;
 
    // 선택된 타일 인디게이터
    public Transform tileSelectionIndicator;
 
    // tileSelectionIndicator의 좌표를 표시한다.
    public Point pos;
 
    void Start()
    {
        // StateMachine.cs의 ChangeState를 호출해서 InitBattleState로 상태를 변경시킨다.
        ChangeState<InitBattleState>();
    }
}
cs

#코드설명

배틀컨트롤러는 씬에서 사용되는 개체들을 변수로 가지고 있으며, 게임이 시작되면 상태머신에 InitBattleState로 상태를 변경하라고 호출합니다.


Battle State

Assets\Scripts\Controller\Battle States 에 C#스크립트 BattleState.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;
 
 
public abstract class BattleState : State
{
    protected BattleController owner;
 
    // 아래 변수들은 BattleController가 가지고 있는 변수들입니다.
    public CameraRig cameraRig { get { return owner.cameraRig; } }
    public Board board { get { return owner.board; } }
    public LevelData levelData { get { return owner.levelData; } }
    public Transform tileSelectionIndicator { get { return owner.tileSelectionIndicator; } }
    public Point pos { get { return owner.pos; } set { owner.pos = value; } }
 
 
    protected virtual void Awake()
    {
        owner = GetComponent<BattleController>();
    }
 
    // InputController의 moveEvent와 fireEvent 핸들러에 함수를
    // 등록시킵니다.
    // AddListeners() 는 해당 State 상태로 변경될 때 호출됩니다.
    protected override void AddListeners()
    {
        InputController.moveEvent += OnMove;
        InputController.fireEvent += OnFire;
    }
 
    protected override void RemoveListeners()
    {
        InputController.moveEvent -= OnMove;
        InputController.fireEvent -= OnFire;
    }
 
 
    protected virtual void OnMove(object sender, InfoEventArgs<Point> e)
    {
 
    }
 
    protected virtual void OnFire(object sender, InfoEventArgs<int> e)
    {
 
    }
 
    // 선택된 타일 인디케이터(게임오브젝트)의 위치를 변경합니다.
    protected virtual void SelectTile(Point p)
    {
        if (pos == p || !board.tiles.ContainsKey(p))
            return;
        pos = p;
        tileSelectionIndicator.localPosition = board.tiles[p].center;
    }
}
cs

#코드설명

BattleState 에서는 이벤트핸들러에 함수를 연결시키거나, 해제하고 선택된 타일의 위치를 변경시키는 역활을 합니다.

BattleController의 Start() 함수가 호출되면 BattleState가 현재 상태가 되어 AddListeners() 가 호출됩니다.


Board

Assets\Scripts\View Model Component 에 C# 스크립트 Board.cs를 추가하고 아래 코드를 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections.Generic;
public class Board : MonoBehaviour
{
    [SerializeField] GameObject tilePrefab;
    public Dictionary<Point, Tile> tiles = new Dictionary<Point, Tile>();
    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);
        }
    }
}
cs

#코드 설명

우리가 만든 LevelData를 토대로 필드에 타일들을 불러오는 역활을 하는 클래스입니다.


CameraRig

Assets\Scripts\View Model Component 에 C# 스크립트 CameraRig.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 CameraRig : MonoBehaviour 
{
  public float speed = 3f;
  public Transform follow;
  Transform _transform;
  
  void Awake ()
  {
    _transform = transform;
  }
  
  void Update ()
  {
    if (follow)
      _transform.position = Vector3.Lerp(_transform.position, follow.position, speed * Time.deltaTime);
  }
}
cs

#코드 설명

카메라가 follow에 등록된 게임오브젝트를 따라다니는 코드입니다.

Vector3.Lerp(시작값, 목표지점, t) t에 0.5 를 입력하면 시작값과 목표지점의 중간지점으로 이동한다.


Init Battle State

Assets\Scripts\Controller\Battle States 에 C#스크립트 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
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);
        yield return null;
 
        // 현재 상태를 MoveTargetState로 변경한다.
        owner.ChangeState<MoveTargetState>();
    }
}
cs

# 코드 설명

InitBattleState 는 BattleState를 상속받았으며 BattleState는 State를 상속받은 상태입니다. InitBattleState는 배틀 상태의 설정값들을 세팅해주는 역활을 합니다.

BattleController의 Start() 함수가 호출되면 BattleState상태가 되어 BattleState.Enter()가 호출된 후 InitBattleState.Enter() 가 호출됩니다.


MoveTargetState

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

1
2
3
4
5
6
7
8
9
10
using UnityEngine;
using System.Collections;
public class MoveTargetState : BattleState
{
    protected override void OnMove(object sender, InfoEventArgs<Point> e)
    {
        SelectTile(e.info + pos);
    }
}
 
cs

#코드 설명

현재 선택된 타일 인디케이터의 위치를 변경시키는 코드입니다.


Scene Setup

Assets\Scenes 폴더에 Battle 이라는 씬을 만듭니다.

Battle 씬을 더블 클릭한 다음 Hierarchy뷰에 빈게임오브젝트를 생성하고 이름을 BattleController 로 설정합니다. BattleController의 Position.X , Y, Z 를 0으로 초기화시킵니다.

BattleController 에 BattleController.cs를 컴포넌트로 추가합니다.

BattleController 의 자식오브젝트로 빈게임오브젝트를 추가하고 이름을 Camera Rig로 설정합니다.

Camera Rig 오브젝트에 CameraRig.cs와 InputController.cs를 컴포넌트로 추가합니다.

CameraRig 컴포넌트의 Follow에 Assets\Prefabs\TileSelectIndicator를 연결합니다.

CameraRig 게임오브젝트의 자식오브젝트로 빈게임오브젝트를 만들고 이름을 Heading로 설정합니다.

Heading의 Rotation.Y 값을 45도로 설정합니다.

Heading 게임오브젝트의 자식오브젝트로 빈게임오브젝트를 만들고 이름을 Pitch로 설정합니다.

Pitch의 Position.X 값을 35.264 로 변경합니다.

Picth 게임오브젝트의 자식오브젝트로 MainCamera를 이동시킵니다.

MainCamera의 Projection을 Orthographic으로 변경하고 Size를 5로 설정합니다.

BattleController 의 자식오브젝트로 빈게임오브젝트를 추가하고 이름을 Board로 설정합니다.

Board에 Board.cs를 컴포넌트로 추가하고 TilePrefab에 Assets\Prefabs\Tile을 연결합니다.

BattleController 의 자식오브젝트로 Assets\Prefabs\TileSelectIndicator를 드래그 앤 드롭합니다.

BattleController의 BattleController.cs 컴포넌트의 변수들을 모두 연결해줍니다.


테스트하기

유니티를 재생하면 레벨데이타에 저장된 타일들이 불러오고 방향키를 누를 때마다 선택된 타일 인디케이터의 위치가 변경되는 것을 확인할 수 있습니다.

 


작성후기

상태머신이라는 디자인 패턴에 대해서 다뤘습니다.

처음에는 굉장히 복잡하고 어렵습니다.

하지만, 이렇게 코드를 작성한 이유는 객체지향적 설계를 통한 최적화된 코드를 위해서입니다. 만줄짜리 코드 한번 작성하고 나면 이걸 왜 해야하는지 이해가 될겁니다.

처음에는 어렵지만 한번 익숙해지면 이거만큼 편해지는 것이 없는 것이 바로 디자인패턴인듯 싶습니다. 각 클래스 별로 어떤 역활을 담당하고 있는지를 파악해보세요.

이 강좌가 어렵습니다. 입문, 초보 분께는 추천하고 싶지 않네요. 머리만 아파질 뿐..

최소 자기가 생각하는 기능은 개발세발 코딩이라도 만들 수 있는 분께 추천합니다.