Player Movement - UQcsse3200/2026-studio-2 GitHub Wiki

Player Movement

Introduction

This page documents the Sprint 3 implementation in PR #196, PR #211 and PR #229. Earlier revision-specific test results are retained as historical evidence.

The Player Movement System handles walking, sprinting, jumping and ground detection for a side-on platformer. Movement is impulse-driven through Box2D rather than setting position directly, so the player accelerates, carries momentum, and interacts properly with gravity and collisions.

Controls

Input Action
A / D or arrow keys Walk left or right
Shift Sprint while held; attempt dash on press
Space Ground jump or boosted release while grappled
W / S (while grappled) Climb / descend the rope
S Request drop through a one-way ledge

The merged input dispatches both grapple descent and ledge drop on S, retaining both systems rather than choosing one handler. Releasing W/S stops rope-length input and clears the held debug-movement key state.

Architecture

flowchart LR
    Input["KeyboardPlayerInputComponent"] -->|walk / sprint / jump| Actions["PlayerActions"]
    Actions -->|impulses and dash velocity| Body["PhysicsComponent / Box2D"]
    Actions -->|ground raycast| Terrain["STANDABLE terrain / character bodies"]
    Actions -->|grappleSwing| Grapple["GrappleComponent"]
    Actions -->|jumpStart / dash events| Animation["PlayerAnimationController"]

KeyboardPlayerInputComponent

Tracks which movement keys are held in a small boolean array and combines them into a single direction. Holding both left and right cancels out to zero rather than picking one:

float x = 0;
if (keysHeld[LEFT]) x -= SPEED;
if (keysHeld[RIGHT]) x += SPEED;
walkDirection.set(x, 0);

It then fires either "walk" with the direction, or "walkStop" if the result is zero. Sprint and jump fire their own events. The input component holds no movement logic of its own.

PlayerActions

Listens for the movement events and applies the physics. It also owns the grounded check and the paused state.

Movement Lifecycle

Walking and Sprinting

Walking works by impulse rather than by setting velocity. Each frame the component works out the gap between current and desired horizontal velocity, and applies an impulse to close it:

float desiredVelocityX = walkDirection.x * MAX_SPEED.x * speedMultiplier;
float impulseX = (desiredVelocityX - velocity.x) * body.getMass() * control;
body.applyLinearImpulse(new Vector2(impulseX, 0), body.getWorldCenter(), true);

Only the x component is touched. Vertical velocity is left entirely to gravity and jumping, which is what separates this from the top-down movement in the base template. Setting y here would fight gravity and break jumps.

Sprinting multiplies the target speed by SPRINT_MULTIPLIER. Pressing Shift also attempts a dash; after the burst, ordinary movement uses the sprint target speed. Releasing Shift starts the grace period described below.

Air Control

The control multiplier scales how much of the velocity gap is closed each frame:

float control =
    dashRecoveryRemaining > 0f ? DASH_RECOVERY_CONTROL : (isGrounded ? 1f : AIR_CONTROL);

Outside dash recovery, ground control is full; dash recovery uses 0.2. In the air it drops to 10%, so the player can steer slightly but can't instantly change direction. This matters most when landing from a grapple swing at high speed, where full air control would snap the player back to walking pace the moment a direction key was pressed.

Jumping

An accepted ground jump emits jumpStart and queues one upward impulse after the takeoff wind-up. Repeated requests during the wind-up do not restart it. JUMP_FORCE is 41; the earlier duplicate immediate impulse has been removed.

PlayerActions determines airborne transitions from groundedness and vertical velocity. It emits fallStart when descent begins and landed when the player returns to the ground. Walking off ledges and dropping through platforms therefore enter falling without requiring a jump request.

A grapple jump cancels any queued ground impulse before releasing the rope and applying its upward boost. This prevents a delayed ground impulse from firing on top of the rope jump.

Ground Detection

The feet raycast uses PhysicsLayer.STANDABLE, combining solid terrain with physical character bodies. Enemy bodies can support the player, while their separate NPC damage sensors do not count as ground.

New enemy integrations should preserve the distinction between physical support and damage detection. Treating a damage sensor as ground could allow jumping merely by overlapping an enemy.

Pausing

PlayerActions tracks pause state through togglePaused. A walking request received while paused calls stopWalking() rather than accepting movement. Simulation pausing also involves the wider physics/entity services; this handler alone does not establish whether physics continues. Death disables player input, stops horizontal walking and clears any queued jump.

Dash and Sprint Release

Shift attempts a horizontal dash in the movement direction, falling back to the last facing direction. The burst uses velocity 14 for 0.15 seconds, temporarily disables gravity and holds vertical velocity at zero. On completion it restores gravity and applies a 0.1-second recovery with control multiplier 0.2. Hurt interrupts the dash.

