역자의 말: 프로토타입 제작 때문에 아무튼 요즘은 무브먼트쪽을 좀 관심 있게 보고 있다보니...
원문
NielsvdMarel
▚ Walking, Running and Sprinting
nielsvdmarel.nl
▚ 프로젝트 요약:
이 프로젝트에서는 고품질 루트 모션 기반 무브먼트 시스템을 구현합니다.
이전 프로젝트와 학교 과제에서 학습한 루트 모션, 애니메이션 스크립팅, 무브먼트 시스템 등의 지식을 종합적으로 적용하고자 합니다.
정확한 애니메이션 기반 움직임을 구현하면 개인 프로젝트와 협업 프로젝트 모두에서 원하는 품질에 한 걸음 더 다가갈 수 있을 것입니다.
또한 이 프로젝트를 이후 프로젝트의 기반으로 활용하여, 움직임 자체가 항상 제가 추구하는 품질 수준을 유지하도록 하고 싶습니다.
페이지 목차:
▌사양:
출시: 2022년 4월
프로젝트 유형: 개인 프로젝트
플랫폼: PC, Windows
소요 기간: 1개월 (풀타임 아님)
엔진 및 도구: Unity 3D - C#
▌기여 항목:
○ 완전히 동작하는 루트 모션 기반 무브먼트 시스템
○ 입력, 카메라 회전, 플레이어 회전을 사용한 목표 방향 계산
○ 출발/정지 루트 모션 구현
○ 걷기/달리기/전력 질주 구현
○ 제자리 회전 구현
○ 동적 점프 구현
○ 낙하 구현
○ 착지 애니메이션 구현
▌프로젝트 목표:
○ 완전한 AAA 품질의 루트 모션 무브먼트 시스템 제작
○ 다른 개인 프로젝트의 기반으로 사용할 핵심 무브먼트 시스템 제작
○ 목표 방향(desired direction) 변수 활용에 대해 연구한 내용 적용
○ Unity에서 C#으로 루트 모션 시스템 구현. 실험은 Unreal의 블루프린트로 진행
▚ 목표 회전 계산
플레이어가 이동을 시작할 때 특정 출발 및 정지 애니메이션이 재생되어야 합니다. 지원하는 애니메이션은 45도, 90도, 135도, 180도 출발 애니메이션입니다. 어떤 애니메이션을 재생해야 하는지 알기 위해, 이동 전에 "목표" 이동 회전값을 결정하는 계산이 필요합니다.
이 계산은 현재 플레이어 회전, 카메라 시선 방향, 현재 입력을 사용하여 목표 회전값을 결정합니다. 코드는 아직 진행 중이지만, 핵심 계산은 아래와 같습니다:

