TECHARTNOMAD | MAZELINE.TECH

TECH.ART.FLOW.IO

[번역] UE5 차세대 캐릭터 이동 솔루션 Mover 분석

jplee 2026. 8. 24. 01:56

역자의 말: 프로토타입 준비 한답시고 요즘은 통 ChatGPT 와 놀고 있느라 정신이 있는건지 없는건지 모르겠네요. 여전히 지금 만들 게임에서는 ALS 면 충분해 보이긴 합니다만 GASP 업데이트 소식도 꾸준하니까 또 한번 지하철에서 스으윽 살펴볼 꺼리가 있어야 했습니다. '아... 뭐 앞으로 자차로 운전해서 다닐거라서 저 스스로도 지하철에서 읽은 일들은 좀 줄겠지만 운전할때 음성으로 읽기로라도 듣고 다녀야겠어요'


저자: Shadow

0. 머리말

UE 버전: 5.7.1

GASP: 5.7

게임을 만들 때 가장 중요한 것은 플레이어가 캐릭터를 조작해 움직이게 하는 것이다. 움직이는 것이 고작 Box 하나라 하더라도 말이다.

그래서 이 글은 먼저 Mover부터 분석한다. 다만 네트워크 부분은 그다지 깊게 다루지 않고, 주로 Mover의 기본 프레임워크를 살펴본다.

몇 가지 발표 자료 링크는 글 말미에 붙여 두었다.

1. Mover란 무엇인가

공식 설명은 다음과 같다.

Mover는 차세대 캐릭터 이동 방안을 만들기 위한 언리얼의 실험적 플러그인이다.

더욱 모듈화된 설계와 더욱 강력한 네트워크 동기화 기능을 갖추고 있다.

또한 점차 캐릭터 이동 컴포넌트 시스템(Character Movement Component, 줄여서 CMC)를 대체해 나갈 예정이다.

기존 CMC와 비교한 공식 자료는 다음과 같다.

이렇게까지 대단하게 말하지만, 네트워크 부분은 일단 제쳐 두자. 내가 보기에 Mover는 간단히 말하면 일종의 유한 상태 머신 FSM이다. 다양한 이동 모드를 자유롭게 확장할 수 있고, 임시 효과를 구현하기 위한 여러 부속 컴포넌트도 갖추고 있다. 모든 것을 movementMode에 밀어 넣어 끝없이 부풀릴 필요가 없는 것이다.

2. Mover 사용 방법

1. 플러그인 활성화 및 컴포넌트 추가

Mover를 활성화하려면 Mover만 활성화하면 된다.

또한 MoverExamples 안에는 몇 가지 예제가 들어 있다.

5.7 버전의 Mover에서는 이미 블루프린트를 기반으로 일부 확장이 가능하며, 공식에서는 앞으로 가능한 한 모든 과정의 생성을 블루프린트 안에서 완료할 수 있도록 하겠다고 말한다.

Mover의 가장 기본이 되는 것은 MoverComponent다. 하지만 바로 사용할 수 있도록 그 위에 CharacterMoverComponent를 한 겹 더 감싸 두었다. 이것을 추가하면 기존 CMC와 거의 비슷한 효과를 얻을 수 있다.

이 컴포넌트는 기본적으로 Walking, Falling, Flying이라는 몇 가지 이동 상태를 추가하고 처리한다.

물론 직접 더 많은 상태를 추가할 수도 있다. 예를 들어 GASP는 Sliding을 추가했다.

Mover의 모듈화는 바로 이 부분에 드러난다. 몇 가지 MovementMode가 고정되어 있는 CMC와 달리, Mover는 각 모듈을 탈착 가능한 형태로 만들었고, 각 모듈의 구현 세부 사항을 사용자에게 넘겼다.

2. 입력 추가

3인칭 템플릿의 로직을 본 적이 있다면 CMC의 AddMovementInput이 낯설지 않을 것이다.

하지만 Mover에서는 사용자의 입력을 한 겹 감싸고, 각 조작에 필요한 매개변수를 임시로 기록한다. 마지막으로 MoverInputProducerInterface를 구현하고 그 안의 ProduceInput 함수를 구현해, OutInputCmd의 사용자 입력 캐시에 기록한다.

