주의 : 공부하면서 정리한 것이기 때문에 잘못된 내용이 있을 수 있습니다.


개요



언리얼 엔진에서 캐릭터를 다루기 위해서 알아야 할 가장 중요한 컴포넌트가 무엇이냐고 질문을 하신다면, 저는 "가장 중요한 것은 UCharacterMovementComponent 입니다" 라고 대답을 할 것입니다.


대부분의 사람들이 이 컴포넌트를 아무 생각없이 사용하고 있지만 이 컴포넌트의 동작을 제대로 이해하지 못한다면 많은 문제에 부딪히게 됩니다. 


튜토리얼같은 것들을 따라 할때는 제한된 조건에서 캐릭터를 움직이기 때문에 이것을 이해해야 할 필요성을 별로 느끼지 못합니다. 하지만 복합적인 상황을 고려하면 반드시 이해해야만 합니다.


몇 가지 간단한 예를 들어 보도록 하겠습니다.

    • RootMotion 을 실행하고 있을 경우에 ACharacter::LaunchCharacter() 류의 메서드 호출이 제대로 동작하지 않습니다.
    • AIController 를 연결했을 경우에 AController::SetControlRotation() 류의 메서드 호출이 제대로 동작하지 않습니다.


결국 조금만 복잡한 상황을 처리하거나 깊이 들어 가면 CharacterMovement 컴포넌트의 동작을 제대로 이해하지 않고서는 작업을 진행하기 어려운 상태가 된다는 것입니다. 그리고 이는 작업의 방향에도 큰 영향을 끼칩니다.


위에서 예로 들었던 RootMotion 을 생각해 봅시다. 만약 idle 이나 walk 를 RootMotion 으로 만들었다면, 플레이어 캐릭터가 몹을 때렸을 때 밀려나게 만들 수 없을 것입니다. 그러므로 그런 상황을 가정했다면 idel 이나 walk 를 RootMotion 이 아니도록 만들어야만 합니다. 아니면 자신만의 CharacterMovement 컴포넌트를 구현해야합니다.


이러한 문제점들을 해결하기 위한 목적 이외에도, CharacterMovement 컴포넌트는 RVO Avoidance System, Navigation System, AI system, Animation System, Physics System, Input System 등과 연결되어 있기 때문에 잘 이해해야 할 시스템입니다.


저도 이 컴포넌트에 대한 이해없이 그냥 예제 보면서 따라하다가 무의미하게 시간만 낭비했습니다. 그래서 이 컴포넌트에 대한 이해도를 높여 보려는 목적으로 이 문서를 작성하게 되었습니다. 부족함이 많은 글이겠지만, 모쪼록 처음 접하는 사람들에게 도움이 되었으면 합니다.


이 문서에서는 모든 요소들에 대해서 세부적으로 다루지는 않습니다. 코드를 분석하는데 도움이 될 수 있는 가이드를 제공할 뿐입니다. 그리고 여기에서는 standalone 으로 플레이한다는 가정을 깔고 있습니다. Client-Server 모델에서는 약간 동작이 다른데 그 부분은 알아서 분석하시기 바랍니다.


다음과 같은 참고 문서들을 확인하시기 바랍니다.


기본 : 속도와 가속도



플레이어가 1초 후에 X 축으로 1 미터 앞에 있기를 원한다고 합시다. 그러면 이 목표를 달성하기 위한 메서드를 만들어야 합니다. 간단하게 생각하면 UCharacterMovement::SetLocation( const FVector& NewLocation, const float Time ) 과 같은 메서드를 만들 수 있습니다. 이것을 기획자가 사용한다면 매우 멋진 인터페이스가 될 것입니다.


하지만 이것을 실제 구현하고 있다고 생각해 봅시다. 순간이동이 아니기 때문에 0 초와 1 초 사이의 무수히( ? ) 많은 프레임에서의 위치를 계산해야 합니다. 등속도 운동을 해야 할까요? 아니면 등가속도 운동을 해야 할까요?


혹은 중간에 뭔가 장애물이 있다고 합시다. 이것을 어떻게 처리해야 할까요?


결국 월드와의 상호작용을 고려한다면 일관된 법칙을 가질 필요가 있습니다. 이미 PhysicsX 라는 물리 엔진을 기반으로 깔고 가고 있기 때문에 이에 맞추는 것이 여러모로 편할 것입니다. 


