Platformers + Gravity
Demo game + lesson on how you can make your very own platformer
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;
_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
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.
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
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 |