여기서 가장 중요한 일은 AddDataToCollection이다. 이후 운동 시뮬레이션에 사용할 매개변수를 모아 두는 곳이다. Mover는 가장 기본적인 CharacterDefaultInputs를 제공하며, 여기에는 기본 이동과 점프에 관련된 매개변수가 들어 있다. 그 밖에도 필요한 구조체를 직접 정의해 데이터를 전달할 수 있다. GASP에서는 걸음걸이, 웅크리기 등의 상태를 전달하기 위해 CusotmInputs라는 구조체를 정의했다.

3. 사용자 지정 이동 모드

여기서는 GASP의 Slide를 예로 든다. SmoothWalkingMode를 부모 클래스로 블루프린트를 만든다.

GenerateSimpleWalkMove 함수만 구현하면 된다.

하지만 이것은 비교적 간단한 예일 뿐이다. SimpleWalkMove 클래스에서 공식이 이미 많은 것을 처리해 두었기 때문이다. 조금 포장된 CMC에 가깝다고 할 수 있다. 사용자 지정 SlideMovementMode에서 하는 일도 그 안의 일부 매개변수, 예를 들면 최대 속도나 가감속 등을 수정하는 정도다.

그리고 이런 데이터를 수정하는 근거가 바로 ProduceInput에서 전달한 구조체다.

4. 전환 조건

자신의 MovementMode를 정의한 뒤 Mover의 List에 넣었다고 해서 바로 적용되는 것은 아니다.

우리의 상태로 언제 전환하고, 언제 다시 돌아올 것인지 Mover에 알려 주어야 한다.

그래서 GASP는 Transition 블루프린트도 두 개 작성했다.

그 안에서 Evaluate를 구현하고 조건이 만족되면 목표 MovementMode로 전환한다. 이후 해당 Transition을 MoverComponent의 대응하는 Mode에 연결하면 된다. (여기서 Falling이나 Fly에는 연결하지 않았는데도 어떻게 전환되는지 궁금할 수 있다. 이 부분은 뒤에서 자세히 말하겠다.)

3. Mover 구조

1. MovementMode와 FSM

위 그림은 누구나 익숙할 것이다. 애니메이션 상태 머신은 보통 이런 상태들을 여러 전환 조건과 함께 구성한다.

유한한 상태 사이에서 시스템의 동작을 표현하는 방식이다. 시스템은 어느 순간에도 하나의 상태에만 존재하며, 전환 조건을 통해 다른 상태로 바뀐다. 이것이 바로 유한 상태 머신 FSM의 설계다.

Mover에서는 등록된 모든 이동 모드를 UMovementModeStateMachine이 함께 관리하고, 언제나 하나의 활성 모드가 존재한다. 상태 전환은 Simulation Tick에서 발생한다.

MoverComponent 안에는 ModeFSM이 있다. 그리고 SimulationTick에서 MoverComponent가 처리하는 일은 각종 검증과 네트워크 관련 처리를 빼면, 사실상 FSM의 상태를 갱신하는 것이 핵심이다.

여기서 QueueNextMode는 사실상 강제 전환 역할을 한다. 현재 사용되는 상황은 비행이다. 비행에 들어갈 때 위의 produceInput에서 SuggestedMovementMode를 Fly로 바꾼다. 그러면 여기까지 실행된 뒤 검사를 통과하면 상태를 강제로 Fly로 전환한다. 이런 구현은 Unity의 AnyState와 조금 비슷해서, 여러 상태를 자유롭게 끼워 넣을 수 있다.

FSM의 SimulationTick에서는 최초의 초기화와 한 번의 상태 전환을 제외하면, SimulationTick이 while (TotalUsedMs < TimeStep.StepMs)인 동안 반복해서 subStep을 수행한다.

따라서 한 프레임 안에서도 여러 번 상태를 전환할 수 있다. 예를 들어 Fly 모드에서 Falling으로 돌아왔지만 원래 땅에 있었다면, 같은 프레임에 다시 Walking으로 전환해 매끄럽게 이어 갈 수 있다. Falling에서 한 프레임 멈춘 뒤 Walking으로 떨어지는 식이 아니다.

WalkingMode를 예로 들면, 먼저 전진이나 벽을 따라 미끄러지는 등의 동작을 시도한다. 현재 프레임에서 앞으로 10m 이동하려 했지만 5m 지점에서 장애물에 부딪혔다면, 시간의 50%를 소모한다. 이때 발밑이 떠 있는 것을 감지하면 다음 상태를 air로 전환하고, 남은 시간을 ReamineTime에 넣어 다른 Mode를 갱신한다.

