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.