UE5 の ThirdPersonCharacter の AddMovementInput が Location/Rotation にどのように反映されるかを確認する

動機

例えば Scratch で 2D ゲームを作ろうとしたとき、キャラクターはその「座標」を変更して移動させます。つまり、プレイヤーの入力は「座標の変更」に反映されており、それがダイレクトにコードに現れます。

Unreal Engine においても、例えば「動く足場」のようなものを作るのであれば、それは Actor の LocationOffset を操作するノードを使って座標を変えていくことで実現することができます。

一方で、ThirdPersonCharacter を使うと突然 AddMovementInput ノードが出現し、それを使うことでなんとも素敵な動きができてしまいます。

愚かな私は、きっと「Unreal Engine がこのノードで魔法のような何かをしてくれているからキャラクターは動くのだ」と思っていたのですが、そんな訳もなく(実際、魔法のようにいろいろとしてくれてはいるのですが)、最終的には Location/Rotation に反映されているだけということを確認したくなりました。

以降では、全てではありませんが適宜 Githubpermalink も付けます。ほとんどの箇所は省略しつつコードを引用するので、不明な箇所は適宜 Github の UnrealEngine のコードを参照してください。コードは 5.3.2-release タグのものを使っています。

では、確認していきましょう 🚀

TPS のテンプレートを作ったら最初にお目にかかるこの AddMovementInput 、これが実際何をやっているのか追ってみましょう。

C++ プロジェクトを作った場合には以下のコードが該当します。これ自体はテンプレートのコードですが、C++ で TPS のプロジェクトを作ると、これベースのキャラクターが作成されているはずです。

// TP_ThirdPersonCharacter.cpp
 void ATP_ThirdPersonCharacter::Move(const FInputActionValue& Value)
 {
     // input is a Vector2D
     FVector2D MovementVector = Value.Get<FVector2D>();
 
 
     if (Controller != nullptr)
     {
         // find out which way is forward
         const FRotator Rotation = Controller->GetControlRotation();
         const FRotator YawRotation(0, Rotation.Yaw, 0);
 
 
         // get forward vector
         const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
     
         // get right vector 
         const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
 
 
         // add movement 
         AddMovementInput(ForwardDirection, MovementVector.Y);
         AddMovementInput(RightDirection, MovementVector.X);
     }
 }
//Pawn.cpp
 void APawn::AddMovementInput(FVector WorldDirection, float ScaleValue, bool bForce /*=false*/)
 {
     UPawnMovementComponent* MovementComponent = GetMovementComponent();
     if (MovementComponent)
     {
         MovementComponent->AddInputVector(WorldDirection * ScaleValue, bForce);
     }
     else
     {
         Internal_AddMovementInput(WorldDirection * ScaleValue, bForce);
     }
 }

Header は以下です。

// Pawn.h
     /**
      * Add movement input along the given world direction vector (usually normalized) scaled by 'ScaleValue'. If ScaleValue < 0, movement will be in the opposite direction.
      * Base Pawn classes won't automatically apply movement, it's up to the user to do so in a Tick event. Subclasses such as Character and DefaultPawn automatically handle this input and move.
      *
      * @param WorldDirection    Direction in world space to apply input
      * @param ScaleValue        Scale to apply to input. This can be used for analog input, ie a value of 0.5 applies half the normal value, while -1.0 would reverse the direction.
      * @param bForce            If true always add the input, ignoring the result of IsMoveInputIgnored().
      * @see GetPendingMovementInputVector(), GetLastMovementInputVector(), ConsumeMovementInputVector()
      */
     UFUNCTION(BlueprintCallable, Category="Pawn|Input", meta=(Keywords="AddInput"))
     ENGINE_API virtual void AddMovementInput(FVector WorldDirection, float ScaleValue = 1.0f, bool bForce = false);

Base Pawn classes won't automatically apply movement, it's up to the user to do so in a Tick event. Subclasses such as Character and DefaultPawn automatically handle this input and move.

とあるので、 Pawn のサブクラスであっても自動で movement は適用されないということのようです。 CharacterDefaultPawn を使いましょうと。

GetMovementComponent

さて、Pawn.cppUPawnMovementComponent* MovementComponent = GetMovementComponent(); に戻ります。 これは Pawn.h に定義があります。

// Pawn.h
    /** Return our PawnMovementComponent, if we have one. By default, returns the first PawnMovementComponent found. Native classes that create their own movement component should override this method for more efficiency. */
    UFUNCTION(BlueprintCallable, meta=(Tooltip="Return our PawnMovementComponent, if we have one."), Category=Pawn)
    ENGINE_API virtual UPawnMovementComponent* GetMovementComponent() const;

この関数の Implementations を調べると Character.cppDefaultPawn.cpp が出てきます。 Pawn.cpp の実装と合わせて見てみましょう。

// Pawn.cpp
 UPawnMovementComponent* APawn::GetMovementComponent() const
 {
    return FindComponentByClass<UPawnMovementComponent>();
 }
// Character.cpp
 UPawnMovementComponent* ACharacter::GetMovementComponent() const
 {
    return CharacterMovement;
 }
