# 원문은 이곳에서 보실수 있습니다.내용이 길어 총 3편으로 나눠서 올리겠습니다.

(2편에 이어서 계속)

## 우리의 첫번째 실용적인 유한 상태 기계

enemy에 대해서 더 주의깊게 생각하고, 상태를 정의한 다음, enemy들을 한 번에(at a time) 하나의 상태에 넣어보자.

enemy는 다음 중 하나의 상태에 있을 수 있다 : sleeping, following, attacking, being hit, dying.

우리는 실제 다음과 같이 작동하기를 바란다: Part 1/Scene 2
우리는 FiniteStateMachine1.cs파일을 만든 다음, 파일안에서 가장 먼저 해야할 일은 상태를 정의하는 열거형을 만드는 것이다:

 0102030405060708091011 `public` `enum` `EnemyStates``{``    ``sleeping = 0,``    ``following = 1,``    ``attacking = 2,``    ``beingHit = 3,``    ``dying = 4``}` `public` `EnemyStates currentState = EnemyStates.sleeping;`

이제, 우리는 오직 한 번에 하나의 상태만을 처리하고, 프로세스를 한 가운데로 모으기위해 모든 로직을 변경해야 한다. 이는 각 상태와 관련된 코드를 더 찾기 쉽게 한다.

