캐릭터 이동 구현
분석한 내용을 직접 구현해 보자.
구조
구조는 유사하게 Input을 처리하는 스크립트와 이를 참조하여 캐릭터를 제어하는 구조로 설계하였다.

이동 관련한 데이터는 따로 SO로 분리하여 관리하려 한다.
이동
이동부터 구현해 보자.
흐름을 정리하자면 다음과 같다.
- 입력 방향의 각도를 구한다 = Atan2
- 카메라 회전 각도를 구한다 = camera eulerAnge.y
- 둘을 합해 최종 캐릭터 회전 방향을 구해 회전시킨다
- 현재 이동 속도를 구한다
- 현재 속도가 유효 범위밖이라면 속도를 조정한다 = Lerp
- 최종 이동 = 수평, 수직 포함
입력 방향에 대한 각도를 구하는 것은 Atan2와 Reg2Deg를 통해 구해내면 된다.
이후, 카메라의 회전은 mainCamera의 참조에서 transform을 받아와 오일러 각도를 받으면 된다.
지금은 수평적인 방향을 결정하고 있기 때문에 카메라의 y축을 받아와 더해주면 최종적으로 이동할 각도가 계산된다.
_targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg + mainCamera.transform.eulerAngles.y;
계산된 각도로 캐릭터를 회전시킨다.
float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity, playerMovementData.rotationSmoothTime);
transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
그리고 현재 속력을 구해 유효한 속도인지 판단한 후 조정해준다.
float currentHorizontalSpeed = new Vector3(parkourCharacterController.velocity.x, 0.0f, parkourCharacterController.velocity.z).magnitude;
float speedOffset = 0.1f;
if (currentHorizontalSpeed < _targetSpeed - speedOffset || currentHorizontalSpeed > _targetSpeed + speedOffset)
{
_speed = Mathf.Lerp(currentHorizontalSpeed, _targetSpeed * parkourInputController.moveInput.magnitude, Time.deltaTime * playerMovementData.speedChangeRate);
_speed = Mathf.Round(_speed * 1000f) / 1000f;
}
else
{
_speed = _targetSpeed;
}
그리고 최종적으로 이동 방향, 속도를 통해 캐릭터를 이동시킨다.
Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;
parkourCharacterController.Move(targetDirection.normalized * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
점프(수직 이동)
점프를 구현해 보자.
여기서 구해지는 최종 수직 속도는 Move에서 수직 이동에 적용된다.
점프 흐름을 간단하게 구현해 보면 다음과 같다.
- 땅에 붙어있을 때
- 점프 입력이 들어오면 $\sqrt{2gh}$를 통해 수직 속도를 구한다.
- 공중에 있을 때
- 점프 입력이 들어오지 못하게 막는다
- 지속적으로 중력 방향 쪽으로 힘을 가한다
수직 속도를 구하는 부분은 다음과 같다.
if (_grounded)
{
if (_verticalVelocity < 0)
{
_verticalVelocity = -2f;
}
if (parkourInputController.jump)
{
_verticalVelocity = Mathf.Sqrt(2.0f * playerMovementData.gravity * playerMovementData.jumpForce);
}
}
수직 속도가 0 이하로 떨어지면 -2로 초기화하는 이유는 땅에 닿았을 때도 지속적으로 중력 방향으로 눌러주기 위함이다.
_verticalVelocity -= playerMovementData.gravity * Time.deltaTime;
그리고 최종 속도를 계산해 준다.
이는 Move에서 수직 속도로 적용된다.
Ground Check
캐릭터가 땅에 닿았는지 확인하는 로직을 구현해 보자.
땅에 닿는 순간 점프가 가능하도록 변경해주어야 하며 애니메이션을 적용해야 하지만 애니메이션의 경우는 다음에 추가로 작업하자.
체크는 그리 어렵지 않다.
캐릭터 발밑에서 스피어 체크를 통해 충돌체를 확인하면 된다.
private void GroundCheck()
{
var checkPosition = transform.position;
checkPosition.y -= 0.1f;
_grounded = Physics.CheckSphere(checkPosition, 0.2f);
}
카메라 회전
마지막으로 카메라 회전을 구현해 보자.
카메라 회전도 그리 어렵지 않다.
특정 값이상 look input이 들어온다면 카메라의 yaw, pitch를 조정해 주면 된다.
이후 카메라가 360º회전 하는 것을 방지하기 위해 Clamp를 구현하여 방지해 주면 된다.
private void RotateCamera()
{
if (parkourInputController.lookInput.sqrMagnitude >= _threshold)
{
_cinemachineTargetYaw += parkourInputController.lookInput.x;
_cinemachineTargetPitch += parkourInputController.lookInput.y;
}
_cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, bottomClamp, topClamp);
cinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch, _cinemachineTargetYaw, 0.0f);
}
private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
{
if (lfAngle < -360f) lfAngle += 360f;
if (lfAngle > 360f) lfAngle -= 360f;
return Mathf.Clamp(lfAngle, lfMin, lfMax);
}