CharaterMovement 컴포넌트의 기본은 velocity( 속도 ) 와 acceleration( 가속도 ) 을 제어하는데 있습니다. 최종적으로 이를 통해서 캐릭터의 Location, Rotation 이 결정됩니다. 그리고 수많은 시스템들이 속도와 가속도를 결정하는데 영향을 주게 됩니다.


기억이 잘 안 나는 분들이 있을 것이기 때문에 기본적인 뉴턴 운동법칙에 대해서 복습해 보도록 하겠습니다. 뉴턴 운동법칙은 크게 3 가지 법칙으로 나뉩니다.

  • 1법칙 : 관성의 법칙.
  • 2법칙 : 가속도의 법칙.
  • 3법칙 : 작용과 반작용의 법칙.


우리가 주목할 것은 가속도의 법칙입니다. 그 정의는 다음과 같습니다.


물체의 운동량의 시간에 따른 변화율은 그 물체에 작용하는 알짜힘과 ( 크기와 방향에 있어서 ) 같다.


출처 : 뉴턴 운동 법칙, 위키피디아.


이를 수식으로 쓰면 다음과 같습니다. 여기에서 F 는 알짜힘, P( = mv ) 는 운동량, m 은 질량, v 는 속도, a 는 가속도입니다.




일반적으로 질량이라는 것이 시간에 따라 변하지 않으므로 다음과 같이 식을 작성할 수 있습니다.




이제 우리가 어떤 물체를 움직이는 함수를 작성한다고 해 봅시다. 그러면 하나의 값을 사용해서 물체를 움직일 수 있습니다. 그것은 "힘"입니다.



UCharacterMovementComponent::AddForce() 메서드의 7 라인에서 Force 를 Mass 로 나누는 것을 볼 수 있습니다. 결국 PendingForceToApply 라는 것은 가속도를 의미하게 됩니다.


이제 실제 이 가속도는 어디에서 사용되는 것일까요? 다음과 같은 호출순서에 의해 가속도가 처리됩니다.



그림 1


3 번 호출의 내용을 보면 다음과 같습니다.



12 라인의 PendingForceToApply * DeltaSeconds 를 통해 Velocity 를 얻어 내고 있습니다. 가속도라는 것은 초당 속도의 변화량이기 때문에 DeltaSeconds 를 곱하는 것이죠. 만약 가속도가 100 이라면 1 초 후에는 속도가 100 이 증가되기를 원한다는 의미가 됩니다. 그러므로 0.1 초 동안 증가한 속도는 10 입니다( 이 설명은 왜 하고 있는지 ㅡㅡ;; ).


어쨌든 이런 식으로 캐릭터의 이동을 처리하게 됩니다. 보통 입력시스템과 관련한 플루프린트에서는 AddForce() 를 호출하게 됩니다.


본 주제로 돌아 가서 1초 동안 1 미터를 이동하려면 어떻게 해야 할까요? 이 경우에는 복잡한 계산이 필요합니다. 보통 캐릭터 입력은 등가속도 운동과 등속도 운동이 모두 포함되기 때문에 단순하게 계산하기 어렵습니다( 미적분이 필요합니다 ). 그러므로 이는 기획자가 사용하기에는 적절하지 않습니다. 프로그래머나 물리를 좀 아는 기획자가 미리 테이블을 만들어서 전달해 줄 필요가 있습니다. 아니면 기획자가 감으로 해 보는 수밖에 없습니다.


여기에서는 설명을 단순화하기 위해서 마치 속도가 한 메서드 내부에서만 계산되는 것처럼 설명했습니다. 하지만 실제로는 매우 많은 메서드 내에서 이 속도를 제어하게 됩니다. 앞에서도 언급했듯이 많은 다양한 시스템들의 영향을 받게 됩니다.


UpdatedComponent



CharaterMovement 컴포넌트가 실제로 제어하는 것은 어떤 요소일까요?


Character 의 경우에는 자신의 RootComponent 를 CharacterMovement 컴포넌트가 제어해야 하는 요소로 지정하고 있습니다.



위의 생성자의 13 라인을 보면 UpdatedComponent 에 CapsuleComponent 를 할당하고 있는 것을 볼 수 있습니다. 즉 CharacterMovement 컴포넌트는 루트 컴포넌트를 제어하게 된다는 의미입니다.


이제 CharacterMovement 컴포넌트 내에서는 UpdatedComponent 에 접근해서 이런 저런 일들을 수행하게 됩니다. 예를 들어 다음과 같은 메서드가 있습니다( UCharacterMovementComponent 는 궁극적으로 UMovementComponent 를 상속하고 있습니다 ).



PerformMovement



