오늘 큰 틀에서는 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개로 할 수 있으면 좋겠다. 그러나 욕심은 내지 말아야겠다. 괜히 무리하게 분량을 채우려고 하다가 내용을 다 습득하지 못하는 것보다는 적더라도 확실하게 알고 넘어가는 것이 좋겠다. 앞으로도 오늘같은 느낌으로 왜 이런 코드를 작성했는지, 왜 이런 기능을 사용하는지 등등을 생각하고 찾아보면서 정리해야겠다.
'개발 > 개발일지' 카테고리의 다른 글
3D 게임 개발 공부 3 - Lighting(2), Terrain, Navigation (0) | 2023.04.06 |
---|---|
3D 게임 개발 공부 2 - Animation, StateMachine, TopDownCamera, Lighting 등 (0) | 2023.04.05 |
3D 게임 개발 공부 0 - 시작 (0) | 2023.04.04 |
Tales Saga Chronicle Blast 개발일지 23 - 일본어 로컬라이징 (0) | 2023.04.02 |
Tales Saga Chronicle Blast 개발일지 22 - 2장 완성, 중간 정리 (0) | 2023.03.31 |