오늘은 어제 못한 Lighting의 나머지 부분과 Terrain, Navigation, 적 캐릭터의 AI와 FSM(Finite State Machine) 구현 일부를 했다.

 

가장 중요해보이는 내용은 Light Probe였다. 이전에 몇 번 사용했는데, 그냥 Light를 사용할 때 배치하는 것 정도로 막연히 알고 있었다. 실제로는, 저기 보이는 노란색 구, Light Probe 하나하나에 주변의 빛에 대한 정보가 저장되어 동적인 오브젝트가 가까이 다가오면 빛에 대한 효과를 주는 것이었다. 이전의 LightMap은 정적인 오브젝트에 대한 빛을 저장하고, 이 Light Probe는 일정 지점마다 빛에 대한 정보를 저장해서 동적인 오브젝트에 대한 빛의 처리를 하는 셈이다. 따라서, 빛이 없거나 동적인 오브젝트가 접근하지 못하는 곳에는 배치를 하지 않고, 빛에 대한 처리가 필요한 곳은 촘촘하게 배치하는 식으로 Lighting 작업을 훨씬 효율적으로 처리할 수 있게 해주는 기능이다. 이 Light Probe도 Bake를 해주어야 했는데, Lighting을 Bake하는 데에는 1분 정도의 시간이 걸렸지만 Light Probe는 생각보다 금방 Bake가 되었다.

그 다음은 Reflection Probe에 대해서도 배웠다. 거울 같은 효과를 줄 수 있는 기능이다. 이것도 다른 것과 마찬가지로 Bake를 해서 미리 정보를 저장시켜놓아 효율적으로 사용할 수 있고, 혹은 실시간으로 렌더링을 하게끔 설정할 수도 있다고 한다. 게임 내에서 거울로 설정을 할 수도 있을 것 같다. 일부 게임에서는 거울이 있긴 하지만 캐릭터가 가까이 가도 비치지 않는 경우가 있는데, 성능 상의 이유로 미리 Bake하여 사용해서 그런 것 같다. 이외에도 철로 된 갑옷에 텍스쳐를 입혀 주변 물체가 반사되어 보이는 효과도 낼 수 있다는 것 같다.

다음으로는 Terrain에 대해서 배웠다. 지형인데, 이전에 배운 적이 있어서 편안하게 들었다. 새롭게 알게 된 내용은, 중앙 좌측에 보이는 문처럼 오브젝트를 배치할 때, Terrain에 Hole을 뚫어서 배치한다는 내용이나, 나무, 풀 등도 Light Probe의 영향을 받는다는 것이었다.

이외에는 TreeIt이라고 하는, 무료로 나무 모델을 만들 수 있는 프로그램에 대해서도 소개를 받았다.

Navigation의 기능에 대해서도 더 자세히 배웠다. 이전에 Nav Mesh에 대해서 배웠는데, 이를 더 잘 활용할 수 있는 방법과 같은 것이었다. Off Mesh Link를 사용해서 Navigation 기능을 활용할 때 지형을 올라가거나 내려갈 수 있고, Nav Mesh Obstacle 컴포넌트를 등록해서 장애물로 활용하며 길을 막는 문 등을 만들 수도 있다.

One Page Dungeon이라는 무료 사이트에 대해서도 소개받았다. 엔터키를 누르면 손쉽게 던전에 대한 컨셉이나, 배치 등을 짜임새있게 랜덤으로 짜주는 좋은 사이트다. 게임으로 던전을 만들 때 참고하면 좋을 것 같은 사이트다.

ProGrids에 대해서도 배웠다. 유니티 패키지에 정식으로 등록되어 있는 기능이다. 위의 사진처럼 격자를 만들어주고, 오브젝트를 움직일 때 일정 간격만큼 움직이게 할 수 있다. 위에서는 1로 등록되어있어서 움직이면 Position값이 1로 나누어 떨어지게끔 움직인다. 유니티의 기본 움직임은 굉장히 작은 소수점 단위로도 움직이기 때문에 오와 열을 맞추기가 굉장히 어려운데, 이 기능을 사용하면 던전 배치를 훨씬 쉽게 할 수 있을 것 같다.

 

 

오늘은 약간 기능들에 대한 개념적인 부분을 많이 배운 것 같다. AI 구현 모델과 FSM(Finite State Machine)에 대해서도 배우고 기본적인 구현을 하기도 했는데, 아직 구현할 부분이 좀 남은 것 같고, 현재까지 구현한 부분은 코드가 어렵지 않아서 정리할 내용이 많지 않아 강의를 조금 더 듣고, 적 AI 구현에 관한 내용을 한 글에 정리할 예정이다.

 

슬슬 윤곽이 잡혀가는 것 같다. 앞으로 남은 부분은 4장 / 적 AI 구현, 5장 / 전투 시스템(근접 공격, 원거리 공격, NPC 전투?), 6장 / 인벤토리, 아이템, 상점, 장비 교체, 7장 / 플레이어 상태창, 다이얼로그, 퀘스트, 레벨 디자인 등이다. 생각보다 진행이 순조로워서 빠르면 일주일 안에 마무리할 수 있을 것 같다. 이후 게임을 직접 구현하면서 더 배우고, 간단하게 포트폴리오를 만들 생각이다. 더 부지런히 배워야겠다.

마작 게임 작혼에서 작걸을 달성했다. 롤로 따지면 실버~골드 언저리쯤 될 것 같다.

동장전보다는 반장전 위주로 플레이했다.

친선전까지 포함하면 반장전만 300판 넘게 했는데 아직 역만을 한 번도 못해봤다. 최고 역이 배만이다. 일찍 끝나는 경우도 있지만 연짱 등도 있으니 평균적으로 반장전 한 번에 6판은 넘게 할 것 같은데, 1800판이 넘는다. 역만 출현 확률이 0.15~0.2%정도라는 것을 감안하면 3회 정도는 했어야 할 것 같은데... 아직까지 기약이 없다.

 

동풍전 340판 + 반장전 287판 한 친구는 대삼원, 자일색, 심지어 구련보등까지 해봤는데... 음... 나에게도 곧 좋은 기회가 찾아오겠지?

'잡담' 카테고리의 다른 글

합정 애니플러스 & 홍대 애니메이트 방문  (0) 2023.04.16
작혼 첫 역만 달성 - 쓰안커  (0) 2023.04.07
남을 돕는 일과 행복  (0) 2023.04.05
근황과 2023년 목표  (0) 2023.03.10
JLPT N1 합격 후기  (6) 2023.01.24

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

 

 

 

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

남을 돕는 일은 천국에 재산을 쌓는 일이라는 얘기가 있다. 실제로 그러한지 입증할 수는 없는 일이지만, 나는 적어도 남을 돕는 일이 세상에 아름다움을 쌓는 일이라고 생각한다. 여기서 조금 더 나아가서 행복해질 수 있는 방법이라고도 할 수 있을 것 같다.

 