목표 각도 계산 코드
💻위 이미지의 코드 재구성 (C#)
private void CalculateDesiredAngle() {
Vector3 delta;
delta = m_Camera.transform.rotation.eulerAngles.normalized - this.transform.rotation.eulerAngles.normalized;
m_Input2D = playerControls.ReadValue<Vector2>();
m_Input = new Vector3(m_Input2D.y, m_Input2D.x, 0);
float target_rotation_ = Mathf.Atan2(m_Input2D.x, m_Input2D.y) * Mathf.Rad2Deg;
Vector3 InputTargetRotation = new Vector3(0, target_rotation_, 0);
float rotationX;
float rotationY;
float rotationZ;
rotationX = m_Camera.transform.eulerAngles.x;
rotationY = m_Camera.transform.eulerAngles.y;
rotationZ = m_Camera.transform.eulerAngles.z;
Vector3 trueCamRotation = new Vector3(WrapAngle(rotationX), WrapAngle(rotationY), WrapAngle(rotationZ));
trueCamRotation.x *= -1;
Vector3 truePlayerRotation = new Vector3(WrapAngle(transform.eulerAngles.x),
WrapAngle(transform.eulerAngles.y), WrapAngle(transform.eulerAngles.z));
Vector3 PlayerCamRotation = truePlayerRotation - trueCamRotation;
Vector3 CamAndPlayer;
CamAndPlayer.x = WrapAngle(PlayerCamRotation.x);
CamAndPlayer.y = WrapAngle(PlayerCamRotation.y);
CamAndPlayer.z = WrapAngle(PlayerCamRotation.z);
Vector3 InputCamPlayer = InputTargetRotation - CamAndPlayer;
Vector3 FinalDelta;
FinalDelta.x = WrapAngle(InputCamPlayer.x);
FinalDelta.y = WrapAngle(InputCamPlayer.y);
FinalDelta.z = WrapAngle(InputCamPlayer.z);
m_DesiredDirection = FinalDelta.y;
if (m_DesiredDirection > 0) {
m_NormalizedRotationDirection = 1;
}
else if (m_DesiredDirection < 0) {
m_NormalizedRotationDirection = -1;
}
else if (m_DesiredDirection == 0) {
m_NormalizedRotationDirection = 0;
}
}
Unity에서 회전 각도는 +180 또는 -180을 초과하는 값이 될 수 있으므로, 회전값을 래핑하는 커스텀 wrap 함수를 만들었습니다. 이 덕분에 계산된 값을 애니메이션 컨트롤러에서 직접 사용할 수 있게 되었습니다.

WrapAngle 함수 코드
💻위 이미지의 코드 재구성 (C#)
// 각도 값을 -180 ~ 180 범위로 래핑
private static float WrapAngle(float angle)
{
angle %= 360;
if (angle > 180)
return angle - 360;
if (angle < -180)
return angle + 360;
return angle;
}
애니메이션을 비활성화하면, 계산된 목표 회전값이 디버그 창에서 확인 가능합니다.
이 목표 회전값은 출발 및 제자리 회전 기능의 핵심이며, 고품질 루트 모션 무브먼트 시스템을 만드는 데 중요한 요소입니다.
▚ 출발 및 정지 이동
출발 애니메이션은 플레이어 속도가 0에서 증가할 때 트리거됩니다. 플레이어가 목표 방향으로 곧바로 이동하게 하려면, 다양한 출발/출발 회전 애니메이션을 갖추어야 목표 방향으로의 회전이 고품질로 보이고 느껴집니다.
위에서 보여드린 계산 이후, 목표 회전값은 매 프레임 애니메이션 컨트롤러에 설정됩니다.
아래 이미지에서 볼 수 있듯이, 애니메이션 컨트롤러에는 매우 많은 상태가 있습니다. 기능을 추가할수록 애니메이션 컨트롤러는 점점 복잡해집니다.

애니메이터 컨트롤러 전체 상태
Idle 상태에서 설정된 플레이어 속도를 확인합니다. 이 속도가 0.01보다 높으면 출발 애니메이션 블렌드 트리 상태로 전환합니다.
여기서 오른쪽에 보이듯이, 목표 방향 값이 올바른 루트 모션 출발 회전 애니메이션을 직접 재생하는 데 사용됩니다.
이를 통해 올바른 출발 회전이 재생되고, 이어서 목표 속도의 전진 이동이 따릅니다.

Idle에서 이동으로의 전환 상태
출발 결과는 아래에서 확인할 수 있습니다.
출발 블렌드 트리:
▚ 제자리 대기 회전
구현하고 싶었던 정말 깔끔한 기능은 제자리 대기 회전(In Place Idle Turning)입니다. 캐릭터가 대기 상태에서 목표 회전값에 도달하면, 제자리 회전이 활성화되어 목표 회전에 맞추는 것입니다.
코드에서 목표 회전의 절대값이 90도를 초과하는지 확인합니다.

제자리 회전 처리 코드
💻위 이미지의 코드 재구성 (C#)
// 대기 상태 제자리 회전 처리
private void HandleIdleTurns()
{
if (Mathf.Abs(desiredRotation) > 90f)
{
animator.SetBool("TurnInPlace", true);
// 회전 방향 결정
if (desiredRotation > 0)
animator.SetFloat("TurnDirection", 1f); // 오른쪽 회전
else
animator.SetFloat("TurnDirection", -1f); // 왼쪽 회전
}
else
{
animator.SetBool("TurnInPlace", false);
}
}
이 조건이 충족되면 TurnInPlace 불리언을 트리거합니다. 애니메이터 컨트롤러가 Idle 상태일 때, 활성 회전 방향을 사용하여 왼쪽 또는 오른쪽 회전을 결정합니다.

제자리 회전 애니메이터 상태
결과는 대기 동작 중 매우 깔끔한 회전 움직임을 보여주며, 루트 모션 무브먼트 시스템의 목표 품질을 높여줍니다.
▚ 걷기, 달리기, 전력 질주
주요 전진 이동은 출발 상태 이후의 중요한 상태입니다. 목표 이동 속도는 클램핑된 x 입력값과 클램핑된 y 입력값을 합산하여 계산합니다.

이동 속도 계산 코드
💻위 이미지의 코드 재구성 (C#)
// 이동 속도 계산
private void CalculateMovementSpeed()
{
float x = Mathf.Clamp(inputX, -1f, 1f);
float y = Mathf.Clamp(inputY, -1f, 1f);
float speed = Mathf.Clamp01(Mathf.Abs(x) + Mathf.Abs(y));
animator.SetFloat("Speed", speed, 0.1f, Time.deltaTime);
}
WalkRunSprint(전진) 상태에 진입하면, 이 속도 파라미터를 사용하여 목표 속도의 전진 이동 애니메이션으로 부드럽게 블렌딩합니다.

걷기/달리기/전력질주 블렌드 트리
이동 속도를 전환할 때 블렌딩이 균형 있고 올바르게 동작하도록 하는 것은, 걷기에서 달리기로 또는 전력 질주로 정확하게 전환하는 데 매우 중요합니다.
달리기와 전력 질주가 이 프로젝트의 주요 2가지 이동 속도로 설정되어 있지만, 예를 들어 가파른 언덕을 오를 때와 같은 경우에는 걷기가 동적으로 활성화될 수 있습니다.
걷기/달리기/전력 질주 블렌드 트리:

▚ 점프, 낙하, 착지
점프와 낙하를 올바르게 처리하는 것은 매우 중요합니다. 정적 점프 애니메이션을 통한 이동 대신, 점프 시작, 낙하, 착지를 분리하는 것이 더 낫다는 것을 배웠습니다. 이렇게 하면 캐릭터가 갑자기 지면에서 떨어져도 착지 로직을 적용할 수 있습니다.

점프 입력 처리 코드
💻위 이미지의 코드 재구성 (C#)
// 점프 입력 처리 (새로운 Unity Input System 사용)
private void HandleJumping()
{
if (jumpAction.triggered && isGrounded)
{
animator.SetBool("Jump", true);
isGrounded = false;
}
}
새로운 Unity 입력 시스템을 사용하여 먼저 플레이어가 점프 키를 누르고 있는지 확인합니다.
플레이어 접지 확인 외에 나머지 점프 로직은 애니메이션 컨트롤러에서 처리됩니다.
접지 확인:

접지 확인 코드
💻위 이미지의 코드 재구성 (C#)
// 접지 상태 확인
private void CheckIsGrounded()
{
// 발 위치에서 확인
bool feetGrounded = Physics.Raycast(
feetPosition.position, Vector3.down, groundCheckDistance, groundLayer);
// 루트 중심 위치에서 확인
bool rootGrounded = Physics.Raycast(
transform.position, Vector3.down, groundCheckDistance, groundLayer);
isGrounded = feetGrounded || rootGrounded;
animator.SetBool("IsGrounded", isGrounded);
}
지면과의 충돌 확인 외에도 발 위치와 루트 중심 위치에서 지면까지의 거리도 확인합니다.
이 거리가 지정된 값보다 낮으면 접지 상태임을 의미합니다. 이 방법은 경사면이나 계단 같은 특수한 경우에서도 일관된 접지 값을 유지하는 데 도움이 되었습니다.
점프 시작 블렌드 트리:

점프 시작:
점프가 트리거되고 플레이어가 접지 상태이면 점프 시작 상태가 재생됩니다. 현재 플레이어 속도를 사용하여 해당 속도에 맞는 올바른 점프 애니메이션을 재생합니다.

점프 속도별 애니메이터 상태
점프 시작 애니메이션이 끝난 후에도 여전히 낙하 중이면, 루프되는 낙하 애니메이션으로 전환됩니다.
착지:여러 조건이 충족되면 착지 애니메이션을 시작합니다. 낙하 중에 타이밍을 맞추어 애니메이션을 시작하는 것보다, 플레이어가 이미 지면에 닿은 상태(접지)에서 착지 애니메이션을 시작하는 것이 더 효과적이라는 것을 배웠습니다.
또한 점프의 최고점에 도달한 이후에만 착지 애니메이션이 트리거되도록 하였습니다.
착지 블렌드 트리:

'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [YOUTUBE] 인프런 루키스AI 온라인 세미나 공유 (0) | 2026.07.03 |
|---|---|
| [요약/번역] Smooth-Maximum, the most useful function (0) | 2026.07.03 |
| [번역][갓 오브 워] 새로운 시점 기반 전투 시스템의 혁신 (0) | 2026.06.30 |
| [번역] UF2025(Orlando) — Unity의 그림자, UEFN Scene Graph와 Verse 심층 분석 (0) | 2026.06.24 |
| [번역] UE6에서 Verse 동작시키는 방법 (0) | 2026.06.24 |