Before I rip out the movement code for the UE5 Pawn, I want to make sure I understand how everything works. Movement has a lot of implications for collision and general physics systems so knowing what happens after you send an input is critical. I need to make sure my custom collision system doesn’t fight with the UE5 client code.
It all starts with Input. In my case I am using the default Third Person template, meaning I have a third person character configured by default when I created the project. UE5 uses the new UEnhancedInputComponent which is added to the character as the UInputComponent. More details can be found on the Enhanced Input page. There’s two (common) types of inputs you can bind, Actions and Axis (there is more, but for my purposes I’m ignoring touch/gesture). The docs state:
An Enhanced Input Component is a transient component that enables an Actor to bind enhanced actions to delegate functions, or monitor those actions. Input components are processed from a stack managed by the PlayerController and processed by the PlayerInput. These bindings will not consume input events
What this ends up looking like in our character class is this:
//Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
//Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ThisClass::Move);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &ThisClass::StopMoving);
//Looking
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ThisClass::Look);
You’ll notice it mixes the delegate calls with the base Character class, and our overloaded (in this case) PMOClientCharacter class, which I renamed to use the ThisClass aliases. The BindAction delegates call our methods with an FInputActionValue. With a keyboard this is usually a (+/-)1.0 value. Mouse and Game pad controller values can be much smaller/larger.
For Move some additional calculations must be done given our rotation, so we know which is forward and which is right. (We could be upside down!) we then pass these values to the Pawn::AddMovementInput method. If it has a UPawnMovementComponent or not, we end up calling into Pawn::Internal_AddMovementInput, with the direction * scale as you can see in this mermaid chart:
If you look into the details of the UPawnMovementComponent you can see almost every method just calling back into the PawnOwner->Internal_<SomeMethod>. So we can basically ignore this class altogether.
So that adds our new acceleration value to the ControlInputVector, then what happens? Also, what is control input vector?
/**
* 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;
While it seems like we are getting somewhere, this ends up being the end of this process. Which was confusing as it just seems to be written to this ControlInputVector in the Pawn class then, just returns? Clearly something must be reading this value somewhere.
ACharacter and UCharacterMovementComponent Classes
In the Third Player base project, you’ll notice your A<GameName>Character inherits from the ACharacter class. You’ll also notice in your auto-generated A<GameName>Character constructor sets some values in the GetCharacterMovement() which is an accessor for UCharacterMovementComponent.
The majority of interesting logic is handled in the UCharacterMovementComponent, but you will notice looking at the ACharacter code that it’s built for client/server RPC.
//////////////////////////////////////////////////////////////////////////
// Server RPC that passes through to CharacterMovement (avoids RPC overhead for components).
// The base RPC function (eg 'ServerMove') is auto-generated for clients to trigger the call to the server function,
// eventually going to the _Implementation function (which we just pass to the CharacterMovementComponent).
//////////////////////////////////////////////////////////////////////////
UFUNCTION(unreliable, server, WithValidation)
void ServerMovePacked(const FCharacterServerMovePackedBits& PackedBits);
void ServerMovePacked_Implementation(const FCharacterServerMovePackedBits& PackedBits);
bool ServerMovePacked_Validate(const FCharacterServerMovePackedBits& PackedBits);
//////////////////////////////////////////////////////////////////////////
// Client RPC that passes through to CharacterMovement (avoids RPC overhead for components).
//////////////////////////////////////////////////////////////////////////
UFUNCTION(unreliable, client, WithValidation)
void ClientMoveResponsePacked(const FCharacterMoveResponsePackedBits& PackedBits);
void ClientMoveResponsePacked_Implementation(const FCharacterMoveResponsePackedBits& PackedBits);
bool ClientMoveResponsePacked_Validate(const FCharacterMoveResponsePackedBits& PackedBits);
RPCs in UE are made up of three methods, the name of the method, the _Implementation and _Validate. Validation is for accepting the RPC, and Implementation is well, obvious. I’m pretty sure there’s some macro magic some where the controls of this, I just forget where.
The Implementation’s just call into the UCharacterMovementComponent. This class is about 12,968 lines of code, Yikes. Luckily, it’s just a component so we can trace it’s execution by following through the TickComponent method:
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();
}
Ahh ha! There’s the call to grab the InputVector from the UPawnMovementComponent. The if (!bUsingAsyncTick) block will most likely always be true, because AsyncCharacterMovement is disabled (meaning, 0) by default, and requires a Cvar to be enabled.
The tick component method has a lot of calls to test various conditions, like are we ragdoll’ing? If so, we probably don’t care about that InputVector any more as gravity/physics system will handle it differently. Further down the method we see the following block:
// 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);
The last line shows us calling into ControlledCharacterMove with our InputVector. The ControlledCharacterMove method checks if we are jumping first and makes sure that we are of ROLE_Authority (which we will be in a single player game). In this case we call PerformMovement.
If we are not, then we call ReplicateMoveToServer with our InputVector translated to an acceleration value.
PerformMovement
This is where the magic happens, and by magic, I mean ~380 lines of code is executed.
- First they check if the component (player) can even move, or if it’s simulating physics, meaning it will be moved in another method
- It then checks if there’s any additional velocity added to the character, and adds it to the characters root motion
- It creates something called a FScopedMovementUpdate, which appears to be used to create almost like a list of movements that can be scheduled/deferred etc. To me it seems almost like a transaction, where it’s preparing all the movements but doesn’t change various aspects such as the bounds/transforms until everything is ready.
- It does some check if we are on a platform, or something else that is moving underneath us (like a boat)
- Checks if the character state is going to change. which weirdly looks like it just checks if the character will crouch/un-crouch?
- Checks if the character has
PendingLaunchVelocity(which I assume means the character class called LaunchCharacter) - Clears pending forces (impulse/force/launch etc.)
- Re-runs the same from Step #2 regarding velocity
- If we have a root motion animation, we tick the characters pose
- We then apply that root motion to the velocity of the character, if we don’t have root motion, then we basically do the same thing without taking the root motion into consideration. Both of these conditions update
Velocityof the character - Clear our Jump inputs
- Call
StartNewPhysicswith 0 iterations set, this method is interesting because itswitches on ourMovementMode(MOVE_Walking,MOVE_Fallingetc.) and calls an appropriate method, passing in the 0 for iterations. (From now on we are only looking at walking) - In
PhysWalkingwe iterate N times, where N is <MaxSimulationIterations(8 by default) we also track time here so we decrement our remaining time for each iteration. - For each iteration we save a bunch of values, then calculate the velocity (against ground friction / braking deceleration) and apply the root motion to velocity.
- If this application of root motion to velocity would make us fall, we recurse back into
StartNewPhysicsand decrement our Iteration count. - If we still have time left, we call into
MoveAlongFloorwhich sees if we should step up, and does some other calculations. This method may call intoSlideAlongSurfacewhich calls the parentUMovementComponent::SlideAlongSurfaceto actually move the component (and I assume the character) - After
MoveAlongFloorbasically it does a ton of checks whether we changed state(s) like, we fell off a ledge, or are now swimming, the floor changed, like we stepped up a ramp. - At this point we’ve moved and are back into the
PerformMovementmethod, next it appears that rotation is updated by calling intoPhysicsRotation. - After
PhysicsRotationwe apply any changes to the root animation (if it has one). - The OnMovementUpdated event is triggered
- Network events are fired for replication
- Finally, our new values are saved to old values for
LastUpdateLocation,LastUpdateRotationandLastUpdateVelocity
So, that’s how movement from you pressing an input down to moving the character (sort of!) works! Take this with a grain of salt however, I didn’t run this through a debugger, just read the code so I may have made some mistakes or missed some very obvious important things.
What I have learned is there is no way in hell I’m using this movement component, I’ll be writing a far jankier, but simpler one!