세상을 살다 보면 종종 이기적인 사람을 찾아볼 수 있다. 남이 베푸는 것을 받기만 하고 본인이 남에게 베푸는 것은 꺼리는 사람이다. 이런 사람들을 Give&Take에서 따와 Taker라고 부르기도 한다. 사람마다 생각은 각기 다르겠지만, 아마 본인이 남에게 베푸는 것을 본인의 손해라고 생각하는 기조에서 나온 행동이 아닐까 싶다. 정도나 경우에 따라 조금 다르겠지만 대부분의 경우 남을 돕는 일이 본인에게 이득이 되는 행동이라고 생각한다.

 

먼저 지인을 돕는 경우에 대해 생각해 보자. 지인과는 지속적인 교류가 있을 것이다. 만약 지인에게 도움을 받았는데 지인이 도움을 요청했을 때 외면하게 된다면, 향후 그 지인으로부터 도움을 받기 힘들 것이다. 인간관계는 서로 얽혀있는 경우가 많기 때문에 이런 일이 반복된다면 다른 지인에게 도움을 받기도 어려워질 것이고, 대인관계에 어려움을 겪게 될 수도 있다. 남의 도움을 받지 않고 스스로 살아가면 되지 않느냐는 이야기도 하는 사람도 있겠지만, 아마 남의 도움 없이 살아갈 수 있는 사람은 없다고 생각한다. 본인의 인생을 되돌아보면 살아가면서 알게 모르게 남에게 크고 작은 도움을 받아오며 지금까지 살아왔을 것이다. 누군가의 작은 도움이 나에게는 큰 도움이 되는 경우가 많기 때문에 이런 것을 잃게 된다는 것은 큰 손해라고 얘기하지 않을 수가 없다. 내가 남에게 베푸는 1의 도움은 나에게는 여유분이고, 내가 받게 될 다른 사람의 1의 도움이 나에게는 부족함을 메우는 10의 도움이 될 것이기 때문이다.

 

내가 겪은, 다른 사람의 작은 도움이 큰 도움이 되는 경우에 대해서도 간단히 이야기하고 싶다. 올해 초 겨울에 스키장에 갔을 때의 일이다. 스키를 타다가 넘어져서 스키가 벗겨지는 경우가 있다. 스키는 그 자리에 박히고 나는 넘어져서 아래로 미끄러져 내려가 나는 아래에, 스키는 저 위에 놓여있게 된다. 나 스스로 올라가려면 경사에, 불편한 스키 부츠를 신고 한참을 힘들게 올라가야겠지만, 이런 경우 스키를 타고 내려오는 다른 사람이 스키를 가져다준다. 물론 안전을 위해서도 필요한 행동이지만, 다른 사람의 1분도 채 안 되는 시간의 도움으로 인해 나의 수십 분을 아낄 수 있는 셈이다. 이렇게 도움을 받은 사람은 자신이 받은 도움을 기억하며 같은 처지에 처한 다른 사람을 발견하면 도와줄 것이다. 일종의 선순환인 셈이다. 나 또한 이때 스키를 타면서 넘어진 사람과 친구를 도와주기도 하였다. 이외에도 중학생 때 중고폰을 살 때, 어느 아저씨께서 학생이니까 만 원을 깎아주시겠다고 하신 적이 있다. 아마 아저씨께는 큰돈은 아니셨겠지만, 당시 학생이었던 나에게 만 원은 굉장히 큰돈이었다. 아직까지도 기억에 남아 나도 같은 상황에서 같은 행동을 하겠노라고 늘 다짐한다.

 

이런 사례를 통해서 전혀 모르는 타인을 돕는 행위에 대해서도 말해보겠다. 종종 매체에서는 타인에게 도움을 줬는데 알고 보니 면접관이었다든지, 지인의 가족이었다든지 하는 식으로 비치기도 한다. 하지만 실제로 이러는 경우는 거의 없다고 봐도 무방할 것이다. 타인은 지인과는 다르게 내가 돕는다고 해서 이 사람에게 무언가 보답을 받을 것을 기대하기는 어렵다. 하지만 보답을 받은 사람은 감사를 느낄 것이고, 모든 사람이 그렇진 않겠지만 자신이 놓였던 것과 같은 처지에 놓인 사람이 도움을 요청하면 도우려고 할 것이다. 이렇게 도움을 주고받는 사람들이 점점 늘어나면서 남을 돕는 풍조가 생기면 내가 도움을 받을 기회도 늘어날 것이다. 더욱이 도움을 주는 행위 자체에서도 뿌듯함과 같은 긍정적인 마음을 느낄 수 있고, 다른 사람이 도움을 주는 행동을 보는 행위와 같이 훈훈함을 자아내는 광경을 더 많이 볼 수 있게 될 것이다.

 

 

정리하자면, 남에게 도움을 받기만 하고 돕지 않는 행위는 본인만 이득을 챙기는 행위가 아니라 손해를 보는 행위라고 생각한다. 오히려 남을 돕는 행위가 나에게 이득이 될 가능성이 높다. 이것은 단지 지인을 도울 때만 해당하는 것이 아니라, 전혀 모르는 타인을 도울 때도 마찬가지다. 남을 도움으로써 세상에 아름다움이 쌓이고, 결과적으로 본인도 행복해지는 길이라고 말할 수 있겠다. 물론 덮어놓고 무작정 남을 돕기만 하자는 것은 아니다. 내가 여유가 있을 때 기꺼이 도울 수 있는 범위 내에서 다른 사람을 돕는다면 세상은 더 아름다워질 것이고, 더 행복해질 수 있으리라.

'잡담' 카테고리의 다른 글

작혼 첫 역만 달성 - 쓰안커  (0) 2023.04.07
작혼 작걸 달성  (0) 2023.04.05
근황과 2023년 목표  (0) 2023.03.10
JLPT N1 합격 후기  (6) 2023.01.24
AI그림의 발전과 나의 인식 변화, AI그림의 미래에 대한 생각  (0) 2022.12.23

오늘 큰 틀에서는 RigidBody, CharacterController, Nav Mesh Agent 이 세 가지를 다루는 법에 대해서 배웠다.

 

강의를 듣는 와중에, #region과 #endregion을 보고 이건 뭘까 싶어서 찾아보니 코드를 정리할 수 있는 좋은 기능이었다. #region와 #endregion 사이에 있는 코드를 한 번에 닫고 열 수 있고, 태그를 달 수 있기 떄문에 특정한 기능을 수행하는 다양한 함수들을 묶어서 작업을 할 때 필요없는 부분은 접어놓는 식으로 편리하게 작업할 수 있었다. 또, 어느정도 주석의 역할도 할 수 있는 것 같다. 예를 들어서 192~357줄까지는 UI 관련된 작업을 하는 함수, 1489~1537줄까지는 스토리 관련 작업을 하는 함수, 1538~1673줄까지는 텍스트 작업을 하는 함수 등으로 나눠놓으면 어떤 역할을 하는지 알 수 있다. 또, 같은 역할을 하는 함수끼리 모아놨기 때문에 코드의 가독성도 많이 좋아질 것 같다.

이런식으로 접어놓으면 1700줄짜리 코드가 한 눈에 들어온다! 먼저 이런 #region을 사용해서 작성했던 코드들을 묶어서 정리하는 작업을 했다.

 