Falling에서도 마찬가지다. 원래 10m를 내려가야 했는데 5m 지점에서 땅에 닿았다면, 남은 50%의 시간 동안 Ground로 전환해 그쪽에서 움직임을 처리한다.

WalkingMode 부분 코드:

WalkingMode 부분 코드:
{
    ......
    bool bMoved = UMovementUtils::TrySafeMoveUpdatedComponent(Params.MovingComps, CurMoveDelta, TargetOrientQuat, true, MoveHitResult, ETeleportType::None, MoveRecord);
    .......
    {
        // 무언가에 부딪혔다(경사면일 수도 있고, 장벽일 수도 있다)        PercentTimeAppliedSoFar = MoveHitResult.Time;
        ......
    }
    {
        ......
        PercentTimeAppliedSoFar += PercentAvailableToSlide * SlideAmount;
        ......
    }
    ......
    if (!CurrentFloor.IsWalkableFloor() && !CurrentFloor.HitResult.bStartPenetrating)
    {
        ......
        OutputState.MovementEndState.RemainingMs = Params.TimeStep.StepMs - (Params.TimeStep.StepMs * PercentTimeAppliedSoFar);
        ......
        return;
    }
    ......
}

각 SubStep에서는 다음 작업을 수행한다.

  • InstantEffects 적용
  • Modifiers, LayeredMoves 활성화 및 갱신
  • 현재 모드의 GenerateMove + SimulationTick
  • Transition과 NextMode를 통한 모드 전환 처리

그리고 마지막에는 RemainingMs를 처리해 SubStep을 다시 수행할지 확인한다.

또한 매번 전진하려 할 때 막히면 계속 그 자리에 갇히거나 상태가 무한히 RemainTime을 반환할 수 있다. Mover는 너무 많은 횟수가 누적되면 강제로 시간을 소모해 무한 루프를 피하는 보호 기능도 마련해 두었다.

2. GenerateMove와 SimulationTick

현재의 MovementMode든 앞으로 말할 LayeredMoves든 모두 GenerateMove를 갖고 있다. 이 함수의 최종 출력은 ProposedMove이며, 여기에는 기대 선속도, 각속도, 이동 방향이 들어 있다.

이 단계에서는 실제 운동 데이터를 수정하지 않고, 기대 속도만 계산한다.

GASP에서 사용하는 SmoothWalkingMode 확장은 Acceleration, OverrideMaxSpeed 등을 열어 두어 Generate 단계에서 속도를 조정하고 원하는 운동 효과를 낼 수 있게 한다.

또한 GenerateMove가 있기 때문에 Mover는 이후의 운동을 매우 편리하게 예측할 수 있다. CMC에서는 CharacterMovement 외부에서 갑자기 이동에 영향을 주면 예측할 수 없지만, Mover는 항상 GenerateMove를 호출한다. 블루프린트에 작성한 운동 로직도 모두 실행해 예측할 수 있으므로 MotionMatching의 궤적 예측에 매우 유리하다.

SimulationTick은 실제로 이동을 실행하는 곳이다. 앞 단계의 결과에 따라 실제 이동, 충돌 검사, 환경과의 상호작용, 지면 검사를 실행하고, 상황에 따라 MovementMode를 전환한 뒤 최종 출력 상태를 갱신한다.

앞서 WalkingMode에서 말한 이동 시도, 충돌 검사, 시간 반환 등은 모두 SimulationTick에서 처리된다.

3. InstantEffects

InstantMovementEffect는 이름 그대로 한 프레임 안에 Mover 캐릭터의 이동 상태에 직접 작용하는 조정 방법이다.

이 효과는 한 프레임만 지속된 뒤 자동으로 제거된다. 순간이동, 이동 모드 직접 전환, 한 번의 힘 적용 등이 흔한 사용 사례이며, 여러 즉시 이동 효과가 동시에 존재하며 적용될 수도 있다.

Mover 예제의 CharacterMoverComponent에서는 점프가 이 기능으로 구현되어 있다. CMC의 블랙박스인 Jump와 달리, Mover의 Jump는 사용자 지정 FJumpImpulseEffect로 구현하며 QueueInstantMovementEffect를 호출해 Mover의 큐에 넣는다.

여기서 Struct개인적인 추측이지만, Struct가 더 가볍고 복제나 네트워크 동기화 등의 작업을 더 쉽게 처리할 수 있기 때문이라고 생각한다. UObject처럼 수명 주기 등을 관리할 필요도 없다.

