pin_drop当前位置:知识文库 ❯ 图文
游戏开发:2D平台跳跃游戏 - 从入门到实践详解
教程简介
平台跳跃游戏(Platformer)是最经典的游戏类型之一,从超级马里奥到空洞骑士,这种游戏类型始终深受玩家喜爱。本教程将带你从零开始实现一个完整的2D平台跳跃游戏,涵盖平台游戏设计、角色控制(跑/跳/蹲)、平台生成、敌人AI、道具系统、关卡设计、计分系统等核心内容。通过本教程的学习,你将掌握开发完整2D平台游戏的全套技能。
核心概念
平台游戏设计
平台跳跃游戏的核心要素:
精确的角色控制:响应灵敏的移动和跳跃是平台游戏的灵魂
多样的平台类型:静止平台、移动平台、消失平台、弹跳平台
敌人与障碍:不同行为模式的敌人和环境危险
收集与成长:金币、道具、能力提升
关卡节奏:难度曲线和节奏感
角色控制
代码示例
class PlayerController {
constructor() {
this.speed = 250;
this.jumpForce = -480;
this.doubleJumpForce = -400;
this.gravity = 1200;
this.canDoubleJump = true;
this.coyoteTime = 0.1; // 土狼时间:离开平台后仍可跳跃的时间窗口
this.jumpBufferTime = 0.1; // 跳跃缓冲:提前按跳跃键的容错
this.coyoteTimer = 0;
this.jumpBufferTimer = 0;
}
update(dt, player, keys, platforms) {
// 水平移动
let targetVx = 0;
if (keys['ArrowLeft'] || keys['KeyA']) targetVx = -this.speed;
if (keys['ArrowRight'] || keys['KeyD']) targetVx = this.speed;
player.vx += (targetVx - player.vx) * 12 * dt;
// 跳跃缓冲
if (keys['Space'] || keys['ArrowUp'] || keys['KeyW']) {
this.jumpBufferTimer = this.jumpBufferTime;
}
this.jumpBufferTimer -= dt;
// 土狼时间
if (player.onGround) {
this.coyoteTimer = this.coyoteTime;
this.canDoubleJump = true;
} else {
this.coyoteTimer -= dt;
}
// 跳跃
if (this.jumpBufferTimer > 0) {
if (this.coyoteTimer > 0) {
player.vy = this.jumpForce;
this.coyoteTimer = 0;
this.jumpBufferTimer = 0;
} else if (this.canDoubleJump) {
player.vy = this.doubleJumpForce;
this.canDoubleJump = false;
this.jumpBufferTimer = 0;
}
}
// 可变跳跃高度:松开跳跃键时减少上升速度
if (!(keys['Space'] || keys['ArrowUp'] || keys['KeyW']) && player.vy < -100) {
player.vy *= 0.5;
}
// 重力
player.vy += this.gravity * dt;
// 更新位置
player.x += player.vx * dt;
player.y += player.vy * dt;
// 平台碰撞
player.onGround = false;
for (const p of platforms) {
this.resolvePlatformCollision(player, p);
}
}
}敌人AI
代码示例
class EnemyAI {
constructor(type) {
this.type = type;
this.patrolSpeed = 80;
this.chaseSpeed = 150;
this.detectRange = 200;
this.dir = 1;
}
update(dt, enemy, player) {
switch (this.type) {
case 'patrol':
enemy.vx = this.patrolSpeed * this.dir;
if (enemy.x < enemy.patrolMin || enemy.x > enemy.patrolMax) {
this.dir *= -1;
}
break;
case 'chase':
const dx = player.x - enemy.x;
const dist = Math.abs(dx);
if (dist < this.detectRange) {
enemy.vx = Math.sign(dx) * this.chaseSpeed;
} else {
enemy.vx = this.patrolSpeed * this.dir;
if (enemy.x < enemy.patrolMin || enemy.x > enemy.patrolMax) {
this.dir *= -1;
}
}
break;
}
enemy.x += enemy.vx * dt;
}
}语法与用法
完整的2D平台跳跃游戏
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2D平台跳跃游戏</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a1a; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: 'Segoe UI', sans-serif; color: #fff; }
canvas { border: 2px solid #2a2a4a; border-radius: 8px; }
.info { margin-top: 8px; font-size: 12px; color: #555; text-align: center; }
</style>
</head>
<body>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="info">A/D或方向键移动 | W/空格跳跃(可二段跳) | 踩敌人消灭 | 收集金币 | 到达旗帜过关</div>
<script>
class PlatformerGame {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.W = 800;
this.H = 600;
this.keys = {};
this.state = 'menu'; // menu, playing, gameover, win
this.score = 0;
this.lives = 3;
this.level = 1;
this.coins = 0;
this.time = 0;
this.lastTime = 0;
this.particles = [];
this.setupInput();
}
setupInput() {
document.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
if (['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault();
if (this.state === 'menu' && e.code === 'Space') this.startGame();
if ((this.state === 'gameover' || this.state === 'win') && e.code === 'Space') this.state = 'menu';
});
document.addEventListener('keyup', (e) => { this.keys[e.code] = false; });
this.canvas.addEventListener('click', () => {
if (this.state === 'menu') this.startGame();
if (this.state === 'gameover' || this.state === 'win') this.state = 'menu';
});
}
startGame() {
this.state = 'playing';
this.score = 0;
this.lives = 3;
this.level = 1;
this.coins = 0;
this.loadLevel(this.level);
}
loadLevel(level) {
this.player = {
x: 50, y: 400, w: 24, h: 36,
vx: 0, vy: 0, onGround: false,
facing: 1, canDoubleJump: true,
coyoteTimer: 0, jumpBufferTimer: 0,
invincible: 0, animFrame: 0, animTimer: 0
};
this.particles = [];
this.cameraX = 0;
this.levelWidth = 2400 + level * 400;
this.flagX = this.levelWidth - 80;
// 生成平台
this.platforms = [
{ x: 0, y: 520, w: 300, h: 80, type: 'ground' }
];
// 地面段
let gx = 300;
while (gx < this.levelWidth) {
const gw = 150 + Math.random() * 200;
const gap = 60 + Math.random() * 80;
this.platforms.push({ x: gx + gap, y: 520, w: gw, h: 80, type: 'ground' });
gx += gap + gw;
}
// 浮空平台
for (let i = 0; i < 15 + level * 5; i++) {
const px = 200 + Math.random() * (this.levelWidth - 400);
const py = 250 + Math.random() * 220;
const pw = 80 + Math.random() * 100;
const types = ['normal', 'normal', 'normal', 'moving', 'bounce'];
const type = types[Math.floor(Math.random() * types.length)];
const plat = { x: px, y: py, w: pw, h: 16, type, origY: py, moveRange: 40 + Math.random() * 40, moveSpeed: 1 + Math.random() * 1.5 };
this.platforms.push(plat);
}
// 金币
this.coinList = [];
for (let i = 0; i < 20 + level * 5; i++) {
this.coinList.push({
x: 100 + Math.random() * (this.levelWidth - 200),
y: 200 + Math.random() * 280,
collected: false, bob: Math.random() * Math.PI * 2
});
}
// 敌人
this.enemies = [];
for (let i = 0; i < 5 + level * 3; i++) {
const ex = 300 + Math.random() * (this.levelWidth - 400);
const types = ['patrol', 'patrol', 'chase'];
this.enemies.push({
x: ex, y: 484, w: 28, h: 28,
vx: 0, type: types[Math.floor(Math.random() * types.length)],
dir: Math.random() > 0.5 ? 1 : -1,
patrolMin: ex - 80, patrolMax: ex + 80,
alive: true, animTimer: 0
});
}
// 道具
this.powerups = [];
if (Math.random() > 0.5) {
this.powerups.push({ x: this.levelWidth / 2, y: 300, type: 'heart', collected: false });
}
}
update(dt) {
if (this.state !== 'playing') return;
this.time += dt;
const p = this.player;
// === 玩家控制 ===
const speed = 260;
const jumpForce = -500;
const doubleJumpForce = -420;
const gravity = 1200;
let targetVx = 0;
if (this.keys['ArrowLeft'] || this.keys['KeyA']) { targetVx = -speed; p.facing = -1; }
if (this.keys['ArrowRight'] || this.keys['KeyD']) { targetVx = speed; p.facing = 1; }
p.vx += (targetVx - p.vx) * 10 * dt;
// 跳跃缓冲
if (this.keys['Space'] || this.keys['ArrowUp'] || this.keys['KeyW']) {
if (!this._jumpKeyWasDown) {
p.jumpBufferTimer = 0.1;
this._jumpKeyWasDown = true;
}
} else {
this._jumpKeyWasDown = false;
}
p.jumpBufferTimer -= dt;
// 土狼时间
if (p.onGround) {
p.coyoteTimer = 0.08;
p.canDoubleJump = true;
} else {
p.coyoteTimer -= dt;
}
// 跳跃执行
if (p.jumpBufferTimer > 0) {
if (p.coyoteTimer > 0) {
p.vy = jumpForce;
p.coyoteTimer = 0;
p.jumpBufferTimer = 0;
this.spawnParticles(p.x + p.w / 2, p.y + p.h, 5, '#fff', -50, 50, -100, -30);
} else if (p.canDoubleJump) {
p.vy = doubleJumpForce;
p.canDoubleJump = false;
p.jumpBufferTimer = 0;
this.spawnParticles(p.x + p.w / 2, p.y + p.h, 8, '#4a90d9', -80, 80, -50, 50);
}
}
// 可变跳跃高度
if (!(this.keys['Space'] || this.keys['ArrowUp'] || this.keys['KeyW']) && p.vy < -100) {
p.vy *= 0.6;
}
// 重力
p.vy += gravity * dt;
if (p.vy > 600) p.vy = 600;
// 更新位置
p.x += p.vx * dt;
p.y += p.vy * dt;
// 无敌时间
if (p.invincible > 0) p.invincible -= dt;
// 动画
p.animTimer += dt;
if (Math.abs(p.vx) > 20) {
if (p.animTimer > 0.1) { p.animTimer = 0; p.animFrame = (p.animFrame + 1) % 4; }
} else {
p.animFrame = 0;
}
// === 平台碰撞 ===
p.onGround = false;
for (const plat of this.platforms) {
// 移动平台
if (plat.type === 'moving') {
plat.y = plat.origY + Math.sin(this.time * plat.moveSpeed) * plat.moveRange;
}
if (p.x + p.w > plat.x && p.x < plat.x + plat.w &&
p.y + p.h > plat.y && p.y + p.h < plat.y + plat.h + p.vy * dt + 10 &&
p.vy >= 0) {
p.y = plat.y - p.h;
p.vy = 0;
p.onGround = true;
if (plat.type === 'bounce') {
p.vy = -650;
p.onGround = false;
this.spawnParticles(p.x + p.w / 2, plat.y, 8, '#16c79a', -60, 60, -200, -100);
}
}
}
// 掉落
if (p.y > 650) {
this.playerDie();
return;
}
// === 金币收集 ===
for (const coin of this.coinList) {
if (coin.collected) continue;
const dx = (p.x + p.w / 2) - coin.x;
const dy = (p.y + p.h / 2) - coin.y;
if (Math.sqrt(dx * dx + dy * dy) < 22) {
coin.collected = true;
this.coins++;
this.score += 100;
this.spawnParticles(coin.x, coin.y, 6, '#f0a500', -60, 60, -80, -20);
}
}
// === 道具收集 ===
for (const pu of this.powerups) {
if (pu.collected) continue;
if (p.x + p.w > pu.x - 12 && p.x < pu.x + 12 && p.y + p.h > pu.y - 12 && p.y < pu.y + 12) {
pu.collected = true;
if (pu.type === 'heart') { this.lives = Math.min(this.lives + 1, 5); }
this.spawnParticles(pu.x, pu.y, 10, '#e94560', -80, 80, -100, -20);
}
}
// === 敌人 ===
for (const enemy of this.enemies) {
if (!enemy.alive) continue;
enemy.animTimer += dt;
// AI
if (enemy.type === 'patrol') {
enemy.vx = 70 * enemy.dir;
if (enemy.x < enemy.patrolMin || enemy.x > enemy.patrolMax) enemy.dir *= -1;
} else if (enemy.type === 'chase') {
const dx = p.x - enemy.x;
if (Math.abs(dx) < 250) {
enemy.vx = Math.sign(dx) * 120;
enemy.dir = Math.sign(dx);
} else {
enemy.vx = 60 * enemy.dir;
if (enemy.x < enemy.patrolMin || enemy.x > enemy.patrolMax) enemy.dir *= -1;
}
}
enemy.x += enemy.vx * dt;
// 玩家碰撞
if (p.invincible <= 0 && p.x + p.w > enemy.x && p.x < enemy.x + enemy.w &&
p.y + p.h > enemy.y && p.y < enemy.y + enemy.h) {
if (p.vy > 0 && p.y + p.h < enemy.y + enemy.h / 2 + 10) {
// 踩杀
enemy.alive = false;
p.vy = -350;
this.score += 200;
this.spawnParticles(enemy.x + enemy.w / 2, enemy.y + enemy.h / 2, 10, '#e94560', -100, 100, -150, -50);
} else {
// 受伤
this.playerHit();
}
}
}
// === 旗帜(通关) ===
if (p.x + p.w > this.flagX) {
this.score += 1000;
this.level++;
if (this.level > 3) {
this.state = 'win';
} else {
this.loadLevel(this.level);
}
return;
}
// === 摄像机 ===
const targetCamX = p.x - this.W / 3;
this.cameraX += (targetCamX - this.cameraX) * 5 * dt;
this.cameraX = Math.max(0, Math.min(this.cameraX, this.levelWidth - this.W));
// === 粒子 ===
for (let i = this.particles.length - 1; i >= 0; i--) {
const pt = this.particles[i];
pt.x += pt.vx * dt;
pt.y += pt.vy * dt;
pt.vy += 300 * dt;
pt.life -= dt;
if (pt.life <= 0) this.particles.splice(i, 1);
}
}
playerHit() {
const p = this.player;
if (p.invincible > 0) return;
this.lives--;
p.invincible = 1.5;
p.vy = -300;
p.vx = -p.facing * 200;
this.spawnParticles(p.x + p.w / 2, p.y + p.h / 2, 8, '#e94560', -100, 100, -150, -50);
if (this.lives <= 0) {
this.state = 'gameover';
}
}
playerDie() {
this.lives--;
if (this.lives <= 0) {
this.state = 'gameover';
} else {
this.player.x = 50;
this.player.y = 400;
this.player.vx = 0;
this.player.vy = 0;
}
}
spawnParticles(x, y, count, color, vxMin, vxMax, vyMin, vyMax) {
for (let i = 0; i < count; i++) {
this.particles.push({
x, y,
vx: vxMin + Math.random() * (vxMax - vxMin),
vy: vyMin + Math.random() * (vyMax - vyMin),
life: 0.4 + Math.random() * 0.4,
size: 2 + Math.random() * 3,
color
});
}
}
render() {
const ctx = this.ctx;
const W = this.W, H = this.H;
if (this.state === 'menu') {
this.renderMenu(ctx, W, H);
return;
}
if (this.state === 'gameover') {
this.renderGameOver(ctx, W, H);
return;
}
if (this.state === 'win') {
this.renderWin(ctx, W, H);
return;
}
// === 游戏画面 ===
// 天空
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#1a1a4a');
skyGrad.addColorStop(0.6, '#2a3a5a');
skyGrad.addColorStop(1, '#3a5a4a');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// 远景山
ctx.fillStyle = '#1a2a3a';
ctx.beginPath();
ctx.moveTo(0, 400);
for (let x = 0; x <= W; x += 5) {
const wx = x + this.cameraX * 0.2;
ctx.lineTo(x, 350 - Math.sin(wx * 0.003) * 80 - Math.sin(wx * 0.007) * 30);
}
ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.fill();
ctx.save();
ctx.translate(-Math.round(this.cameraX), 0);
// 平台
for (const plat of this.platforms) {
if (plat.x + plat.w < this.cameraX - 50 || plat.x > this.cameraX + W + 50) continue;
if (plat.type === 'ground') {
ctx.fillStyle = '#2a4a2a';
ctx.fillRect(plat.x, plat.y, plat.w, plat.h);
ctx.fillStyle = '#3a6a3a';
ctx.fillRect(plat.x, plat.y, plat.w, 6);
// 草
ctx.fillStyle = '#4a8a4a';
for (let gx = plat.x; gx < plat.x + plat.w; gx += 8) {
ctx.fillRect(gx, plat.y - 2, 3, 4);
}
} else {
let color = '#5a4a3a';
if (plat.type === 'moving') color = '#4a5a6a';
if (plat.type === 'bounce') color = '#2a6a5a';
ctx.fillStyle = color;
ctx.fillRect(plat.x, plat.y, plat.w, plat.h);
ctx.fillStyle = plat.type === 'bounce' ? '#3a8a7a' : '#7a6a5a';
ctx.fillRect(plat.x, plat.y, plat.w, 4);
}
}
// 旗帜
ctx.fillStyle = '#8a7a6a';
ctx.fillRect(this.flagX, 380, 4, 140);
ctx.fillStyle = '#e94560';
ctx.beginPath();
ctx.moveTo(this.flagX + 4, 380);
ctx.lineTo(this.flagX + 40, 395);
ctx.lineTo(this.flagX + 4, 410);
ctx.fill();
// 金币
for (const coin of this.coinList) {
if (coin.collected) continue;
if (coin.x < this.cameraX - 50 || coin.x > this.cameraX + W + 50) continue;
const bob = Math.sin(this.time * 3 + coin.bob) * 4;
ctx.fillStyle = '#f0a500';
ctx.beginPath();
ctx.arc(coin.x, coin.y + bob, 8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffd700';
ctx.beginPath();
ctx.arc(coin.x - 2, coin.y + bob - 2, 3, 0, Math.PI * 2);
ctx.fill();
}
// 道具
for (const pu of this.powerups) {
if (pu.collected) continue;
const bob = Math.sin(this.time * 2) * 5;
ctx.fillStyle = '#e94560';
ctx.beginPath();
ctx.arc(pu.x, pu.y + bob, 10, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 12px Arial';
ctx.textAlign = 'center';
ctx.fillText('+', pu.x, pu.y + bob + 4);
}
// 敌人
for (const enemy of this.enemies) {
if (!enemy.alive) continue;
if (enemy.x < this.cameraX - 50 || enemy.x > this.cameraX + W + 50) continue;
ctx.save();
ctx.translate(enemy.x + enemy.w / 2, enemy.y + enemy.h / 2);
ctx.scale(enemy.dir, 1);
// 身体
ctx.fillStyle = enemy.type === 'chase' ? '#c73652' : '#e94560';
ctx.beginPath();
ctx.arc(0, 2, 14, 0, Math.PI * 2);
ctx.fill();
// 眼睛
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(3, -3, 5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#111';
ctx.beginPath(); ctx.arc(5, -3, 2.5, 0, Math.PI * 2); ctx.fill();
// 嘴
ctx.strokeStyle = '#111';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(2, 5, 5, 0, Math.PI);
ctx.stroke();
ctx.restore();
}
// 玩家
const p = this.player;
if (p.invincible <= 0 || Math.floor(p.invincible * 10) % 2 === 0) {
ctx.save();
ctx.translate(p.x + p.w / 2, p.y + p.h / 2);
ctx.scale(p.facing, 1);
// 身体
ctx.fillStyle = '#4a90d9';
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
// 头
ctx.fillStyle = '#ffd5a0';
ctx.beginPath();
ctx.arc(0, -p.h / 2 - 2, 10, 0, Math.PI * 2);
ctx.fill();
// 帽子
ctx.fillStyle = '#e94560';
ctx.fillRect(-10, -p.h / 2 - 12, 20, 6);
ctx.fillRect(-6, -p.h / 2 - 16, 12, 6);
// 眼睛
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(3, -p.h / 2 - 3, 3, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#111';
ctx.beginPath(); ctx.arc(4, -p.h / 2 - 3, 1.5, 0, Math.PI * 2); ctx.fill();
ctx.restore();
}
// 粒子
for (const pt of this.particles) {
ctx.globalAlpha = pt.life * 2;
ctx.fillStyle = pt.color;
ctx.beginPath();
ctx.arc(pt.x, pt.y, pt.size * pt.life, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.restore();
// HUD
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, W, 36);
ctx.fillStyle = '#fff';
ctx.font = '14px monospace';
ctx.textAlign = 'left';
ctx.fillText(`分数: ${this.score}`, 10, 24);
ctx.fillText(`关卡: ${this.level}/3`, 150, 24);
ctx.fillText(`金币: ${this.coins}`, 260, 24);
ctx.fillStyle = '#e94560';
ctx.fillText(`${''.repeat(this.lives)}`, 370, 24);
}
renderMenu(ctx, W, H) {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, '#0a0a2a'); grad.addColorStop(1, '#1a2a4a');
ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#e94560';
ctx.font = 'bold 52px Arial'; ctx.textAlign = 'center';
ctx.fillText('超级冒险', W / 2, 180);
ctx.fillStyle = '#4a90d9';
ctx.font = '22px Arial';
ctx.fillText('2D 平台跳跃游戏', W / 2, 220);
ctx.fillStyle = '#fff';
ctx.font = '18px Arial';
ctx.fillText('点击或按空格开始', W / 2, 350);
ctx.fillStyle = '#888';
ctx.font = '13px monospace';
ctx.fillText('A/D移动 | W/空格跳跃 | 踩敌人消灭', W / 2, 450);
ctx.fillText('收集金币 | 到达旗帜过关 | 共3关', W / 2, 475);
}
renderGameOver(ctx, W, H) {
ctx.fillStyle = '#1a0a0a'; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#e94560';
ctx.font = 'bold 48px Arial'; ctx.textAlign = 'center';
ctx.fillText('游戏结束', W / 2, 200);
ctx.fillStyle = '#fff'; ctx.font = '20px Arial';
ctx.fillText(`最终分数: ${this.score}`, W / 2, 280);
ctx.fillText(`收集金币: ${this.coins}`, W / 2, 320);
ctx.fillStyle = '#888'; ctx.font = '16px Arial';
ctx.fillText('点击或按空格返回', W / 2, 420);
}
renderWin(ctx, W, H) {
ctx.fillStyle = '#0a1a0a'; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#16c79a';
ctx.font = 'bold 48px Arial'; ctx.textAlign = 'center';
ctx.fillText('恭喜通关!', W / 2, 200);
ctx.fillStyle = '#fff'; ctx.font = '20px Arial';
ctx.fillText(`最终分数: ${this.score}`, W / 2, 280);
ctx.fillText(`收集金币: ${this.coins}`, W / 2, 320);
ctx.fillStyle = '#f0a500'; ctx.font = '16px Arial';
ctx.fillText('你是真正的冒险家!', W / 2, 380);
ctx.fillStyle = '#888';
ctx.fillText('点击或按空格返回', W / 2, 440);
}
start() {
this.lastTime = performance.now();
requestAnimationFrame((t) => this.loop(t));
}
loop(currentTime) {
const dt = Math.min((currentTime - this.lastTime) / 1000, 0.05);
this.lastTime = currentTime;
this.update(dt);
this.render();
requestAnimationFrame((t) => this.loop(t));
}
}
const game = new PlatformerGame();
game.start();
</script>
</body>
</html>浏览器兼容性
注意事项与最佳实践
土狼时间(Coyote Time):离开平台后给予短暂跳跃窗口(约0.1秒),大幅提升操作手感。
跳跃缓冲(Jump Buffer):在落地前短暂按跳跃键也能触发跳跃,提升容错。
可变跳跃高度:松开跳跃键时减少上升速度,让玩家可以精确控制跳跃高度。
加速度移动:角色起步和停止使用加速度而非瞬间速度,手感更自然。
踩杀判定:玩家从上方接触敌人时消灭敌人,需要精确的碰撞判定区域。
无敌帧:受伤后短暂无敌,避免连续受伤。
摄像机平滑跟随:摄像机使用插值跟随玩家,避免生硬的视角切换。
关卡难度曲线:从简单到复杂,逐步引入新机制。
代码规范示例
代码示例
/**
* 平台游戏角色控制器
* 实现精确的平台游戏操作手感
*/
class PlatformPlayerController {
/**
* @param {Object} config - 控制器配置
*/
constructor(config = {}) {
this.moveSpeed = config.moveSpeed || 260;
this.jumpForce = config.jumpForce || -500;
this.doubleJumpForce = config.doubleJumpForce || -420;
this.gravity = config.gravity || 1200;
this.maxFallSpeed = config.maxFallSpeed || 600;
this.acceleration = config.acceleration || 10;
this.coyoteTime = config.coyoteTime || 0.08;
this.jumpBufferTime = config.jumpBufferTime || 0.1;
this.jumpCutMultiplier = config.jumpCutMultiplier || 0.6;
this._coyoteTimer = 0;
this._jumpBufferTimer = 0;
this._canDoubleJump = true;
this._jumpKeyWasDown = false;
}
/**
* 更新玩家物理和控制
* @param {Object} player - 玩家对象
* @param {number} dt - 帧间隔时间
* @param {Object} input - 输入状态
* @param {Array} platforms - 平台数组
*/
update(player, dt, input, platforms) {
this._handleHorizontal(player, dt, input);
this._handleJump(player, dt, input);
this._applyGravity(player, dt);
this._updatePosition(player, dt);
this._resolveCollisions(player, platforms);
}
/** @private */
_handleHorizontal(player, dt, input) {
let targetVx = 0;
if (input.left) { targetVx = -this.moveSpeed; player.facing = -1; }
if (input.right) { targetVx = this.moveSpeed; player.facing = 1; }
player.vx += (targetVx - player.vx) * this.acceleration * dt;
}
/** @private */
_handleJump(player, dt, input) {
// 跳跃缓冲
if (input.jumpPressed && !this._jumpKeyWasDown) {
this._jumpBufferTimer = this.jumpBufferTime;
}
this._jumpKeyWasDown = input.jumpPressed;
this._jumpBufferTimer -= dt;
// 土狼时间
if (player.onGround) {
this._coyoteTimer = this.coyoteTime;
this._canDoubleJump = true;
} else {
this._coyoteTimer -= dt;
}
// 执行跳跃
if (this._jumpBufferTimer > 0) {
if (this._coyoteTimer > 0) {
player.vy = this.jumpForce;
this._coyoteTimer = 0;
this._jumpBufferTimer = 0;
} else if (this._canDoubleJump) {
player.vy = this.doubleJumpForce;
this._canDoubleJump = false;
this._jumpBufferTimer = 0;
}
}
// 可变跳跃高度
if (!input.jumpHeld && player.vy < -100) {
player.vy *= this.jumpCutMultiplier;
}
}
/** @private */
_applyGravity(player, dt) {
player.vy += this.gravity * dt;
if (player.vy > this.maxFallSpeed) player.vy = this.maxFallSpeed;
}
/** @private */
_updatePosition(player, dt) {
player.x += player.vx * dt;
player.y += player.vy * dt;
}
/** @private */
_resolveCollisions(player, platforms) {
player.onGround = false;
for (const plat of platforms) {
if (player.x + player.w > plat.x && player.x < plat.x + plat.w &&
player.y + player.h > plat.y && player.y + player.h < plat.y + plat.h + 15 &&
player.vy >= 0) {
player.y = plat.y - player.h;
player.vy = 0;
player.onGround = true;
}
}
}
}常见问题与解决方案
问题1:角色操作手感生硬
原因:缺少加速度、土狼时间和跳跃缓冲。
解决方案:实现加速度移动、土狼时间、跳跃缓冲和可变跳跃高度。
问题2:角色卡在平台边缘
原因:碰撞检测只检查了上方碰撞。
解决方案:分别处理四个方向的碰撞,优先处理最小重叠方向。
问题3:移动平台上角色不跟随
原因:移动平台改变位置时没有带动角色。
解决方案:记录平台位移量,将位移量加到角色位置上。
问题4:敌人行为单一
原因:只有简单的巡逻AI。
解决方案:实现多种AI类型:巡逻、追踪、跳跃、飞行等。
问题5:关卡缺乏趣味性
原因:平台布局随机,缺乏设计。
解决方案:手动设计关键段落,随机填充细节,确保关卡有节奏感。
总结
本教程带你从零实现了一个完整的2D平台跳跃游戏。我们学习了平台游戏的核心设计原则,实现了精确的角色控制(包括土狼时间、跳跃缓冲、可变跳跃高度等提升手感的技巧),设计了多种平台类型(静止、移动、弹跳),实现了敌人AI(巡逻和追踪),添加了金币收集和道具系统,设计了3个关卡的完整游戏流程。
关键要点:
土狼时间和跳跃缓冲大幅提升操作手感
可变跳跃高度让玩家精确控制跳跃
加速度移动比瞬间速度更自然
踩杀判定需要精确的碰撞区域
受伤后无敌帧避免连续受伤
摄像机平滑跟随提升视觉体验
关卡难度应从简单到复杂递进
多种平台和敌人类型增加游戏趣味
掌握这些技术后,你将能够开发出操作手感优秀、内容丰富的2D平台跳跃游戏。
常见问题
什么是2D平台跳跃游戏?
2D平台跳跃游戏是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。
如何学习2D平台跳跃游戏的实际应用?
教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。
2D平台跳跃游戏有哪些注意事项?
常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。
2D平台跳跃游戏适合初学者吗?
本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。
2D平台跳跃游戏的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别