//DefaultPawn.cpp
 UPawnMovementComponent* ADefaultPawn::GetMovementComponent() const
 {
    return MovementComponent;
 }

CharacterCharacterMovement はコンストラクタで入れられています。

https://github.com/EpicGames/UnrealEngine/blob/128c1721ae7c6169f98a17ede9fe220f9ac279cb/Engine/Source/Runtime/Engine/Private/Character.cpp#L91

// Character.cpp
 CharacterMovement = CreateDefaultSubobject<UCharacterMovementComponent>(ACharacter::CharacterMovementComponentName);
 if (CharacterMovement)
 {
    CharacterMovement->UpdatedComponent = CapsuleComponent;
 }

UCharacterMovementComponent も以下の通り UPawnMovementComponent のサブクラスです。

// CharacterMovementComponent.h
 class UCharacterMovementComponent : public UPawnMovementComponent, public IRVOAvoidanceInterface, public INetworkPredictionInterface

DefaultPawnMovementComponent はコンストラクタで入れられています。

https://github.com/EpicGames/UnrealEngine/blob/128c1721ae7c6169f98a17ede9fe220f9ac279cb/Engine/Source/Runtime/Engine/Private/DefaultPawn.cpp#L47

// DefaultPawn.cpp
 MovementComponent = CreateDefaultSubobject<UPawnMovementComponent, UFloatingPawnMovement>(ADefaultPawn::MovementComponentName);
 MovementComponent->UpdatedComponent = CollisionComponent;

UPawnMovementComponent のサブクラスの UFloatingPawnMovement が入っていることが分かります。

ここまでで、Character, DefaultPawn のそれぞれで UPawnMovementComponent またはそのサブクラスが取得できていることがわかりました。 Pawn に関しては FindComponentByClass の結果なので、存在するかどうかによりそうです。

MovementComponent->AddInputVector(WorldDirection * ScaleValue, bForce);

だいぶ脱線しましたが、再び APawn::AddMovementInput に戻って、続いて MovementComponent->AddInputVector です。

// PawnMovmenetComponent.h
    /**
    * Adds the given vector to the accumulated input in world space. Input vectors are usually between 0 and 1 in magnitude. 
    * They are accumulated during a frame then applied as acceleration during the movement update.
    *
    * @param WorldVector       Direction in world space to apply input
    * @param bForce            If true always add the input, ignoring the result of IsMoveInputIgnored().
    * @see APawn::AddMovementInput()
    */
    UFUNCTION(BlueprintCallable, Category="Pawn|Components|PawnMovement")
    ENGINE_API virtual void AddInputVector(FVector WorldVector, bool bForce = false);
// PawnMovementCompnent.cpp
 void UPawnMovementComponent::AddInputVector(FVector WorldAccel, bool bForce /*=false*/)
 {
    if (PawnOwner)
    {
        PawnOwner->Internal_AddMovementInput(WorldAccel, bForce);
    }
 }

コメントのままですが、WorldAccel という名前の Input Vectors は、1フレームの間に蓄積され、 movement update の間に加速度として適用されるということのようです。 Input vectors の大きさ (magnitude) は通常 0 - 1 なんですね 💡💡

AddInputVectorvirtual ですが、CharacterMovementComponentUFloatingPawnMovement で継承はされていません。

// Pawn.h
    /** Internal function meant for use only within Pawn or by a PawnMovementComponent. Adds movement input if not ignored, or if forced. */
    ENGINE_API void Internal_AddMovementInput(FVector WorldAccel, bool bForce = false);
code:Pawn.cpp
 void APawn::Internal_AddMovementInput(FVector WorldAccel, bool bForce /*=false*/)
 {
    if (bForce || !IsMoveInputIgnored())
    {
        ControlInputVector += WorldAccel;
    }
 }

先程のコメントにもあったように ControlInputVectorWorldAccel が蓄積されているように見えます。
これも継承ができるクラスでは無いので AddMovementInput はここまでになります。

次は蓄積された ControlInputVector がどのように加速度に反映されるかを確認していきましょう。

ControlInputVector

Pawn.h に定義があります。

// Pawn.h
    /**
    * Accumulated control input vector, stored in world space. This is the pending input, which is cleared (zeroed) once consumed.
    * @see GetPendingMovementInputVector(), AddMovementInput()
    */
    UPROPERTY(Transient)
    FVector ControlInputVector;

コメントもある程度想定どおりです。蓄積されていて、消費されるとゼロになるということですかね。
GetPendingMovementInputVector というのがあるの知りませんでした。(単なる Pawn に付けると消費されることがないのでずっとたまり続けてしまいます)

ではこれが使われているところを検索すると Internal_ConsumeMovementInputVector が見つかりました。

// Pawn.cpp
 FVector APawn::Internal_ConsumeMovementInputVector()
 {
    LastControlInputVector = ControlInputVector;
    ControlInputVector = FVector::ZeroVector;
    return LastControlInputVector;
 }

LastControlInputVector が出てきました。

