The player character you’ve been building in this tutorial now has a Sprint Movement Modifier and Grappling Hook Movement Mode. In this part of the series, you’ll implement another Movement Mode class that gives the player the ability to run along walls if the player is moving fast enough. Understanding both the Grappling Hook and Wall Run implementations gives you a good base for learning the Mover system so you can start creating your own custom Movement Modes.
Similar to the Grappling Hook, creating the Wall Run move also involves making an Actor Component that checks for wall running conditions and facilitates communication between the Pawn and Movement Mode. Unlike with the Grappling Hook, the player still has some control over character movement and can perform a jump action while in the Wall Run Movement Mode.
The wall running Actor Component will perform line traces to the sides of the character, detecting walls and passing information about the wall and wall jump status to the Wall Run Movement Mode. When the Movement Mode is active, it automatically moves the player along the wall until the wall run ends. You’ll also implement wall jumping and modify the Pawn’s existing jump logic to execute the wall jump.
Create the Wall Running Component
Your Wall Run Actor Component will be responsible for:
Finding valid walls and wall information using a line trace.
Handling wall jump requests.
Enforcing cooldowns to prevent re-entering a wall run and restarting jump input.
Set Up a New Actor Component Class
To set up a new Wall Running component, follow these steps:
In Unreal Editor, go to Tools > New C++ Class and create a new Actor Component class.
Name the class
WallRunActorComponentand click Create Class.Declare the following exposed public functions. Give them a
UFUNCTIONmacro withBlueprintCallableandCategory = “Wall Run”.bool FindWall(FVector& OutWallLocation, FVector& OutWallNormal) const;void SetWallJump();void SetCooldown();void ResetCooldown();void ResetWallJump();
Declare a
publicboolvariable namedbShouldWallJumpand set its default value tofalse. NoUPROPERTYmacro is needed here.C++// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "WallRunActorComponent.generated.h" UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )Declare
protectedvariables. Expose them to Blueprints with aUPROPERTYthat containsEditAnywhereandCategory = "Wall Run".float TraceDistance = 85.f;bool bIsOnCooldown = false;float ForwardAngleDegrees = 15.f;float WallRunCD = 0.8f;float WallJumpCD = 0.2f;float WallNormalTolerance = 0.35f;
C++protected: // How long the line trace will be UPROPERTY(EditAnywhere, Category = "Wall Run") float TraceDistance = 85.f; // Indicates if the wall run cooldown is currently active UPROPERTY(EditAnywhere, Category = "Wall Run") bool bIsOnCooldown = false; // The angular offset (degrees) used to bias left/right wall traces more toward the player's forward directionWallJumpCDandbShouldWallJumpalso affect normal jumping behavior. No jump movements can activate whileWallJumpCDis active.Also in the
protectedsection, declare aFTimerHandlefor the wall running cooldown and the jump cooldown.C++FTimerHandle ResetCDTimerHandle; FTimerHandle ResetWallJumpTimerHandle;A
TimerHandleis used by Unreal Engine’s TimerManager to store and control a timer instance. It’s like a remote control for your timer.
The finished WallRunActorComponent.h file should look like this:
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "WallRunActorComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
Set up WallRunActorComponent.cpp
In WallRunActorComponent.cpp, add the following includes:
#include "WallRunActorComponent.h"
// --- New Code Start ---
#include "GameFramework/Actor.h"
#include "Engine/World.h"
// --- New Code End ---
Disable Ticking in the Constructor
This Movement Mode uses cooldowns, jump actions, and wall detection to control movement behavior, so per-frame updates and checks aren’t needed. Disable ticking to avoid unnecessary overhead.
The component’s constructor should look like this:
UWallRunActorComponent::UWallRunActorComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
Define SetCooldown()
SetCooldown() starts the wall run cooldown timer.
This component class has functions that work with timers and line traces. These operations use the UWorld, which is the top-level runtime context for everything in your level. Always check that the UWorld is valid before calling functions or storing pointers that rely on it.
The SetCooldown() function does the following:
Sets
bIsOnCooldownto true, indicating a wall run cooldown is active.Gets the
UWorldand checks that it’s valid.Calls
SetTimer()on theUWorld’sFTimerManagersoResetCooldown()runs on this component after the wall running cooldown expires. PassSetTimer()the following arguments:ResetCDTimerHandle: The FTimerHandle identifier for the timer.this: The owning object.&UWallRunActorComponent::ResetCooldown: Pointer to a function to call after the timer has expired.WallRunCD: The number of seconds the timer should run.false: Whether the timer loops or not.
void UWallRunActorComponent::SetCooldown()
{
// Activates wall run cooldown state, blocking wall run attempts
bIsOnCooldown = true;
if (UWorld* World = GetWorld())
{
// Schedules ResetCooldown() to run after WallRunCD seconds (one-shot timer)
World->GetTimerManager().SetTimer(
ResetCDTimerHandle, this, &UWallRunActorComponent::ResetCooldown, WallRunCD, false);
Define ResetCooldown()
ResetCooldown() sets bIsOnCooldown to false.
void UWallRunActorComponent::ResetCooldown()
{
bIsOnCooldown = false;
}
Define SetWallJump()
SetWallJump() performs the same operations as SetCooldown() but for the wall jump cooldown.
void UWallRunActorComponent::SetWallJump()
{
// Marks that a wall jump has been requested (consumed by Movement Mode)
bShouldWallJump = true;
if (UWorld* World = GetWorld())
{
// Schedules ResetWallJump() to clear the jump request after WallJumpCD seconds (one-shot timer)
World->GetTimerManager().SetTimer(
ResetWallJumpTimerHandle, this, &UWallRunActorComponent::ResetWallJump, WallJumpCD, false);
Define ResetWallJump()
ResetWallJump() sets bShouldWallJump to false.
void UWallRunActorComponent::ResetWallJump()
{
bShouldWallJump = false;
}
Define FindWall()
The FindWall() function performs a line trace from either side of the character and then returns true if it finds a valid wall and false if it doesn’t. The angles of these line traces also mean that the player must be running roughly alongside a wall to detect it — approaching a wall head on won’t register a hit.
The function also sets two parameters to pass by reference:
OutWallLocation: The location of the wall hit by the line trace.OutWallNormal: The surface normal of the wall hit by the line trace.
The Wall Run Movement Mode uses OutWallLocation and OutWallNormal to calculate the player's starting position on the wall and the direction they move along its surface.
To implement FindWall(), you’ll use rotators to orient line trace directions by applying a yaw offset to a base direction. Vectors are useful when working with directions, while rotators are best when you want to modify orientation.
To define variables and gather data needed for the line trace in FindWall(), follow these steps:
In the function, store the component’s owning player and return
falseif the player isn’t valid.C++bool UWallRunActorComponent::FindWall(FVector& OutWallLocation, FVector& OutWallNormal) const { AActor* Owner = GetOwner(); if (!Owner) return false; }Return
falseif wall running is currently on cooldown.C++bool UWallRunActorComponent::FindWall(FVector& OutWallLocation, FVector& OutWallNormal) const { AActor* Owner = GetOwner(); if (!Owner) return false; if (bIsOnCooldown) return false; }Declare and get two new
FVectors:Start: the player’s current location.Right: The player’s right vector.
C++bool UWallRunActorComponent::FindWall(FVector& OutWallLocation, FVector& OutWallNormal) const { AActor* Owner = GetOwner(); if (!Owner) return false; if (bIsOnCooldown) return false; // --- New Code Start --- FVector Start = Owner->GetActorLocation(); FVector Right = Owner->GetActorRightVector(); // --- New Code End ---Convert the player’s
Rightvector into aFRotator:Declare a
FRotatornamedRightRot. AnFRotatorstores the pitch (up/down tilt), yaw (turn left/right), and roll (sideways tilt) around each axis.Use
FRotationMatrix::MakeFromX(Right)to convert the player’sRightvector into a full 3D coordinate frame. This rotation matrix usesRightas the forward X axis and calculates Y and Z to maintain orthogonality.Call
.Rotator();on theFRotationMatrixto convert it to aFRotatorto store inRightRot.The vector is first converted into a rotation matrix to fully define the orientation in 3D space before converting it to a rotator. Converting directly from vector to rotator only defines the forward direction (yaw and pitch).
C++FRotator RightRot = FRotationMatrix::MakeFromX(Right).Rotator();Rotate the
RightRot’sYawforward byForwardAngleDegreesto bias the angle forward so a line trace will detect walls the player is moving towards, not directly beside.Get the normalized vector of
RightRotto use in the line trace.C++bool UWallRunActorComponent::FindWall(FVector& OutWallLocation, FVector& OutWallNormal) const { AActor* Owner = GetOwner(); if (!Owner) return false; if (bIsOnCooldown) return false; FVector Start = Owner->GetActorLocation(); FVector Right = Owner->GetActorRightVector(); // Rotate the side traces slightly forwardRepeat the process for the left side, using the negated
Rightvector to build the rotation matrix. Because the left direction faces the opposite way, addForwardAngleDegreesto theYawinstead of subtracting it, so both traces angle toward the player's forward direction.C++// Rotate the side traces slightly forward FRotator RightRot = FRotationMatrix::MakeFromX(Right).Rotator(); RightRot.Yaw -= ForwardAngleDegrees; FVector RightDir = RightRot.Vector(); // --- New Code Start --- FRotator LeftRot = FRotationMatrix::MakeFromX(-Right).Rotator(); LeftRot.Yaw += ForwardAngleDegrees; FVector LeftDir = LeftRot.Vector(); // --- New Code End ---Declare an array with
LeftDirandRightDiras elements.To set up variables and collision query parameters to perform line traces and store hits, declare the following local variables:
FHitResult HitFCollisionQueryParams Params;
Add
AddIgnoredActor(Owner);toParamsso line traces ignore the Pawn.C++FRotator LeftRot = FRotationMatrix::MakeFromX(-Right).Rotator(); LeftRot.Yaw += ForwardAngleDegrees; FVector LeftDir = LeftRot.Vector(); // --- New Code Start --- // Set up traces TArray<FVector> Directions = { LeftDir, RightDir }; FHitResult Hit; FCollisionQueryParams Params;Setting up the
Paramsvariable creates and configures collision query settings for the line traces so the trace doesn’t register hits on the player.
To perform the line trace and save a hit, follow these steps:
Add a
forloop that repeats the line trace for eachFVector(right and left) in theDirectionsarray.C++FHitResult Hit; FCollisionQueryParams Params; Params.AddIgnoredActor(Owner); // --- New Code Start --- for (const FVector& Dir : Directions) { } // --- New Code End ---In the loop, calculate where the trace should end by starting at the player and moving
TraceDistanceunits (cm) in theDirvector’s direction.C++for (const FVector& Dir : Directions) { // --- New Code Start --- FVector End = Start + Dir * TraceDistance; // --- New Code End --- }Perform a line trace with
GetWorld()->LineTraceSingleByChannel(), storing the boolean result in a new variable namedbHit. Pass the line traceHit,Start,End, theECC_Visibilitycollision channel (used for line-of-sight camera checks), andParams.This simulates a line trace from
StarttoEnd, colliding with any visible objects that aren’t the player, and stores the hit result data inHit. The line trace returnstrueif it hit something andfalseif it didn’t.C++for (const FVector& Dir : Directions) { FVector End = Start + Dir * TraceDistance; // --- New Code Start --- bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params); // --- New Code End --- }
To process the hit result, follow these steps:
After the line trace and still within the
forloop, add anifstatement to checkbHit.C++for (const FVector& Dir : Directions) { FVector End = Start + Dir * TraceDistance; bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params); // --- New Code Start --- if (bHit) {If there’s a hit, get and save the Actor that was hit, and check that the Actor is valid.
Ignore the Actor if it has a
NoWallRuntag.C++if (bHit) { AActor* HitActor = Hit.GetActor(); if (!HitActor) continue; // If Actor is tagged NoWallRun, skip it and continue to next loop iteration if (HitActor->ActorHasTag(FName("NoWallRun"))) { continue; }In the level editor, you’ll add this tag to Actors in your level to block wall running on those Actors.
Get and save the hit’s impact normal, and then calculate the cross product of the impact normal and a
FVector::UpVector.C++if (bHit) { AActor* HitActor = Hit.GetActor(); if (!HitActor) continue; // If Actor is tagged NoWallRun, skip it and continue to next loop iteration if (HitActor->ActorHasTag(FName("NoWallRun"))) { continue; }Similar to the dot product calculations you used in Grappling Hook Movement Mode, you can use the cross product to measure how different the hit surface normal is from an up (vertical) vector. The magnitude of the cross product equals the area of the parallelogram formed by these two vectors. The area of that parallelogram is smaller if the vectors are similar and larger if they are perpendicular.
So, if the hit surface normal is parallel to the up vector (a floor or ceiling), the cross product is zero; if it’s perpendicular to the up vector (a vertical wall), the cross product has its highest possible magnitude.
Add an
ifstatement to compare the cross product withWallNormalTolerance.Cross.Size()returns the magnitude of the Cross vector.If the hit surface passes the wall check:
Get the impact point of the line trace hit and save it to
OutWallLocation.Get the safe normal of the impact normal and save it to
OutWallNormal.Return
true.
C++if (bHit) { AActor* HitActor = Hit.GetActor(); if (!HitActor) continue; // If Actor is tagged NoWallRun, skip it if (HitActor->ActorHasTag(FName("NoWallRun"))) { continue; }After the
if (bHit)statement, at the end of the function, returnfalse.
The finished WallRunActorComponent.cpp file should look like this:
// Copyright Epic Games, Inc. All Rights Reserved.
#include "WallRunActorComponent.h"
#include "GameFramework/Actor.h"
#include "Engine/World.h"
// Sets default values for this component's properties
UWallRunActorComponent::UWallRunActorComponent()
{
PrimaryComponentTick.bCanEverTick = false;
Create the Wall Running Movement Mode
The Wall Run Movement Mode builds on what you learned implementing the Grappling Hook Movement Mode. With wall running, you’ll use more variables that interact with the movement.
The Movement Mode will:
Get the location and normal vector from a target wall (passed from the Wall Run Actor Component).
Tilt the camera to give visual feedback to the player.
Check the wall’s location and vector every frame in case the wall curves or changes to a different angle.
Constrict the player camera so they can’t look too far from the direction the Pawn is running along the wall, as it would feel unnatural to look back while wall running forward.
Create and Set Up the Movement Mode Class
First, create a new Movement Mode class for wall running.
To set up the Wall Run Movement Mode class, follow these steps:
In Unreal Editor, create a new C++ class of type BaseMovementMode. Name it
WallRunMovementMode.In the header file, add the following includes and forward declarations:
C++// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "MovementMode.h" // --- New Code Start --- #include "Camera/CameraComponent.h" #include "WallRunActorComponent.h" // --- New Code End ---In the
UCLASSmacro, addBlueprintableandBlueprintTypespecifiers.In the
publicsection, declare the class constructor.C++UCLASS(Blueprintable, BlueprintType) class PARKOURGAME_API UWallRunMovementMode : public UBaseMovementMode { GENERATED_BODY() public: UWallRunMovementMode(const FObjectInitializer& ObjectInitializer); };Declare overrides for the core functions and
CommonLegacySettings(same as the Grappling Hook Movement Mode).C++UCLASS(Blueprintable, BlueprintType) class PARKOURGAME_API UWallRunMovementMode : public UBaseMovementMode { GENERATED_BODY() public: UWallRunMovementMode(const FObjectInitializer& ObjectInitializer); virtual void GenerateMove_Implementation(const FMoverSimContext& SimContext, const FMoverTickStartData& StartState,const FMoverTimeStep& TimeStep, FProposedMove& OutProposedMove) const override;In the cpp file, include the following headers:
C++// Copyright Epic Games, Inc. All Rights Reserved. #include "WallRunMovementMode.h" #include "GameFramework/Pawn.h" #include "Camera/CameraComponent.h" #include "MoveLibrary/MovementUtils.h" #include "MoveLibrary/FloorQueryUtils.h" #include "MoverComponent.h" #include "DefaultMovementSet/Settings/CommonLegacyMovementSettings.h" #include "DefaultMovementSet/InstantMovementEffects/BasicInstantMovementEffects.h"
Implement Core Functions
To set up the class constructor and GenerateMove functions, follow these steps:
Add a class constructor that registers shared settings and adds the
Mover_IsOnGroundgameplay tag.C++UWallRunMovementMode::UWallRunMovementMode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { SharedSettingsClasses.Add(UCommonLegacyMovementSettings::StaticClass()); GameplayTags.AddTag(Mover_IsOnGround); }Add the implementation for
GenerateMove. This is the same setup used in the Grappling Hook Movement Mode.C++//Movement handled by simulation tick void UWallRunMovementMode::GenerateMove_Implementation(const FMoverSimContext& SimContext, const FMoverTickStartData& StartState, const FMoverTimeStep& TimeStep, FProposedMove& OutProposedMove) const { const UMoverComponent* MoverComp = GetMoverComponent(); const FCharacterDefaultInputs* CharacterInputs = StartState.InputCmd.InputCollection.FindDataByType<FCharacterDefaultInputs>(); const FMoverDefaultSyncState* StartingSyncState = StartState.SyncState.SyncStateCollection.FindDataByType<FMoverDefaultSyncState>(); check(StartingSyncState); const float DeltaSeconds = TimeStep.StepMs * 0.001f;
Define InitializeWallRun()
InitializeWallRun() runs once at the start of a wall run movement once a valid wall is detected. It does the following to set up the initial state needed for wall running:
Positions the character at a consistent offset from the wall.
Computes the tangent direction to move along the wall.
Aligns that direction with the player’s forward movement.
Applies an initial camera roll based on which side the wall is on.
To implement InitializeWallRun(), follow these steps:
In the header file, in the protected section, declare the function.The function takes the following arguments:
float DeltaSeconds: time step for the current frameUMoverComponent* MoverComp: the player’s Mover Component
C++// Initializes wall run state when a valid wall is first detected // Computes starting position, movement direction along the wall, and applies initial camera tilt void InitializeWallRun(float DeltaSeconds, UMoverComponent* MoverComp);Declare the following
publicvariables and references:C++// World-space location where the wall was detected (set by wall detection logic) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) FVector HitLocation = FVector::ZeroVector; // Surface normal of the detected wall (used to determine orientation and movement direction) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) FVector WallNormal = FVector(0, 0, 0); // Desired offset distance from the wall to keep the character from clipping into it UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover)Your Pawn Blueprint will use
FindWall()in theWallRunActorComponentto provideHitLocationandWallNormalto the Movement Mode class.In the .cpp file, implement the function. First, calculate the
StartLocationwhere the player should begin on the wall by offsettingHitLocation.C++void UWallRunMovementMode::InitializeWallRun(float DeltaSeconds, UMoverComponent* MoverComp) { // Offset start position slightly away from the wall StartLocation = HitLocation + WallNormal * DistanceFromWall; }Calculate the cross product of the detected wall’s surface normal and an up vector and get the safe normal of the resulting vector. Save the result as the
WallTangentVector, or the direction that runs along the wall’s surface.The wall tangent becomes the axis the player moves along during the wall run.
C++void UWallRunMovementMode::InitializeWallRun(float DeltaSeconds, UMoverComponent* MoverComp) { // Offset start position slightly away from the wall StartLocation = HitLocation + WallNormal * DistanceFromWall; // --- New Code Start --- // Compute a horizontal direction along the wall (perpendicular to wall normal) WallTangentVector = FVector::CrossProduct(FVector::UpVector, WallNormal).GetSafeNormal(); // --- New Code End --- }The resulting vector of a cross product is always perpendicular to the input vectors.
Get and save the player’s current forward vector to compare with the wall tangent vector.
C++void UWallRunMovementMode::InitializeWallRun(float DeltaSeconds, UMoverComponent* MoverComp) { // Offset start position slightly away from the wall StartLocation = HitLocation + WallNormal * DistanceFromWall; // Compute a horizontal direction along the wall (perpendicular to wall normal) WallTangentVector = FVector::CrossProduct(FVector::UpVector, WallNormal).GetSafeNormal(); // --- New Code Start --- // Ensure wall run direction aligns with player forward directionThe wall tangent can point in either direction along the wall. Add an
ifstatement that checks if the dot product between the tangent and the player’s forward vector is less than 0. If it is, negate the wall tangent to align it with forward movement.C++FVector ForwardVector = MoverComp->GetOwner()->GetActorForwardVector(); // --- New Code Start --- if (FVector::DotProduct(WallTangentVector, ForwardVector) < 0.f) WallTangentVector *= -1.f; // --- New Code End ---Add an
ifstatement to check that the player’s camera is valid. If it is, you can set up the camera to roll slightly while wall running.In the
ifstatement, detect which side of the player the wall is on by measuring alignment between the wall normal and player’s right vector:Get and save the player’s right vector with
GetActorRightVector().Calculate the dot product of
WallNormalandPlayerRightand save it in a newfloatnamedWallSideSign.
C++// Ensure wall run direction aligns with player forward direction FVector ForwardVector = MoverComp->GetOwner()->GetActorForwardVector(); if (FVector::DotProduct(WallTangentVector, ForwardVector) < 0.f) WallTangentVector *= -1.f; // --- New Code Start --- if (Camera) { // Determine which side of the player the wall is on (left/right) FVector PlayerRight = MoverComp->GetOwner()->GetActorRightVector();If the dot product is greater than 0, the wall is on the right side of the player. If the dot product is less than 0, the wall is on their left side.
In the header file, you set a default
CameraTargetRollof11degrees, which is appropriate if the wall is on the right side of the player. If the wall is to the left, negate the default value to roll the other way.Declare a new float named
FinalRolland set it to the inverse ofCameraTargetRollifWallSideSignis less than 0.Store the result in a new variable instead of modifying
CameraTargetRoll(a fixed tuning parameter). InvertingCameraTargetRolldirectly alters the base configuration for future calculations.C++if (Camera) { FVector PlayerRight = MoverComp->GetOwner()->GetActorRightVector(); float WallSideSign = FVector::DotProduct(WallNormal, PlayerRight); // --- New Code Start --- float FinalRoll = (WallSideSign > 0.f ? CameraTargetRoll : -CameraTargetRoll); // --- New Code End --- }Get the current relative rotation of the player camera, set the roll to
FinalRoll, and then apply the new rotation settings back to the camera component.C++if (Camera) { FVector PlayerRight = MoverComp->GetOwner()->GetActorRightVector(); float WallSideSign = FVector::DotProduct(WallNormal, PlayerRight); float FinalRoll = (WallSideSign > 0.f ? CameraTargetRoll : -CameraTargetRoll); // --- New Code Start --- TargetRot = Camera->GetRelativeRotation(); TargetRot.Roll = FinalRoll; Camera->SetRelativeRotation(TargetRot);After the
ifstatement, setbIsLocationInitializedtotrueto avoid calling this function again in future frames.
Your finished InitializeWallRun() function should look like this:
void UWallRunMovementMode::InitializeWallRun(float DeltaSeconds, UMoverComponent* MoverComp)
{
// Offset start position slightly away from the wall
StartLocation = HitLocation + WallNormal * DistanceFromWall;
// Compute a horizontal direction along the wall (perpendicular to wall normal)
WallTangentVector = FVector::CrossProduct(FVector::UpVector, WallNormal).GetSafeNormal();
// Ensure wall run direction aligns with player forward direction
FVector ForwardVector = MoverComp->GetOwner()->GetActorForwardVector();
Define ApplyMovementAlongWall()
ApplyMovementAlongWall() uses the setup from InitializeWallRun() to move the player continuously along the wall each frame of the wall run movement.
This function:
Advances the character along the wall tangent (the direction computed during initialization).
Orients the player to face down the wall tangent.
Maintains a consistent offset from the wall surface and keeps movement aligned to the wall plane.
To implement ApplyMovementAlongWall(), follow these steps:
In the header file, in the protected section, declare the function with the following arguments:
float DeltaSeconds: The time step for the current frame.UMoverComponent* MoverComp: The player's Mover Component.USceneComponent* UpdatedComponent: The scene component to move.FMoverDefaultSyncState& OutputSyncState: The Mover sync state to update.
C++// Moves the character along the wall surface each tick void ApplyMovementAlongWall(float DeltaSeconds, UMoverComponent* MoverComp, USceneComponent* UpdatedComponent, FMoverDefaultSyncState& OutputSyncState);Declare the following
publicvariables:C++// Tracks how long the current wall run interpolation has been active, in seconds float ElapsedTime = 0.f; // Duration in seconds for the lerp alpha to travel from 0 to 1 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) float TotalTime = 1.3f; // Speed at which the character moves along the wall surface, in units (cm) per second UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) float WallRunSpeed = 1500.f;In the
.cppfile, defineApplyMovementAlongWall()and incrementElapsedTimebyDeltaSeconds:C++void UWallRunMovementMode::ApplyMovementAlongWall(float DeltaSeconds, UMoverComponent* MoverComp, USceneComponent* UpdatedComponent, FMoverDefaultSyncState& OutputSyncState) { // Advance elapsed time for this frame ElapsedTime += DeltaSeconds; }To move the player each frame, you’ll lerp them from their current location forward by
WallRunSpeedcm/s.Declare a new
FVectorto calculate and store the lerp endpoint. AddWallTangentVectorscaled byWallRunSpeedtoStartLocation.C++void UWallRunMovementMode::ApplyMovementAlongWall(float DeltaSeconds, UMoverComponent* MoverComp, USceneComponent* UpdatedComponent, FMoverDefaultSyncState& OutputSyncState) { // Advance elapsed time for this frame ElapsedTime += DeltaSeconds; // --- New Code Start --- // Calculate the lerp target FVector LerpEndLocation = StartLocation + WallTangentVector * WallRunSpeed; // --- New Code End --- }For the lerp’s alpha weight, calculate the ratio of
ElapsedTimetoTotalTime.C++// Calculate the lerp target FVector LerpEndLocation = StartLocation + WallTangentVector * WallRunSpeed; // --- New Code Start --- // Interpolation weight: 0 at the start of the lerp, 1 at TotalTime seconds float Alpha = ElapsedTime / TotalTime; // --- New Code End ---Lerp
LocationToPlaceActorbetweenStartLocationandLerpEndLocationusingAlpha. Set the world location ofUpdatedComponenttoLocationToPlaceActor.C++// Interpolation weight: 0 at the start of the lerp, 1 at TotalTime seconds float Alpha = ElapsedTime / TotalTime; // --- New Code Start --- // Lerp the character's placement location between start and target LocationToPlaceActor = FMath::Lerp(StartLocation, LerpEndLocation, Alpha); // --- New Code End ---When you implement
SimulationTicklater in this part of the tutorial, you’ll makeStartLocationandElapsedTimeupdate every frame. This means that each frame, the player restarts a new lerp from their current position towards a fresh target ahead; they are always near the beginning of a new lerp and they should never reachLerpEndLocation.Apply the new location to
UpdatedComponent.C++// Lerp the character's placement location between start and target LocationToPlaceActor = FMath::Lerp(StartLocation, LerpEndLocation, Alpha); // --- New Code Start --- UpdatedComponent->SetWorldLocation(LocationToPlaceActor); // --- New Code End ---Just like in the Grappling Hook Movement Mode, you’ll define
UpdatedComponentinSimulationTick’s setup code, retrieving it from Mover. It’s the scene component that Mover moves in the world (usually the Pawn’s root collision component).Apply the correct movement direction and orientation to the Pawn while on the wall:
Set
UpdatedComponent’s velocity toWallTangentVector * WallRunSpeedto orient the character in the correct direction.C++// Apply location, velocity, and rotation to UpdatedComponent UpdatedComponent->SetWorldLocation(LocationToPlaceActor); // --- New Code Start --- UpdatedComponent->ComponentVelocity = WallTangentVector * WallRunSpeed; // --- New Code End ---To rotate the player and ensure they continue to face down the wall, you’ll need to pass a rotator yaw value to
UpdatedComponent.Use
ToOrientationRotator()to convertWallTangentVectorto aFRotatorand store the result in a new variable namedWallRunRotation. Then, zero out the pitch and roll to keep the character upright.C++// Apply location, velocity, and rotation to UpdatedComponent UpdatedComponent->SetWorldLocation(LocationToPlaceActor); UpdatedComponent->ComponentVelocity = WallTangentVector * WallRunSpeed; // --- New Code Start --- FRotator WallRunRotation = WallTangentVector.ToOrientationRotator(); // Keep the character upright; zero out pitch and roll WallRunRotation.Pitch = WallRunRotation.Roll = 0.f; // --- New Code End ---Apply the new rotation to
UpdatedComponent.C++// Keep the character upright; zero out pitch and roll WallRunRotation.Pitch = WallRunRotation.Roll = 0.f; // --- New Code Start --- UpdatedComponent->SetWorldRotation(WallRunRotation); // --- New Code End ---
Update the
OutputSyncStateto keep Mover in sync with the player’s state: CallOutputSyncState.SetTransforms_WorldSpace(), passing the same location, rotation, and world velocity you just applied toUpdatedComponent.C++UpdatedComponent->SetWorldRotation(WallRunRotation); // --- New Code Start --- // Sync OutputSyncState with the same transforms to keep Mover in sync OutputSyncState.SetTransforms_WorldSpace( LocationToPlaceActor, WallRunRotation, WallTangentVector * WallRunSpeed, FVector::ZeroVector, nullptr);Remember that
UpdatedComponentandOutputSyncStatemust always receive identical transforms. Mover uses both to track the character's position, and mismatched values can cause desync.As long as there is a wall, the player should never reach the end of the lerp, but it’s best practice to add a safety check just in case this happens.
At the end of the function, add an
ifstatement to check ifAlphais approaching 1. In theifstatement, resetElapsedTimeand setStartLocationto the character's current world location so the next call of this function can calculate a new lerp end target further in front of the player.C++// Sync OutputSyncState with the same transforms to keep Mover in sync OutputSyncState.SetTransforms_WorldSpace( LocationToPlaceActor, WallRunRotation, WallTangentVector * WallRunSpeed, FVector::ZeroVector, nullptr); // --- New Code Start --- // Safety fallback: reset interpolation state if Alpha approaches 1
Your finished ApplyMovementAlongWall() function should look like this:
void UWallRunMovementMode::ApplyMovementAlongWall(
float DeltaSeconds, UMoverComponent* MoverComp,
USceneComponent* UpdatedComponent, FMoverDefaultSyncState& OutputSyncState)
{
// Advance elapsed time for this frame
ElapsedTime += DeltaSeconds;
// Calculate the lerp target: one WallRunSpeed unit along the wall from StartLocation
FVector LerpEndLocation = StartLocation + WallTangentVector * WallRunSpeed;
Define ProcessWallJump()
ProcessWallJump() checks whether a wall jump has been requested and, if so, applies the appropriate launch velocity and transitions the character to Falling.
This function:
Checks if a wall jump has been requested.
Applies a launch velocity that carries the character off the wall and upward.
Transitions to Falling Movement Mode.
To implement ProcessWallJump(), follow these steps:
In the header file, declare
ProcessWallJump()with aboolreturn type. The function takes the following arguments:C++// Checks if a wall jump has been requested and, if so, applies the launch velocity and transitions to Falling bool ProcessWallJump(UMoverComponent* MoverComp, USceneComponent* UpdatedComponent, FMoverDefaultSyncState& OutputSyncState, const FVector& FoundWallNormal, const FVector& FoundWallLocation);Declare the following
publicmember variables:C++// Velocity applied along the wall tangent when the character performs a wall jump UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) float WallJumpForwardVelocity = 1500.f; // Velocity applied away from the wall surface when the character performs a wall jump UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) float WallJumpHorizontalVelocity = 500.f; // Velocity applied upward when the character performs a wall jump UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover)In the
.cppfile, defineProcessWallJump(). Start with anifstatement that checks thatWallRunComponenthas a value andbShouldWallJumpistrue. For this function, all code except for a return statement goes inside thisifstatement.C++bool UWallRunMovementMode::ProcessWallJump(UMoverComponent* MoverComp, USceneComponent* UpdatedComponent, FMoverDefaultSyncState& OutputSyncState, const FVector& FoundWallNormal, const FVector& FoundWallLocation) { // Check if a wall jump has been requested if (WallRunComponent && WallRunComponent->bShouldWallJump) { } }Inside the
ifstatement, create a newWallRunRotationrotator fromWallTangentVectorand zero out its pitch and roll.C++if (WallRunComponent && WallRunComponent->bShouldWallJump) { // --- New Code Start --- // Build rotation from wall tangent direction, keeping the character upright FRotator WallRunRotation = WallTangentVector.ToOrientationRotator(); WallRunRotation.Pitch = 0.f; WallRunRotation.Roll = 0.f; // --- New Code End --- }Set the world location of
UpdatedComponenttoLocationToPlaceActor.You already set the world location in
ApplyMovementAlongWall(), but since you’ll update theOutputSyncStatein this function, it’s best practice to ensure thatUpdatedComponentandOutputSyncStatestay in sync.C++if (WallRunComponent && WallRunComponent->bShouldWallJump) { FRotator WallRunRotation = WallTangentVector.ToOrientationRotator(); WallRunRotation.Pitch = 0.f; WallRunRotation.Roll = 0.f; // --- New Code Start --- UpdatedComponent->SetWorldLocation(LocationToPlaceActor); // --- New Code End --- }Set
ComponentVelocityonUpdatedComponentto the combined wall jump velocity: forward along the wall tangent, horizontal away from the wall surface, and upward.C++UpdatedComponent->ComponentVelocity = WallTangentVector * WallJumpForwardVelocity + FoundWallNormal * WallJumpHorizontalVelocity + (MoverComp->GetOwner()->GetActorUpVector() * WallJumpUpwardVelocity);Set the world rotation of
UpdatedComponenttoWallRunRotation.C++UpdatedComponent->SetWorldRotation(WallRunRotation);Sync
OutputSyncStatewith the same location, rotation, and velocity.C++// Sync OutputSyncState with the same transforms to prevent Mover desync OutputSyncState.SetTransforms_WorldSpace(LocationToPlaceActor, WallRunRotation, WallTangentVector * WallJumpForwardVelocity + FoundWallNormal * WallJumpHorizontalVelocity + (MoverComp->GetOwner()->GetActorUpVector() * WallJumpUpwardVelocity), FVector::ZeroVector, nullptr);Queue the
Fallingmovement mode and returntrue. End theifstatement.C++// Check if a wall jump has been requested if (WallRunComponent && WallRunComponent->bShouldWallJump) { // Build rotation from wall tangent direction, keeping the character upright FRotator WallRunRotation = WallTangentVector.ToOrientationRotator(); WallRunRotation.Pitch = 0.f; WallRunRotation.Roll = 0.f; // Apply location, composite launch velocity, and rotation to UpdatedComponent UpdatedComponent->SetWorldLocation(LocationToPlaceActor);After the
ifstatement, returnfalsefor the case where no wall jump was requested.
Your finished ProcessWallJump() function should look like this:
bool UWallRunMovementMode::ProcessWallJump(UMoverComponent* MoverComp, USceneComponent* UpdatedComponent, FMoverDefaultSyncState& OutputSyncState, const FVector& FoundWallNormal, const FVector& FoundWallLocation)
{
// Check if a wall jump has been requested
if (WallRunComponent && WallRunComponent->bShouldWallJump)
{
// Build rotation from wall tangent direction, keeping the character upright
FRotator WallRunRotation = WallTangentVector.ToOrientationRotator();
WallRunRotation.Pitch = 0.f;
WallRunRotation.Roll = 0.f;
Define DetectSharpTurnBreak()
DetectSharpTurnBreak() runs each frame during a wall run to detect sudden direction changes along the wall's surface. If the difference in wall angle is too large, it queues the Falling Movement Mode to end the wall run.
To implement DetectSharpTurnBreak(), follow these steps:
In the header file, declare
DetectSharpTurnBreak()in the protected section. The function takes the player’s Mover Component as an argument.C++// Ends wall run if player encounters a sharp direction change along the wall bool DetectSharpTurnBreak(UMoverComponent* MoverComp);In the
publicsection, declare the following member variables:C++// Maximum angle change between wall tangent directions across frames before the wall run is broken UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) float MaxAngleDifference = 50.f; // Wall tangent direction from the previous frame, used to detect sharp turns during a wall run FVector PreviousWallTangent = FVector::ZeroVector;In the
.cppfile, defineDetectSharpTurnBreak(). Start with anifstatement that skips the angle comparison on the first frame (whenPreviousWallTangentis still zero).C++bool UWallRunMovementMode::DetectSharpTurnBreak(UMoverComponent* MoverComp) { // Skip on the first frame. PreviousWallTangent is zero until at least one frame has run if (!PreviousWallTangent.IsNearlyZero()) { } }PreviousWallTangentstarts asFVector::ZeroVectoron the first frame. Without this guard, the dot product computes against a zero vector and returns an incorrect angle.Inside the
ifstatement, calculate the angle betweenWallTangentVectorandPreviousWallTangent.Get the dot product (cosine) of the two vectors, pass the result to
FMath::Acosto get the angle between them, and then useFMath::RadiansToDegreesto convert to degrees to compare withMaxAngleDifference.C++bool UWallRunMovementMode::DetectSharpTurnBreak(UMoverComponent* MoverComp) { // Skip on the first frame. PreviousWallTangent is zero until at least one frame has run if (!PreviousWallTangent.IsNearlyZero()) { // --- New Code Start --- // Angle between last frame's wall tangent and the current one float AngleDegrees = FMath::RadiansToDegrees( FMath::Acos(FVector::DotProduct(WallTangentVector, PreviousWallTangent))); // --- New Code End ---If
AngleDegreesexceedsMaxAngleDifference, callQueueNextMode()to transition toFallingand returntrue.C++if (!PreviousWallTangent.IsNearlyZero()) { // Angle between last frame's wall tangent and the current one float AngleDegrees = FMath::RadiansToDegrees( FMath::Acos(FVector::DotProduct(WallTangentVector, PreviousWallTangent))); // --- New Code Start --- // Sharp turn detected — end the wall run if (AngleDegrees > MaxAngleDifference) {After the outer
ifstatement, storeWallTangentVectorasPreviousWallTangentfor the next frame, then returnfalse.C++// Skip on the first frame. PreviousWallTangent is zero until at least one frame has run if (!PreviousWallTangent.IsNearlyZero()) { // Angle between last frame's wall tangent and the current one float AngleDegrees = FMath::RadiansToDegrees( FMath::Acos(FVector::DotProduct(WallTangentVector, PreviousWallTangent))); // Sharp turn detected — end the wall run if (AngleDegrees > MaxAngleDifference) {
Your finished DetectSharpTurnBreak() function should look like this:
bool UWallRunMovementMode::DetectSharpTurnBreak(UMoverComponent* MoverComp)
{
// Skip on the first frame. PreviousWallTangent is zero until at least one frame has run
if (!PreviousWallTangent.IsNearlyZero())
{
// Angle between last frame's wall tangent and the current one
float AngleDegrees = FMath::RadiansToDegrees(
FMath::Acos(FVector::DotProduct(WallTangentVector, PreviousWallTangent))
);
Define UpdateCameraClamp()
UpdateCameraClamp() runs each frame during a wall run to keep the player's camera yaw within a defined offset of the wall run direction. Without this constraint, the player could freely rotate their camera away from the wall while wall running.
To implement UpdateCameraClamp(), follow these steps:
In the header file, declare
UpdateCameraClamp()in the protected section. The function takesDeltaSecondsand the Mover Component as arguments.C++// Restricts maximum camera yaw movements during a wall run void UpdateCameraClamp(float DeltaSeconds, UMoverComponent* MoverComp);In the
publicsection, declare the following member variables:C++// Interpolation speed used to smoothly move the camera toward the clamped yaw target during a wall run UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) float WallRunCameraInterpolationSpeed = 4.f; // Maximum yaw offset allowed between the camera direction and the wall run direction UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Mover) float MaxYawOffset = 15.f;In the
.cppfile, startUpdateCameraClamp()’s implementation with an if statement that retrieves theAPlayerControllerfrom the owning Pawn. The rest of the function runs only if the cast succeeds.C++void UWallRunMovementMode::UpdateCameraClamp(float DeltaSeconds, UMoverComponent* MoverComp) { // Only proceed if the owning Pawn has a player controller if (APlayerController* PlayerController = Cast<APlayerController>(Cast<APawn>(MoverComp->GetOwner())->GetController())) { } }Inside the
ifstatement, get the current control rotation from the player controller.C++void UWallRunMovementMode::UpdateCameraClamp(float DeltaSeconds, UMoverComponent* MoverComp) { // Only proceed if the owning Pawn has a player controller if (APlayerController* PlayerController = Cast<APlayerController>(Cast<APawn>(MoverComp->GetOwner())->GetController())) { // --- New Code Start --- FRotator ControlRot = PlayerController->GetControlRotation(); // --- New Code End --- } }Compute the wall run direction by taking the cross product of
WallNormaland the world up vector, then normalize the result.C++if (APlayerController* PlayerController = Cast<APlayerController>(Cast<APawn>(MoverComp->GetOwner())->GetController())) { FRotator ControlRot = PlayerController->GetControlRotation(); // --- New Code Start --- // Direction along the wall surface (perpendicular to the wall normal, in the horizontal plane) FVector WallRunDir = FVector::CrossProduct(WallNormal, FVector::UpVector); WallRunDir.Normalize(); // --- New Code End --- }The cross product vector is perpendicular to the
WallNormalandUpVectorvectors.If the dot product of
WallRunDirwith the Actor's forward vector is negative, flipWallRunDirso it always points in the direction the character is running.C++if (APlayerController* PlayerController = Cast<APlayerController>(Cast<APawn>(MoverComp->GetOwner())->GetController())) { FRotator ControlRot = PlayerController->GetControlRotation(); // Direction along the wall surface (perpendicular to the wall normal, in the horizontal plane) FVector WallRunDir = FVector::CrossProduct(WallNormal, FVector::UpVector); WallRunDir.Normalize(); // --- New Code Start --- if (FVector::DotProduct(WallRunDir, MoverComp->GetOwner()->GetActorForwardVector()) < 0.f)The cross product gives a direction perpendicular to the wall, but it could point either forward or backward relative to the character. This check ensures
WallRunDiralways matches the character's travel direction.After the dot product
ifstatement, convertWallRunDirto aFRotator.C++if (FVector::DotProduct(WallRunDir, MoverComp->GetOwner()->GetActorForwardVector()) < 0.f) { WallRunDir *= -1.f; } // --- New Code Start --- FRotator WallRunRot = WallRunDir.Rotation(); // --- New Code End ---Calculate the normalized yaw difference between the current control rotation and the wall run direction.
C++// Yaw offset (in degrees) between the camera and the wall run direction, normalized to [-180, 180] float YawDelta = FRotator::NormalizeAxis(ControlRot.Yaw - WallRunRot.Yaw);FRotator::NormalizeAxiswraps the angle into the range [-180, 180] so that yaw comparisons work correctly across the 0/360 degree boundary.Clamp
YawDeltato the range [-MaxYawOffset,MaxYawOffset] degrees:C++// Clamp the offset to the allowed range float ClampedYawDelta = FMath::Clamp(YawDelta, -MaxYawOffset, MaxYawOffset);Construct
TargetRotationfrom the wall run direction's yaw plus the clamped offset. Preserve the camera's current pitch and roll.C++// Build the target rotation: wall run yaw + clamped offset, preserving camera pitch and roll FRotator TargetRotation = WallRunRot; TargetRotation.Yaw += ClampedYawDelta; TargetRotation.Pitch = ControlRot.Pitch; TargetRotation.Roll = ControlRot.Roll;At the end of the outer
ifstatement, interpolate the control rotation towardTargetRotationand apply the rotation to the player controller.C++// Smoothly move the camera toward the target rotation FRotator SmoothedRot = FMath::RInterpTo(ControlRot, TargetRotation, DeltaSeconds, WallRunCameraInterpolationSpeed); PlayerController->SetControlRotation(SmoothedRot);FMath::RInterpTointerpolates between two rotators at a given speed per second.DeltaSecondskeeps the interpolation frame-rate independent.
Your finished UpdateCameraClamp() function should look like this:
void UWallRunMovementMode::UpdateCameraClamp(float DeltaSeconds, UMoverComponent* MoverComp)
{
// Only proceed if the owning Pawn has a player controller
if (APlayerController* PlayerController = Cast<APlayerController>(Cast<APawn>(MoverComp->GetOwner())->GetController()))
{
FRotator ControlRot = PlayerController->GetControlRotation();
// Direction along the wall surface (perpendicular to the wall normal, in the horizontal plane)
FVector WallRunDir = FVector::CrossProduct(WallNormal, FVector::UpVector);
WallRunDir.Normalize();
Define SimulationTick_Implementation
Now, you’ll pull all the code you’ve implemented together in SimulationTick and define how the Movement Mode works.
Each frame, SimulationTick will:
Run standard Mover setup to initialize movement components and state variables.
If not initialized, run the
InitializeWallRun()function.Call
ApplyMovementAlongWallto move the player forward.Call
ProcessWallJumpto check if it should wall jump.Check if there is still a wall nearby and stop the wall run if no wall is found.
If the wall run movement can continue, update
HitLocation,WallNormal,WallTangentVector,StartLocation, and resetElapsedTime.Use updated wall information to check for sharp turns.
Update the camera clamp.
Add Standard Setup Code
At the start of SimulationTick_Implementation, add the same movement setup code you added to the Grappling Hook Movement Mode:
void UWallRunMovementMode::SimulationTick_Implementation(const FSimulationTickParams& Params, FMoverTickEndData& OutputState)
{
/*
// --- Standard Movement Mode Setup Code STARTS ---
*/
UMoverComponent* MoverComp = GetMoverComponent();
const FMoverTickStartData& StartState = Params.StartState;
USceneComponent* UpdatedComponent = Params.MovingComps.UpdatedComponent.Get();
UPrimitiveComponent* UpdatedPrimitive = Params.MovingComps.UpdatedPrimitive.Get();
FProposedMove ProposedMove = Params.ProposedMove;
Initialize Wall Running
Call InitializeWallRun() if the wall run movement hasn’t been initialized yet.
To check for a valid wall hit location and initialize a new wall run, follow these steps:
After the standard movement setup code, add an if statement that checks if
FindWall()has run and setHitLocationto non-zero value.This prevents the wall run logic from executing before a wall has been detected.
C++// Only run wall run logic if a valid wall hit has been registered if (HitLocation != FVector(0, 0, 0)) { }Inside the
ifstatement, check that this is the first frame of the wall run (bIsLocationInitializedis false). If so, callInitializeWallRun()to set up the starting position and state.C++// Only run wall run logic if a valid wall hit has been registered if (HitLocation != FVector(0, 0, 0)) { // --- New Code Start --- // First frame of wall run — set up starting position and state if (!bIsLocationInitialized) { InitializeWallRun(DeltaSeconds, MoverComp); } // --- New Code End ---
Move Player Along the Wall
Move the player and then call FindWall() to get the wall location and normal at the player’s new location. You’ll use this updated wall information to perform checks to detect if the movement should end.
To move the player and update FindWall() data, follow these steps:
After the inner
ifstatement, callApplyMovementAlongWall()to move the character along the wall this frame.C++// Only run wall run logic if a valid wall hit has been registered if (HitLocation != FVector(0, 0, 0)) { // First frame of wall run — set up starting position and state if (!bIsLocationInitialized) { InitializeWallRun(DeltaSeconds, MoverComp); }Before updating the
FindWall()check from the player’s new location, check thatWallRunComponenthas a value.In the
ifstatement, declare two vectors forFindWallto save the new wall information to, then callFindWall().C++// Only run wall run logic if a valid wall hit has been registered if (HitLocation != FVector(0, 0, 0)) { // First frame of wall run — set up starting position and state if (!bIsLocationInitialized) { InitializeWallRun(DeltaSeconds, MoverComp); } // Move the character along the wall surface
End the Wall Run
Use the wall information in the player’s new location to check for and process a wall jump, update the rest of the wall data, and detect sharp turns.
To check for conditions that end the wall run, follow these steps:
In the
if (WallRunComponent)statement, callProcessWallJump(). If it returnstrue, return out ofSimulationTickearly. A wall jump has been applied and the Movement Mode is transitioning toFalling.C++// Refresh wall data and handle state transitions if (WallRunComponent) { FVector FoundWallLocation; FVector FoundWallNormal; bool bFoundWall = WallRunComponent->FindWall(FoundWallLocation, FoundWallNormal); // --- New Code Start --- // Apply wall jump if requested; transition to Falling and return if (ProcessWallJump(MoverComp, UpdatedComponent, OutputSyncState, FoundWallNormal, FoundWallLocation))If
FindWall()returnedfalse, queue theFallingmovement mode andreturn.C++// Apply wall jump if requested; transition to Falling and return if (ProcessWallJump(MoverComp, UpdatedComponent, OutputSyncState, FoundWallNormal, FoundWallLocation)) { return; } // --- New Code Start --- // Wall lost — end the wall run if (!bFoundWall) {To check for sharp turns in the player’s new location, you need to update the rest of the wall data first. You’ll use the updated wall data for calculations during the next frame.
If the wall is still present, update stored wall data for the next frame.
C++// Wall lost — end the wall run if (!bFoundWall) { MoverComp->QueueNextMode("Falling", false); return; } // --- New Code Start --- // Wall still present — update stored wall data for next frame if (bFoundWall)Setting
ElapsedTimeto0every frame keeps the lerp alpha inApplyMovementAlongWallfrom accumulating.After the
if (bFoundWall)statement, callDetectSharpTurnBreak(). If it returnstrue, the wall run has ended andDetectSharpTurnBreak()queued the Falling Movement Mode.C++// Wall still present — update stored wall data for next frame if (bFoundWall) { HitLocation = FoundWallLocation; WallNormal = FoundWallNormal; WallTangentVector = FVector::CrossProduct(FVector::UpVector, WallNormal); WallTangentVector.Normalize(); StartLocation = HitLocation + WallNormal * DistanceFromWall; // Reset each frame so Alpha in ApplyMovementAlongWall doesn't accumulate ElapsedTime = 0.f;
Update the Player Camera
Near the end of SimulationTick, after the if (DetectSharpTurnBreak(MoverComp)) statement, you know the player is continuing the wall run movement, so call UpdateCameraClamp() to constrain the camera yaw to the wall run direction.
// Only run wall run logic if a valid wall hit has been registered
if (HitLocation != FVector(0, 0, 0))
{
// First frame of wall run — set up starting position and state
if (!bIsLocationInitialized)
{
InitializeWallRun(DeltaSeconds, MoverComp);
}
// Move the character along the wall surface
ApplyMovementAlongWall(DeltaSeconds, MoverComp, UpdatedComponent, OutputSyncState);
The full SimulationTick function should look like this:
// Runs every tick to orchestrate wall run movement — calls all other functions in this module in sequence
void UWallRunMovementMode::SimulationTick_Implementation(const FSimulationTickParams& Params, FMoverTickEndData& OutputState)
{
/*
// --- Standard Movement Mode Setup Code STARTS ---
*/
UMoverComponent* MoverComp = GetMoverComponent();
const FMoverTickStartData& StartState = Params.StartState;
USceneComponent* UpdatedComponent = Params.MovingComps.UpdatedComponent.Get();
UPrimitiveComponent* UpdatedPrimitive = Params.MovingComps.UpdatedPrimitive.Get();
Define Deactivate()
Deactivate() runs when the player character switches out of the Wall Run Movement Mode. It resets PreviousWallTangent, the camera rotation, and the variables initialized in InitializeWallRun. It also starts a new cooldown timer with SetCooldown().
void UWallRunMovementMode::Deactivate(const FMoverEventContext& Context, FName NextModeName, const FMoverSimContext& SimContext)
{
Super::Deactivate(Context, NextModeName, SimContext);
HitLocation = FVector::ZeroVector;
WallNormal = FVector::ZeroVector;
WallTangentVector = FVector::ZeroVector;
PreviousWallTangent = FVector::ZeroVector;
ElapsedTime = 0.f;
bIsLocationInitialized = false;
Define OnRegistered() and OnUnregistered()
Just like in your Grappling Hook Movement Mode, use OnRegistered() to set up references when the Movement Mode is connected to the Mover system and use OnUnRegistered() for unbinding events and clearing external references when the Movement Mode is removed from the system.
OnRegistered() initializes the CommonLegacySettings, the Mover Component reference, the WallRunActorComponent reference, and the player’s camera component reference.
void UWallRunMovementMode::OnRegistered(const FName ModeName, const FMoverSimContext& SimContext)
{
Super::OnRegistered(ModeName, SimContext);
CommonLegacySettings = GetMoverComponent()->FindSharedSettings<UCommonLegacyMovementSettings>();
ensureMsgf(CommonLegacySettings, TEXT("Failed to find instance of CommonLegacyMovementSettings on %s. Movement may not function properly."), *GetPathNameSafe(this));
UMoverComponent* MoverComp = GetMoverComponent();
WallRunComponent = MoverComp->GetOwner()->FindComponentByClass<UWallRunActorComponent>();
Camera = MoverComp->GetOwner()->FindComponentByClass<UCameraComponent>();
}
OnUnregistered() resets the CommonLegacySettings to a null pointer and calls the parent function.
void UWallRunMovementMode::OnUnregistered(const FMoverSimContext& SimContext)
{
CommonLegacySettings = nullptr;
Super::OnUnregistered(SimContext);
}
Complete Code
The finished WallRunMovementMode.h file should look like this:
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "MovementMode.h"
#include "Camera/CameraComponent.h"
#include "WallRunActorComponent.h"
#include "WallRunMovementMode.generated.h"
The finished WallRunMovementMode.cpp file should look like this:
// Copyright Epic Games, Inc. All Rights Reserved.
#include "WallRunMovementMode.h"
#include "GameFramework/Pawn.h"
#include "Camera/CameraComponent.h"
#include "MoveLibrary/MovementUtils.h"
#include "MoveLibrary/FloorQueryUtils.h"
#include "MoverComponent.h"
#include "DefaultMovementSet/Settings/CommonLegacyMovementSettings.h"
#include "DefaultMovementSet/InstantMovementEffects/BasicInstantMovementEffects.h"
Add Wall Running to the Character Blueprint
Now that you’ve implemented the movement behavior, you can add the Movement Mode to BP_ParkourPawn.
Add the Component and Movement Mode
To add the Wall Run Movement Mode to the player character, follow these steps:
In Unreal Editor, open your Pawn Blueprint.
In the Components panel, add a WallRunActor component.
Select the ExtendedCharacterMoverComponent.
In the Details panel, in the Mover section, expand Movement Modes.
Add a new map element to the Movement Modes list and enter
WallRunas the mode name. You’ll reference this name in Event Graph logic.In the new map element’s dropdown list, select Wall Run Movement Mode.
Compile the Blueprint.
Queue the Movement Mode
Unlike sprint and grapple, wall running requires environmental conditions, not an input, to queue the Movement Mode, so you’ll use Event Tick to check for and queue the Wall Run movement mode.
The logic here will check for these conditions:
Is the player falling (i.e. just finished a jump, grapple, or similar movement)?
Is there a wall in range?
Is the player moving fast enough to wall run?
When working in node graphs, ensure Context Sensitive is enabled in the node actions list as often as possible so Unreal Engine can automatically add appropriate targets to function nodes for you.
To queue the Movement Mode, follow these steps:
In the Pawn’s EventGraph tab, in the My Blueprint panel, expand the EventGraph list and double-click Event Tick to find it in the graph.
Drag off the Parent: Tick node and add a Branch node to test if the player is in the Falling Movement Mode.
Add the falling condition:
For the Branch node’s Condition pin, connect an Is Falling (ExtendedCharacterMoverComponent) node.
To access this option, enable Context Sensitive in the node actions list.
Drag off the Branch node’s True pin and add a second Branch node. This Branch node will test if a wall is in range and the character is moving at an appropriate velocity to start wall running.
Add the Find Wall condition:
Below the second Branch node, add a Find Wall (WallRunActor) node.
Add the player speed condition:
In the Variables list, create a new float variable named
WallRunSpeedThreshold. Compile the Blueprint and enter750as its default value.A speed threshold of 750 cm/s makes the wall run only work if the player is sprinting.
Below the FindWall node, right-click to add a new node, search for
get velocity mover, and add a Get Velocity (ExtendedCharacterMoverComponent) node.You only need the player’s horizontal speed, so isolate the X and Y values from the velocity result. In the Get Velocity node, right click the Return Value pin and select Split Struct Pin. Connect the Return Value X and Return Value Y pins to a new Make Vector node.
Drag off the Make Vector node’s Return Value pin, add a Vector Length node, and then check if its Return Value is greater or equal to WallRunSpeedThreshold.
Getting the length of the flattened vector tells you how fast the player is moving horizontally.
Connect the Find Wall node’s Return Value and player speed condition with an And boolean node, and then connect the And node to the second Branch node’s Condition pin.
In the Variables list, create two new Vector variables:
OutWallLocationOutWallNormal
You’ll use these variables to store the vectors that
FindWall()calculated and then pass these values to the WallRun Movement Mode when queueing it.From the second Branch node’s True pin, connect a Set OutWallLocation node, then connect a Set OutWallNormal node. Connect the Find Wall function node’s outputs to the respective Set nodes to save these values for later.
After the second Set node, add a Queue Next Movement Mode (ExtendedCharacterMoverComponent). Enter
WallRunas the Desired Mode Name.Add a comment to group these nodes together.
Save and compile the Blueprint.
Your final EventTick logic should look like this:
Check for Double Jump While Wall Running
Your character includes some Blueprint logic that handles jump actions. Every frame, the Blueprint checks for jump input and if the player has pressed jump while being on the ground, it triggers a Double Jump event that creates a multi-jump move. When you created the Wall Running Movement mode, you added the Mover_IsOnGround tag so the conditions for jumping will be true even when wall running. You’ll modify the existing jump-handling logic to check if the player is in the Wall Run Movement Mode and call SetWallJump() if they are.
You implemented SetWallJump() in the WallRunActorComponent class. This function sets bShouldWallJump to true, starts the wall jump cooldown, and queues ResetWallJump(). Then next tick, ProcessWallJump() in the Movement Mode checks bShouldWallJump.
To check for jump input during a wall run, follow these steps:
In the My Blueprint panel, double-click the Double Jump event to find it in the graph. This logic applies a multi jump to whatever the current Movement Mode is doing. Instead of always running the existing logic, you want to instead call
SetWallJump()if the player is wall running.Delete the wire coming from the Event DoubleJump node and add some space between the event node and the other nodes.
Drag off the event node pin and add a Branch node to check if the player is currently in the Wall Run Movement Mode.
Set up the Branch node’s Condition to check if the active Movement Mode is named WallRun:
Below the Branch node, add a Get Movement Mode Name (ExtendedCharacterMoverComponent).
Drag off the Return Value pin, add an Equal operator node and enter
WallRunin the second pin’s text box.Connect the Equal node’s return value pin to the Condition pin.
Connect the Branch node’s False pin to the existing Queue Layered Movement Node.
Drag off the Branch node’s True pin and add a Set Wall Jump (WallRunActor) node.
Save and compile the Blueprint.
Add a Wall Run Option to OnMovementModeChanged Event
Just like you did with the Grappling Hook Movement Mode, you’ll need to initialize the Wall Run Movement Mode with some information when it becomes the active Movement Mode.
To set up the Wall Run Movement mode with the found wall location and normal from FindWall(), follow these steps:
In the My Blueprint panel, double-click OnMovementModeChanged_Event to find it in the graph.
In the Switch on Name node, add a new pin, and use the Details panel to name it
WallRun.Similar to the Grappling Hook setup, drag off the WallRun pin, add a Cast To WallRunMovementMode node, and connect its Object pin to a Find Movement Mode By Name (ExtendedCharacterMoverComponent) node.
In the Find Movement Mode node, enter
WallRunas the Movement Mode Name.After the Cast To node, connect a Set Hit Location node and a Set Wall Normal node.
In the node actions list, you may need to turn off Context Sensitive to find nodes for these Wall Run Movement Mode variables.
Connect the Cast To node’s As Wall Run Movement Mode pin to the Target pin of both Set nodes.
In the My Blueprint panel, from the Variables list, drag OutWallLocation onto the Hit Location pin and drag OutWallNormal onto the Wall Normal pin.
Save and compile your Blueprint.
Test the Wall Running Movement
Play your game, run toward a wall, and jump at the wall to try running along it. Try to jump while wall running both in a location where the wall run can continue and in a location where jumping ends the wall run. Experiment with building different angles of wall segments to see what angles continue or end a wall run.
For jumping from wall to wall, try positioning parallel walls about three meters apart.
Adjust Wall Running Behavior
To adjust wall running behavior, select your Pawn’s ExtendedCharacterMoverComponent and expand the Movement Mode > WallRun properties. Adjust Wall Run Speed and the wall jump velocity values to speed up or slow down the wall run movement.
Mark Walls as Runnable or Not Runnable
When you defined the FindWall() function in the Wall Run Actor Component class, you added this check for a NoWallRun Actor tag:
// If Actor is tagged NoWallRun, skip it and continue to next loop iteration
if (HitActor->ActorHasTag(FName("NoWallRun")))
{
continue;
}
To add this Actor tag to a wall Actor in your level and prevent the player from wall running on it, follow these steps:
In the level editor, select a wall or other surface in your level.
In the Details panel, search for
tags. In the Actor section, next to the Tags list, click + to add a new Actor tag.Enter
NoWallRunas the tag’s name.
However, it’s best to design this Actor tag check so that you have to add tags to as few Actors as possible. Depending on what portion of your level should be wall runnable, you can negate the if statement and change the tag name to make this Actor tag check more convenient for your project.
For example, to make Actors opt in to being wall runnable instead of opting out, change the Actor tag check in your code to this:
// If Actor does not have "WallRunnable" tag, skip it and continue to next loop iteration
if (!HitActor->ActorHasTag(FName("WallRunnable")))
{
continue;
}
In this case, you’d add the WallRunnable tag to valid walls in your level instead of restricting walls with the NoWallRun tag.
Next Up
Your Pawn character now has all their movement behaviors! To round out your game, you’ll add checkpoints and a respawn mechanic, and then build your parkour obstacle course.