그 다음에는 Rigidbody에 대한 강의를 들었다. 전에도 몇 번 사용해 보았지만 새롭게 알게 된 내용들이 꽤 있었다. 먼저 캐릭터의 발 끝을 기준으로 잡기 위해서 Collider와 캡슐의 y값을 1씩 올려준다는 것. 캐릭터가 바닥에 닿아있는지 확인하기 위해서 RaycastHit를 사용하는데, 캐릭터의 발이 바닥에 끼거나 살짝 들어가있을 때 등의 문제를 방지하기 위해서 y값이 0.1만큼 높은 지점에서부터 체크한다든가 하는 것이다. 이런 경험을 바탕으로 얻어낸 지식들을 알 수 있어 좋았다.

 

이외에는 제곱근을 구하는 Mathf.Sqrt() 라든지, 밑이 e인 로그값을 구하는 Mathf.Log 등의 함수를 사용하는 것을 보았다. 점프나 대시에 사용되었는데, 적절한 이동속도를 뽑기에 적합한 함수라 그런 것일까? 싶은 생각정도까지 했다. 찾아보니 비슷한 얘기가 있었다. 아마 점프에는 Sqrt()를 사용하고, 대시에는 Log()를 사용하면 적절한 값이 나오는 것 같다.

 

이외에 약간 이해가 안 가는 부분은 Time.deltaTime와 관련된 내용이었다. Time.deltaTime은 프레임과 상관없이 동일한 결과를 내기 위해 사용하는 것으로 알고 있는데, 한 식에 두 번이나 들어가서 살짝 의아했다.

Vector3 dashVelocity = Vector3.Scale(transform.forward, _dashDistance
                * new Vector3(Mathf.Log(1f / (Time.deltaTime * _rigidbody.drag + 1)) / -Time.deltaTime, 0, 
                Mathf.Log(1f / (Time.deltaTime * _rigidbody.drag + 1)) / -Time.deltaTime));

 

내가 이해한 바로는 Time.deltaTime은 직전 프레임을 완료하는데 걸린 시간의 값을 가지는 것으로 알고있다. 때문에, 60프레임이라고 가정하면, Update 함수가 초당 60번 호출될 것이고, Update에서 어떤 값, 예를 들면 speed에 Time.deltaTime을 곱해주면 매 호출마다 1/60이 곱해진 꼴이라 이것이 60번 실행되면 1초에 speed의 값 만큼만 이동을 할 것이다. 30프레임이 나오는 경우도, 초당 30번으로 절반의 횟수만큼만 호출되지만 여기는 1/30으로 2배에 해당하는 값을 곱해주기 때문에 결국 똑같은 결과가 나오는 셈이다.

 

그냥 단순히 적절한 dashVelocity 값을 구하고 Time.deltaTime을 곱해주면 안되나? 하는 생각도 들었다. 강의의 아쉬운 점이 이런 부분인 것 같다. 질문을 할 수가 없으니... 대충 당장은 이 분은 이렇게 구현하셨구나 정도로 이해하기로 했다. 아마 나중에 직접 게임을 개발하면서 이유를 꺠달을 수도 있을 것 같다.

 

이외에는 FixedUpdate() 관련된 내용도 알게 되었다. Update()는 프레임에 따라서 호출이 불규칙하기 때문에, rigidbody를 움직일 때는 FixedUpdate()를 쓰는 것이 좋다고 한다. 프레임이 끊겨서 호출되지 않은 사이에 지형을 통과한다든지 이런 문제가 발생할 수도 있는 것 같다. FixedUpdate()는 프레임과 상관없이 일정한 간격으로 호출되기 떄문에 이런 문제를 방지할 수 있는 것 같다.

 

private void FixedUpdate()
    {
        if(_inputDirection != Vector3.zero)
        {
            _rigidbody.MovePosition(transform.position + _inputDirection * _speed * Time.fixedDeltaTime);
        }
    }

 

그리고 이 부분을 수정했다. 원래 예제 코드에서는 if문이 없는데, 입력이 없을 때 캐릭터가 혼자서 회전을 하는 문제를 해결하기 위해 추가했다. 입력이 없어서 _inputDirection의 값이 없어도 _speed와 Time.fixedDeltaTime에 의해서 조금씩이나마 움직이는 문제가 있어서 입력이 없을 때는 작동하지 않도록 설정했다.

 

 

Rigidbody에 대해 배우고 고민한 내용은 이정도인 것 같다.

 

다음은, Rigidbody가 아니라 CharacterController를 사용하여 똑같은 기능을 구현하였다. 아, 강의 자료에 코드가 전부 있었는데, 직접 코드를 작성하면서 생각하는 점이나 느끼는 점도 있어서 기존의 코드들은 빼두고 직접 작성했다.

 

먼저 CharacterController는 Rigidbody를 사용했을 때와 비슷한 결과를 낼 수 있지만, 기본적인 물리 법칙을 적용받지 않아서 좀 더 자유롭게 설계를 할 수 있는 것 같다. 예를 들면 Rigidbody를 적용한 오브젝트는 공중에 두면 중력으로 인해 바닥으로 떨어지고, 움직이면 공기 저항을 받기도 하지만 CharacterController는 이런 물리 법칙이 적용되지 않는다. 때문에 스크립트에서 직접 중력으로 인해 바닥에 떨어지는 기능 등을 구현해야한다. 조금 번거롭긴 하지만, 그만큼 자유롭게 다양한기능을 추가할 수 있는 것 같다.

 

그리고 Rigidbody에서는 땅에 닿아있는지 체크를 하기 위해 직접 함수를 구현했는데, CharacterController에는 자체적으로 isGrounded라는 값이 있어서 편리하게 값을 처리할 수 있다.

 

단, 문제가 있었는데 점프 키가 제대로 작동되지 않는 문제였다. 테스트를 해보니 1초에도 수십 번씩 땅에 닿아있다가, 공중에 떠있다가 상태가 계속 변경되었다. 여러 방면으로 찾아보니 해결책은 CharacterController의 Min Move Distance의 값을 0으로 수정해주는 것이었다. 기존 값은 아마 0.001인데, 이 이하의 움직임은 무시하라는 의미다. 찾아보니 isGrounded는 이전 움직임에서 지면과 충돌했는지에 따라 값이 변한다는 것 같았다. 아마 미세하게 움직이면서 충돌을 감지해야 하는데, Min Move Distance로 인해 이 미세한 움직임이 무시되어서 그런 것 같다. 아무튼 Min Move Distance의 값을 0으로 수정해주니 정상적으로 작동했다.

 

CharacterController는 따로 물리 엔진을 적용해줘야 하는 번거로움이 있기 때문에 아마 rigidbody를 더 많이 사용하게 될 것 같다. 하지만 좀 특별한 움직임을 하는 오브젝트를 만들 필요가 있다면 CharacterController를 활용할 수 있을 것 같다.

 

그 다음으로는 Nav Mesh Agent에 대해서 배웠다. 이런 게 있는 줄은 처음 알았다. 게임에서 자동 진행이라든지, 자동 이동 등 길찾기가 필요한 경우가 종종 있는데, 이런걸 어떻게 구현했을까? 종종 생각을 했는데, Unity에 쉽게 길찾기를 도와주는 기능이 있었다. 캐릭터가 오브젝트의 윗부분에서 움직일 수 있는지 정보를 받고, 물체와의 최소 거리(캐릭터의 크기)만큼 거리를 띄워서 장애물에 걸리지 않게, 낭떠러지에 떨어지지 않게끔 이런 부분을 제외한 나머지 땅을 캐릭터가 이동할 수 있는 땅으로 Mesh를 Bake하는 것 같다. 위의 사진에서 파란색에 해당하는 부분이 캐릭터가 이동할 수 있는 땅이다. 그리고 지정된 위치까지 가기 위해서 이 Bake된 Mesh를 바탕으로 길찾기를 진행하는 것 같다. 참 편리한 기능이다.

 