// Pawn.h
    /** Internal function meant for use only within Pawn or by a PawnMovementComponent. LastControlInputVector is updated with initial value of ControlInputVector. Returns ControlInputVector and resets it to zero. */
    ENGINE_API FVector Internal_ConsumeMovementInputVector();
    /**
    * The last control input vector that was processed by ConsumeMovementInputVector().
    * @see GetLastMovementInputVector()
    */
    UPROPERTY(Transient)
    FVector LastControlInputVector;

コメントに ConsumeMovementInputVector が出てきました。これを見てみましょう。

// Pawn.h
    /**
    * Returns the pending input vector and resets it to zero.
    * This should be used during a movement update (by the Pawn or PawnMovementComponent) to prevent accumulation of control input between frames.
    * Copies the pending input vector to the saved input vector (GetLastMovementInputVector()).
    * @return The pending input vector.
    */
    UFUNCTION(BlueprintCallable, Category="Pawn|Input", meta=(Keywords="ConsumeInput"))
    ENGINE_API virtual FVector ConsumeMovementInputVector();
// Pawn.cpp
 FVector APawn::ConsumeMovementInputVector()
 {
    UPawnMovementComponent* MovementComponent = GetMovementComponent();
    if (MovementComponent)
    {
        return MovementComponent->ConsumeInputVector();
    }
    else
    {
        return Internal_ConsumeMovementInputVector();
    }
 }

Returns the pending input vector and resets it to zero.

こうかかれているのでおそらく Internal_ConsumeMovementInputVector と通らない場合でも、同様に ControlInputVector はクリアされて LastControlInputVector が返されるということでしょう。

This should be used during a movement update (by the Pawn or PawnMovementComponent) to prevent accumulation of control input between frames.

movement update というのは、まだはっきりとしませんが、1フレームの中での処理の一部なのではないかと思います。おそらく蓄積の後に確実に consume を行わせるために、movement update という特定の期間に消費させろということでしょうか。

コメントに出てくる GetLastMovementInputVector は単に LastControlInputVector を返しているだけです。

ちなみに、このメソッドも virtual ですが、継承しているものはありませんでした。

ConsumeMovementInputVector の呼び出し箇所

ConsumeMovementInputVector の呼び出し箇所を追っていきます。 - APawn::Unpossesed - APawn::Restart

上記二箇所が該当しましたが、多少期待と違います。Tick のような場所で呼ばれることを期待しましたが異なっているようです。

MovementComponent->ConsumeInputVector

一応まだ見ていない MovementComponent->ConsumeInputVector を見てみます。

// PawnMovementComponent.h
    /* Returns the pending input vector and resets it to zero.
    * This should be used during a movement update (by the Pawn or PawnMovementComponent) to prevent accumulation of control input between frames.
    * Copies the pending input vector to the saved input vector (GetLastMovementInputVector()).
    * @return The pending input vector.
    */
    UFUNCTION(BlueprintCallable, Category="Pawn|Components|PawnMovement")
    ENGINE_API virtual FVector ConsumeInputVector();
// PawnMovementComponent.cpp
 FVector UPawnMovementComponent::ConsumeInputVector()
 {
    return PawnOwner ? PawnOwner->Internal_ConsumeMovementInputVector() : FVector::ZeroVector;
 }

PawnOwnerInternal_ConsumeMovementInputVector を呼び出しています。
そして、コメントが ConsumeMovementInputVector とほぼ同じことに気づきます。このメソッドも virtual ですが継承はありません。

さて、 Internal_ConsumeMovementInputVector はすでに見た通りですし、これに継承もありません。
ConsumeMovementInputVector の呼び出し元に期待する場所が見つからないので続いてこの ConsumeInputVector がどこで実行されているか見ていきます。

先ほどのコメントどおりであれば by the Pawn or PawnMovementComponent のような場所の movement update 期間に実行されているはずです。

ConsumeInputVector の呼び出し箇所

CharacterMovementComponentFloatingPawnMovement の 2 箇所で見つかりました。

CharacterMovementComponent

// CharacterMovementComponent.cpp
 void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
 {
    SCOPED_NAMED_EVENT(UCharacterMovementComponent_TickComponent, FColor::Yellow);
    SCOPE_CYCLE_COUNTER(STAT_CharacterMovement);
    SCOPE_CYCLE_COUNTER(STAT_CharacterMovementTick);
    CSV_SCOPED_TIMING_STAT_EXCLUSIVE(CharacterMovement);
 
    FVector InputVector = FVector::ZeroVector;
    bool bUsingAsyncTick = (CharacterMovementCVars::AsyncCharacterMovement == 1) && IsAsyncCallbackRegistered();
    if (!bUsingAsyncTick)
    {
        // Do not consume input if simulating asynchronously, we will consume input when filling out async inputs.
        InputVector = ConsumeInputVector(); // ここ 🏁
    }
    // 略
 }
 
 void UCharacterMovementComponent::BuildAsyncInput()
 {
    if (CharacterMovementCVars::AsyncCharacterMovement == 1  && IsAsyncCallbackRegistered())
    {
        FCharacterMovementComponentAsyncInput* Input = AsyncCallback->GetProducerInputData_External();
        if (Input->bInitialized == false)
        {
            Input->Initialize<FCharacterMovementComponentAsyncInput::FCharacterInput, FCharacterMovementComponentAsyncInput::FUpdatedComponentInput>();
        }
 
        if (AsyncSimState.IsValid() == false)
        {
            AsyncSimState = MakeShared<FCharacterMovementComponentAsyncOutput, ESPMode::ThreadSafe>();
        }
        Input->AsyncSimState = AsyncSimState;
 
        const FVector InputVector = ConsumeInputVector(); // ここ 🏁
        FillAsyncInput(InputVector, *Input);
 
        PostBuildAsyncInput();
    }
 }
 // BuildAsyncInput の呼び出し元はここだけ
 void UCharacterMovementComponent::PrePhysicsTickComponent(float DeltaTime, FCharacterMovementComponentPrePhysicsTickFunction& ThisTickFunction)
 {
    BuildAsyncInput();
 }

