오늘 배운 내용은 Animation, StateMachin, TopDownCamera, Lighting이다.
Mixamo
www.mixamo.com
Mixamo라는 사이트에서 무료로 캐릭터 모델과 애니메이션을 다운받을 수 있다. 강의에서는 YBot이라는 모델과, 3종류의 Idle, Walk 애니메이션을 다운받아 사용했다.
애니메이션 관련 기능을 구현할 때 새로 배운 것 중 하나는 StateMachine이다. 주로 상태 머신 혹은 유한 상태 머신이라고도 하는 것 같은데, 개념은 알고 있었지만 직접 다뤄본 것은 처음이었다. 스크립트를 작성하기도 했는데, StateMachineBehaviour를 상속받아서 구현했다. MonoBehaviour랑 비슷한 느낌의 OnStateEnter, OnStateUpdate 두 가지 함수를 사용했다. 그런데 이 함수와 관련된 정보는 약간 찾아보기가 힘들었다. 이렇다 할만한 좋은 정보글을 발견할 수 없었다. 내가 이해한 바로는, OnStateEnter는 스크립트가 등록된 State가 시작될 때 한 번 호출되고, 이후에는 State가 유지되는 동안 OnStateUpdate가 프레임마다 호출되는 듯 하다.
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// animator.IsInTransition(0) -> 현재 Base Layer에 있을 때
// If transitioning away from this state reset the random idle parameter to -1.
if (animator.IsInTransition(0) && animator.GetCurrentAnimatorStateInfo(0).fullPathHash == stateInfo.fullPathHash)
{
animator.SetInteger(_hashRandomIdle, -1);
}
// If the state is beyond the randomly decide normalised time and not yet transitioning
if (stateInfo.normalizedTime > _randomNormalTime && !animator.IsInTransition(0))
{
animator.SetInteger(_hashRandomIdle, Random.Range(0, _numberOfStates + 1));
}
}
구현 도중 animator.IsInTransition(0) 라는 코드를 작성했는데, index가 0인 Base Layer에서 현재 애니메이션이 전환 중인지(트랜지션 중인지)를 bool값으로 return해준다. animator.GetCurrentAnimatorStateInfo(0).fullPathHash == stateInfo.fullPathHash 이 부분이 살짝 이해하기 어려웠는데, AnimatorStateInfo에 아마 이 스크립트를 등록한 애니메이션의 State 정보가 저장되어있는 것 같다. 즉, 현재 animator가 이 스크립트가 등록되어 있는 애니메이션의 State와 같은지를 확인하는 그런 내용인 것 같다.
Debug로 간단히 테스트해보니 다른 애니메이션을 진행하다가 이 상태로 진입하게 되었을 때, 상태에는 진입하여 OnStateUpdate는 호출되지만, 애니메이션이 바로 전환되지는 않기 때문에 animator.GetCurrentAnimatorStateInfo(0).fullPathHash == stateInfo.fullPathHash의 값이 false로 나오는 것 같다. 아마 이 부분을 체크하기 위해 넣은 코드인 것 같다.
다음으로는 normalizedTime인데, stateInfo.normalizedTime은 현재 얼마나 애니메이션이 진행되었는지를 알려준다. normalizedTime이 1이면 1번 재생이 완료되었다는 얘기인 셈이다. _randomNormalTime은 위에서 0f~5f까지의 값을 받기 때문에 이 랜덤 값만큼 애니메이션이 완료되면 Idle의 애니메이션을 변경한다.
단, 여기서 강의에서는 Random.Range(0, _numberOfStates)라고 되어있었는데, 나는 _numberOfStates+1을 인자로 갖도록 했다. _numberOfStates는 Idle이 총 2개라 2의 값을 가지고 있는데, Random.Range는 2번째 인자보다 1 작은 값까지만 나오기 때문에 0과 1만 나오는 문제가 있었다. 문제는 Idle이 변경되는 경우가 이 값이 1이거나 2일 때였기 때문에, 0이 나오면 기존 애니메이션을 계속하지만 이미 normalizedTime이 _randomNormalTime보다 크기 때문에 다시 랜덤을 돌리게 되고, 결국 1로만 상태가 변환되는 문제가 있었다. 강의에서 강사님이 한 애니메이션만 반복되는 것을 보고 약간 당황하셨는데, 챕터 3에서 구현된 아바타를 보니 상태 전환이 0과 1일 때 하도록 변경되어있는 것을 발견했다.
readonly int moveHash = Animator.StringToHash("Move");
그리고 이런식으로 StringToHash라는 것을 사용하기도 했는데, String을 비교연산하는 것에는 비용이 많이 들기 때문에 Animation 관련 처리를 할 때는 int값을 가지는 Hash로 변환해서 작업을 하는 것 같다.
Animation 관련해서 고민한 내용은 이 정도인 것 같다. 확실히 글로 쓰다보니 내용을 정확하게 작성하기 위해서 더 알아보게 되는 것 같다. 위 내용이 길지 않지만 작성하는 데 2시간 정도 소요되었다...
이후에는 TopDownCamera를 구현해보았다. 단어 그대로 위에서 아래를 바라보는 카메라이다.
Vector3.SmoothDamp(transform.position, finalPosition, ref _referenceVelocity, _smoothSpeed);
여기서 새롭게 본 함수는 Vector3.SmoothDamp였다. transform.position에서 finalPosition까지 약 _smoothSpeed의 시간 동안에 걸쳐서 부드럽게 이동하는 함수인 것 같다. 이 과정에서 속도를 _referenceVelocity에 저장하는 듯 하다.
private void OnDrawGizmos()
{
Gizmos.color = new Color(1f, 0f, 0f, 0.5f);
if (_target)
{
Vector3 lookAtPosition = _target.position;
lookAtPosition.y += _lookAtHeight;
Gizmos.DrawLine(transform.position, lookAtPosition);
Gizmos.DrawSphere(lookAtPosition, 0.25f);
}
Gizmos.DrawSphere(transform.position, 0.25f);
}
그리고 기즈모를 그리기도 했다. 이전에 슈팅 게임에서 베지어 곡선을 구현하면서 기즈모를 그려봤는데, 이번에는 선 뿐만 아니라 구도 그려보았다.
또, 새롭게 Editor를 상속받는 스크립트도 작성해보았다. 에디터를 통해 Handles를 사용해 씬에서 직접 화면을 보며 스크립트의 값을 편리하게 조절할 수 있었다.
// Draw distance circle
Handles.color = new Color(1f, 0f, 0f, 0.15f);
Handles.DrawSolidDisc(targetPosition, Vector3.up, _targetCamera._distance);
Handles.color = new Color(0f, 1f, 0f, 0.75f);
Handles.DrawWireDisc(targetPosition, Vector3.up, _targetCamera._distance);
이 두 가지 중 위의 DrawSolidDisc가 빨간색 꽉 찬 원을 그리고, DrawWireDisc가 바깥쪽의 테두리를 그려준다.
Handles.color = new Color(1f, 0f, 0f, 0.5f);
_targetCamera._distance = Handles.ScaleSlider(_targetCamera._distance, targetPosition, -cameraTarget.forward, Quaternion.identity, _targetCamera._distance, 0.1f);
_targetCamera._distance = Mathf.Clamp(_targetCamera._distance, 2f, float.MaxValue);
또, ScaleSlider라는 것을 추가하여 씬 내에서 정육면체를 끌어당기면서 편리하게 값을 변경할 수 있었다. Clamp를 이용해서 최소값과 최대값을 제한하기도 하였다.
// Create Labels
GUIStyle labelStyle = new GUIStyle();
labelStyle.fontSize = 15;
labelStyle.normal.textColor = Color.white;
labelStyle.alignment = TextAnchor.UpperCenter;
Handles.Label(targetPosition + (-cameraTarget.forward * _targetCamera._distance), "Distance", labelStyle);
이외에는 이런 GUIStyle을 추가하고, 이 GUIStyle을 사용해서 라벨을 달기도 하였다.
이런 에디터 기능을 잘 활용하면 게임 설계를 좀 더 편리하게 할 수 있고, 디버깅도 훨씬 편하게 할 수 있을 것 같다. 나중에 게임 개발을 할 때, 이 에디터 기능을 활용한 툴을 만들어봐야겠다.
다음으로는 Lighting에 관련된 내용을 배웠다. 강의가 2개로 나뉘어있던데, 1번은 개념적인 부분에 대한 설명이 주된 내용이었다. Lighting이란 광원으로부터 빛이 나와서 오브젝트에서 반사되며 눈, 즉 화면으로 어떻게 보이는지 이런 것들을 다루는 것인 것 같다. 이렇게 직접 광원으로부터 나온 빛이 반사된 빛과 주변광으로부터 반사된 빛의 값을 합쳐서 화면에 표시한다고 한다. 빛의 효과도 여러 가지가 있고, Lighting을 할 때 드는 비용이 많이 들다 보니, 최적화를 위해서 움직이지 않는 오브젝트들은 LightMap이라고 하는 텍스쳐에 미리 Baking을 해서 저장해두고 이것을 씌워서 보여주는 방식으로 작동하기도 한다는 것 같다. 다만 이 방식을 사용하면 효율은 좋아지겠지만, 디테일이 떨어질 수 있어보였다. 하지만 아무래도 빛이 얼마나 디테일한가보다는 게임의 최적화가 중요할테니 이 기능을 많이 활용하게 될 것 같다는 생각이 들었다.
오늘 공부한 내용은 여기까지다. 카메라가 따라다니는 기능이 굉장히 신기하고 재미있었다. 이외에도 많이 배웠는데... 배울 내용이 너무 많은 것 같다! 사실 지금까지 배운 내용들도 깊게 파고 들어가면 훨씬 어렵고 복잡할텐데, 앞으로도 더 많은 개념들이 남아있는 것이 참... 쉽지 않게 느껴졌다. 검색하면서 다른 개발자분들이 정리한 내용들도 내가 모르는 내용들이 너무 많아서, 아 참 갈 길이 멀구나. 배울 것이 많구나. 많이 부족하구나. 하는 것을 느꼈다. 그래도 뭐... 당장 모든 내용을 알 필요는 없을 것 같다. 기초적인, 범용적인 것들을 익히고, 필요한 것은 그때그때 익힐 수 있을 정도만 되도 좋을 것 같다. 더 노력해야겠다. 그리고 강의가... 나쁘진 않은데, 설명이 부족한 부분들이 꽤 많아서 혼자 이런 부분들을 채워가는 게 조금 어려운 것 같다. 그래도 그만큼 직접 공부하는 것이니 더 공부가 잘 되는 것 같기도 하다. 하나하나 설명해주면서 떠먹여주면 오히려 기억에 잘 안남을 것 같기도 하고... ㅋㅋㅋ. 약간의 어려움이나 스트레스가 좋은 자극이 되는 것 같다.
'개발 > 개발일지' 카테고리의 다른 글
3D 게임 개발 공부 4 - State Machine을 사용하여 적 캐릭터 AI 구현 (0) | 2023.04.07 |
---|---|
3D 게임 개발 공부 3 - Lighting(2), Terrain, Navigation (0) | 2023.04.06 |
3D 게임 개발 공부 1 - region, RigidBody, CharacterController, Nav Mesh Agent (1) | 2023.04.04 |
3D 게임 개발 공부 0 - 시작 (0) | 2023.04.04 |
Tales Saga Chronicle Blast 개발일지 23 - 일본어 로컬라이징 (0) | 2023.04.02 |