유니티로 3D게임을 만들던 도중 몬스터와 플레이어가 같은 State클래스를 사용하다가 문제가 발생했다.
현재 State클래스는 다음과 같이 있다.(부모클래스 State 상속받은)
- IdleState : 대기 상태. 입력이 들어오면 Move 상태로 전환
- MoveState : 목적지 좌표로 이동하는 상태
- AttackState : 공격 상태
- ChaseState : 정해진 타겟을 계속 추적하는 상태
- MonsterIdleState : IdleState를 상속받는 몬스터전용 IdleState
(참고사항: Player, Monster는 Character를 부모클래스로 둔다.)
사건의 발단은 이렇다. 플레이어는 MoveState 때, 마우스클릭으로 새로운 좌표가 들어오면 그 좌표로 움직임을 갱신해야 한다.
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveState : State { Vector3 _destination; Vector3 _velocity = Vector3.zero; override public void Start() { _destination = _character.GetTargetPosition(); _character.SetAnimationTrigger("MOVE"); } override public void Stop() { } override public void Update() { //움직임 관련 코드 } override public void UpdateInput() { //입력이 들어오면 해당 좌표로 갱신 _destination = _character.GetTargetPosition(); } } |
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class ChaseState : State { Vector3 _velocity = Vector3.zero; override public void Start() { _character.SetAnimationTrigger("MOVE"); } override public void Stop() { } override public void Update() { //타겟위치를 계속 따라간다. //공격범위 안으로 들어오면 AttackState로 } override public void UpdateInput() { } } |
ChaseState는 플레이어 뿐만 아니라 몬스터도 사용한다. 몬스터의 탐색범위 안에 플레이어가 들어오면 몬스터는 Idle->Chase 상태로 전환되고, 플레이어를 계속 쫓아온다.
플레이어 역시 타겟(몬스터)를 클릭하면 타겟이 공격범위에 들어올 때까지 계속 쫓아간다.
문제는 Chase 상태일 때, 플레이어는 입력을 받으면 Move 상태로 전환되어 해당 좌표로 이동을 하고 싶다는 것이다.
"그냥 UpdateInput을 override하면 되잖아?" 라고 할 수 있지만, 이 State는 몬스터도 "같이" 사용하고 있다는 것이다.
"그럼 MonsterIdleState처럼 상속 받아버리자". 이 역시 좋지 않은 방법이다. UpdateInput이 필요한 부분을 전부 상속을 받아야 한다. 유지보수도 쉽지 않을 뿐더러 상속은 최대한 뒤로 미루는게 좋다.
리팩토링이 필요한 시점이다.
방법은 여러가지 있지만 내가 찾은 해결방안은 두가지다.
1. 플레이어, 몬스터용 State를 따로 만들자.
2. UpdateInput()함수를 지우고 되게 만들자.
1번 같은 경우 가장 빠르고 쉬운 방법이다. 하지만 비슷한 기능을 하는 클래스가 여러개 생길 가능성이 있다.
그럼 2번으로 리팩토링을 진행해보자
1. State 클래스의 UpdateInput() 함수 삭제
2. 에러가 발생하는 부분 확인하고 주석처리(리팩토링이 완전히 끝나고 나서 삭제하자)
플레이어 클래스의 Update함수 내 마우스 입력을 받는 곳에서 호출해주던 UpdateInput 함수 역시 주석처리하였다.
3. 하나의 룰을 정해야 할때.
State의 Update에서 몬스터나 플레이어 타입을 확인해 일일이 if문 처리를 하는 방법도 있다. 하지만 이렇게 되면 타입이 하나씩 추가될 때마다 예외처리는 점점 늘어날 것이다.
이럴 때는 요구사항 단계에서 파악된 요소를 바탕으로 룰을 하나 정하는 것이다.
- 플레이어는 입력을 받는다.
- 몬스터는 입력을 받지 않는다.
룰이 정해졌으면 룰에 맞게 예외처리를 하자. 주의할 점은 예외처리를 하고 룰을 정하는게 아닌, 룰을 정하고 그에 맞게 예외처리를 해야한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class IdleState : State { override public void Start() { _character.SetAnimationTrigger("IDLE"); } override public void Update() { if(_character.IsSetMovePosition()) { _character.ChangeState(Character.eState.MOVE); } } } |
이제 다른건 신경쓰지말고 어느 시점에서 _isSetMovePosition을 true or false로 만드는게 중요하다.
플레이어 클래스를 보자.
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 | void UpdateInput() { if (InputManager.Instance.IsMouseDown()) { Vector3 mousePosition = InputManager.Instance.GetCursorPosition(); Ray ray = Camera.main.ScreenPointToRay(mousePosition); RaycastHit hitInfo; if (Physics.Raycast(ray, out hitInfo, 100.0f, 1 << LayerMask.NameToLayer("Ground") |1 << LayerMask.NameToLayer("HitArea"))) { if(LayerMask.NameToLayer("Ground") == hitInfo.collider.gameObject.layer) { _targetPosition = hitInfo.point; _targetObject = null; _isSetMovePosition = true; } if(LayerMask.NameToLayer("HitArea") == hitInfo.collider.gameObject.layer) { HitArea hitDetector = hitInfo.collider.GetComponent<HitArea>(); Character character = hitDetector.GetCharacter(); switch (character.GetCharacterType()) { case eCharacterType.MONSTER: //적으로 파악 => 추적 state _targetObject = hitInfo.collider.gameObject; ChangeState(eState.CHASE); break; } } } } } |
다른 State들도 룰에 맞게 예외처리를 해주자.
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveState : State { Vector3 _destination; Vector3 _velocity = Vector3.zero; override public void Start() { _destination = _character.GetTargetPosition(); _character.SetAnimationTrigger("MOVE"); } override public void Update() { if (_character.IsSetMovePosition()) { //입력이 들어오면 해당 좌표로 갱신 _destination = _character.GetTargetPosition(); } //움직임 관련코드 } |
moveState는 UpdateInput이 사라지고 다음과 같이 변경되었다.
UpdateInput이 사용되었던 다른 State에서 위와 같이 수정해주면 된다.
이로써 _isSetMovePosition 값 하나로 유연하게 게임을 변경할 수 있게 되었다. 클래스를 새로 만들지 않고 말이다.
예를 들면 NPC도 입력을 받아야한다면 _isSetMovePosition값을 어디선가 true로 바꿔주면 되는 것이다.
기나긴 리팩토링이 끝났다.
이번 리팩토링에서 기억해야할 건
* 불필요한 예외처리로 해결하려고 하지말 것.
* 룰이 있어 예외가 생기는 것. 그 반대가 되면 안된다. 룰은 기획에서 프로그래머가 파악해야되는 하나의 요구사항
* 상속은 최대한 뒤로 미루자. 상속보다는 위임을.