DY
GAMEPLAY PROGRAMMER
Back to blog
·Diego Yagüe

Building a Character Controller from Scratch in Unreal Engine 5

A deep dive into extending Unreal's Character Movement Component to support wall-running, coyote time, and variable jump height — and why feel matters more than physics accuracy.

Unreal EngineC++Character MovementGameplay

Why Roll Your Own?

Every game programmer eventually reaches the moment where they realize the built-in character controller just isn't going to cut it. Physics engines are built for correctness. Great platformers are built for feel. The two are rarely the same.

The Character Movement Component

Unreal's UCharacterMovementComponent (CMC) is one of the most battle-hardened systems in any game engine — and one of the most misunderstood. Most tutorials tell you to subclass it. Few tell you how to think about it.

The CMC works in discrete movement modes. At each tick, it:

  1. Reads input
  2. Determines the current movement mode (Walking, Falling, Flying, etc.)
  3. Calls the matching PhysX function
  4. Resolves collisions and applies the final position

The key insight is that you can add your own movement modes to extend this pipeline cleanly.

Wall Running

Wall running requires detecting a wall while airborne, transitioning to a custom movement mode, constraining velocity along the wall surface, and ejecting back to Falling mode at the right time.

void UMyCharacterMovement::PhysWallRunning(float deltaTime, int32 iterations)
{
    // 1. Check we still have a valid wall to run on
    FHitResult wallHit;
    if (!FindWallToRunOn(wallHit))
    {
        SetMovementMode(MOVE_Falling);
        return;
    }

    // 2. Project velocity onto the wall plane
    FVector wallNormal = wallHit.Normal;
    Velocity = FVector::VectorPlaneProject(Velocity, wallNormal);

    // 3. Apply gravity partially (wall grab has some grip)
    Velocity.Z -= GetGravityZ() * deltaTime * WallRunGravityScale;

    // 4. Move
    FVector delta = Velocity * deltaTime;
    SafeMoveUpdatedComponent(delta, UpdatedComponent->GetComponentQuat(), true, wallHit);
}

Coyote Time

The single most impactful feel improvement for any platformer. It's three lines of logic:

// In TickComponent:
if (WasOnGroundLastFrame && IsInAir())
{
    CoyoteTimeRemaining = CoyoteTimeDuration; // ~0.1–0.15s
}
if (CoyoteTimeRemaining > 0 && PlayerPressedJump())
{
    DoJump();
    CoyoteTimeRemaining = 0;
}
CoyoteTimeRemaining -= deltaTime;

Players will never notice it consciously, but they'll notice when it's gone.

Variable Jump Height

Cutting vertical velocity when the jump button is released creates enormously more responsive jump arcs without changing the initial impulse:

if (!bIsJumpButtonHeld && Velocity.Z > 0)
{
    Velocity.Z *= JumpCutMultiplier; // ~0.5
}

The Rule: Feel Over Physics

If you're shipping a game and you have to choose between physically accurate movement and movement that feels great, choose feel. Players have a remarkably forgiving model of what "should" happen — as long as they can predict it.