動機
例えば Scratch で 2D ゲームを作ろうとしたとき、キャラクターはその「座標」を変更して移動させます。つまり、プレイヤーの入力は「座標の変更」に反映されており、それがダイレクトにコードに現れます。
Unreal Engine においても、例えば「動く足場」のようなものを作るのであれば、それは Actor の Location
や Offset
を操作するノードを使って座標を変えていくことで実現することができます。
一方で、ThirdPersonCharacter
を使うと突然 AddMovementInput
ノードが出現し、それを使うことでなんとも素敵な動きができてしまいます。
愚かな私は、きっと「Unreal Engine がこのノードで魔法のような何かをしてくれているからキャラクターは動くのだ」と思っていたのですが、そんな訳もなく(実際、魔法のようにいろいろとしてくれてはいるのですが)、最終的には Location/Rotation
に反映されているだけということを確認したくなりました。
以降では、全てではありませんが適宜 Github の permalink も付けます。ほとんどの箇所は省略しつつコードを引用するので、不明な箇所は適宜 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 は適用されないということのようです。 Character
や DefaultPawn
を使いましょうと。
GetMovementComponent
さて、Pawn.cpp
の UPawnMovementComponent* 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.cpp
と DefaultPawn.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; }
Character
の CharacterMovement
はコンストラクタで入れられています。
// Character.cpp CharacterMovement = CreateDefaultSubobject<UCharacterMovementComponent>(ACharacter::CharacterMovementComponentName); if (CharacterMovement) { CharacterMovement->UpdatedComponent = CapsuleComponent; }
UCharacterMovementComponent
も以下の通り UPawnMovementComponent
のサブクラスです。
// CharacterMovementComponent.h class UCharacterMovementComponent : public UPawnMovementComponent, public IRVOAvoidanceInterface, public INetworkPredictionInterface
DefaultPawn
の MovementComponent
はコンストラクタで入れられています。
// 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
なんですね 💡💡
AddInputVector
は virtual
ですが、CharacterMovementComponent
や UFloatingPawnMovement
で継承はされていません。
// 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; } }
先程のコメントにもあったように ControlInputVector
に WorldAccel
が蓄積されているように見えます。
これも継承ができるクラスでは無いので 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; }
PawnOwner
の Internal_ConsumeMovementInputVector
を呼び出しています。
そして、コメントが ConsumeMovementInputVector
とほぼ同じことに気づきます。このメソッドも virtual
ですが継承はありません。
さて、 Internal_ConsumeMovementInputVector
はすでに見た通りですし、これに継承もありません。
ConsumeMovementInputVector
の呼び出し元に期待する場所が見つからないので続いてこの ConsumeInputVector
がどこで実行されているか見ていきます。
先ほどのコメントどおりであれば by the Pawn or PawnMovementComponent
のような場所の movement update 期間に実行されているはずです。
ConsumeInputVector の呼び出し箇所
CharacterMovementComponent
と FloatingPawnMovement
の 2 箇所で見つかりました。
CharacterMovementComponent
- https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L1457
- https://github.com/EpicGames/UnrealEngine/blob/072300df18a94f18077ca20a14224b5d99fee872/Engine/Source/Runtime/Engine/Private/Components/CharacterMovementComponent.cpp#L13044
// 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
されていることが確認できました。
では、ここからはいよいよそれぞれがどのように Character
や Pawn
を動かしているかみていきます。もちろん見るのは先ほどの ConsumeInputVector
の周辺となります。
// 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
を見てみます。
// 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(); } // 略 }
渡された InputVector
は ConstrainInputAcceleration
, ConstrainInputAcceleration
と順に処理されています (Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector))
)。
まずは ConstrainInputAcceleration
から見ていきます。
// 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
です。
// CharacterMovementComponent.cpp FVector UCharacterMovementComponent::ScaleInputAcceleration(const FVector& InputAcceleration) const { return GetMaxAcceleration() * InputAcceleration.GetClampedToMaxSize(1.0f); }
どうやら InputVector
は 1.0f
に Clamp されたうえで、MaxAcceleration
に掛けられるようです。( 0 - 1 の間になるというのはここが関係しているのかとも思いましたが、ちょっと判断できないです)
改めて Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector))
を確認しますが、ここまでの計算結果が Acceleration
に代入されています。
ということで、次に追っていくのは Acceleration
になります。
Acceleration
まずは定義です。
// 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
を計算した後も処理が続いています。
// 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
を確認します。
// 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
のソースです。
// 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; }
上記だけを見ると、 Velocity
は OldVelocity
と LastUpdateVelocity
に代入されているだけです。
では change position
のコメントが付いている StartNewPhysics
を見てみます。
// 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);
// 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
を見てみます。
// CharacterMovementComponent.h /** @note Movement update functions should only be called through StartNewPhysics()*/ ENGINE_API virtual void PhysWalking(float deltaTime, int32 Iterations);
ここもだいぶ省略してコードを貼っています。
// 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
というそのままの名前の関数がでてきました。
// 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);
で利用しています。
// 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;
// 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
を呼び出しています。
// 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);
// 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_Direct
と SetRelativeRotation_Direct
は internal な関数ですが、 _Direct
が無いものと同じと考えれば良さそうです。
はい、ようやくひとつの入力が Location
, Rotation
の変更になるところまでたどり着きました。
一通り追ってみての感想
終盤は疲れもあって足早になりましたが、とりあえず目的の Location/Rotation の変更箇所までたどり着けました。
コードの引用には省略箇所が多いのですが、その中には多く RootMotion
に関するものや、 Collision
に関するものが入っていました。
魔法ではないことは分かりましたが、それでも CharacterMovementComponent
はキャラクターを動かすためのたくさんのノウハウが入っているのだろうと想像ができるようになりました。
現在接地している Floor の判定も保持しているようであり、常にキャラクターの状況を把握しているのを考えると気が遠くなります。
まだまだ導入部分でしか無いと思いますが、今後も少しずつ内部の動きも確認していければと思います。