뮤냐 어택의 기획과 시스템을 재조정하면서 코드베이스도 이참에 깔끔하게 정리하기 위해 오늘 이것저것 테스트를 해봤다.


유니티를 나름 4때부터 만졌으면서도 자주 헷갈리는 부분이 Awake, Start, OnEnable, OnDisable이고, 


유니티 메카님(애니메이터)는 이번 프로젝트에서 처음 만진 물건이다보니 호출 타이밍에 대해 정확히 모르고 좀 막무가내로 돌려버린 부분이 있음. 특히 StateMachineBehaviour이라던가 말이지...


평소에 기술 관련 글을 올릴 때는 개발일지 올려가며 갤에 홍보를 겸하고 있으니 기브 & 테이크라는 생각으로 올리곤 했는데 이번에는 나 스스로도 헷갈리는 부분이 굉장히 많은 파트라 일부러 한번 정리해두고 작업 시작하고 싶어서 글을 올린다. 초보 유니티 입문하는 갤러들에게 굉장히 유용할거라고 생각함.


------------------------------


*Start와 Awake와 OnEnable


기본은 유니티 매뉴얼(https://docs.unity3d.com/Manual/ExecutionOrder.html)과 동일한 순서라고 생각하면 됨.



viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796491955f6d08b38a23d015d47ba2c725f35cf201977a54bca


Awake는 게임오브젝트가 유니티 내부에서 Instantiate될때 첫 호출되고, Start는 모든 오브젝트의 Instantiate가 마무리된 후에 호출된다.




그런데, 오브젝트 풀링 등을 이용하기 시작하면서 헷갈리는 문제가 발생하기 시작함.


오브젝트 풀링은 보통 gameObject.setActive(true) 와 setActive(false)를 이용하여 필요한 오브젝트를 여러번 Instantiate 하지 않고 재활용하는게 요점인데,


여기서부터 Awake, Start, OnEnable, OnDisable 호출 순서가 점점 골치아파지기 시작하는거지.



------------------------------------


본론으로 들어가기 전에 확실하게 짚고 넘어가야 하는 부분은 이거임.


1. gameObject.SetActive(false)

2. this.enabled = false


완전히 다르다는 점이다!


뭐가 다른 것인가 하면



viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796491955f6d08b38a23d015d47ec7d95b6e83330e551d481f2

gameObject.SetActive와 this.enabled는 위 캡쳐의 1번 2번 각각에 대해 대응된다고 생각하면 됨.


게임 오브젝트 자체를 disable시키느냐, 게임오브젝트 속의 스크립트 컴포넌트 하나를 disable시키느냐의 차이라고 이해하자.


그럼 이게 Awake, Start, OnEnable과 무슨 상관일까?


-----------------------------------------


경우 1) 게임오브젝트가 Disabled 상태일때


위 스샷의 1번 체크박스를 Disable시켜둔 프리팹, 또는 씬 속의 게임오브젝트가 있다고 치자. 

예를 들어 이펙트를 씬에다 갖다놓고, disable 상태로 게임을 시작했다가, 플레이어가 적을 히트하는 등의 특정 행동에 다다르면 이펙트를 enable시켜서 재생하는 경우를 생각할 수 있겠다.


이 경우에는 게임오브젝트가 활성화되지 않은 것으로 간주, Awake가 호출되지 않는다. 즉, Awake는 게임오브젝트의 활성화 여부에 따라 호출된다.


게임 오브젝트가 애초에 비활성화 상태니, 그 하위 컴포넌트들의 초기화 함수인 Start 역시 호출되지 않는다. 


즉, 

A) 게임오브젝트를 비활성화한 상태로 게임을 시작하면, 또는

B) 프리팹 설정에서 1번 체크박스를 꺼놓은 프리팹을 게임 중에 Instantiate(prefab) 으로 생성할 경우,


Awake도, Start도, OnEnable도 모두 호출되지 않는다.


경우 2)게임오브젝트가 Enabled지만 컴포넌트는 Disabled 상태일때


위 스샷의 1번 체크박스는 켜져 있지만, 2번 체크박스는 꺼진 상태를 뜻한다.


이 경우 일단 게임오브젝트 자체는 활성화되어 있기에, Awake는 호출된다.


그러나 OnEnable() 과 Start()는 호출되지 않는다. 이 두 함수는 컴포넌트의 활성화 여부에 따라 호출된다.


게임이 돌아가는 도중에 체크박스를 켜거나 GetComponent<Component>().enabled = true 설정을 하면, 그 이후 OnEnable과 Start가 호출되게 된다.




전체적으로 재정리하면 다음 그림과 같다.



viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796491955f6d08b38a23d015d47be7998dbcdab151aa936d177



-----------------------------------------


Update와 StateMachineBehaviour


(*이 부분은 유니티 메카님을 좀 알아야 이해 가능. 메카님 생각보다 별로 안 어려우니 튜토리얼들 봐가면서 연습하자)


유니티의 상태 머신에는 각각 State에다가 StateMachineBehaviour라는 스크립트를 붙일 수 있다.


viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796495c07a1bf8d3ea33b06542cb92b149e74b9bd5a90f5f6f97d1c7d1c