一つは期待通り UCharacterMovementComponent::TickComponent です🎉 良かった。

もう一つは UCharacterMovementComponent::BuildAsyncInput です、このメソッドは UCharacterMovementComponent::PrePhysicsTickComponent 経由で Tick として登録されているようです。詳細は今のところ深入りできていません。

FloatingPawnMovement

// FloatingPawnMovement.cpp
 void UFloatingPawnMovement::ApplyControlInputToVelocity(float DeltaTime)
 {
    const FVector ControlAcceleration = GetPendingInputVector().GetClampedToMaxSize(1.f);
 
    const float AnalogInputModifier = (ControlAcceleration.SizeSquared() > 0.f ? ControlAcceleration.Size() : 0.f);
    const float MaxPawnSpeed = GetMaxSpeed() * AnalogInputModifier;
    const bool bExceedingMaxSpeed = IsExceedingMaxSpeed(MaxPawnSpeed);
 
    if (AnalogInputModifier > 0.f && !bExceedingMaxSpeed)
    {
        // Apply change in velocity direction
        if (Velocity.SizeSquared() > 0.f)
        {
            // Change direction faster than only using acceleration, but never increase velocity magnitude.
            const float TimeScale = FMath::Clamp(DeltaTime * TurningBoost, 0.f, 1.f);
            Velocity = Velocity + (ControlAcceleration * Velocity.Size() - Velocity) * TimeScale;
        }
    }
    else
    {
        // Dampen velocity magnitude based on deceleration.
        if (Velocity.SizeSquared() > 0.f)
        {
            const FVector OldVelocity = Velocity;
            const float VelSize = FMath::Max(Velocity.Size() - FMath::Abs(Deceleration) * DeltaTime, 0.f);
            Velocity = Velocity.GetSafeNormal() * VelSize;
 
            // Don't allow braking to lower us below max speed if we started above it.
            if (bExceedingMaxSpeed && Velocity.SizeSquared() < FMath::Square(MaxPawnSpeed))
            {
                Velocity = OldVelocity.GetSafeNormal() * MaxPawnSpeed;
            }
        }
    }
 
    // Apply acceleration and clamp velocity magnitude.
    const float NewMaxSpeed = (IsExceedingMaxSpeed(MaxPawnSpeed)) ? Velocity.Size() : MaxPawnSpeed;
    Velocity += ControlAcceleration * FMath::Abs(Acceleration) * DeltaTime;
    Velocity = Velocity.GetClampedToMaxSize(NewMaxSpeed);
 
    ConsumeInputVector(); // ここ 🏁
 }
 
 void UFloatingPawnMovement::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
 {
    if (ShouldSkipUpdate(DeltaTime))
    {
        return;
    }
 
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
 
    if (!PawnOwner || !UpdatedComponent)
    {
        return;
    }
 
    const AController* Controller = PawnOwner->GetController();
    if (Controller && Controller->IsLocalController())
    {
        // apply input for local players but also for AI that's not following a navigation path at the moment
        if (Controller->IsLocalPlayerController() == true || Controller->IsFollowingAPath() == false || bUseAccelerationForPaths)
        {
            ApplyControlInputToVelocity(DeltaTime);
        }
            // if it's not player controller, but we do have a controller, then it's AI
            // (that's not following a path) and we need to limit the speed
            else if (IsExceedingMaxSpeed(MaxSpeed) == true)
            {
                Velocity = Velocity.GetUnsafeNormal() * MaxSpeed;
            }
            
        LimitWorldBounds();
        bPositionCorrected = false;
   
        // Move actor
        FVector Delta = Velocity * DeltaTime;
    // 略・・・

ここまでで、CharacterMovementComponent (Character), FloatingPawnMovement (DefaultPawn) の両方で Tick の中で ConsumeInputVector されていることが確認できました。

では、ここからはいよいよそれぞれがどのように CharacterPawn を動かしているかみていきます。もちろん見るのは先ほどの ConsumeInputVector の周辺となります。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L1457

// CharacterMovementComponent.cpp
 void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
 {
    FVector InputVector = FVector::ZeroVector; // 💡 InputVector
    bool bUsingAsyncTick = (CharacterMovementCVars::AsyncCharacterMovement == 1) && IsAsyncCallbackRegistered();
    if (!bUsingAsyncTick)
    {
        // Do not consume input if simulating asynchronously, we will consume input when filling out async inputs.
        InputVector = ConsumeInputVector();  // 💡 InputVector
    }
 
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
    
    const bool bIsSimulatingPhysics = UpdatedComponent->IsSimulatingPhysics();
    // 略
    if (CharacterOwner->GetLocalRole() > ROLE_SimulatedProxy)
    {
        // Perform input-driven move for any locally-controlled character, and also
        // allow animation root motion or physics to move characters even if they have no controller
        const bool bShouldPerformControlledCharMove = CharacterOwner->IsLocallyControlled() 
                                                      || (!CharacterOwner->Controller && bRunPhysicsWithNoController)        
                                                      || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion());
        
        if (bShouldPerformControlledCharMove)
        {
            ControlledCharacterMove(InputVector, DeltaTime);  // 💡 InputVector

かなりのコードを省略しています。ConsumeInputVector と関連する InputVector のみに焦点を当てています。
これだけであればとてもシンプルで、ConsumeInputVector で取得した InputVector をそのまま ControlledCharacterMove に渡しているだけです。

では続けて ControlledCharacterMove を見てみます。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L5981

// CharacterMovementComponent.cpp
 void UCharacterMovementComponent::ControlledCharacterMove(const FVector& InputVector, float DeltaSeconds)
 {
    {
        SCOPE_CYCLE_COUNTER(STAT_CharUpdateAcceleration);
 
        // We need to check the jump state before adjusting input acceleration, to minimize latency
        // and to make sure acceleration respects our potentially new falling state.
        CharacterOwner->CheckJumpInput(DeltaSeconds);
 
        // apply input to acceleration
        Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));  // 💡 InputVector
        AnalogInputModifier = ComputeAnalogInputModifier();
    }
 
    // 略
 }

渡された InputVectorConstrainInputAcceleration, ConstrainInputAcceleration と順に処理されています (Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector)))。