강의 내에서는 마우스 클릭을 입력받아 마우스로 지정한 위치로 이동하는 기능을 구현했다. 디아블로도 이런식으로 움직이기 때문에 아마 이렇게 구현한 것 같다.

 

이를 구현할 때 Ray와 Raycast를 사용했다. 레이캐스트에 관한 내용은 해당 블로그를 참조했다.

 

유니티 레이캐스트 Raycast 충돌 / Ray의 모든 것 :: Chameleon Studio (tistory.com)

 

유니티 레이캐스트 Raycast 충돌 / Ray의 모든 것

해당 티스토리 페이지는 필자가 유니티 C# 개발을 하면서 학습한 내용들을 기록하고 공유하는 페이지입니다 ! - 틀린 부분이 있거나, 수정된 부분이 있다면 댓글로 알려주세요 ! - 해당 내용을 공

chameleonstudio.tistory.com

 

// Process mouse left button input
        if (Input.GetMouseButtonDown(0))
        {
            // Make ray from screen to world
            Ray ray = _camera.ScreenPointToRay(Input.mousePosition);

            // Check hit from ray
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100, _groundLayerMask))
            {
                Debug.Log("We hit " + hit.collider.name + " " + hit.point);

                // Move our character to what we hit
                _navMeshAgent.SetDestination(hit.point);
            }

 

 

요컨대 이런 식으로 카메라에서 제공하는 기능을 통해 스크린에 찍힌 방향으로 ray를 만들어주고, 해당 방향으로 Ray를 발사해서 최대 100까지의 거리 이내에 _groundLayerMask (땅이 등록되어 있음)를 만나면 RaycastHit라는 타입의 변수에 결과를 저장하는 듯 하다. 이 안에는 hit.collider로 어떤 collider와 만났는지, hit.point로 어떤 좌표에서 만났는지 등의 정보를 저장하는 듯 하다. 이를 통해서 네비메쉬의 목적지를 정할 수 있다. Update에서 이런 작업이 끝난 이후에 LateUpdate()에서 transform의 위치를 이동시켜주는 식으로 구현이 되었다.

 

이것도 검색해보니 굉장히 편리하고 좋은 기능인 것 같지만 만능은 아닌 것 같다. 상황이 받쳐준다면 편리하게 사용하기에 좋은 기능인 것 같다.

 

 

 

아무튼, 오늘 공부했던 내용 정리가 끝났다. 인상깊은 부분이나 새롭게 알게된 것들을 위주로 적었는데도 생각보다 내용이 꽤 많다. 피곤하기도 하여 글이 조금 두서가 없을 수도 있겠다. 나중에 이런 내용들을 깔끔하게 정리해볼 수 있는 기회가 있으면 좋겠다. 근데 아마 안 할 것 같다. ㅋㅋ. 그래도 글로 적으면서 내가 몰랐던 부분에 대해서 더 찾아보게 되고, 더 잘 알 수 있게 된 것 같다. 오늘은 강의 3개를 정리했으니 내일은 가능하면 5개로 할 수 있으면 좋겠다. 그러나 욕심은 내지 말아야겠다. 괜히 무리하게 분량을 채우려고 하다가 내용을 다 습득하지 못하는 것보다는 적더라도 확실하게 알고 넘어가는 것이 좋겠다. 앞으로도 오늘같은 느낌으로 왜 이런 코드를 작성했는지, 왜 이런 기능을 사용하는지 등등을 생각하고 찾아보면서 정리해야겠다.

이전에 슬슬 취업을 다시 한 번 도전해봐야겠다 싶었다고 글을 썼다. 근데 생각이 조금 바뀌었다. 아직까지 2D 게임 개발 경험밖에 없다는 문제가 있었다. 지금이라면 충분히 2D 게임을 만드는 회사에는 도전해볼만 하지만, 3D 게임을 만드는 회사에 도전하기는 힘들지 않을까 싶었다. 3D를 다뤄본 적도 있긴 하지만, 딱히 이렇다 할 결과물은 없다는 것이 문제다.

그리고 참고를 위해 다른 게임 프로그래머들의 포트폴리오를 찾아봤는데, 우와 이런 것도 만들었구나 하는 생각이 들기도 하였고, 내가 약간 모자라보여서 주눅이 들기도 하였다. 그러다 좀 지나서 생각이 바뀌었다. 나도 저렇게 해봐야겠다고. 그래서 3D게임 개발을 경험해보기로 했다. 2D 게임을 전문적으로 개발하는 회사에 들어가는 것도 나쁘진 않겠지만, 만약 그렇게 되더라도 3D 개발 경험이 있는 것과 없는 것에는 큰 차이가 있을 것이라고 생각한다. 그래서 여유가 있을 때 경험해보기로 하였다.

 

얼마전 구입했던 강의로 공부를 시작하기로 했는데, 간략하게 구입한 강의의 정보나 느낀점 등을 적어보겠다.

아, 글을 쓰기에 앞서 내 돈 주고 구매했고, 이벤트 같은거 참여하기 위해서 쓴 글도 아님을 밝힌다. 지극히 개인적인 소감을 적을 것이다.

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스 (fastcampus.co.kr)

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

fastcampus.co.kr

 

먼저 제일 먼저 관심을 가지게 되었던 건 이 강의다. 포트폴리오를 어떻게 준비하면 좋을까 싶어서 찾아보다가 발견하였다. 내가 매력적으로 느꼈던 부분은 3D 게임을 개발해볼 수 있겠다는 점과 실제 신입 개발자가 작성한 포트폴리오를 보여주며 어떤 방향으로 작성해야 할지 알려준다는 부분이었다. 당시 포트폴리오로 어떤 내용을 넣어야 할지, 어떤 방향으로 작성해야 할지 고민을 많이 했었다. 검색해보면 관련 내용이 아주 없는 것은 아니지만, 필요한 내용을 찾기가 굉장히 어려웠다. 돈을 받고 파는 강의는 중요하고 필요한 부분을 알려줄테니 내가 필요한 정보를 빠르고 정확하게 얻을 수 있겠다는 생각을 했다. 그런데 가격이 좀 비싸서 고민이 되었다. 괜찮은 선택인가 싶어서 검색도 해봤다. 별로 좋지 않다는 악평도 있었고, 어렵긴 하지만 굉장히 도움이 된다는 내용도 있었다. 무슨 환급 이벤트에 참여하기 위해 작성한다는 호의적으로 작성된 글도 있었다. 고민을 하다가 혹시나 별로라고해도 이렇게 실제 현업에서 일한 분들이 알려주는 것을 배울 기회가 얼마나 더 있겠느냐는 생각이 들었다. 아무것도 안하는 것보단 무엇이든 해보는 것이 좋겠다 싶은 생각도 들었다. 거기에 이 강의를 발견하기 전날에 딱 이 강의의 가격만큼의 돈이 생겨서 어쩌면 이를 위한 것은 아니었을까 싶은 생각에 홀린 듯이 구매했다. 생각해보면 종종 이런 경우가 있다. 뭔가 필연적으로 연결된 것 같은 느낌을 강하게 받을 때가 있는데, 항상 그 느낌을 따를 때 좋은 결과를 보았던 것 같다. 이 강의를 구매한 선택도 지금 생각해보면 굉장히 옳은 선택이었던 것 같다.