자신만의 InstanceEffect를 구현하려면 상속한 뒤 대응하는 메서드를 구현하면 된다. 그중 핵심은 ApplyMovementEffect다. Jump에서는 수직 방향 속도를 계산한 뒤 OutputState에 다시 설정한다. Jump 외에도 Mover 공식에는 ApplyVelocityEffect와 TeleportEffect가 있으며, 예제 프로젝트에서도 확인할 수 있다.

FSMSimulationTick이 시작되면 InstanceEffect를 MoverComponent에서 자신의 큐로 복사한다.

그 뒤 FSM Simulation의 각 SubTick에서 이 InstanceEffect를 적용해 보고, 결과를 해당 SubTick의 StartState로 다시 복사한다. 모드 전환이 있으면 즉시 실행한다. 이렇게 하면 구체적인 이동 계산 로직전에 이러한 효과가 적용될 수 있다.

4. LayeredMoves

LayeredMove는 MoverActor의 운동에 영향을 주는 일련의 요소로, 보통 유효한 시간 동안만 적용된다. 점프, 대시, 폭발 충격력 등에 자주 사용한다.

처음 Simulation이 시작될 때 FSM에서 Tick되는 MovementMode는 하나뿐이다. 하지만 LayeredMove를 사용하면 그 위에 여러 요소가 공동으로 최종 결과에 영향을 줄 수 있다.Unity의 Animator를 사용해 본 적이 있다면 Layer가 낯설지 않을 것이다. Animator에서는 여러 Layer가 함께 작동하고, 각 Layer는 additive나 override로 동작할 수 있다. Mask를 사용해 일부 부위에만 적용할 수도 있고, 순서대로 적용되어 최종 결과를 출력한다. LayeredMove도 매우 비슷하다.

LayeredMove는 Simulation 중에 Tick되며 ProposedMove를 생성한다. 여러 Layer를 동시에 활성화할 수 있고, 우선순위와 혼합 모드에 따라 서로 섞인다. 최종적으로는 현재 MovementMode의 결과와 LayeredMove의 혼합 결과를 다시 섞는다.

또한 LayeredMove는 시작할 때 PerferredMovementMode를 사용해 모드를 전환할 수 있고, 이후 과정에서도 InstantEffect나 QueueNextMode를 통해 전환을 대기열에 넣을 수 있다.

InstantEffect와 마찬가지로 구조체 형태이며, 핵심 함수는 이동 결과를 생성하는 GenerateMove다. 또한 일정 시간 동안 적용하기 위한 DurationMs 속성이 있다. 매 Tick마다 IsFinish 함수를 통해 적용이 끝났는지 판단하고, 끝났다면 소멸한다.

이 함수를 직접 재작성해 사용자 지정 만료 로직을 구현할 수도 있다.

공식 예제를 보면 앞서 InstanceEffect에서 본 Jump를 LayeredMove에서도 구현할 수 있다. 역시 구조체 형태지만, 이번에는 구체적인 로직이 GenerateMove에 들어간다. 그 로직은 마찬가지로 수직 속도를 점프 속도로 수정하는 것이다. 다만 InstanceEffect와 달리 생성자에서 100ms의 지속 시간을 부여한다. 한 프레임만 지속되는 것이 아니다.

하지만 또 다른 예인 LayeredMove_Launch는 속도를 특정 값으로 직접 설정하고 MixMode도 선택할 수 있다. 그런데 duration=0.f다. 여기서 문제가 생긴다. 똑같이 순간적인 효과라면 InstanceEffect로 할 수 있는 일을 LayeredMove도 할 수 있어 보이는데, 언제 어느 쪽을 써야 할까?

우선 FSM Simulation에서 가장 핵심인 것은 현재 Movement 자체의 GenerateMove다. InstantEffect는 그 전에 실행되어 결과가 Movement로 전달되고, Movement의 계산에 사용된다. 반면 LayeredMove는 MovementMode의 계산이 끝난 뒤 결과를 결합한다. 따라서 InstantEffect의 결과는 LayeredMove에도 영향을 준다.

또한 LayeredMove는 우선순위와 혼합 모드에 따라 융합되므로, 원하는 방식으로 정확히 적용되지 않을 수 있다. 그래서 정확한 적용이 필요한 점프나 순간이동 등에는 InstantEffect가 더 적합하다.