まずは ConstrainInputAcceleration から見ていきます。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L7577

// CharacterMovementComponent.cpp 
 FVector UCharacterMovementComponent::ConstrainInputAcceleration(const FVector& InputAcceleration) const
 {
    // walking or falling pawns ignore up/down sliding
    const double InputAccelerationDotGravityNormal = FVector::DotProduct(InputAcceleration, -GetGravityDirection());
    if (!FMath::IsNearlyZero(InputAccelerationDotGravityNormal) && (IsMovingOnGround() || IsFalling()))
    {
        return FVector::VectorPlaneProject(InputAcceleration, -GetGravityDirection());
    }
 
    return InputAcceleration;
 }
 
 // ヘッダで FVector GetGravityDirection() const { return GravityDirection; }
 // コンストラクタで GravityDirection = DefaultGravityDirection; となっている
 const FVector UCharacterMovementComponent::DefaultGravityDirection = FVector(0.0, 0.0, -1.0);
  • InputAccelerationDotGravityNormal は渡された InputVector(InputAceleration) と重力方向 (GravityDirection、通常は (0, 0, -1)) の内積を取っています。
  • IsNearlyZero で使うので同じ方向ではないかのチェックだけでしょう。
  • IsMovingOnGround() || IsFalling() のチェックをしているので、地上か空中(かつ落ちている) 場合のみ加速度の計算をFVector::VectorPlaneProject(InputAcceleration, -GetGravityDirection()) を使うようです。
  • 地上や落下中は仮に斜め上等に入力があったとしても、重力方向に正規化した加速度とするようです。

続いて ScaleInputAcceleration です。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L7590

// CharacterMovementComponent.cpp 
 FVector UCharacterMovementComponent::ScaleInputAcceleration(const FVector& InputAcceleration) const
 {
    return GetMaxAcceleration() * InputAcceleration.GetClampedToMaxSize(1.0f);
 }

どうやら InputVector1.0fClamp されたうえで、MaxAcceleration に掛けられるようです。( 0 - 1 の間になるというのはここが関係しているのかとも思いましたが、ちょっと判断できないです)

改めて Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector)) を確認しますが、ここまでの計算結果が Acceleration に代入されています。

ということで、次に追っていくのは Acceleration になります。

Acceleration

まずは定義です。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Classes/GameFramework/CharacterMovementComponent.h#L629

// CharacterMovementComponent.h
    /**
    * Current acceleration vector (with magnitude).
    * This is calculated each update based on the input vector and the constraints of MaxAcceleration and the current movement mode.
    */
    UPROPERTY()
    FVector Acceleration;

続いて Acceleration でざっと Usage を調べると、CharacterMovementComponent.cpp の内部だけで 50 箇所あります。

ちょっと Acceleration から見ていくのは大変そうです 😵

ControlledCharacterMove

先ほどは省略しましたが、改めて ControlledCharacterMove を確認します。
Acceleration を計算した後も処理が続いています。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L5981