Dash attempts reject active dash, cooldown, pause, grapple attachment and an already-used airborne allowance. The nominal cooldown is 1 second, but landing and ground/grapple jumps reset cooldown and air-dash allowance. Sprint release has a 0.12-second grace period; repressing Shift cancels the pending stop. Distinct dash events currently both select air_dash; see Animations.

Ledge Dropping and Respawn

S emits updateLedgeDrop(true). PlayerActions sets droppingFromLedge and wakes the physics body. PhysicsContactListener.preSolve() disables a ledge contact while the flag is active; the matching endContact() clears it so subsequent ledges can catch the player. Releasing S does not itself clear the drop flag. Ordinary solid platforms are unaffected by the ledge-drop path.

The level team's RespawnComponent emits respawnAtCheckpoint below y = -10. GameArea.respawn() moves the existing player to the last active checkpoint in configuration order, or to the configured spawn if none is active. This repositions the existing player; the grapple lifecycle now clears the charge, flying arrow and rope on respawn.

W/S rope climbing, terrain wrapping and persistent bends are also merged. See Player Grappling for limits and implementation.

Events

Event Fired by Handled by
walk Input PlayerActions
walkStop Input PlayerActions
jump Input PlayerActions
sprint / sprintStop Input PlayerActions
togglePaused Pause menu PlayerActions
grappleSwing PlayerActions GrappleComponent
updateLedgeDrop S input PlayerActions / contact listener
respawnAtCheckpoint RespawnComponent Active game area
Event Payload Source Purpose
jumpStart None PlayerActions Accepted ground-jump takeoff; animation and jump sound
fallStart None PlayerActions Begin falling presentation
landed None PlayerActions Begin landing recovery

Animation and sound should follow accepted gameplay events. Listening to the raw jump request would also respond to rejected mid-air attempts.

Tuning

Constant Value Purpose
MAX_SPEED 5 Target walking velocity in metres per second
SPRINT_MULTIPLIER 1.75 Speed multiplier while sprinting
JUMP_FORCE 41 Single ground-jump impulse after takeoff wind-up
AIR_CONTROL 0.1 Fraction of steering available mid air
ROPE_JUMP_MULTIPLIER 0.7 Scales the jump when leaving a grapple

Testing

After changing jump timing or ground detection, verify:

Action Expected result
Jump from rest One upward launch followed by landing recovery
Walk off a ledge Falling begins without takeoff animation
Repeatedly press Space in mid-air No additional ground jump or rejected-jump sound
Land on an enemy Landing is recognised and another jump is available
Jump from a grapple during ground-jump wind-up No second delayed ground impulse

PlayerActionsTest, PlayerMovementRegressionTest and the animation tests cover the associated state transitions. PlayerInputStateTest covers input handling; PlayerLedgeDropIntegrationTest covers the ledge-drop integration. See Player — Testing for test commands and revision-specific results.

Known Limitations

If the game window loses focus while a movement key is held, keyUp never fires and the key stays registered as held, so the player keeps walking until it's pressed and released again.

There is no coyote time or jump buffering, so jumps must be timed precisely at the edge of a platform.

Walking is horizontal. Ladders and crouching are not implemented in the reviewed path. Rope climbing changes attached rope length rather than enabling free vertical movement. Integrated dash/jump/climbing feel has not been independently playtested in this documentation update.

PR #145 reviewers reported walking-animation state sticking after dash. Sprint and dash sharing Shift, bow cooldown and jump delay were also raised as game-feel feedback, not established defects; changing those controls/timings requires a separate design decision.

Relevant Development Links

PR Work
#196 Regression coverage, collision fixes, maintainability and Sonar coverage reporting
#211 Charged grapple, rope physics, flight wrapping and corner climbing
#229 Movement, melee and animation overhaul

Contribution Attribution

Player movement, sprinting, jumping, ground detection and air control were implemented by Darcy Franklin (DarcyMF).

Later work includes Tristan Sheedy's jump/animation/sizing integration, Rohan Shirkhedkar's SOLID-layer and rope-climbing work, and Josiah Enns's dash-animation integration. Dash mechanics and recovery commits are authored as abhatia1be24-bit. Aryan Bhatia's confirmed account is AryanBhatia006; this documentation does not assume those two handles are equivalent.

Dhiren Malik (deerainn) resolved the player/main input and action conflicts while preserving the level team's ledge and respawn features, and added the 12 movement plus seven ledge regression tests. Ledge/contact and checkpoint/respawn implementation retain the level team's attribution.