하지만 일정 시간 지속되는 운동에는 LayeredMove를 써야 한다. 공식 예제의 지속적인 목표 방향 이동, 일정 시간 동안의 대시, Montage 단계에서의 RootMotion 등이 그런 경우다.

예제 레벨은 /MoverExamples/Maps/L_LayeredMoves.L_LayeredMoves에서 찾을 수 있다.

5. Modifier

FMovementModifierBase는 이동 시뮬레이션에 간접적인 영향을 주는 변경 사항을 적용한다. 직접 이동을 제안하거나 실행하지는 않지만, 시뮬레이션과 동기화되어 실행된다.캐릭터의 가감속 같은 이동 매개변수나 캡슐 크기 등을 수정하는 데 흔히 사용한다. 현재 이동 컴포넌트에서는 같은 유형의 Modifer동시에 하나만 활성화할 수 있다.

LayeredMove와 마찬가지로 duration이 있고, 지속 시간을 정의할 수 있다. 기본값 -1은 무한히 지속된다는 뜻이다. LayeredMove가 활성화와 만료 시의 OnStart, OnEnd만 갖는 것과 달리, Modifier에는 매 프레임 이동 전후에 실행되는 Pre/PostMovement도 있다. 다만 LayeredMove처럼 운동 자체를 바꿀 수는 없다.

현재 Mover에는 기본 Modifier가 하나만 있다. FStanceModifier는 캐릭터를 서로 다른 자세로 전환하는 데 사용된다. 예를 들면 웅크리기와 서기의 전환이다. 엎드리기도 나열되어 있지만 아직 구현되지 않았다.하는 일은 간단하다. Start에서 캡슐의 크기를 웅크린 자세에 맞게 줄인다. 이때 작은 디테일이 하나 있는데, 캡슐을 줄여 캐릭터가 공중에 뜨는 것을 피하기 위해 아래쪽으로 조금 이동시키는 InstanceEffect를 함께 보낸다. 마지막으로 OnEnd에서 기본값으로 되돌린다.

따라서 이동 효과를 생성하는 LayeredMove와 달리, Modifier는 매개변수를 수정해 이동 능력을 바꾸는 역할에 가깝다.

6. Transition

앞서 MovementMode의 SimulationTick에서는 환경 검사충돌과 지면 검사 등을 바탕으로 이동 상태를 전환한다고 했다. 하지만 MovementMode 안에 고정된 전환 외에도 독립적인 Transition이 있다.

핵심은 Evaluate 함수다. 상태 머신에 그어 둔 하나의 연결선과 비슷하다. 매번 조건이 만족되는지 평가하고, 만족하면 전환하려는 목표 상태를 반환한다.

앞서 말한 InstantEffect와 LayeredMove 같은 추가 효과도 MovementMode를 바꿀 수 있고, 일부 전환은 SimulationTick에 고정되어 있으며 일부는 Transition으로 처리한다. 그렇다면 왜 모두 Transition 안에 넣지 않을까?

핵심은 실행 시점이 다르다는 것이다. Evaluate는 GenerateMove 이후, SimulationTick 이전에 위치한다. Transition이 발동하면 SimulationTick을 바로 건너뛰고 남은 시간을 다음 상태에 넘겨 SubTick을 실행한다.

예를 들어 GASP의 슬라이드 Slide는 SimulationTick에서 환경이 어떻다는 것을 발견해 슬라이드하는 것이 아니다. 입력이 조건을 만족하면 되는 것이며, 구체적인 이동 시뮬레이션 과정에 의존하지 않는다. 이런 것은 Transition에 작성하는 편이 좋다. 여러 상태에 자유롭게 연결할 수 있기 때문이다. 같은 예로 Example의 집라인 시스템 UZiplineStartTransition도 있다.

반대로 이동 시뮬레이션 과정에서만 발생하는 전환은 MovementMode 안에 작성하는 편이 좋다. Transition에도 OnTrigger 함수가 있지만 관련 정보를 얻을 수 없기 때문이다. 예를 들어 Falling에서 지면을 감지했을 때 Mover는 상태만 바꾸는 것이 아니라 충돌 정보를 이용해 캐릭터를 지면 쪽으로 당긴다. 이런 정보는 SimulationTick에서만 얻을 수 있다. 물론 먼저 지면 쪽으로 당긴 뒤 어떤 정보를 Transition에 보내 상태를 전환할 수도 있다. 하지만 그 순간 더 높은 우선순위의 일이 발생해 전환이 실행되지 않았다면, 이미 지면 쪽으로 당겨 놓은 보정이 남아 오류가 생긴다.

