Game Status: Not Started

Lesson

Platformer Player

PlatformerPlayer extends the base Player class to add gravity, jumping, and platformer-style collision resolution against Barrier objects — giving your character the physics it needs to walk, fall, and land on platforms.


Gravity System

Gravity is simulated by maintaining a verticalVelocity value that is decremented each frame, causing the player to accelerate downward continuously.

1 Key Properties

js
const playerData = {
	id: 'Mario',
	greeting: 'Let\'s-a go!',
	src: spriteSrc,
	SCALE_FACTOR: 10,
	STEP_FACTOR: 1600,
	ANIMATION_RATE: 20,
	INIT_POSITION: {
		x: width * 0.1,
		y: height * 0.3,
	},
	pixels: { height: 384, width: 288 },
	orientation: { rows: 2, columns: 3 },
	down: { row: 0, start: 0, columns: 3 },
	downRight: { row: 0, start: 0, columns: 3 },
	downLeft: { row: 1, start: 0, columns: 3 },
	left: { row: 1, start: 0, columns: 3 },
	right: { row: 0, start: 0, columns: 3 },
	up: { row: 0, start: 0, columns: 3 },
	upLeft: { row: 1, start: 0, columns: 3 },
	upRight: { row: 0, start: 0, columns: 3 },
	hitbox: { widthPercentage: 0.2, heightPercentage: 0.2 },
	debugHitbox: false,
	debugHitboxColor: 'rgba(57, 255, 20, 0.95)',
	jumpSoundSrc: marioJumpAudioSrc,
	jumpSoundVolume: 0.8,
	keypress: { up: 87, left: 65, down: 83, right: 68 },
	jumpVelocity: 6,
	gravityAcceleration: 0.12,
};
jsthis.verticalVelocity = 0;          // Current vertical speed
this.gravityAcceleration = 0.4;     // How fast the player falls each frame
this.jumpVelocity = 8;              // Upward speed applied on jump
this.isGrounded = false;            // Whether the player is on the ground

2 Per-Frame Gravity Loop

Each frame, update() runs this sequence:

js// 1. Subtract gravity from vertical velocity (pulls player down)
this.verticalVelocity -= this.gravityAcceleration;
// 2. Convert to engine velocity (flip sign)
this.velocity.y = -this.verticalVelocity;
// 3. Move, draw, and check collisions
super.move();
super.draw();
super.collisionChecks();
// 4. After collisions, update grounded state
this.isGrounded = this._groundedThisFrame;
Gravity keeps subtracting every frame, so the player accelerates downward continuously unless a collision handler sets _skipGravityThisFrame = true.

3 Jumping

In updateVelocity(), a jump is triggered when the up key is pressed and the player is grounded (or has barrier support beneath them):

jsif (upPressed && (this.isGrounded || this._hasBarrierSupport()) && !this._jumpPressedLatch) {
    this.verticalVelocity = this.jumpVelocity;  // Jump upward
    this.isGrounded = false;
    this._jumpPressedLatch = true;               // Prevent hold-to-jump
}

_jumpPressedLatch prevents the jump from re-triggering while the key is held. It resets when the key is released.


4 Detecting a Hit — isCollision(other)

This overrides the base Player method. It computes two trimmed rectangles — one for the player, one for other — and checks if they overlap using a standard AABB test:

jsconst hit = (
    thisRect.left   < otherRect.right  &&
    thisRect.right  > otherRect.left   &&
    thisRect.top    < otherRect.bottom &&
    thisRect.bottom > otherRect.top
);

It also computes directional touch flags for both sides of the collision:

jstouchPoints.this.top    // Player's top edge is touching the other's top
touchPoints.this.bottom // Player's bottom edge is touching the other's bottom
touchPoints.this.left   // ...and so on
These flags tell handleCollisionState() how the objects are touching, not just that they're touching.

Collision Resolution — handleCollisionState()

Resolution behaves differently depending on whether the colliding object is a Barrier. Non-barrier collisions defer to super.handleCollisionState(). For Barrier objects, four cases are handled:

5 Barrier Cases

1 — Landing on Top

jsif (touchPoints.top && this.verticalVelocity <= 0) {
    this.position.y = otherObject.y - (this.height - insets.bottom); // Snap to surface
    this.verticalVelocity = 0;
    this.isGrounded = true;
    this._skipGravityThisFrame = true;  // Don't re-apply gravity this frame
}

The guard verticalVelocity <= 0 is critical — it prevents this block from running on the same frame the player jumps, which would cancel the jump immediately.

2 — Hitting the Underside

jsif (touchPoints.bottom) {
    this.position.y = otherObject.y + otherObject.height - insets.top; // Push below
    this.verticalVelocity = 0;  // Kill upward momentum
}

3 — Left Wall

jsif (touchPoints.left) {
    this.position.x = otherObject.x - (this.width - insets.right);
}

4 — Right Wall

jsif (touchPoints.right) {
    this.position.x = otherObject.x + otherObject.width - insets.left;
}

Changes to GameObject for preserving held inputs

6Why clearPressedKeysOnCollisionexists

Platformer movement is continuous and the game reads held keys every frame to move the player. An earlier fix cleared pressedKeys before every collision pass to prevent stuck inputs caused by blocking dialogs (like alert()), which steal focus and prevent keyup from firing. This prevented clean movement in platformers, however, where holding a key is essential.

Setting clearPressedKeysOnCollision = false on an object opts it out of the key-clearing behavior, preserving held input through the collision pass. However, for backwards compatability, if you don't set this property, the old key-clearing behavior will still run. Breaking other games is fun but is unfortuately not the goal of this change
− Before
// Clear any stale pressed key state on // player-like objects before running // collision reactions. This avoids cases // where a blocking dialog (for example // `alert()`) steals focus and prevents // keyup events from firing, leaving // movement keys stuck. try { for (const obj of this.gameEnv.gameObjects) { if (obj && typeof obj === 'object' && obj.pressedKeys && typeof obj.pressedKeys === 'object') { obj.pressedKeys = {}; } } } catch (_) {}
+ After
// Some games rely on preserving held // inputs (for example platformers). // Keep legacy key clearing behavior // unless an object opts out. if (this.clearPressedKeysOnCollision !== false) { try { for (const obj of this.gameEnv.gameObjects) { if (obj && typeof obj === 'object' && obj.pressedKeys && typeof obj.pressedKeys === 'object') { obj.pressedKeys = {}; } } } catch (_) {} }
js// Opt out of key clearing for objects that need continuous held-input reading
this.clearPressedKeysOnCollision = false;

Key Properties Reference

Property Type Default Description
verticalVelocity number 0 Current vertical speed; positive = moving up
gravityAcceleration number 0.4 Amount subtracted from verticalVelocity each frame
jumpVelocity number 8 Upward speed applied when a jump is triggered
isGrounded boolean false Whether the player is currently on a surface
_jumpPressedLatch boolean false Prevents hold-to-jump; resets on key release
_skipGravityThisFrame boolean false Set by landing handler to suppress gravity for one frame