Mario Like - Day 3
Advanced LevelEvery frame, we apply gravity to the player's vertical velocity, then update position. This creates the "weight" feel.
const player = {
x: 100,
y: 300,
width: 32,
height: 48,
vx: 0, // Horizontal velocity
vy: 0, // Vertical velocity
grounded: false,
speed: 5,
jumpForce: -12
};
const GRAVITY = 0.5;
const MAX_FALL_SPEED = 15;
function updatePhysics() {
// Apply gravity
player.vy += GRAVITY;
// Clamp falling speed
if (player.vy > MAX_FALL_SPEED) {
player.vy = MAX_FALL_SPEED;
}
// Apply velocity to position
player.x += player.vx;
player.y += player.vy;
}
Without MAX_FALL_SPEED, the player would accelerate forever when falling, making precise landings impossible.
Platform collision is tricky - we need to detect from which direction the player is colliding and respond correctly.
function handlePlatformCollision() {
player.grounded = false;
for (const platform of platforms) {
if (!isColliding(player, platform)) continue;
// Calculate overlap on each axis
const overlapTop = (player.y + player.height) - platform.y;
const overlapBottom = (platform.y + platform.height) - player.y;
const overlapLeft = (player.x + player.width) - platform.x;
const overlapRight = (platform.x + platform.width) - player.x;
// Find smallest overlap to determine collision side
const minOverlap = Math.min(overlapTop, overlapBottom, overlapLeft, overlapRight);
if (minOverlap === overlapTop && player.vy > 0) {
// Landing on top
player.y = platform.y - player.height;
player.vy = 0;
player.grounded = true;
} else if (minOverlap === overlapBottom && player.vy < 0) {
// Hitting from below
player.y = platform.y + platform.height;
player.vy = 0;
} else if (minOverlap === overlapLeft) {
// Hitting left side
player.x = platform.x - player.width;
} else if (minOverlap === overlapRight) {
// Hitting right side
player.x = platform.x + platform.width;
}
}
}
A good jump feels responsive. We use variable jump height - hold the button longer to jump higher.
const JUMP_FORCE = -12;
const JUMP_CUT_MULTIPLIER = 0.5; // When releasing early
function handleJump() {
// Start jump
if (keys.jump && player.grounded) {
player.vy = JUMP_FORCE;
player.grounded = false;
}
// Variable jump height - cut jump short if button released
if (!keys.jump && player.vy < 0) {
player.vy *= JUMP_CUT_MULTIPLIER;
}
}
// Coyote time - brief window to jump after leaving platform
let coyoteTimer = 0;
const COYOTE_TIME = 6; // frames
function updateCoyoteTime() {
if (player.grounded) {
coyoteTimer = COYOTE_TIME;
} else {
coyoteTimer--;
}
}
// Allow jump if grounded OR within coyote time
const canJump = player.grounded || coyoteTimer > 0;
Named after Wile E. Coyote running off cliffs - gives players a few frames grace period to jump after leaving a platform. Makes the game feel more forgiving.
Basic enemies patrol back and forth between boundaries. They reverse direction when hitting walls or platform edges.
const enemy = {
x: 300,
y: 350,
width: 32,
height: 32,
speed: 1.5,
direction: 1, // 1 = right, -1 = left
patrolMin: 200, // Left boundary
patrolMax: 500 // Right boundary
};
function updateEnemy(enemy) {
// Move in current direction
enemy.x += enemy.speed * enemy.direction;
// Reverse at patrol boundaries
if (enemy.x <= enemy.patrolMin) {
enemy.direction = 1;
enemy.x = enemy.patrolMin;
} else if (enemy.x + enemy.width >= enemy.patrolMax) {
enemy.direction = -1;
enemy.x = enemy.patrolMax - enemy.width;
}
}
// Player stomps enemy if landing on top
function checkEnemyCollision(player, enemy) {
if (!isColliding(player, enemy)) return;
const playerBottom = player.y + player.height;
const enemyTop = enemy.y;
if (player.vy > 0 && playerBottom < enemyTop + 10) {
// Stomp! Player bounces, enemy dies
enemy.alive = false;
player.vy = -8; // Bounce up
} else {
// Hit from side - player takes damage
damagePlayer();
}
}
The camera follows the player with a "dead zone" - it only moves when the player reaches the edge of a central region.
const camera = {
x: 0,
y: 0,
width: canvas.width,
height: canvas.height
};
// Dead zone - camera doesn't move if player is in this region
const DEAD_ZONE = {
left: canvas.width * 0.3,
right: canvas.width * 0.7,
top: canvas.height * 0.3,
bottom: canvas.height * 0.7
};
function updateCamera() {
// Player position relative to camera
const playerScreenX = player.x - camera.x;
const playerScreenY = player.y - camera.y;
// Push camera if player exits dead zone
if (playerScreenX < DEAD_ZONE.left) {
camera.x = player.x - DEAD_ZONE.left;
} else if (playerScreenX > DEAD_ZONE.right) {
camera.x = player.x - DEAD_ZONE.right;
}
// Clamp camera to world bounds
camera.x = Math.max(0, Math.min(WORLD_WIDTH - camera.width, camera.x));
}
// When drawing, offset everything by camera position
function draw() {
ctx.save();
ctx.translate(-camera.x, -camera.y);
// Draw world objects at their world positions
drawPlatforms();
drawEnemies();
drawPlayer();
ctx.restore();
}
Coins float and spin. When collected, they play an animation before being removed.
const coins = [
{ x: 150, y: 280, collected: false, animTimer: 0 },
// ... more coins
];
function updateCoins() {
for (const coin of coins) {
if (coin.collected) {
// Collection animation - float up and fade
coin.animTimer++;
coin.y -= 2;
if (coin.animTimer > 20) {
coin.remove = true;
}
continue;
}
// Bob up and down
coin.displayY = coin.y + Math.sin(Date.now() / 200) * 3;
// Check collection
if (isColliding(player, coin)) {
coin.collected = true;
score += 100;
playSound('coin');
}
}
// Remove finished animations
coins = coins.filter(c => !c.remove);
}
function drawCoin(coin) {
ctx.save();
ctx.translate(coin.x + 8, coin.displayY + 8);
// Spin effect using scale
const scaleX = Math.cos(Date.now() / 150);
ctx.scale(scaleX, 1);
// Draw coin
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.arc(0, 0, 8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}