따라서 일반적이고 이동 정보와 무관한 전환은 Transition으로 처리하는 편이 더 좋고 재사용성도 높다. 구체적인 이동 정보가 있어야 실행되는 전환은 SimulationTick에 두면 된다. 다만 너무 고정적으로 작성하지 말고 여지를 남겨야 한다. 예를 들어 Fly를 끄면 Mover는 바로 Falling으로 돌아가는 것이 아니라 Air로 돌아간다. 앞으로 더 많은 상태를 확장할 수 있도록 여지를 둔 것이다.

7. 네트워크

나도 이 부분은 잘 모른다. 간단히 이해한 바로는 이렇다. 전통적인 CMC는 먼저 한 번 시뮬레이션한 뒤 그 결과와 입력을 함께서버 반면 Mover는 입력을 바로 서버로 보내고 입력을 공유한다.

4. GASP의 Mover 관련 세부 사항

1. Rotate

언리얼 기술 발표에서는 다음과 같이 말한다.

“CMC와 비교하면 Mover에서는 기본적인 것부터 하고 싶은 일을 무엇이든 할 수 있습니다. CMC에서는 회전에 사용할 수 있는 선택지가 기본적으로 두 가지입니다. Controller 방향으로 회전하거나 입력 방향으로 회전하는 것이죠. 하지만 Mover에서는 원하는 어떤 일이든 완전히 자유롭게 할 수 있습니다.”

사실 여기서 나는 이 설명이 조금 맞지 않다고 생각한다(내가 잘못 이해했을 수도 있다?). 캐릭터의 방향에 관해서라면 CMC의 두 모드를 모두 끄고 Tick에서 직접 SetActorRotation을 사용자 지정해도 된다. ALS는 바로 이런 방식입니다. 또한 Mover는 DefaultInput에 Orientation이 하나 추가된 것뿐이므로, 본질적으로 방향을 직접 제어할 수 있습니다. 그리고 이후 MovementMode에서도 필요한 값을 추가로 조정할 수 있습니다.

공식은 특정 영역에 들어가면 목표를 향해 회전하는 예로 Mover의 유연성을 보여 주기도 한다. 하지만 실제로는 ControlRotation을 입력하기 전에 targetActor가 있는지 판단하고, 있다면 Actor를 향하는 방향을 ControlRotation 입력으로 사용하는 것이다.하지만 CMC를 사용하더라도 ALS에서 블루프린트로 ABP에 값을 전달하는 인터페이스 함수곳에서 똑같은 판단을 할 수 있다.

다만 ControlRotation, VelocityDir, CharacterOrientation을 분리한 Mover의 방식은 표준 CMC의 두 모드보다 확실히 더 넓은 조작 공간을 제공한다.

예를 들어 Strafe 모드에서는 RotationOffset을 직접 적용하면 캐릭터가 반대 방향에서 돌아오는 상황이 생긴다. (여기서는 뒤에서 말할 135° 카메라 회전 보호도 함께 제거해야 확인할 수 있다.)

GASP는 여기서 제한을 하나 둔다. RotationOffset을 먼저 제한해 현재의 정방향과 역방향 ±179° 범위 안에서만 변하도록 한다. 그러면 뒤쪽에서 크게 돌아오는 상황이 생기지 않고 Pawn이 항상 필요한 방향을 바라볼 수 있다.

Mover는 카메라가 빠르게 회전할 때도 몇 가지 제한을 둔다. 캐릭터의 회전 속도가 고정되어 있으면 캐릭터가 정면에서 바로 뒤를 향하게 될 수 있다. 이때 Aiming 상태라면 이상한 상황이 생긴다. BlendTree로 AimOffset을 만들었다면 정상 범위를 넘어 목표를 가리키지 못하고, 뒤에서 돌아오면서 BlendTree가 튀거나 ALS처럼 이상한 모습이 나타난다.

구체적인 방법은 각도를 넘어섰을 때

회전 속도

높여 회전을 따라가게 하는 것이다. 그러면 이 각도를 넘지 않는다.

2. SmoothWalking

