이는 누군가를 위한 학습 관련 블로그가 아닌 개인의 평범한 공부일지로써 틀리거나 애매모호한 부분이 있을 수 있습니다. 만약 틀린 부분을 발견시 댓글을 달아주신다면 감사하겠습니다.
1. Instantiating Prefabs
Prefabs in Code | Instantiation | Spawner Objects | Good Coding Practice |
Prefabs
우리는 게임 오브젝트를 코드상에서 동적으로 생성시키고 싶을때 Prefab을 이용해야 한다. Prefab (프리팹)은 우리가 게임 오브젝트를 복사하여 여러개 만들수 있는 포괄적인 오브젝트로써 오브젝트의 새로운 Instance를 생성시키면서 Prefab들을 Instantiate하여 (말 그대로 인스턴스화 하여 씬에서 특정 Game Object을 여러개로 복사시켜주는 과정) 여러개의 게임 오브젝트를 복사하는 것이 가능하다. 이때 Prefab을 생성하는 과정은 다음과 같다.
- 원하는 형태로 GameObject를 꾸며준다 (적 캐릭터, 아이템 등)
- Hierarchy View의 GameObject를 Project View로 드래그 & 드롭한다.
- Hierarchy View에 있는 GameObject를 삭제한다.
Instantiation
- Slide Subtitle
Instantiate(GameObject original);
-> original GameObject (Prefab)을 복제해서 생성. (복제되는 Object의 모든 Component 정보가 원본과 완전히 동일)
Prefab을 인스턴스화 (instantiate)을 하는 방법은 Instantiate() 이라는 Method를 쓰는 방법이 있다. 이때 아래와 같이 public으로 GameObject Class를 prefab이라는 Object으로 바꿔주면 유니티 인스펙터 상에서 직접 prefab으로써 복사하려는 Game Object를 선택할 수 있다. 말 그대로 샘플로 불러오려는 GameObject을 코드상으로 Class를 object (객체)로 바꿔준 다음 그 샘플을 Instantiate() 복사한 여러개의 GameObject들을 GameObject object으로 Start()안에 새롭게 선언된 object (객체)로 저장해주면 된다.
public GameObject prefab; // GameObject중 prefab이라는 변수로 선언하는데 이 GameObject은 public 접근자를 가지고 있어 유저가 직접 선택 가능
void Start()
{
// GameObject이라는 Class를 object이라는 object (객체)로 만든 다음 Instantiate()을 통해 prefab이라는 object에 해당하는 GameObject을 생성
// 복사된 개체는 GameObject object으로 선언된 object이라는 object (객체)로써 저장되기 시작한다.
GameObject object = Instantiate (prefab);
}
이렇게 코드를 작성하면 다음과 같은 현상이 일어난다.
그 다음 다음과 같이 Project View로 생성한 Player의 Prefab을 Object Spawner 스크립트 안 prefab이라는 파라미터로 드래그 & 드롭 해주어야 한다. 이때 내가 자주 했던 실수는 드래그 & 드롭을 해주지 않고 그대로 파라미터를 선택하여 Scene View에만 있는 GameObject만 선택하여 Prefab으로 설정하지 않은 실수를 저질렀다.
그렇게 되면 이와 같이 Scene View에서 나타나지 않았던 Player가 Game을 실행했을때 복사되어 Player (Clone)이라는 이름으로 생성되는 것을 알 수 있다.
- Prefab Type
이때 GameObject 뿐만이 아닌 특정 Componenet와 함께 Prefab으로 복사하여 Instantiate해주는 방법이 있는데 이는 다음과 같이 코드로써 작성 할 수 있다. 이 때 알 수 있는 사실은 GameObject type으로 prefab을 선언해주지 않고도 Instantiate을 활용하면 GameObject이 (Component와 함께) 저절로 복제되는 것을 알 수 있다.
// 우선 Rigidbody라는 Class (Component)를 찾아 prefabWithRigidy라고 Object로 바꿈
public Rigidbody prefabWithRigidbody;
void Start()
{
// Instantiate object (GameObject을 복사해준 다음)
// and get rigidbody component (rigidbody component를 기존에 있던 Prefab의 성질과 동일하게 같이 복사하여라)
Rigidbody rigidbody = Instantiate (prefabWithRigidbody);
}
위와 같은 코드가 아니면 아래와 같이 작성하여 GameObject을 사용하고도 불러오려는 Component를 부여해줄 수 있기는 하다.
// GameObject중 prefab이라는 변수로 선언하는데 이 GameObject은 public 접근자를 가지고 있어 유저가 직접 선택 가능
public GameObject prefab;
void Start ()
{
// instantiate object (prefab 파라미터로 등록된 GameObject prefab을 복사하고 그 clone들을 object이라는 변수로 저정하여라
GameObject object = Instantiate (prefab);
// get rigidbody component (복제된 클론들에게 Rigidbody component를 부여하고 rigidbody라는 변수로 저장하여라)
// 이는 나중에 변수 rigidbody로써 Rigidbody component안의 property들을 코드로 수정 가능하게 함
Rigidbody rigidbody = object.GetComponent<Rigidbody>();
}
만일 Component가 아니고 작성한 Script (Class) 즉 코드를 통해 움직이게 만들었거나 어떠한 행동을 하게끔 만드는 Prefab들을 복제하고 싶다면 다음과 같이 또 코드를 작성할 수 있다. 이는 Component로 type을 선언해주는 방식과 동일하다.
// Chase를 가진 GameObject중 prefab이라는 변수로 선언하는데 이 GameObject은 public 접근자를 가지고 있어 유저가 직접 선택 가능
public Chase chaseCarPrefab;
void Start ()
{
// Instantiate object (GameObject을 복사해준 다음)
// and get Chase script (behaviour) (Chase script를 기존에 있던 Prefab의 성질과 동일하게 같이 복사하여라)
Chase chaseCar = Instantiate (chaseCarPrefab);
}
이때 Instantiate()을 시도하기 전 알아두면 좋은 코딩 습관은 다음과 같다.
- prefab의 type을 더욱 세부적으로 그리고 구체적으로 작성하면 (GameObject이 아닌 Rigidbody 처럼: 어차피 Rigidbody로 선언해도 GameObject은 저절로 복사되기 때문) 기존 오리지널 prefab이 가지고 있ㄴ느 component 들도 같이 복사시킬 수 있어 더욱 안전하게 에러없이 코드를 작성할 수 있다.
- public 접근자로 직접 유저가 복제하려는 prefab을 설정하기보다는 직접적으로 스크립트상에서 복제하려는 GameObject가 뭔지 알아채게 코드를 작성하는 것이 좋고 또한 prefab에 소속되어 있는 component들도 저절로 복제되게끔 세팅해주어야 한다.
Instantiation을 하고 나면은 복제된 GameObject (Clone)을 또 새로운 variable로 지정하여 clone만의 이름이나 포지션, 회전값등의 속성들을 코드상으로 바꿔줄 수 있다.
Chase car = Instantiate (chaseCarPrefab);
// name the car (car가 Chase라는 type으로 선언되었으므로 car.name이 아닌 car.gameObject.name으로 해야 더욱 정확하게 코드를 작성하는 습관을 갖게 된다.
car.gameObject.name = "Car" + nCars;
// set its position & rotation (Chase Class안에 들어있는 포지션, 로테이션 값을 새로 설정)
car.transform.position = Vector3.zero;
car.transform.rotation = Queaternion.identity;
// set its target (Chase Class안에 들어잇는 target값을 새롭게 설정하여 clone에 부여)
car.target = target;
이 때 유의해야 할 점은 Instantiate()이 Start() Method전에 입력되었다면 Instantiate() 즉 복제 과정이 끝나기 전까지는 Start()안에 있는 이벤트가 실행되지 않는다. 이는 Java나 C#에서 보이는 일반적인 Object의 생성과정하고 다르다.
이와 같은 방식이 일반적으로 작성하는 코드의 방식이라면 (Constructor 방식)
void SpawnWithConstructor ()
{
Object obj1 = new Object (); // Object Class를 현재 Class에서 obj1이라는 객체로 인스턴스화
public Object () // obj 1 constructor
{
// initialise obj1 // obj1의 속성값들을 여기서 변경
}
Object obj2 = new Object (); // Object Class를 현재 Class에서 obj2이라는 객체로 인스턴스화
public Object () // obj2 constructor
{
// initialise obj2 // obj1의 속성값들을 여기서 변경
}
// do other things
return;
}
Instantiate()이 들어간 유니티에서의 코드방식은 다음과 같다. (Instantiation 방식)
// prefab에 입력된 GameObject들을 Instantiate()을 통해 복제하여라. 이 과정이 끝나기 전까지는 다음 Event method가 실행되지 않는다.
void SpawnWithInstantiate ()
{
Object obj1 = Instantiate (prefab);
Object obj2 = Instantiate (prefab);
// do other things
return;
}
// 윗 과정이 끝나면 obj1의 속성값들을 부여
void Start() // obj1 Start event
{
// initialise obj1
}
// 윗 과정이 끝나면 obj2의 속성값들을 부여
void Start() // obj2 Start event
{
// initialise obj2
}
Spawner Object
앞서 이미지 설명에서도 말했듯이 Instantiate()이 들어가는 스크립트가 포함된 SpawnerObject은 Empty Object으로 설정해주는 것이 옳다. 이때 Spanwer Object의 transform의 속성들을 바꾸면 prefab으로부터 복제되는 모든 Object들의 위치/회전/크기 값들을 모두 코드상에서 한번에 변경해줄 수 있다. 이를 코드로 작성하면 다음과 같다.
public class CarSpawner : MonoBehaviour
{
public Chase chaseCarPrefab;
public Transform target; // 바꿀 target 값
public int nCars = 100;
void Start ()
{
for (int i = 0; i < nCars; i++) // i 값이 0부터 nCar 값 (100)까지 1씩 늘어날 동안
{
// Chase Script를 포함하여 chaseCarPrefab으로 등록된 prefab을 복사하여라
Chase car = Instantiate (chaseCarPrefab);
// 이때 복제된 GameObject의 이름은 "Car + i(로 지정된 숫자)"라고 명명하고
car.gameObject.name = "Car" + i;
// Chase Class안에 있는 target값을 새롭게 target으로 바꾸어라 이때 바꿔진 target값은 유저가 직접 transform 값으로 지정할 수 있다.
car.target = target;
// set the car as a child of this object in the hierarchy
// 복제된 car GameObject의 transform 속성을 지금 현재 이 CarSpanwer이라는 이름의 스크립트가 들어간 Spanwer Object (Empty)의 trasnform값과 동일하게 하여라.
// 이때 복제된 car GameObject Clone들이 CarSpawner Object의 child로써 Hierarchy view에 표시된다.
// 이때 SpanwerObject (Empty GameObject)은 복제된 클론들의 부모로써 역할함으로 Parent Object라고 불리게 된다.
car.transform.parent = transform;
}
}
}
이때 알수 있는 사실은 맨 아래 적혀진 코드 car.transform.parent = transform; 이와 같이 작성하면 복제된 clone들이 hierarchy상에서 spawner의 자식으로 설정되는 것을 알 수 있다.
Good Coding Practices
- 항상 Prefab을 선언할때 variable의 이름이 ~Prefab으로 끝나야 하게 작성해야 한다.
- 앞서 언급했지만 prefab을 선언할 때 GameObject이 아닌 특정 component나 Behaviour (Script)를 type으로써 선언해주면 더욱 좋다. (이는 GameObject을 복제해줄 뿐만 아니라 선언한 Component나 Behaviour도 자동으로 복제하여 GameObject에 부여해주기 때문이다.)
- 만약 Prefab Object안에 여러개의 Component가 있고 이들을 전부 Clone들에게 부여해주고 싶을때는 코드상에 RequireComponent를 집어넣어 여러개가 동시에 에러없이 Component가 부여될 수 있도록 하여라
// example
[RequireComponent(typeof(Rigidbody))]
- Spanwer를 생성할때는 포지션으로 vector값 (0, 0, 0)에 위치하여 있는 Empty Object으로 생성하여라
- 복제된 Clone들이 Hierarchy상에 Spawner의 자식으로써 있게 만들어 디자이너가 이 Object들이 특정 Spanwer의 복제본들인지 인지 가능하게끔 만들어라.
2. Destroying Objects
gameObject Field | Destroying Object | OnDestroy() Event |
gameObject Field
MonoBehaviour Class에는 기본적으로 gameObject이라는 public field를 가지고 있다 (MonoBehaviour Class안에서 GameObject gameObject; 으로써 GameObject의 Class가 Object으로 변해 우리가 스크립트상에서 이용가능할때). 그럼으로 인해 우리가 스크립트 상에서 Scene View에서 생성된 GameObject들을 코드상으로 불러올 수 있는 것이다. 이때 이와 같이 스크립트가 부여된 GameObject이 게임이 실행됬을시 콘솔창에서 자신의 이름을 출력하는 것을 알 수 있다.
void Start ()
{
// print the name of the object this is attached to
Debug.Log ("name = " + gameObject.name;
}
이 gameObject field는 또한 Built-in Component (내장 컴포넌트)를 가지고 있는데 (GameObject Class안에 있는 여러 component들 즉 속성들) 이는 Transform, name, tag 등 다양하게 이루어져 있다.
public Transform target; // Transform Component가 type으로 선언된 변수를 target으로 설정
void Start ()
{
// print the name of the target object
// target으로 설정된 Transform을 가진 GameObject의 이름을 출력하라
Debug.Log ("target" + target.gameObject.name);
}
Destroying Object
우선 기본적인 gameObject Field의 대한 설명을 마쳤으니 본격적으로 특정 object을 없애는 원리에 대해 알아보겠다. 이를 Destroy라고 하는데 이를 시행하는 방법은 간단하게 Destroy() Method를 사용하는 방법이 있다.
void Update ()
{
// Destroy this game object. (지금 이 스크립트가 연결되어 있는 GameObject을 없애 버려라)
Destroy (GameObject);
}
이때 유의해야 할 점은 this를 사용하는 방법인데 이때 Destroy (this)를 사용하기 보단 특정 GameObject을 variable로 선언하여 준다음 그 GameObject의 gameObject를 지워주는 방식으로 코드를 작성하면 더욱 좋은 연습이 된다.
public GameObject target;
public Transform target; // 이와 같이 작성되도 아래 target의 gameObject으로 지정되었으므로 동일하게 작동 가능하다
void Update ()
{
// Remove the object (현재 script에 연결된 object를 지워벼러라)
Destroy (target.gameObject);
// 이렇게 쓰는 것들은 권장되지 않거나 작동하지 않는다.
Destroy (this);
Destroy (this.gameObject);
}
이때 응용할 수 있는 방식으로는 Destroy()를 GameObject만 없애려고 쓰는 것이 아닌 특정 Component만을 없애려고 할때에도 충분히 사용 가능하다.
void Update ()
{
// Remove the Rigidbody component from this object (현재 script에 연결된 object의 rigidbody component를 지워벼러라)
Rigidbody rigidbody = GetComponent <Rigidbody>();
Destroy (rigidbody);
}
OnDestroy Event
GameObject이 Destroy될때 이는 곧바로 없어지지 않는다. 이는 왜냐하면 OnDestroy Event가 먼저 없애려고 하는 GameObject의 모든 Component로 불려지기 때문이다. 이 과정은 Destroy() method가 실행되고 난 후 이루어지며 프레임이 끝날때까지는 GameObject이 없어지지 않을 것이다. 이러한 Event의 과정은 다음과 같다.
OnApplicationOut -> OnDisable -> OnDestroy // Decommisioning
이와 같은 코드를 통해 스크립트가 들어간 GameObject이 없어졌을때 OnDestroy() 안에 들어가는 event를 실현시킬 수 있다.
public GoldPickup goldPrefab; // GoldPickup이라는 Script를 가진 prefab을 유저가 직접 설정가능케함
void OnDestroy() // 만약 해당 script가 들어간 GameObject가 없어지면
{
// spawn gold at this location when destroyed
// goldPickup이라는 script를 가진 prefab의 clone들을 생성하라
GoldPickup gold = Instantiate (goldPrefab);
// 해당 clone들의 포지션을 지금 이 script가 들어간 GameObject의 위치로 변경
gold.transform.position = transform.position;
}
3. State Machine
Finite State | State Machine Programming Pattern | Turbo Boost Example |
State Machine
일반적으로 우리는 각 MonoBehaviour를 게임 상태의 일부를 추적하고 해당 상태에 영향을 미치는 작업 및 이벤트에 응답하는 "State Machine"으로 디자인한다. Finite State Machine은 State들의 컬렉션이자 특정 State가 다른 어느 State로 옮겨 간 것을 의미한다. 이때 Finite State Machine (FSM)은 이러한 아이디어를 실현시킬 수 있는 방법중 하나로써 각기 게임의 모든 행동들을 State로 정의하여 특정 행동이 다른 행동으로 바뀔때 State를 다른 State로 바꿔진다는 방식을 코딩을 할 수 있다.
- Case Study 1: Guard FSM
만약 게임을 만들때에 게임 디자이너가 만든 이러한 스토리 라인이 있다고 가정해보자.
가드들이 길거리를 돌아다니고 있다 (Idle). 가드들이 만약 무언가 수상한 것을 보거나 들었다면 그들은 플레이어의 증거들을 찾기 위해 Caution Mode로써 조사에 돌입할 것이다. 만약 그들이 플레이어들을 찾아냈다면 가드들은 곧바로 다른 가드들을 호출한 다음 경고 Alert Mode에 돌입할 것이다. 그때 만약 플레이어가 가드의 눈을 피해 도망을 친다면 그들은 Evasion Mode에 돌입하여 플레이어들을 쫓을 것이다. 그러다 만약 다시 플레이어를 발견한다면 Alert Mode로 들어갈 것이다. 그들이 만약 계속해서 플레이어를 찾지 못한다면 Caution Mode로 완화되어 다시 플레이어들의 증거를 찾기 위해 돌아다닐 것이다. 만약 가드들이 플레이어들에게 공격을 당하는등 제3의 상황이 생긴다면 그들은 Hold-up Mode로 들어갈 것이다.
이때 여러 Mode들이 언급된 것을 알 수 있는데 이 Mode들은 각기 GameObject의 State를 나타내며 특정 Event에 따라 State들이 전가되어 여러번 바뀌는 것을 알 수 있다. 이를 Finite State Machine이라 한다.
- Case Study 2: Villager FSM
또 다른 스토리 라인으로는 이러한 경우가 있다.
어느날 마을 주민들이 그들의 집으로 향하려고 마을에 있는 특정 위치에 방문한다. 만약 그들이 플레이어를 발견한다면 그들은 플레이어를 향해 반가워 할 것이고 이야기를 나누게 될 것이다. 만약 플레이어가 속한 클랜이 마을 주민들이 적대심을 가진 그룹들이라면 그들은 위협을 느끼고 플레이어를 향해 공격하거나 도망을 갈 것이다.
이러한 FSM 예시들로 개발자들이 알 수 있는 사실들로는 FSM에는 무조건 다음과 같은 요소들이 포함되어있어야 한다는 것을 깨달을 수 있다.
- The required States (필수적인 State들 )
- The variables controlling them (State들을 컨트롤할 필수 변수들. 이는 event를 가리키며 코드상에서 주로 boolean variable로 나타나진다. 그렇다고 맨날 boolean variable로 지정되는것은 아니다.)
- Interactive Game (Media): 이로인해 State는 게임을 플레이하는 유저가 선택하는 방식에 따라 바뀌게 되게 유도하고 그러므로 인해 결과가 결정나는 방식으로 기본적인 시스템을 구축하여 만들어야 한다. 이는 여러개의 경우의 수를 동반하게 된다.
- Finite State Machiner as Graphics
우리들은 주로 FSM을 노드가 있는 그래프로 그려 각각의 state가 나타나있고 arrow들이 그들의 관계를 설명하는 방식으로 시각적으로 나타낼 수 있다.
- Implementing State using Enumerations
이때 FSM을 유니티 코드 상에서 구현하는 방법은 Enumeration이라는 방법인데 이는 다음과 같은 예시로 작성된다.
public class GuardFSM : MonoBehaviour
{
enum State { // State의 종류로는 다음과 같다.
Idle,
Search,
Chase,
Kill
}
private State state; // GameObject의 State값을 조절할 수 있는 variable state 선언
void Start () // 게임이 실행되고 난 첫번째 프레임에
{
// guard is idle
state = State.Idle; // state가 idle인 상태여라
}
}
Enumerations
그렇다면 정확한 Enumeration이란 무엇일까? 이는 고정된 명명된 값 집합이 있는 새 variable type을 정의한다. 즉 state을 적용하고 싶지만 숫자로된 정보가 없을경우 enumeration을 통해 열거 가능하다. 쉽게 말하자면 수로써 정의되는 variable type이 아니지만 특정 의미를 가진 variable들을 만들고 싶을때 Enumeration을 쓰면 된다. 이때 Enumeration은 숫자가 아닌 직접 이름으로 입력하여 코드를 더욱 가독성있게 만들 수 있다.
Enumerations를 활용한 코드의 포맷은 다음과 같이 작성되며
switch (state) // GameObject이 state 상태일때
{
case State.State1: // GameObject의 State가 State1이라면
DoState (); // DoState() Method안에 입력된 Event를 수행해라
if (eventA) // 만약 State1인상태에서 eventA가 감지되면
{
// transition to new State
state = State.State2; // State1에서 State2로 state를 변경해라
// initialise State2
InitState2 (); // ??
}
else if (eventB) // 그게 아니고 State1인 상태에서 eventB가 감지되면
{
// etc
}
break;
case State2; // GameObject의 State가 State2이라면
DoState2 (); // DoState2() Method안에 입력된 Event를 수행해라
if (eventC) // 만약 State1인상태에서 eventC가 감지되면
{
// etc
}
break;
//etc
}
Enumeration을 활용한 코드의 예시는 다음과 같다. 이때 transition들은 boolean type으로 미리 선언됬다는 하에 아래와 같은 코드가 작성되었다.
void Update ()
{
switch (state) // GameObject이 state 상태일때
{
case State.Idle: // State가 Idle일 경우
DoIdle (); // DoIdle() Method안에 적혀진 event를 실행
if (canSeePlayer) // 이때 만약 Idle인 상태에서 canSeePlayer가 적용되면
{
state = State.Chase; // State를 Chase로 바꾸어라
}
else if (heardSomething) // 아니면 만약 Idle인 상태에서 heardSomething이 적용되면
{
state = State.Search; // State를 Search로 바꾸어라
}
// break을 써 넣어 아무것도 해당하지 않을 경우 Idle 상태를 빠져나가라. 보통 State가 끝날때 break을 써넣어야 함
break;
case State.Chase: // State가 Chase일 경우
// etc
break;
case State.Search: // State가 Search일 경우
DoSearch (); // DoSearch() Method안에 적혀진 event를 실행
if (canSeePlayer) // 이때 만약 Search인 상태에서 canSeePlayer가 적용되면
{
state = State.Chase; // State를 Chase로 바꾸어라
}
else if (searchTimer <= 0) // 아니면 만약 Search인 상태에서 searchTimer의 값이 0 이하라면
{
state = State.Idle; // State를 Idle로 바꾸어라
}
break;
case State.Kill: // State가 Chase일 경우
// etc
break;
}
}
Good Coding Practices
- 항상 코드를 작성하기 전 FSM Diagram을 직접 그려보아 필요한 State와 Event가 모두 구현되는것을 확실하게 하라
- 항상 State작성을 명확히하고 Enumeration과 State of variable들을 사용하여 코드를 더욱 가독성있게 만들어야 한다. 다른 값들에게 숨겨져 있는 모호한 State의 작성은 권장되지 않는다.
- 모든 State간의 transition들을 한 곳에 모아두어 게임 코드의 가독성을 높이고 디버깅을 쉽게끔 만들어라.
- 항상 여러개의 if statement보단 switch statement를 사용하여 코드가 더욱 쉽게 수정될 수 있도록 만들어라.
Turbo boost
이때 앞서 설명한 FSM을 사용하여 Turbo Boost (가속도)가 생기는 간단한 게임의 한 특징을 코드로써 구현할 수 있다. 우리가 원하는 방향은 Turbo키가 눌러졌을때 Player의 속도가 빨라지고 때졌을때 속도가 원상복귀 되는 그러한 게임을 만들고자 한다.
이때 Enumerations을 사용하지 않고 일반적으로 위에서 만든 FSM을 구현하면 다음과 같다.
public class Drive : MonoBehaviour
{
public float normalSpeed = 5; // 일반적인 스피드
public float turboSpeed = 10; // Turbo키가 눌려졌을때의 스피드
void Update ();
{
float speed; // Update() Method안 Local Variable speed
if (Input.GetButton ("Turbo")) // 만약 Turbo키가 눌려졌다면
{
speed = turboSpeed; // speed는 TurboSpeed로 설정
}
else // 그게 아니면
{
speed = normalSpeed; // speedsms normalSpeed로 설정
}
// move at current speed, 위에서 설정한 속도로 앞으로 움직이게끔 만들어라
transform.Translate (speed * Time.deltaTime * Vector3.forward);
}
}
- Adding Timeout
그렇다면 타임아웃을 FSM에 추가하면 어떻게 코드를 작성할 수 있을까? 마치 Turbo키가 눌러져 있는 상태여도 Timer가 특정 시간을 지나면 다시 스피드가 원상복귀 하도록 수정해 보았다.
public class Drive : MonoBehaviour
{
public float normalSpeed = 5; // 일반적인 스피드
public float turboSpeed = 10; // Turbo키가 눌려졌을때의 스피드
public float turboDuration = 5; // sec
public float turboTimer = 0;
public bool turboActivated = false;
void Update ();
{
float speed; // Update() Method안 Local Variable speed
if (turboActivated) // 만약 turboActivated가 true라면
{
speed = turboSpeed; // speed는 TurboSpeed로 설정
turboTimer -= Time.deltaTime; // turboTimer는 차감되기 시작
if (turboTimer <= 0) // 만약 turboActivated가 true인 상태에서 turboTimer가 0보다 작다면
{
turboActivated = false; // turboActivated는 false로 변함
speed = normalSpeed; // speed도 normalSpeed로 변경
}
}
else // 만약 turboActivated가 true가 아니라면
{
speed = normalSpeed; // speed는 normalSpeed로 설정
if (Input.GetButtonDown ("Turbo")) // 만약 turboActivated가 true가 아닌 상태에서 Turbo버튼이 눌렸다면
{
turboActivated = true; // turboActivated는 true로 변함
speed = turboSpeed; // speed도 turboSpeed로 변함
turboTimer = turboDuration; // turboTimer는 turboDuration으로 변해서 처음 시간으로 초기화됨
}
}
// move at current speed, 위에서 설정한 속도로 앞으로 움직이게끔 만들어라
transform.Translate (speed * Time.deltaTime * Vector3.forward);
}
}
이 때 더욱 복잡한 coolDown인 상태를 집어넣어 Turbo키가 눌려져도 얼마동안은 turbo가 작동되지 않도록 세팅해주려고 하면 다음과 같이 코드를 작성할 수 있다. 우선 FSM을 그려보자면 다음과 같은데...
이번에는 Enumerations들을 활용하여 코드를 적어보기로 하였다.
public class Drive : MonoBehaviour
{
public float normalSpeed = 5;
public float turboSpeed = 10;
enum TurboState
{
Available,
Active,
Cooldown
};
private TurboState turboState;
public float turboDuration = 10;
public float cooldownDuration = 5;
private float turboTimer = 0;
void Start()
{
// initially available
turboState = TurboState.Available;
}
void Update()
{
switch (turboState)
{
case TurboState.Available:
speed = normalSpeed;
// transition to Active when button is pressed
if (Input.GetButtonDown("Turbo"))
{
turboState = TurboState.Active;
speed = turboSpeed;
turboTimer = turboDuration;
}
break;
// cont…
case TurboState.Active:
speed = turboSpeed;
turboTimer -= Time.deltaTime;
// transition to Cooldown when timer runs out
if (turboTimer <= 0)
{
turboState = TurboState.Cooldown;
speed = normalSpeed;
turboTimer = cooldownDuration;
}
break;
// cont…
case TurboState.Cooldown:
speed = normalSpeed;
turboTimer -= Time.deltaTime;
// transition to Available when timer runs out
if (turboTimer <= 0)
{
turboState = TurboState.Available;
}
break;
}
// move at current speed
transform.Translate(speed * Time.deltaTime * Vector3.forward);
}
}
Platformer jump
이때 Platformer Jump는 다음과 같이 물리적인 방식과 게임상에서의 방식으로 나누어지는데 Hover가 적용된 방식으 Platformer Jump를 구현하려면 Enumerations와 State의 원리들을 이용하여 FSM을 구성한다음 코드를 작성해야 한다.
- Physic-based Curve
실제로는 사람이 점프를 하면 다음과 같은 물리 그래프로 점프를 하는 것을 알 수 있다. 시간이 지남에 따라 높이가 올라갔다가 내려간다.
- Actual Jump
그렇지만 게임상에서는 Hover라는 개념을 추가해 공중에서 머무는 시간이 조금 더욱 길도록 만들어 줄 수 있다. 이때 각기 모든 state의 변화도 그래프 상에 나타내었다.
위 그래프를 기반으로 FSM을 완성해보자면 다음과 같이 만들어 줄 수 있다. 각기 Event (Transition)에 따라 State가 다른 State로 변하는 것을 알 수 있고 이 FSM을 코드로 완성해 보려면은 State를 활용한 Switch statement와 Enumerations의 활용을 적극적으로 권장하는 바이다.
이번 수업으로 인해 Prefab들이 복제되는 방식, Destruction의 원리 그리고 Finite State Machine이 무엇인가에 대해 알아보았다. 많은 것들이 있지만 게임 개발에 있어 핵심적인 부분들인 것 같다.