캐릭터의 최종 위치를 결정하는 핵심 메서드는 UCharacterMovementComponent::PerformMovement() 입니다. 여기에서는 여러 가지 입력을 평가해서 UpdatedComponent 에 그 결과를 반영합니다.


그림 2


전체적으로 크게 이동 처리와 회전 처리로 구분됩니다. 그림 2 의 3, 4, 5, 6 은 상황에 맞게 여러 가지 입력으로부터 속도를 계산합니다. 그리고 7 은 현재 상황( walking, jumping )에 맞게 이동 계산을 수행합니다. 그리고 8, 9 에서는 회전 계산을 수행합니다. 그리고 나서 10, 11 에서 이벤트를 처리합니다.


StartNewPhysics() 에서는 MovementMode 에 따라 다음과 같이 호출을 분기시켜 줍니다.


 MovementMode

 관련 Method

 비고

 MOVE_None

 

 

 MOVE_Walking

 PhysWalking()

 

 MOVE_NavWaling

 PhysNavWalking()

 

 MOVE_Falling

 PhysFalling()

 Jump 및 Launch 에 의해 실행됨

 MOVE_Flying

 PhysFlying()

 

 MOVE_Swimming

 PhysSwimming()

 

 MOVE_Custom

 PhysCustom()

 


RootMotion



RootMotion 과 CharacterMovement 컴포넌트의 관계를 잘 이해할 필요가 있습니다.


RootMotion 이라는 것은 애니메이션의 Root 본을 기준으로 캐릭터를 제어하는 모드를 의미합니다. 자세한 내용은 언리얼의 [ Root Motion ] 을 참고하시면 됩니다( 원래는 한국어 번역도 있었던 것 같은데, 지금은 한국어만 안 되네요 ).


RootMotion 모드일 때 캐릭터의 위치을 애니메이션 기준으로 제어해야 하기 때문에, 그림 2 의 5, 6 을 통해서 속도를 계산합니다. 


SkeletalMesh 의 pose 는 USkeletalMeshComponent::TickPose() 에 의해 결정됩니다. 그런데 일반적으로는 이것이 USkinnedMeshComponent::TickComponent() 에 의해 호출되지만, RootMotion 일 때는 UCharacterMovementComponent::TickComponent() 에 의해 호출된다는 차이가 있습니다. 이는 원하는 시점에 정확하게 애니메이션을 시뮬레이션하기 위함이 아닌가 싶습니다.


RootMotion 의 경우에 5, 6 을 처리한다는 것은, 캐릭터에 대한 입력이 애니메이션인 경우라고 생각하시면 될 것 같습니다.


AI



아래 블루프린트는 캐릭터를 초당 한 번씩 회전시키려는 의도로 만들어졌습니다.



만약 AIController 를 사용하는 것이 아니라 일반 PlayerController 를 사용하고 있다면 위의 코드는 정상적으로 동작합니다. 그런데 AIController 만 붙이면 해당 호출이 무시됩니다.


그 이유를 구현 측면에서 좀더 파 보자면, AAIController::UpdateControlRotation() 에서 AAIController::SetControlRotation() 를 호출하고 있기 때문입니다. 그래서 블루프린트 내의 호출이 무시되는 것입니다.



AIController 는 FocalPoint 나 FocusActor 로부터 회전을 산출하도록 설계되어 있기 때문에, 이런 문제가 발생하는 것입니다. 결국 사용자가 각 컨트롤러의 구현 방식을 이해하지 못했다면 CharacterMovement 컴포넌트의 제어에 어려움이 발생합니다.


결론



CharacterMovementController 는 기본적으로 속도를 계산해 UpdateComponent 에 반영해 주는 일을 수행합니다. 그 과정에서 여러 요소들이 간섭을 하게 됩니다.


이 문서는 그러한 모든 요소들에 대해 설명하고 있지는 않습니다. 하지만 캐릭터 이동에 대해 제대로 이해하기 위해서는 이 컴포넌트와 다른 요소들이 어떻게 상호작용하는지 이해할 필요가 있습니다.


이 문서를 통해 캐릭터 이동이나 회전이 우리의 의도와는 다르게 동작할 수 있다는 점을 이해하고 그러한 경우에 어떻게 대처해야 하는지를 이해할 수 있다면, 이 문서를 작성한 의도의 대부분은 달성한 것이 아닐까 싶습니다.


시간이 되면( ? ), 다음에는 CharacterMovement 컴포넌트가 다른 요소들과 어떤 식으로 상호작용하는지에 대한 내용을 다뤄 볼 계획입니다.

+ Recent posts