단순한 WalkingMode에 가속도와 감속도만 있다면 [속도 곡선]은 선형이 된다. 하지만 SmoothWalkingMode는 임계 감쇠 스프링 모델 CriticalSprintDamper라는 [알고리즘]을 사용한다. 이 알고리즘은 진동 없이 빠르게 목표로 이동할 수 있다.

Acceleration/DecelerationSmoothingTime은 스무딩 시간을 제어한다.

또한 회전할 때, 예를 들어 정면으로 걷다가 갑자기 우전방으로 입력하면 속도가 아래 그림처럼 감속한다.

예를 들어 전방으로 걷는 속도가 (100, 0)이고 목표가 (70.7, 70.7)이라고 하자.

기본값은 Lateral 방식의 가속을 사용한다. 목표 속도를 향해 바로 가속도를 적용하므로 가속도는 (-x, +x)가 되고, 그 결과 전체 속도가 감속한다.

Mover는 Directional Acceleration Factor도 제공한다. 1일 때 Directional 방식을 사용한다. 속도는 그 방향으로 가속하지만 속도 제한을 넘지 않도록 한다. 따라서 적용되는 가속도는 (+x, +x)가 되고 감속하지 않는다. 결과는 CMC와 비슷해져 회전할 때 감속이 발생하지 않는다.속도 곡선은 선형이 된다. 하지만 SmoothWalkingMode는 임계 감쇠 스프링 모델 CriticalSprintDamper를 사용한다. 이 알고리즘은 진동 없이 빠르게 목표로 이동할 수 있다.

Acceleration/DecelerationSmoothingTime은 스무딩 시간을 제어한다.

또한 방향을 틀 때, 예를 들어 앞을 향해 걷다가 갑자기 오른쪽 앞 방향으로 입력하면 속도가 아래 그림처럼 감속한다.

앞서 예를 들어 전방으로 걷는 속도가 (100, 0)이고 목표가 (70.7, 70.7)이라고 하자.

기본값은 Lateral 방식의 가속을 사용한다. 목표 속도를 기준으로 직접 가속도를 적용하므로 가속도는 (-x, +x)가 되고, 목표 속도에 가까워지는 과정에서 전체 속도가 감속한다.

Mover는 Directional Acceleration Factor도 제공한다. 값이 1이면 Directional 방식을 사용한다. 해당 방향으로 무조건 가속하되 속도 제한을 두어 이를 넘지 않게 한다. 따라서 적용되는 가속도는 (+x, +x)가 되고 감속하지 않는다. 결과는 CMC와 비슷해져 방향을 틀 때 감속이 발생하지 않는다.

하지만 가속도에만 의존하면 플레이어가 곡선으로 빗나가며 이동하게 된다. Strafe 이동에서는 특히 원하지 않는 현상이고, 가속도가 작을 때 매우 두드러진다(아래 영상). 이 문제를 해결하기 위해 Mover의 SmoothWalkingMode에는 Turning Strength 매개변수가 있다. 속도의 회전을 직접 제어하는 값으로, 속도가 클수록 더 빠르게 회전한다. 덕분에 방향을 틀 때 큰 원을 그리며 호를 따라가는 문제가 생기지 않는다.

3. Slide

Slide는 블루프린트에서 정의한 MovementMode이며, 역시 SmoothWalkingMode를 기반으로 만든다.

Slide의 판정은 Crouch를 감지하고 일정 속도가 있으면 Slide로 들어가는 방식이다. Crouch의 Modifier도 함께 사용한다.

Mode 내부에서 하는 일은 Walking과 비슷하다. WalkingMode가 걸음걸이에 따라 가감속, TurnStrength 등의 매개변수를 조정한다면, Slide는 계산된 경사도에 따라 가감속을 조금 조정한다.

4. Pre/Post SimulationInput

Mover 캐릭터 블루프린트의 입력을 자세히 보면 두 세트가 있다는 것을 알 수 있다. PreSim과 PostSim이다. 이름 그대로 시뮬레이션 전의 입력과 시뮬레이션 후의 입력이다.

일반적인 ProduceInput 과정에서는 처음부터 끝까지 PreSim을 사용한다. 하지만 ABP와 Camera에 데이터를 전달할 때는 모두 PostSim 데이터를 사용한다. 이 둘은 원래 같아야 하지 않을까? 입력은 시뮬레이션 때문에 바뀌지 않을 테니 말이다. 하지만 실제로는 Mover와 Pawn이 서로 다른 빈도로 Tick될 수 있다.