구매할 때는 다른 강의 2개도 포함되어 있는 패키지로 구매했는데, 나에게 필요한 내용이 있음은 분명하나 지금 당장 필요한 내용은 아니라고 생각하여 아직 보진 않았다.

 

강의를 구매하고 가장 먼저 본 것은 포트폴리오 관련 내용이었다. 신입 개발자가 본인이 예전에 잘못 작성했던 포트폴리오를 잘못된 예시로 보여주면서 고치는 내용을 보여주고, 실제 본인의 포트폴리오를 보여주면서 포트폴리오를 작성할 때 어떤 점을 고려해야 하는지 방향성을 알려준다. 이 부분이 굉장히 도움이 되었다. 원래는 포트폴리오를 작성할 때 그냥 겉보기에만 신경을 썼는데, 막상 실제 포트폴리오를 보니 굉장히 겉보기는 초라해보여도 필요한 내용만이 딱딱 들어가있었다. 그리고 이런 것이 훨씬 좋은 포트폴리오라는 생각이 들었다. 그러면서 나는 개발하면서 어떤 기술들을 적용해보면 좋을까 고민하면서 찾아보았고, 그 결과 같은 개발물을 만들더라도 더 많이 배우고 더 성장할 수 있었던 것 같다.

 

이후에는 파트1인 핵심 개념 관련 내용을 보았다. JSON, 다국어 처리, 유니티 커스텀 에디터... 등등 이런 개념적인 부분들을 알려준다. 조금은 생소한 내용이기도 하고, 자세히 알려주는 것이 아니라 단순히 개념 정도를 알려주는 수준이기 때문에 별 도움이 되지 않는다고 느낄 수도 있을 것 같은데, 나에게는 이것도 큰 도움이 되었다. '아, 이런 것도 있구나' 하는 키워드를 얻은 셈이기도 하고, 실제 현업에서 사용되는 기술들이기 때문에 이런 걸 어떻게 적용해보면 좋을까 같은 생각을 하고 찾아보면서 많은 공부가 되었다.

 

그리고 오늘은 3D RPG 게임을 만드는 부분을 보았다. 확실한 것은 기본 지식이 있어야 들을만한 강의라는 점을 느꼈다. 필요한 부분은 설명을 해주지만, 간략한 설명만 해주기 때문에 자세한 부분은 직접 찾아보아야 한다. 이 부분은 왜 이렇게 구현했을까? 이 함수는 어떤 역할일까? 생각하며 분석하고, 관련 내용을 찾아보니 실제 강의 시간보다 몇 배는 족히 걸리는 것 같았다. 이러다보니 드는 생각이 있었다. 게임 학원 출신들을 별로 좋지 않게 보는 사람들이 많다는 얘기를 얼핏 들었는데, 단순히 코드 쓰는 걸 따라 쓰면서 기능만 구현하고, 이를 포트폴리오로 제출하면 이걸 분간하기가 굉장히 어렵겠다는 생각이 들었다. 물론 따라하기만 하면 그만큼 금방 결과물이 나오긴 할테지만... 그것을 스스로 다시 만들거나 새로운 것을 만들지는 못 할 것이다. 결국 그것은 본인의 능력이 아니라는 얘기다. 필요한 기능을 인터넷에서 찾아서 가져올 때도 그것을 이해하고 사용해야 그 상황에 적합하게 수정도 할 수 있을 것이다. 비단 프로그래밍만의 얘기는 아니다. 어떤 일을 할 때 그것에 대한 이해가 있는 사람과 없는 사람의 차이는 크다. 얘기가 좀 샜다. 아무튼, 이런식으로 본인 스스로 찾아가면서 공부를 할 의지가 있다면 굉장히 도움이 될 것 같은 강의라고 생각한다. 나도 따로 찾아보면서 공부를 하고, 이를 정리해야겠다고 생각했다.

 

앞으로 아마 매일 강의를 통해 배운 부분들을 정리해서 블로그에 글을 올릴 것 같다. 컨디션이나 의지에 따라 조금 달라지겠지만 하루에 4개 내외의 강의를 듣고 그 부분에 대한 내용을 정리해야겠다. 그러면 약 10일, 늦어도 2주 안에는 다 마칠 수 있겠다. 마치면 3D게임에 대한 기본적인 지식이 생겼을테니 이를 바탕으로 간단한 게임을 하나 만들어봐야겠다. 그 이후에 취업에 도전하겠다. 목표는 4월 이내로 마치는 것이다. 지금도 한 달 전과 비교해서 많이 성장했다고 느끼는데, 지금으로부터 또 한 달 뒤에는 얼마나 성장해있을지 기대된다. 더 많이 성장하기 위해 노력해야겠다.

현재 개발한 부분에서 어떤 활동을 할 수 있을지 고민해보았다. 일본어 로컬라이징을 계획했었기 때문에 그 부분을 구현해보기로 했다.

 

private void InitializeDialog()
    {
        if(PlayerPrefs.GetString("Language") == "Korean")
        {
        	// 한국어 스토리 내용 추가
            _storyManager.AppendDialog("???", "", "........", 0.25f); // 0
            _storyManager.AppendDialog("???", "", "......님!", 0.5f);
            _storyManager.AppendDialog("???", "", "선생님!", 1f);
            
            // 이하 생략
        }
        
        else if (PlayerPrefs.GetString("Language") == "Japanese")
        {
        	// 일본어 스토리 내용 추가
            _storyManager.AppendDialog("???", "", "........", 0.25f); // 0
            _storyManager.AppendDialog("???", "", "......生!", 0.5f);
            _storyManager.AppendDialog("???", "", "先生!", 1f);
            
            // 이하 생략
        }
    }

 

기존에 로컬라이징을 염두에 두고 만들었기 때문에 스토리 수정은 크게 어렵지 않았다. DialogManager.cs에서 Japanese 부분에 일본어로 번역하여 텍스트를 넣어주기만 하면 되었다. 단, 스크립트 자체에 일본어를 넣으면서 깨지는 글자가 발생해 스크립트를 UTF-8로 인코딩하여 저장해주어야 했다.

 

LikeFont-オンライン画像フォント識別サイト

 

LikeFont-オンライン画像フォント識別サイト

ゲスト、こんにちは。 識別記録を保存するために、 -  第三者登録  - -  おすすめツール  -

ja.likefont.com

이후에는 블루 아카이브의 폰트가 어떤 것인지 찾아보았다. 일본에도 텍스트 이미지를 통해 폰트를 찾아주는 사이트가 있었다. 약 85%의 가장 높은 일치율을 가진 X-ShinYuan-Medium 라는 폰트를 사용했다.

 

