이번에 팀 프로젝트를 진행하면서, 기술 문서로 남길만한 부분들과 트러블 슈팅으로 남길만한 부분들을 옮겨 적어본다.

UML로 플레이어의 상태 패턴을 어떻게 구성할 지 설계했다.
플레이어의 상태는 크게 2가지, 인간 폼, 오징어 폼이 있다. HFSM(계층형 유한 상태 머신 - Hierarchical Finite State Machine) 으로 설계했다. 오징어 폼으로 변신하는 키를 누르는 동안은 오징어 폼, 그 외에는 인간 폼이다. 가장 위의 상태에 해당하는 것은 인간 상태(Human State), 오징어 상태(Squid State)이다. 상태를 변경하는 변수는 아래와 같다
인간 폼에서 플레이어는 공격이 가능하다. 오징어 변신 시, 공격은 불가능하다.
이번 게임의 가장 핵심이 되는 잉크 시스템은, 파티클 시스템으로 이루어져있다.
파티클의 입자를 발사해서 OnParticleCollision() 이벤트 함수를 통해, 충돌체를 검출한다. 충돌체의 지점(contact)에서 normal 벡터 방향으로, 번지는 잉크를 쉐이더로 표현한다. 번지는 잉크의 질감은 Pearlin Noise로 만들어진 모양과 높이를 가지고 표현된다. 하나의 오브젝트에 하나의 RenderTexture가 적용되기 때문에, 잉크를 연결해서 칠해도 유기적으로 연결되어 자연스럽게 표현된다. 칠할 때는 임시 RenterTexture에 칠한 후, 기존의 Material의 basemap과 합성하는 방식이다.
쉐이더를 통해 만든 Material을 적용한 게임 오브젝트는 particle의 입자와 충돌 시, Material이 변형된다. Material의 Render Texture를 덧씌우는 방식을 사용하는데, 해당 Material의 UV맵 좌표를 기준으로 칠하는 것이다 보니까 오브젝트의 UV맵이 정확하게 오브젝트의 모양과 일치할 필요가 있다.
예를 들어, 3D Object - Cube의 경우에는 하나의 UV 맵을 6면이 복사해서 사용하기 때문에 한 면에만 칠해도 나머지 면이 복사되어 칠해진다.
애니메이션은 유니티의 Animation을 사용했다. 플레이어의 캐릭터 모델은 Humornoid rig로 되어 있는 모델을 구해 사용했다.
플레이어의 이동의 경우 Transition(전이)을 자연스럽게 표현하기 위해, BlendTree 2D를 사용했다. 2개로 입력받는 X,Y 이동 입력값을 각각의 Parameter로 해서 Idle, Walk, Run 애니메이션들을 이차원 형태로 표현했다.
플레이어가 총으로 조준할 때, 카메라의 정중앙을 바라보게 하기 위해서 애니메이션 리깅 및 IK를 사용했다. 총을 카메라의 정면 방향으로 움직임에 따라, 플레이어의 상체(척추 포함) 또한 카메라의 정면 방향으로 회전한다.
오징어 모델에 사용되는 마테리얼의 색상을 그냥 변경하면, 눈색도 같이 변해서, 이상해진다. 마테리얼에 사용되는 basemap(texture)자체를 변경해야 한다. 다행히도 해당 오징어에 사용되는 마테리얼의 basemap이 매우 단순한 8pixel 팔레트로만 이루어져 있어, 간단하게 색을 변경 시킬 수 있었다.
오징어의 경우 LOD Group이 적용된 에셋이다. 각 LOD 단계마다 모델, Rig 구조, 애니메이터, 아바타가 있다.

각 LOD마다 Bone구조가 있고, 애니메이터 및 아바타가 있다면, 성능 상 좋지 않다. LOD 계층 위의 최상위에만 애니메이터를 두고 동일 아바타, 동일한 Rig 구조를 쓰게 한다. 각 LOD모델의 Rig 에다가 LOD0의 root를 참조시킨다. 위의 사진과 같다.
LOD0 ,LOD1 까지는 적용이 되는데, LOD2,LOD3은 애니메이션이 적용이 안되는 문제 발생했다.
차선책으로 최상위에서 자식들의 모든 애니메이터를 불러와서, foreach 반복문으로 모든 자식 애니메이터들이 동일한 애니메이션을 재생하는 식으로 해결할 수 있다.
이 또한 성능적으로는 좋지 못하기 때문에, LOD0 레벨을 제외하고는 모두 삭제하고 LOD Group 컴포넌트를 삭제했다
PUN2에서 제시하는 여러 가지 방법들과, 실습을 통해 사용했던 여러 지연 보상 방법들을 써보고, 가장 부드럽게 지연보상이 되는 것을 찾았다.

1. 물리 객체 지연보상
rigidbody의 정보들을 바탕으로 플레이어를 움직인다. 지연보상은 서버 시간 차이만큼 위치에 더해주는 식이다.
이 방법은 Rigidbody가 Kinetic이 아니어야 가능한 방법이다. rotation이 확 돌아가기 때문에, 직접 값을 대입하는 것보다 네트워크에서 전송받은 값을 한 번 캐싱한 다음 Quaternion.RotateTowards를 사용하는 것을 추천한다.

2. Lerp
transform.position, transform.rotation 값들을 비교한다. FixedUpdate에서 수행한다.
1번은 좀 더 변형해서 회전까지 보간을 한다 했을 때, 1번과 2번에 큰 차이는 별로 없는것 같다. 하지만 현재 프로젝트는 Rigidbody가 Local 클라이언트를 제외하고는 Kinetic일 필요가 있기 때문에, 2번 방법을 사용한다.
프로젝트를 진행하면서, 범용적으로 필요할 함수들을 모아놓은 정적(static) 클래스다.