// CharacterMovementComponent.cpp
 void UCharacterMovementComponent::ControlledCharacterMove(const FVector& InputVector, float DeltaSeconds)
 {
    {
        SCOPE_CYCLE_COUNTER(STAT_CharUpdateAcceleration);
 
        // We need to check the jump state before adjusting input acceleration, to minimize latency
        // and to make sure acceleration respects our potentially new falling state.
        CharacterOwner->CheckJumpInput(DeltaSeconds);
 
        // apply input to acceleration
        Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));
        AnalogInputModifier = ComputeAnalogInputModifier();
    }
 
    if (CharacterOwner->GetLocalRole() == ROLE_Authority)
    {
        PerformMovement(DeltaSeconds);
    }
    else if (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client))
    {
        ReplicateMoveToServer(DeltaSeconds, Acceleration);
    }
 }

今回はローカルでの直接の動作だけ確認したいので、 PerformMovement を確認します。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Classes/GameFramework/CharacterMovementComponent.h#L2182

// CharacterMovementComponent.h
    // Movement functions broken out based on owner's network Role.
    // TickComponent calls the correct version based on the Role.
    // These may be called during move playback and correction during network updates.
    //
 
    /** Perform movement on an autonomous client */
    ENGINE_API virtual void PerformMovement(float DeltaTime);

以下は、 かなりざっくり直接ローカルの動きに関わりそうな部分だけにした PerformMovement のソースです。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L2453

// CharacterMovementComponent.cpp
 void UCharacterMovementComponent::PerformMovement(float DeltaSeconds)
 {
    FVector OldVelocity;
    FVector OldLocation;
  
    // Scoped updates can improve performance of multiple MoveComponent calls.
    {
        MaybeUpdateBasedMovement(DeltaSeconds);
        
        OldVelocity = Velocity;
            OldLocation = UpdatedComponent->GetComponentLocation();
    
            ApplyAccumulatedForces(DeltaSeconds);
            
            // Character::LaunchCharacter() has been deferred until now.

            HandlePendingLaunch();
            ClearAccumulatedForces();
        
            // Clear jump input now, to allow movement events to trigger it for next update.
            CharacterOwner->ClearJumpInput(DeltaSeconds);
            NumJumpApexAttempts = 0;
        
        // change position
        StartNewPhysics(DeltaSeconds, 0);   
    
            // Update the character state before we do our movement
            UpdateCharacterStateBeforeMovement(DeltaSeconds);
        
        OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity);
    } // End scoped movement update
    
    // Call external post-movement events. These happen after the scoped movement completes in case the events want to use the current state of overlaps etc.
    CallMovementUpdateDelegate(DeltaSeconds, OldLocation, OldVelocity);
    
    UpdateComponentVelocity();
    
    const FVector NewLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector;
    const FQuat NewRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity;
    
    LastUpdateLocation = NewLocation;
        LastUpdateRotation = NewRotation;
        LastUpdateVelocity = Velocity;
 }

上記だけを見ると、 VelocityOldVelocityLastUpdateVelocity に代入されているだけです。

では change position のコメントが付いている StartNewPhysics を見てみます。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Classes/GameFramework/CharacterMovementComponent.h#L1377

// CharacterMovementComponent.h
    /** changes physics based on MovementMode */
    ENGINE_API virtual void StartNewPhysics(float deltaTime, int32 Iterations);
    
    /** @note Movement update functions should only be called through StartNewPhysics()*/
    ENGINE_API virtual void PhysWalking(float deltaTime, int32 Iterations);

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L3200

// CharacterMovementComponent.cpp
 void UCharacterMovementComponent::StartNewPhysics(float deltaTime, int32 Iterations)
 {
    if ((deltaTime < MIN_TICK_TIME) || (Iterations >= MaxSimulationIterations) || !HasValidData())
    {
        return;
    }
 
    if (UpdatedComponent->IsSimulatingPhysics())
    {
        UE_LOG(LogCharacterMovement, Log, TEXT("UCharacterMovementComponent::StartNewPhysics: UpdateComponent (%s) is simulating physics - aborting."), *UpdatedComponent->GetPathName());
        return;
    }
 
    const bool bSavedMovementInProgress = bMovementInProgress;
    bMovementInProgress = true;
 
    switch ( MovementMode )
    {
    case MOVE_None:
        break;
    case MOVE_Walking:
        PhysWalking(deltaTime, Iterations);
        break;
    case MOVE_NavWalking:
        PhysNavWalking(deltaTime, Iterations);
        break;
    case MOVE_Falling:
        PhysFalling(deltaTime, Iterations);
        break;
    case MOVE_Flying:
        PhysFlying(deltaTime, Iterations);
        break;
    case MOVE_Swimming:
        PhysSwimming(deltaTime, Iterations);
        break;
    case MOVE_Custom:
        PhysCustom(deltaTime, Iterations);
        break;
    default:
        UE_LOG(LogCharacterMovement, Warning, TEXT("%s has unsupported movement mode %d"), *CharacterOwner->GetName(), int32(MovementMode));
        SetMovementMode(MOVE_None);
        break;
    }
 
    bMovementInProgress = bSavedMovementInProgress;
    if ( bDeferUpdateMoveComponent )
    {
        SetUpdatedComponent(DeferredUpdatedMoveComponent);
    }
 }