현재 Mover는 Pawn보다 먼저 Tick된다. 따라서 실행 순서는 Mover -> CharacterBP -> ABP다. 현재 프레임의 입력, 즉 PreSim을 그대로 애니메이션에 사용하면 실제 입력과 표현이 동기화되지 않을 수 있다. 매번 CharacterBPTick에서 먼저 Mover의 LastInput을 저장하고, 즉 PostSim을 사용하면 훨씬 정확해진다.

5. ChaosMover

먼저 링크 하나를 붙여 두자.

A Dive Into Networked Physics-Based Character Movement | Unreal Fest Stockholm 2025

 

발표의 전반부는 ChaosMover를 소개하고, 후반부는 주로 네트워크 동기화를 다룬다.

ChaosMover는 Mover 플러그인을 기반으로 확장해 물리 기반 운동을 추가한 것이다. Chaos 물리 시스템를 통해 캐릭터가 움직이고 세계와 상호작용하게 하며, 물리 네트워크 동기화도 지원한다.

일반적인 캐릭터는 운동학적이고 질량이 무한한 것으로 취급된다. 충돌 쿼리의 결과에 따라 어떻게 움직일지 결정한다. 예를 들어 WalkingMode는 정상적인 전진, 물체에 부딪혀 전진하지 못하는 경우, 물체에 부딪힌 뒤 벽을 따라 미끄러지는 경우 등을 처리한다. 충돌이 발생하면 한 방향으로만 어떤 힘을 적용할 수 있고, 새로운 물리 표현을 추가할 때마다 어떻게 반응할지 직접 처리해야 한다.

반면 ChaosMover가 해결하려는 것은 동적 캐릭터를 만드는 일이다. 캐릭터가 물리 세계의 일부가 되어 물리 세계와양방향 상호작용,할 수 있다. 물체를 밀 수도 있고 물체에 밀릴 수도 있다. 복잡한 이동 상황에서도 물리 해석이 직접 처리하므로 별도의 코드를 작성할 필요가 없다.

특히 네트워크 동기화와 관련된 Debug는 ChaosVisualDebugger를 사용해 프레임 단위로 재생하며 문제를 확인할 수 있다.

ChaosMover는 사용자 지정 제약으로 플레이어를 제어한다. 기본 Mover에서는 플레이어를 목표 위치에 놓으려고 하지만, ChaosMover에서는 플레이어가 목표 위치로 향하도록 하는 제약을 부여한다. 장애물이 없으면 도착하고, 장애물이 있으면 충돌 제약도 함께 생겨 마지막에 같이 해석된다. 충돌 제약력제약력이 전진하려는 힘보다 크므로 벽을 뚫지 않고 올바른 결과를 얻는다.

기본 Mover와 마찬가지로 최초 단계는 ProduceInput으로 입력을 얻는 것이다. 다른 점은 입력을 캐릭터에 설정하는 것이 아니라 제약에 설정하고, 제약을 통해 해석한다는 것이다.

클라이언트와 서버 사이의 잦은 동기화 보정으로 애니메이션이 끊기는 것을 해결하고 물리를 더욱 결정론적으로 만들기 위해, 물리 시뮬레이션에는 고정 스텝 갱신이 필요하다. 게임 스레드는 가변 프레임레이트이므로 시간이 충분할 때 아래 그림처럼 물리 Tick을 한 번 실행한다. 이 때문에 게임 스레드에서 읽는 값에 지연이 생기므로, 앞 몇 프레임의 결과를 보간해 부드럽게 만들어야 한다. 따라서 어느 정도의 지연은 생긴다.

네트워크 동기화를 사용하면 재미있는 것을 많이 만들 수 있지만, 뒤에서 설명하는 네트워크 동기화 부분은 여기서 자세히 파고들지 않고 두 가지 작은 사례만 보자.

6. 정리

Mover는 현재 GASP에서 정식으로 사용되고 있으며, 대부분의 기능을 블루프린트에서 구현할 수 있다.

몇 가지 이전 버전의 미숙함과 비교하면, 이번 버전은 기본적인 MovementMode를 보완해 CMC에 가까운 기능을 갖췄고 일부 측면에서는 오히려 더 뛰어나다.

아직 수정하고 최적화해야 할 문제가 조금 남아 있지만, 지금 당장 Mover로 Demo를 만드는 데에는 전혀 문제가 없다.

It's Mover!