이를 하기 위해서, 간단히 복잡한 코루틴을 제거할 것이다. 그래서 우리는 코드 통합에 집중할 수 있을 것이다. Start, Attack, Hit, Dead 코루틴에게 작별인사를 하고, switch문들과 잃어버린 코루틴 기능을 형성하기 위한 약간의 새로운 변수들을 반갑게 맞이 하자:

 001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 `void` `Update()``{``    ``// 현재 활성화된 상태를 기반으로 코드는 실행 된다.``    ``switch``(currentState)``    ``{``    ``case` `EnemyStates.sleeping:``        ``// 적은 sleeping 상태이다.``//Check the current time against the time for the next Zzzzz to appear``        ``if``(timeOfNextZZZZ < Time.time)``        ``{``//If its time then create the sleeping prefab above the enemies``//head``            ``var newPrefab = Instantiate(sleepingPrefab, _transform.position + Vector3.up * 3f, Quaternion.identity)` `as` `Transform;``            ``newPrefab.forward = Camera.main.transform.forward;``//Calculate a time between 2 and 8 seconds for the next``//Zzzzz``            ``timeOfNextZZZZ = Time.time + 2 + (6 * Random.value);``        ``}``//Now check if the player has appraoched, using sqrMagnitude to avoid``//a performance expensive square root``        ``if``((_transform.position - _player.position).sqrMagnitude < _attackDistanceSquared)``        ``{``  ``//Move the enemy into the following state``            ``currentState = EnemyStates.following;``  ``//Make the enemy target the player``            ``target = _player;``            ``//Where this enemy wants to stand to attack``            ``_attackRotation = Random.Range(60,310);``        ``}``        ``break``;` `    ``case` `EnemyStates.following:``        // enemy는 target을 따라가고 있다.` `        ``//If the target has been destroyed then we go back to sleep``        ``//(Destroyed objects return null and false)``        ``if``(!target)``        ``{``            ``currentState = EnemyStates.sleeping;``            ``return``;``        ``}``        ``//Calculate the vector to the target``        ``var difference = (target.position - _transform.position);``        ``//Ignore a big part of the height difference by dividing it``        ``//by 6``        ``difference.y /= 6;``//Calculate the square of the distance (avoid expensive square root)``        ``var distanceSquared = difference.sqrMagnitude;` `        ``//Too far away to care?``        ``if``( distanceSquared > _sleepDistanceSquared)``        ``{``            ``//If we've got too far away then go to sleep``            ``currentState = EnemyStates.sleeping;``            ``return``;``        ``}` `        ``//Close enough to attack``        ``if``( distanceSquared < _maximumAttackEffectRangeSquared && _angleToTarget < 60f)``        ``{``            ``//Move the enemy into the attacking state``            ``currentState = EnemyStates.attacking;``            ``//Enable the attack animation``            ``_attack.enabled =` `true``;``            ``//Reset the animation to the start``            ``_attack.time = 0;``            ``//Make the animation just play once and hold``            ``//this is so .normalizedTime will become =1``            ``_attack.wrapMode = WrapMode.ClampForever;``            ``//Make the attack animation full power``            ``_attack.weight = 1;``            ``//Indicate that we have yet to strike the target``            ``hasStruckTarget =` `false``;` `            ``return``;``        ``}` `        ``//Move towards the target` `//First decide target position based on the angle of attack we decided on waking up``        ``var targetPosition = target.position + (Quaternion.AngleAxis(_attackRotation, Vector3.up) * target.forward * maximumAttackEffectRange * 0.8f);``        ``//Calculate the basic movement toward the target``        ``var basicMovement = (targetPosition - _transform.position).normalized * speed * Time.deltaTime;``        ``//Zero out the y movement so we can get a bearing angle``        ``//we will apply gravity later``        ``basicMovement.y = 0;` `        ``//Only move when facing the target - so calculate``        ``//the angle between the heading of the enemy and the target``        ``_angleToTarget = Vector3.Angle(basicMovement, _transform.forward);``        ``//Only move the enemy if it is within 70 degrees``        ``if``( _angleToTarget < 70f)``        ``{``            ``//Provide some gravity``            ``basicMovement.y = -20 * Time.deltaTime;``            ``_controller.Move(basicMovement);``        ``}` `        ``break``;``    ``case` `EnemyStates.attacking:``        // enemy는 공격하고 있다.` `//Work out if we are half way through the animation and haven't``//yet added any damage to the target``        ``if``(!hasStruckTarget && _attack.normalizedTime > 0.5f)``        ``{``            ``//Ready to hit, so flag that it is done so we``            ``//only come in here once``            ``hasStruckTarget =` `true``;``            ``//Check if still in range``            ``if``(target && (target.position - _transform.position).sqrMagnitude < _maximumAttackEffectRangeSquared)``            ``{``                ``//Apply the damage``                ``target.SendMessage(``"TakeDamage"``, 1 + Random.value * 5, SendMessageOptions.DontRequireReceiver);``            ``}` `        ``}``    ``//See if the animation is complete yet (normalizedTime will be 1)``        ``if``(_attack.normalizedTime >= 1 -` `float``.Epsilon)``        ``{``    ``//Set the state of the enemy based on whether the target is``    ``//dead or not``            ``currentState = target ? EnemyStates.following : EnemyStates.sleeping;``            ``//Make sure the animation has no effect``            ``_attack.weight = 0;``        ``}``        ``break``;` `    ``case` `EnemyStates.beingHit:``        //` `enemy는 현재 맞고 있다.` `        ``//Check whether the animation is complete``        ``if``(_hit.normalizedTime >= 1 -` `float``.Epsilon)``        ``{``            ``//Animation is complete so set the state based on whether``            ``//the enemy is still alive (in which case chase it).``            ``currentState = target ? EnemyStates.following : EnemyStates.sleeping;``        ``}``        ``break``;` `    ``case` `EnemyStates.dying:``        ``//Wait for the end of the death animation``        ``if``(_die.normalizedTime >= 1 -` `float``.Epsilon)``        ``{``            ``//Animation is complete, destroy the enemy``            ``Destroy(gameObject);``        ``}``        ``break``;``    ``}` `}`

우리의 코드는 더 읽기 쉬워졌다. 우리는 또렷하게 각 상태에 적용되는 로직들을 볼 수 있다.
또, 코드에 트리거를 추가했다.

 01020304050607080910111213141516171819202122232425262728293031323334353637383940 `void` `OnTriggerEnter(Collider hit)``{``    ``// 같은 객체에 있는 다른 충돌체에 영향을 주지 않는지 확인해라.``    ``if``(hit.transform == _transform)``        ``return``;` // 상태를 기반으로 무슨 일을 할 것인지 선택한다.`    ``switch``(currentState)``    ``{``    ``case` `EnemyStates.sleeping:``    ``case` `EnemyStates.following:``        // hit이 Player인가?``        ``if``(hit.transform == _player)``        ``{``            ``//If the player is in the trigger then start following him``            ``target = _player;``            ``currentState = EnemyStates.following;``        ``}``        ``else``        ``{``            ``//See if the thing we hit has an EnemyMood``            ``var rival = hit.transform.GetComponent();``            ``if``(rival)``            ``{``                ``//If the item hit has an EnemyMood then it is an``                ``//enemy``                ``//Get a random number and see if it is bigger than``                ``//the mood - so the lower the mood the more likely``                ``//it is that the subsequent code will run``                ``if``(Random.value > _mood.mood/100)``                ``{``                    ``//We are going to attack this other enemy``                    ``target = hit.transform;``                    ``currentState = EnemyStates.following;``                ``}``            ``}``        ``}``        ``break``;``    ``}` `}`