이렇게. 각 상태별로 원하는 세팅과 업데이트를 해줄 수 있게 OnStateEnter, OnStateUpdate, OnStateExit 등의 유용한 함수가 있고, 


SubStateMachine에 들어갈 때를 구별해주기 위해 OnStateMachineEnter과 OnStateMachineExit라는 함수까지 있다.


그런데 다 좋은데, 유니티에서는 SubStateMachine 만의 Any State 노드가 없다보니 똑같은 조건의 Transition 라인을 수십, 수백개씩 그려줘야하는 그지같은 상황이 자주 발생하곤 했다.


안 그래도 캐릭터 애니메이션 부드럽게 하고 싶어서 만든 온갖 상태들의 개수가 30개 가까이 되는데 거미줄처럼 Transition이 자꾸 생기니 너무 빡쳐서,


결국 이번 작업에서는 transition 라인을 그리지 않고 animator.Play() 함수를 이용해 애니메이터를 관리할 생각이다.


여기서 상태 머신의 Enter, Update, Exit와 Monobehaviour의 Update 사이의 호출 관계를 정리해두고 싶어서 몇 가지를 실험해 봤다.


1)MonoBehaviour.Update가 먼저 호출되고 나서 Animator의 Update가 호출된다.


2)첫 실행 프레임에서 MonoBehaviour.Update->Animator Enter가 호출되고, 그 다음 프레임부터 Monobehaviour.Update->Animator.Update 순서로 호출된다.


viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796491955f6d08b38a23d015d47b67d648e226f1758b1bfe13e



3)animator.Play()를 써서 상태를 전이해도 StateEnter과 StateExit는 멀쩡하게 호출된다.




viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796491955f6d08b38a23d015d47e97a0713abcd3edcf69735e9


검은 네모 각각이 1프레임이다.


KeyDown이 되어 Animator.Play()를 호출하면, 해당 호출 프레임 내에서 곧바로 Exit->Enter가 호출된다!


그럼 Animator.Play()가 아닌, Animator.SetTrigger 등의 일반적인 Transition 조건을 걸어 애니메이션을 호출할 경우에는 어떨까?



viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796491955f6d08b38a23d015d47e97a5415f09a3e83f69735e9


이 경우에도 한 프레임 내에서 Exit/Enter가 모두 이루어진다.


Animator.Play()를 이용할때나 SetTrigger()등의 일반적인 Transition을 이용할때나, 


무조건 동일 프레임 내에서 Enter/Exit 처리가 모두 이루어진다는 귀중한 정보를 얻을 수 있었다.


4)animator.Play()를 이용해 Sub State Machine 속의 State를 재생할때도 마찬가지로 Enter/Exit가 모두 잘 작동한다!


여기서 참고로 조금 설명을 하자면...


Sub State Machine은 연관 있는 State들의 수납장과 비슷한 물건이다. 

Grounded 관련 모션은 Grounded SubState 속에, Air 관련 모션은 Air SubState 속에 넣어두거나 하면 보기에도 편하고 관리도 좀 더 쉬워진다.


근데 유니티에서는 SubState 내에서의 Any State 노드를 제대로 지원하지 않는다. 


이게 왜 문제일까?



viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796491955f6d08b38a23d015d47bd7aab7459712f68aa18af3a


위 그림과 같이, Grounded_A, B, C, ...Z State 각각에 대해, grounded = false 조건일때 Air Substatemachine으로 전이해야 한다고 가정하자.


이 경우 Grounded_A부터 Z까지 26개 상태 전부 다 Transition 하나하나를 찍찍 그어서 연결해야한다!!


grounded = false 외에 airattack = true일때 Air Substatemachine으로 전이하는 조건이 기획에 추가되었다면? 또 26번 줄을 그어야한다!


마땅한 방법이 없나 유니티 포럼에 물어보니 없다고 한다. Sub Statemachine 내부에 Any State 하나만 추가되어 있다면 해결되는 문제인데도, 지원을 안 한다. 아주 그지같다. animator.Play()를 쓰자고 마음먹은 가장 큰 이유다.


어쨌든, animator.Play()를 이용해서 Sub Statemachine을 바로 재생할 수는 없지만, Sub Statemachin 내부의 State를 재생할 수는 있다. 이 경우 Enter과 Exit는 제대로 호출될까?



viewimage.php?id=2abcdd23dad63db0&no=29bcc427b28677a16fb3dab004c86b6fae7cfdc6a7b78ad93aa3c1d4383a9e9b7796495c07a1bf8d3ea33b06542cb92b149e27bbbf5091a5a5ae7d1c7d1c


스샷과 같이, SubStateMachine으로 들어갈때, 한 프레임 내에서 Anim Exit와 SubState의 Enter과 SubAnim의 Enter까지 잘 호출되는 것을 확인할 수 있다.


여기서 주의할 점은, Enter과 Exit 노드를 안 쓰다보니, OnStateMachineEnter과 OnStateMachineExit는 호출되지 않는다는 점이다.


그러나 OnStateEnter과 OnStateExit는 잘 호출되니까 필요하다면 이걸 대신 이용하면 될 것이다.




--------------------------------------


이상 실험결과 정리 끝. 좀 헷갈리는 부분도 있고 어려운 내용도 있을텐데, 누군가(나 포함)에게는 아마 도움이 될 거라 생각하고 올려봄