오늘 배운 내용은 Animation, StateMachin, TopDownCamera, Lighting이다.

 

Mixamo

 

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을 해서 저장해두고 이것을 씌워서 보여주는 방식으로 작동하기도 한다는 것 같다. 다만 이 방식을 사용하면 효율은 좋아지겠지만, 디테일이 떨어질 수 있어보였다. 하지만 아무래도 빛이 얼마나 디테일한가보다는 게임의 최적화가 중요할테니 이 기능을 많이 활용하게 될 것 같다는 생각이 들었다.

 

 

 

오늘 공부한 내용은 여기까지다. 카메라가 따라다니는 기능이 굉장히 신기하고 재미있었다. 이외에도 많이 배웠는데... 배울 내용이 너무 많은 것 같다! 사실 지금까지 배운 내용들도 깊게 파고 들어가면 훨씬 어렵고 복잡할텐데, 앞으로도 더 많은 개념들이 남아있는 것이 참... 쉽지 않게 느껴졌다. 검색하면서 다른 개발자분들이 정리한 내용들도 내가 모르는 내용들이 너무 많아서, 아 참 갈 길이 멀구나. 배울 것이 많구나. 많이 부족하구나. 하는 것을 느꼈다. 그래도 뭐... 당장 모든 내용을 알 필요는 없을 것 같다. 기초적인, 범용적인 것들을 익히고, 필요한 것은 그때그때 익힐 수 있을 정도만 되도 좋을 것 같다. 더 노력해야겠다. 그리고 강의가... 나쁘진 않은데, 설명이 부족한 부분들이 꽤 많아서 혼자 이런 부분들을 채워가는 게 조금 어려운 것 같다. 그래도 그만큼 직접 공부하는 것이니 더 공부가 잘 되는 것 같기도 하다. 하나하나 설명해주면서 떠먹여주면 오히려 기억에 잘 안남을 것 같기도 하고... ㅋㅋㅋ. 약간의 어려움이나 스트레스가 좋은 자극이 되는 것 같다.

+ Recent posts