IsSimulatingPhysics が true の時には、物理シミュレーションが実行されているため、物理演算は実行されないようです。

MovementMode 事に動きが変わるようですが、試しに PhysWalking を見てみます。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Classes/GameFramework/CharacterMovementComponent.h#L1886

// CharacterMovementComponent.h
    /** @note Movement update functions should only be called through StartNewPhysics()*/
    ENGINE_API virtual void PhysWalking(float deltaTime, int32 Iterations);

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L5260

ここもだいぶ省略してコードを貼っています。

// ChracterMovmentComponent.cpp
 void UCharacterMovementComponent::PhysWalking(float deltaTime, int32 Iterations)
 {
    bJustTeleported = false;
    bool bCheckedFall = false;
    bool bTriedLedgeMove = false;
    float remainingTime = deltaTime;
    
    // Perform the move
    while ( 
     (remainingTime >= MIN_TICK_TIME) 
     && (Iterations < MaxSimulationIterations) 
     && CharacterOwner 
     && (CharacterOwner->Controller || bRunPhysicsWithNoController || HasAnimRootMotion() || CurrentRootMotion.HasOverrideVelocity() || (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy)) 
    )
    {
        Iterations++;
        bJustTeleported = false;
        const float timeTick = GetSimulationTimeStep(remainingTime, Iterations);
        remainingTime -= timeTick;
   
        // Save current values
        UPrimitiveComponent * const OldBase = GetMovementBase();
        const FVector PreviousBaseLocation = (OldBase != NULL) ? OldBase->GetComponentLocation() : FVector::ZeroVector;
        const FVector OldLocation = UpdatedComponent->GetComponentLocation();
        const FFindFloorResult OldFloor = CurrentFloor;
   
        RestorePreAdditiveRootMotionVelocity();
   
        // Ensure velocity is horizontal.
        MaintainHorizontalGroundVelocity();
        const FVector OldVelocity = Velocity;
        Acceleration = FVector::VectorPlaneProject(Acceleration, -GravityDirection);
        
        // Apply acceleration
            if( !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() )
            {
                // 🏁 Velocity の算出
                CalcVelocity(timeTick, GroundFriction, false, GetMaxBrakingDeceleration());
                devCode(ensureMsgf(!Velocity.ContainsNaN(), TEXT("PhysWalking: Velocity contains NaN after CalcVelocity (%s)\n%s"), *GetPathNameSafe(this), *Velocity.ToString()));
            }
        
        ApplyRootMotionToVelocity(timeTick);
        
        // Compute move parameters
            const FVector MoveVelocity = Velocity; // 🏁 Velocity の代入
            const FVector Delta = timeTick * MoveVelocity; // 🏁 Velocity の Delta
            const bool bZeroDelta = Delta.IsNearlyZero();
            FStepDownResult StepDownResult;
        
            if ( bZeroDelta )
            {
                remainingTime = 0.f;
            }
            else
            {
                // try to move forward
                MoveAlongFloor(MoveVelocity, timeTick, &StepDownResult); // 🏁 Velocity を利用
            }

CalcVelocity というそのままの名前の関数がでてきました。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Classes/GameFramework/CharacterMovementComponent.h#L1475

// CharacterMovementComponent.h
    /** 
    * Updates Velocity and Acceleration based on the current state, applying the effects of friction and acceleration or deceleration. Does not apply gravity.
    * This is used internally during movement updates. Normally you don't need to call this from outside code, but you might want to use it for custom movement modes.
    *
    * @param   DeltaTime                       time elapsed since last frame.
    * @param   Friction                        coefficient of friction when not accelerating, or in the direction opposite acceleration.
    * @param   bFluid                          true if moving through a fluid, causing Friction to always be applied regardless of acceleration.
    * @param   BrakingDeceleration             deceleration applied when not accelerating, or when exceeding max velocity.
    */
    UFUNCTION(BlueprintCallable, Category="Pawn|Components|CharacterMovement")
    ENGINE_API virtual void CalcVelocity(float DeltaTime, float Friction, bool bFluid, float BrakingDeceleration);

This is used internally during movement updates

とあるので、この辺のコードはやはり movement updates のフェーズと考えて良さそうです。そしてこのメソッドは通常は外部から呼ぶことは無いようです。

Does not apply gravity.

この計算には重力は適用されないようです。

CalcVelocity のソースは以下ですが、Velocity の計算をし Velocity 変数に代入していることは分かったのでここではスルーします。
https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L3531

さて、 PhysWalking に戻りますが、 CalcVelocity の後で const FVector MoveVelocity = Velocity と代入し MoveAlongFloor(MoveVelocity, timeTick, &StepDownResult); で利用しています。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Classes/GameFramework/CharacterMovementComponent.h#L1923

// CharacterMovementComponent.h
    /**
    * Move along the floor, using CurrentFloor and ComputeGroundMovementDelta() to get a movement direction.
    * If a second walkable surface is hit, it will also be moved along using the same approach.
    *
    * @param InVelocity:           Velocity of movement
    * @param DeltaSeconds:         Time over which movement occurs
    * @param OutStepDownResult:    [Out] If non-null, and a floor check is performed, this will be updated to reflect that result.
    */
    ENGINE_API virtual void MoveAlongFloor(const FVector& InVelocity, float DeltaSeconds, FStepDownResult* OutStepDownResult = NULL);
    
    /**
    * Compute a vector of movement, given a delta and a hit result of the surface we are on.
    *
    * @param Delta:                Attempted movement direction
    * @param RampHit:              Hit result of sweep that found the ramp below the capsule
    * @param bHitFromLineTrace:    Whether the floor trace came from a line trace
    *
    * @return If on a walkable surface, this returns a vector that moves parallel to the surface. The magnitude may be scaled if bMaintainHorizontalGroundVelocity is true.
    * If a ramp vector can't be computed, this will just return Delta.
    */
    ENGINE_API virtual FVector ComputeGroundMovementDelta(const FVector& Delta, const FHitResult& RampHit, const bool bHitFromLineTrace) const; 

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L5159

// CharacterMovementComponent.cpp
 void UCharacterMovementComponent::MoveAlongFloor(const FVector& InVelocity, float DeltaSeconds, FStepDownResult* OutStepDownResult)
 {
    // Move along the current floor
    const FVector Delta = RotateGravityToWorld(RotateWorldToGravity(InVelocity) * FVector(1.0, 1.0, 0.0)) * DeltaSeconds;
    FHitResult Hit(1.f);
    FVector RampVector = ComputeGroundMovementDelta(Delta, CurrentFloor.HitResult, CurrentFloor.bLineTrace);
    SafeMoveUpdatedComponent(RampVector, UpdatedComponent->GetComponentQuat(), true, Hit);
    float LastMoveTimeSlice = DeltaSeconds;

さて、ここでも InVelocity に対して多少の調整は入っていますが、いよいよ SafeMoveUpdatedComponent に渡されます。
SafeMoveUpdatedComponent は内部で MoveUpdatedComponent 、そして MoveComponentImpl を呼び出しています。

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/SceneComponent.cpp#L3018

// SceneComponent.cpp
 bool USceneComponent::MoveComponentImpl(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit, EMoveComponentFlags MoveFlags, ETeleportType Teleport)
 {
    // just teleport, sweep is supported for PrimitiveComponents. This will update child components as well.
    const bool bMoved = InternalSetWorldLocationAndRotation(GetComponentLocation() + Delta, NewRotation, false, Teleport);

https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/SceneComponent.cpp#L2852

// SceneComponent.cpp
bool USceneComponent::InternalSetWorldLocationAndRotation(FVector NewLocation, const FQuat& RotationQuat, bool bNoPhysics, ETeleportType Teleport)
 {
    const FRotator NewRelativeRotation = RelativeRotationCache.QuatToRotator_ReadOnly(NewRotationQuat);
    bool bDiffLocation = !NewLocation.Equals(GetRelativeLocation());
    bool bDiffRotation = !NewRelativeRotation.Equals(GetRelativeRotation());
    if (bDiffLocation || bDiffRotation)
    {
        SetRelativeLocation_Direct(NewLocation);
  
        // Here it is important to compute the quaternion from the rotator and not the opposite.
        // In some cases, similar quaternions generate the same rotator, which create issues.
        // When the component is loaded, the rotator is used to generate the quaternion, which
        // is then used to compute the ComponentToWorld matrix. When running a blueprint script,  
        // it is required to generate that same ComponentToWorld otherwise the FComponentInstanceDataCache
        // might fail to apply to the relevant component. In order to have the exact same transform
        // we must enforce the quaternion to come from the rotator (as in load)
        if (bDiffRotation)
        {
            SetRelativeRotation_Direct(NewRelativeRotation);
            RelativeRotationCache.RotatorToQuat(NewRelativeRotation);
        }

SetRelativeLocation_DirectSetRelativeRotation_Direct は internal な関数ですが、 _Direct が無いものと同じと考えれば良さそうです。

はい、ようやくひとつの入力が Location, Rotation の変更になるところまでたどり着きました。

一通り追ってみての感想

終盤は疲れもあって足早になりましたが、とりあえず目的の Location/Rotation の変更箇所までたどり着けました。

コードの引用には省略箇所が多いのですが、その中には多く RootMotion に関するものや、 Collision に関するものが入っていました。
魔法ではないことは分かりましたが、それでも CharacterMovementComponent はキャラクターを動かすためのたくさんのノウハウが入っているのだろうと想像ができるようになりました。
現在接地している Floor の判定も保持しているようであり、常にキャラクターの状況を把握しているのを考えると気が遠くなります。

まだまだ導入部分でしか無いと思いますが、今後も少しずつ内部の動きも確認していければと思います。