極限新圓 Font,極限新圓-Medium Font,X-ShinYuan M Font,X-ShinYuan Font,X-ShinYuan-Medium Font,極限新圓 M Font|極限新圓-Medium Preview Font-TTF Font/Yuanti Font-Fontke.com For Mobile

 

極限新圓 M Preview

Full name:X-ShinYuan-Medium,極限新圓-Medium;Font family:X-ShinYuan,X-ShinYuan M,極限新圓 M;Style:M,Bold;Version:Preview;PostScript name:X-ShinYuan-Medium,極限新圓-Medium;Unique font identifier:X-ShinYuan-Medium,極限新圓-Medium

eng.m.fontke.com

 

이후, 한국어와 동일하게 TextMeshoPro의 기능을 사용해서 SDF 파일을 만들고 적용했다. 찾아보니 한국어를 기본으로 사용하고, 없는 글자는 서브 폰트에 있는 글자를 가져오는 기능도 있었는데, 한국어 범위 내에 히라가나가 포함되어 있어 폰트가 일정하지 않은 문제나, 글씨체에 따라 글자의 위치가 바뀌는 등의 이슈가 있어서 폰트는 따로 적용하기로 하였다.

 

private void DoLocalizing(TMP_Text textObject, TMP_FontAsset font, string text = "", float yPosition = -10)
        {
            textObject.font = font;
            textObject.text = text;

            textObject.GetComponent<RectTransform>().anchoredPosition = new Vector2(textObject.GetComponent<RectTransform>().anchoredPosition.x, textObject.GetComponent<RectTransform>().anchoredPosition.y + yPosition);
        }

 

이를 위해 DoLocalizing()이라는 함수를 만들어서 TMP_Text 객체, 폰트를 받아서 적용하고, 필요에 따라 변경할 텍스트나 텍스트의 y포지션 값을 조정하도록 했다.

 

// 게임이 처음부터 시작할 때 오프닝을 보여줌
        private IEnumerator DoOpening()
        {
            // 생략
            
            if (PlayerPrefs.HasKey("Language"))
            {
                if (PlayerPrefs.GetString("Language").Equals("Korean"))
                {
                    _canvas.transform.Find("Option/OptionWindow/Language/KoreanBtn/Selected").gameObject.SetActive(true);
                    _canvas.transform.Find("Option/OptionWindow/Language/JapaneseBtn/Selected").gameObject.SetActive(false);

                    _canvas.transform.Find("Story/Episode/Dialog/CharacterName").GetComponent<RectTransform>().anchoredPosition = new Vector2(-622, -266);
                    _canvas.transform.Find("Story/Episode/Dialog/DepartmentName").GetComponent<RectTransform>().anchoredPosition = new Vector2(-405, -279);
                    _canvas.transform.Find("Story/Episode/Dialog/DialogText").GetComponent<RectTransform>().anchoredPosition = new Vector2(-14, -433);
                }
                else if (PlayerPrefs.GetString("Language").Equals("Japanese"))
                {
                    _canvas.transform.Find("Option/OptionWindow/Language/KoreanBtn/Selected").gameObject.SetActive(false);
                    _canvas.transform.Find("Option/OptionWindow/Language/JapaneseBtn/Selected").gameObject.SetActive(true);

                    GameObject selection1Btn = _storyManager._selection1Btn.transform.GetChild(0).gameObject;
                    GameObject selection1_1Btn = _storyManager._selection1_1Btn.transform.GetChild(0).gameObject;
                    GameObject selection1_2Btn = _storyManager._selection1_2Btn.transform.GetChild(0).gameObject;

                    // 스토리 로컬라이징
                    DoLocalizing(_storyManager._characterName.GetComponent<TMP_Text>(), _japaneseFontWithUnderlay, "", -24);
                    _storyManager._characterName.GetComponent<TMP_Text>().characterSpacing = -4.5f;

                    DoLocalizing(_storyManager._departmentName.GetComponent<TMP_Text>(), _japaneseFontWithUnderlay, "", -19);
                    _storyManager._departmentName.GetComponent<TMP_Text>().characterSpacing = -4.5f;

                    DoLocalizing(_storyManager._dialogText.GetComponent<TMP_Text>(), _japaneseFontWithUnderlay, "", -17);
                    _storyManager._dialogText.GetComponent<TMP_Text>().characterSpacing = -4.5f;
                    _storyManager._dialogText.GetComponent<TMP_Text>().lineSpacing = 0;

                    DoLocalizing(selection1Btn.GetComponent<TMP_Text>(), _japaneseFont);
                    DoLocalizing(selection1_1Btn.GetComponent<TMP_Text>(), _japaneseFont);
                    DoLocalizing(selection1_2Btn.GetComponent<TMP_Text>(), _japaneseFont);

                    DoLocalizing(_storyManager._windowText, _japaneseFont);
                    DoLocalizing(_storyManager._gameOver2Text, _japaneseFont, "プニプニ:どれだけ剣術を鍛えたところで、我が銃の前では無力……ふっ。");

                    // 인트로 ~ 메인화면 로컬라이징

                    DoLocalizing(_guideText, _japaneseFont, "このゲームはNexon Gamesで開発したブルーアーカイブの\nファンメイドゲームで、公式コンテンツではありません。");

                    DoLocalizing(_startBtn.transform.GetChild(0).GetComponent<TMP_Text>(), _japaneseFontWithUnderlay, "スタート");
                    DoLocalizing(_optionBtn.transform.GetChild(0).GetComponent<TMP_Text>(), _japaneseFontWithUnderlay, "オプション");
                    DoLocalizing(_gameQuitBtn.transform.GetChild(0).GetComponent<TMP_Text>(), _japaneseFontWithUnderlay, "ゲーム終了");

                    DoLocalizing(_option.transform.Find("OptionWindow/Language/Text").GetComponent<TMP_Text>(), _japaneseFont, "言語", -13);
                    DoLocalizing(_option.transform.Find("OptionWindow/OptionText").GetComponent<TMP_Text>(), _japaneseFont, "オプション", -20);
                    DoLocalizing(_option.transform.Find("OptionWindow/SFXSlider/Text").GetComponent<TMP_Text>(), _japaneseFont, "効果音", -13);

                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/ChapterName").GetComponent<TMP_Text>(), _japaneseFont, "レトロチック・ロマン");
                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/ChapterContents").GetComponent<TMP_Text>(), _japaneseFont,
                        "ゲーム開発部で開発したテイルズ・サガ・クロニクルをプレイすることになりました。「今年のクソゲーランキング1位」のゲームを無事にクリアできましょうか?");
                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/ChapterNumber").GetComponent<TMP_Text>(), _japaneseFont, "1章");
                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/EpisodeList").GetComponent<TMP_Text>(), _japaneseFont, "エピソードリスト");
                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/StageAllClear").GetComponent<TMP_Text>(), _japaneseFont, "すべてのエピソードをクリアしました。");
                    _selectStage.transform.Find("SelectStageWindow/ChapterContents").GetComponent<TMP_Text>().lineSpacing = 0;
                    _selectStage.transform.Find("SelectStageWindow/ChapterContents").GetComponent<TMP_Text>().characterSpacing = -5;

                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/Stage1/StageName").GetComponent<TMP_Text>(), _japaneseFont, "テイルズ・サガ・クロニクル");
                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/Stage2/StageName").GetComponent<TMP_Text>(), _japaneseFont, "???");
                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/Stage3/StageName").GetComponent<TMP_Text>(), _japaneseFont, "???");
                    DoLocalizing(_selectStage.transform.Find("SelectStageWindow/Stage4/StageName").GetComponent<TMP_Text>(), _japaneseFont, "???");
                    _selectStage.transform.Find("SelectStageWindow/Stage1/StageName").GetComponent<TMP_Text>().characterSpacing = -13;

                    _selectStage.transform.Find("SelectStageWindow/Stage1/Stage1Enter").GetComponent<Button>().image.sprite = _stageEnterImageJapanese;
                    _selectStage.transform.Find("SelectStageWindow/Stage2/Stage2Enter").GetComponent<Button>().image.sprite = _stageEnterLockedImageJapanse;
                    _selectStage.transform.Find("SelectStageWindow/Stage3/Stage3Enter").GetComponent<Button>().image.sprite = _stageEnterLockedImageJapanse;
                    _selectStage.transform.Find("SelectStageWindow/Stage4/Stage4Enter").GetComponent<Button>().image.sprite = _stageEnterLockedImageJapanse;

                    DoLocalizing(_canvas.transform.Find("Story/EpisodeStart/Text/ChapterNumber").GetComponent<TMP_Text>(), _japaneseFont, "第1話");
                    DoLocalizing(_canvas.transform.Find("Story/EpisodeStart/Text/ChapterName").GetComponent<TMP_Text>(), _japaneseFont, "テイルズ・サガ・クロニクル");
                }
            }
            
            // 이하 생략

 