각 상태가 활성화 상태일 때, 우리는 또한 명확하게 상태 전이를 볼 수 있고, 정확하게 게임 루프와 트리거에서 일어나는 일을 알 수 있다.

완전히 작동하는 FFSM은 앞에서 봤던 우리의 뒤죽박죽한 생각들 보다, 훨씬 더 나은 개발의 기반이 될 것이다.

사실 아직 약간의 문제가 있다. 솔직히 말해서, 이 switch문은 꽤 커질 수 있다. 만약 더 많은 상태들과 많은 로직들이 존재한다면, 아마 우리가 어느 루틴에 있는지 잊어버릴 것이고, 또 각각의 새로운 상태들은 열거형 값들을 비교하기위해 약간의 시간이 걸릴 것이다. 각 상태에 대해 코드를 함께 가지는 것이 훨씬 더 나을 것이다.(이미 Sleeping Update 로직에서 Sleeping Trigger 로직으로 가는데 시간이 걸리며, Page Up과 Page Down 키를 여러번 눌러야 된다.)

보다 더 중요한 것은, 당신이 특정 상태의 로직을 보기 위해서 많은 스크롤을 해야할 때, 당신의 머리 속에 그 상태의 로직 전체를 유지하는 것이 어렵다는 것이다.

실제로, 당신의 코드를 다음과 같이 작성하면 어떨까:

 0102030405060708091011121314151617181920212223242526272829303132 `#region Sleeping` `IEnumerator Sleeping_EnterState()``{``    ``while``(``true``)``    ``{``        ``yield` `return` `new` `WaitForSeconds(Time.time + 2 + (6 * Random.value));``        ``var newPrefab = Instantiate(sleepingPrefab, transform.position + Vector3.up * 3f, Quaternion.identity)` `as` `Transform;``            ``newPrefab.forward = Camera.main.transform.forward;` `    ``}``}` `void` `Sleeping_Update()``{``    ``if``((transform.position - _player.position).sqrMagnitude < _attackDistanceSquared)``    ``{``        ``target = _player;``        ``//Where this enemy wants to stand to attack``        ``_attackRotation = Random.Range(60,310);` `        ``currentState = EnemyStates.Following;` `    ``}``}` `void` `Sleeping_OnTriggerEnter(Collider hit)``{``    ``Following_OnTriggerEnter(hit);``}` `#endregion`
그렇게 멋지지는 않다. 모두 다 함께 있으며, 어떠한 스위치문도 존재하지 않는다. 실제로 당신이 지금 어디에 있고, 현재 상태에 적용되는 로직들이 무엇인지를 쉽게 볼수 있다. 우리는 심지어 약간의 코루틴 기법도 사용할 수 있다.

이 연재의 두번째 글에서, 우리가 정확히 어떻게 목적을 이루고 더 성능 기준에 부합하는 프레임워크를 만드는지에 대해서 알게 될 것이다. reflection cahing과 delegate에 대해서 배울준비를 해라. 이것은 그들이 우리의 FSM 프레임워크를 날라다니게 만드는데 필요하기 때문이다.

## 비디오

당신은 다음의 비디오를 통해서, FSM 프레임워크를 만드는데 더 많은 이해를 얻을 수 있다.

#### '유니티 개발 정보 > 개념' 카테고리의 다른 글

 유한 상태 기계(Finite State Machines, FSM) #2 (2/3)  (1) 2013.12.02 2013.11.30 2013.10.20 2013.10.20 2013.10.13 2013.10.11

### 댓글을 달아 주세요

1. 진라면

좋은 번역 잘 봤습니다. 다음편도 기대하겠습니다.

2013.10.26 10:27
• 아직 미숙한 부분이 많은데 좋은 말씀 감사합니다^^. 앞으로 좀 더 재미있고 유익한 글, 편안한 글을 쓰도록 노력하겠습니다.

2013.10.26 17:01 신고