기본 설정이 한국어로 되어있어서 이런식으로 처음 시작할 때 일본어로 설정이 되어있으면 각종 텍스트들의 폰트와 내용, 위치 등을 변경해주었다.

이외에도 스토리를 시작할 때 나오는 텍스트라든지, 선택지 등은 언어 설정을 확인해서 내용을 변경하는 식으로 적용했다.

 

작업하면서 한국어로 실행할 때도 일본어 관련 정보를 가지고 있고, 체크를 해야하는 등의 문제가 있어서 별로 효율적이지 않다고 느꼈다. 내가 개발한 프로그램은 볼륨이 그렇게 크지 않고 용량을 줄일 필요성이 크지 않기 때문에 따로 작업을 하진 않았지만, 실제로 게임을 개발해서 출시한다면 이런 부분도 고려하고 수정해서 최대한 용량이나 번거로운 작업을 줄여야겠다고 생각했다.

따로 LanguageManager.cs 혹은 JapaneseManager.cs 등의 파일을 만들어서 시작할 때 한 번만 체크하고, 이 안에서 로컬라이징 관련 작업을 수행하도록 하면 될 것 같다. 다만 지금은 소요되는 시간에 비해 이런 작업의 필요성이나 효용성이 낮다고 생각하여 작업하지 않기로 했다.

 

 

작업을 하면서 이미지 리소스를 일본어로 교체해야 하는 경우가 있었는데, 바로 스테이지 입장 버튼이다. 다행히 2개의 입장 버튼을 제외하면 다른 부분은 전부 텍스트로만 되어있어 쉽게 적용할 수 있었다. 로컬라이징을 계획하고 있을 때는 글자가 박혀있는 이미지 리소스를 사용하면 작업이 굉장히 번거로워진다고 들었는데, 실제로 그런 것 같다. 앞으로도 이런 부분을 주의해야겠다고 느꼈다.

 

 

이외에는 언어 설정이 시작할 때 적용되는 것이기 때문에 언어 설정을 변경하면 게임을 재시작해야한다고 알려줄 필요성이 있었는데, 우선은 게임을 종료하도록 하였다. 나중에 적절한 UI를 찾아서 플레이어에게 알려주도록 개선해야겠다.

 

 

결과적으로 완성된 부분은 다음과 같다.

한국어 -> 일본어로 설정을 변경하는 부분도 담았다. 현재 1장까지 번역을 완료했고, 2장은 추후 번역 예정이다. 때문에 일본어 설정으로 시작하면 2장은 진입할 수 없도록 막아놓았다.

 

 

다음에는 커스텀 에디터 툴을 만들어보면 좋을 것 같다. 근데 어디에 적용할지가 살짝 애매하다. 처음엔 스토리에 적용할까 생각했었는데, 이미 작업해놓은 부분이 있어서 혹시 그 부분과 충돌이 나지는 않을까 걱정이다. 한 번 건드려봐야겠다.

Github 주소 (스크립트만 포함)

Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository (github.com)

 

GitHub - Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository

Tales Saga Chronicle Blast의 Script만이 포함된 Repository - GitHub - Mepkatatsu/TSCB_OnlyScript: Tales Saga Chronicle Blast의 Script만이 포함된 Repository

github.com

*사용한 리소스의 저작권 등의 문제로 현재 개발한 부분까지의 스크립트만 따로 모아 public으로 업로드 한 Repository의 주소임

 

파트별 녹화 영상

인트로 ~ 메인화면

 

스토리 1장

 

스토리 2장 ~ 슈팅 게임 시작 전

 

슈팅 게임 (일반 적 페이즈)

 

슈팅 게임 (보스 페이즈)

 

슈팅 게임 종료 ~ 스토리 2장 마무리

 

일본어 로컬라이징 적용 영상 (메인화면 ~스토리 1장) (2023. 04. 02 추가)

*일본어로 옵션을 변경하는 부분부터 시작

 

사용한 에셋

 

DoTween

https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676

 

DOTween (HOTween v2) | 애니메이션 도구 | Unity Asset Store

Use the DOTween (HOTween v2) tool from Demigiant on your next project. Find this & more animation tools on the Unity Asset Store.

assetstore.unity.com

 

TextMeshPro

 

+

 

넥슨게임즈의 블루 아카이브 게임 내 이미지, UI 및 시스템 등 사용

https://play.google.com/store/apps/details?id=com.nexon.bluearchive&hl=ko&gl=US 

 

블루 아카이브 - Google Play 앱

마음 속 담고 싶은 이야기 청춘X학원X밀리터리 RPG "블루 아카이브"

play.google.com

 

 

일자별 활동

2023. 01. 11. : 개발 시작

2023. 01. 11 ~ 2023. 01. 13. : 인트로, 메인 화면, 옵션, 사운드 등 구현

2023. 01. 14. ~ 2023. 01. 19. : 스토리 기본 틀 구현, 스토리 1화 완성

 

2023. 01. 20. ~ 2023. 03. 09. : 개발 중단(취업 준비)

 

2023. 03. 10.  : 개발 재개, GitHub 연동, 픽셀 아트 작업 병행

2023. 03. 11. ~ 2023. 03. 15. : 스토리 선택 화면, 스토리 2화 진행 (게임 이전 부분까지 완성)

2023. 03. 16. ~ 2023. 03. 28. : 슈팅 게임 개발

2023. 03. 29. ~ 2023. 03. 30. : 스토리 2화 완성, 최적화 작업 및 코드 정리

2023. 03. 31. ~ 2023. 04. 02. (추가) : 일본어 로컬라이징

 

 

새로 배우고 적용한 기술 및 개념

오브젝트 풀링: 총알, 적 등의 오브젝트에 적용

베지에 곡선/기즈모: 슈팅 게임에서 적이 곡선으로 이동하는 기능 구현

싱글톤: 각종 Manager.cs 스크립트에 적용

DOTWEEN 에셋: 이미지, 텍스트 Fade, 각종 이동에 사용

 

작성한 스크립트

Manager: 하나만 존재, Controller: 여러 개 존재할 수 있음

 

/Script/

AudioManager.cs: BGM, 효과음 등 오디오를 관리 (106 line)

ButtonManager.cs: 버튼을 관리 (153 line)

DialogManager.cs: 스토리의 대사를 관리, 스토리 작업을 간편하게 하기 위해 분리 (187 line)

GameManager.cs: 게임의 전체적인 부분을 관리 (305 line)

Singleton.cs: 싱글톤이 필요한 Manager에 싱글톤을 일률적으로 적용하기 위해 작성 (42 line)

SliderController.cs: Slider를 관리 (31 line)

StoryManager.cs: 게임의 스토리 관련 부분을 관리 (1655 line)

 

/Script/Game1/

BulletController.cs: 슈팅 게임의 총알의 정보를 저장하고 충돌을 감지함 (38 line)

EnemyController.cs: 적 객체의 정보를 저장하고 이동, 공격 등의 행동을 수행 (204 line)

LazerController.cs: 보스의 4번 스킬인 레이저를 관리 (185 line)

MidoriPlaneController.cs: 플레이어의 비행기를 관리 (23 line)

ShootingGameManager.cs: 슈팅 게임의 전체적인 부분을 관리 (1327 line)

StarColorController.cs: 배경의 별빛이 반짝거리는 기능 구현 (48 line)

=============================================================

 

더보기

게임 개발 관련 느낀점

나는 게임 개발이 즐겁다. : 프로그래밍 자체도 재미있고, 그 중에서도 게임 개발이 가장 즐겁다. 게임 개발이 나의 적성에 굉장히 잘 맞는 일이고 나의 업이라고 생각한다. 앞으로도 계속 게임 개발을 하고 싶다.

 

게임 기획은 굉장히 어려운 일이다. : 기획은 애매한 것을 정하는 일이라고도 하던데, 실제로 그러했다. 게임의 모든 것을 하나부터 열까지 확실하게 정해놓는 일이 기획이다. 게임의 시스템은 물론이고 상황에 어울리는 BGM, 효과음을 찾는 일도 굉장히 고된 작업이다.

 

협업은 굉장히 중요하다. : 이미 존재하는 게임을 배경으로 만드는 작업이었음에도 혼자 시나리오, 기획, 아트, 개발 전반을 혼자 담당하는 것이 무척 어려웠다. 특히나 그림에는 소질이 없기 때문에 게임에 필요한 이미지를 직접 구상하고 그리는 것이 굉장히 힘들었다. 에셋 등을 사용해도 되지만, 에셋 스토어에 내가 원하는 그림이 모두 있지는 않다. 일부 리소스를 실제 게임에서 가져와 사용할 수 없었더라면 개발에 몇 배의 시간은 더 걸렸을 것이다. 내가 개발을 하면서 즐거운 것처럼, 기획, 아트 등의 업무를 하면서 즐거운 사람과 협업한다면 굉장히 큰 시너지를 낼 수 있을 것이다. 또한, 개발하면서 가끔 친구들에게 보여주고 의견을 들으며 수정을 거쳤기 때문에 더 만족스러운 결과물이 나온 것 같다. 나 혼자서는 보지 못하고 놓칠 수 있는 것들이 많이 있기 때문에, 여러 사람들의 의견을 참고하며 개발하는 것이 더 좋은 게임을 만드는 길이라고 느꼈다.

 

계획을 세우는 것은 중요하다. : 기획이 되지 않은 상태로 개발을 하거나 손에 잡히는 대로 작업을 하면 일정이 굉장히 늘어진다. 어떤 작업을 할 것인지 계획을 세우고, 그 작업을 작은 단위로 나누어 할 일, 필요한 일 등을 명확하게 해 놓는 것이 가장 효율적으로 개발할 수 있는 길이다.

 

게임 프로그래머로써 해야할 일 : 같은 기능을 구현한다고 해도 아주 많은 방법이 있다. 그 중에서도 가장 효율적인 방법을 추구해야 한다. 또, 개발자들도 서로 협업을 하기 때문에 알아보기 쉽게 코드를 작성해야한다. 이를 위해 코드를 통일성있게 작성하고, 디자인 패턴이나, 오브젝트 풀링과 같은 개념들을 많이 알고 적용할 수 있도록 꾸준히 공부해야겠다.

 

 

30일간 게임 개발자로서 많이 성장한 것 같다.

처음에는 재미로 시작하고, 취업 준비로 중단하였다가 그럴듯해 보이는 게임을 하나 만들어 보자라는 마음으로 개발을 재개했었다. 도중에 지금 내가 하고 있는 것이 맞는 것인지에 대한 고민도 들었다. 단순히 기능을 구현하기만 하다가 더 효율적인 방법을 찾게 되면서 오브젝트 풀링이라든지 디자인 패턴 등에 대한 개념을 알게 되었다. 취업 준비를 위해서 이것저것 찾아보며 코드를 깔끔하게 작성하는 법 등에 대해서도 알게 되었다.

 

확실히 인간은 실패를 통해 성장하는 것 같다. 취업에 실패해서 개발을 재개하고, 짧은 기간이었지만 그 과정에서 많은 것들을 배웠다. 이전에 재직하던 회사에 계속 다녔더라면이라는 생각도 했었지만, 워낙에 일이 바쁘고 개발 규모도 크지 않아 주먹구구식으로 개발하는 버릇만 들었을 것 같다. 그런 면에서 봤을 때 아마 그렇게 했을 때보다 지금의 내가 더 나은 모습이지 않을까 하는 생각이 들기도 한다.

 

이제는 이전에 지원할 때와는 다르게 남에게도 보여줄 수 있는 개발물이 있고, 혼자 작업한 정리된 코드나, 적용한 기술이나 개념들이 있기 때문에 포트폴리오를 더 올바른 방향으로 작성할 수 있을 것 같다. 이전에는 몇 개월은 걸릴 것이라고 생각했는데 지금도 어느정도 준비가 된 것 같다. 회사에 취업해서 내가 익힌 기술을 활용해 실제 서비스하는 게임 개발에 기여하고 싶다. 또 회사이기에 배울 수 있는 새로운 것들을 배우면서 계속 성장하고 싶다.

 

당분간은 코드 수정 등의 간단한 작업만 하고, 포트폴리오를 작성하면서 다시 취업 준비를 해봐야겠다. 이번에도 실패한다면 다시 개발을 계속하면서 더 많은 기술들을 익히고 적용하면서 더 매력적인 개발자가 될 것이다. 물론 취업에 성공하고 난 이후로도 계속 경쟁력을 유지하기 위해 노력할 것이다. 사람들에게 재미는 물론이고 쾌적한 플레이를 보장하기 위해서도 말이다.

 

+ Recent posts