'# JS小游戏-像素鸟#源码#Javascript
一、背景与问题
Flappy Bird 是一款经典的像素风格休闲游戏,其核心机制是通过重力模拟实现小鸟的飞行轨迹,并通过管道碰撞检测判断游戏是否结束。作为前端开发的常见教学案例,它涉及了动画渲染、物理模拟、事件处理等多个技术点。
在实际开发中,开发者常遇到以下问题:
- 小鸟的运动轨迹不自然
- 管道生成逻辑存在漏洞
- 碰撞检测出现误判
- 游戏性能不稳定
- 代码结构难以维护
通过深入分析 Flappy Bird 的实现原理,可以帮助开发者掌握前端动画开发的核心技术栈。
二、基本原理
1. 物理模拟原理
游戏中的重力模拟采用简单的牛顿运动学公式:
velocity = velocity + gravity * deltaTime
position = position + velocity * deltaTime其中 gravity 是重力加速度(通常取 0.5),deltaTime 是时间间隔。
2. 碰撞检测原理
采用轴对齐包围盒(AABB)检测算法:
function isColliding(a, b) {
return a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y;
}该算法通过比较矩形区域的坐标范围判断是否发生碰撞。
3. 管道生成原理
管道采用固定间隔生成策略:
const PIPE_GAP = 120;
const PIPE_WIDTH = 50;
const PIPE_SPEED = 2;
function generatePipes() {
const topHeight = Math.random() * (canvas.height - PIPE_GAP - 100) + 50;
const bottomHeight = canvas.height - topHeight - PIPE_GAP - 100;
return {
x: canvas.width,
top: {
y: 0,
height: topHeight
},
bottom: {
y: topHeight + PIPE_GAP,
height: bottomHeight
}
};
}三、环境准备
# 创建项目结构
mkdir flappy-bird
cd flappy-bird
touch index.html
touch game.js四、核心实现
1. 游戏主循环
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
function update() {
// 更新小鸟位置
bird.y += velocity;
velocity += gravity;
// 更新管道位置
pipes.forEach(pipe => pipe.x -= PIPE_SPEED);
// 移除离开屏幕的管道
pipes = pipes.filter(pipe => pipe.x + PIPE_WIDTH > 0);
// 检测碰撞
checkCollisions();
}2. 碰撞检测实现
function checkCollisions() {
// 检测与地面/天花板碰撞
if (bird.y + bird.height > canvas.height || bird.y < 0) {
gameOver = true;
}
// 检测与管道碰撞
pipes.forEach(pipe => {
if (isColliding(bird, pipe.top) || isColliding(bird, pipe.bottom)) {
gameOver = true;
}
});
}3. 动画渲染
function draw() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制背景
ctx.fillStyle = '#70c5ce';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制小鸟
ctx.fillStyle = 'yellow';
ctx.beginPath();
ctx.arc(bird.x, bird.y, bird.radius, 0, Math.PI * 2);
ctx.fill();
// 绘制管道
pipes.forEach(pipe => {
ctx.fillStyle = 'green';
ctx.fillRect(pipe.x, pipe.top.y, PIPE_WIDTH, pipe.top.height);
ctx.fillRect(pipe.x, pipe.bottom.y, PIPE_WIDTH, pipe.bottom.height);
});
}五、完整案例
完整的游戏实现如下(包含所有核心逻辑):
<!DOCTYPE html>
<html>
<head>
<title>Flappy Bird</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; background: #70c5ce; }
</style>
</head>
<body>
<canvas id="gameCanvas" width="400" height="600"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const gravity = 0.5;
const bird = {
x: 100,
y: 300,
radius: 20,
velocity: 0
};
const pipes = [];
const PIPE_GAP = 120;
const PIPE_WIDTH = 50;
const PIPE_SPEED = 2;
let gameOver = false;
function generatePipes() {
const topHeight = Math.random() * (canvas.height - PIPE_GAP - 100) + 50;
const bottomHeight = canvas.height - topHeight - PIPE_GAP - 100;
return {
x: canvas.width,
top: {
y: 0,
height: topHeight
},
bottom: {
y: topHeight + PIPE_GAP,
height: bottomHeight
}
};
}
function checkCollisions() {
if (bird.y + bird.radius > canvas.height || bird.y < 0) {
gameOver = true;
}
pipes.forEach(pipe => {
const birdRect = {
x: bird.x - bird.radius,
y: bird.y - bird.radius,
width: bird.radius * 2,
height: bird.radius * 2
};
const pipeTop = {
x: pipe.x,
y: pipe.top.y,
width: PIPE_WIDTH,
height: pipe.top.height
};
const pipeBottom = {
x: pipe.x,
y: pipe.bottom.y,
width: PIPE_WIDTH,
height: pipe.bottom.height
};
if (isColliding(birdRect, pipeTop) || isColliding(birdRect, pipeBottom)) {
gameOver = true;
}
});
}
function isColliding(a, b) {
return a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y;
}
function update() {
bird.velocity += gravity;
bird.y += bird.velocity;
pipes.forEach(pipe => pipe.x -= PIPE_SPEED);
pipes = pipes.filter(pipe => pipe.x + PIPE_WIDTH > 0);
if (!gameOver && pipes.length < 3) {
pipes.push(generatePipes());
}
checkCollisions();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制地面
ctx.fillStyle = '#4CAF50';
ctx.fillRect(0, canvas.height - 100, canvas.width, 100);
// 绘制小鸟
ctx.fillStyle = 'yellow';
ctx.beginPath();
ctx.arc(bird.x, bird.y, bird.radius, 0, Math.PI * 2);
ctx.fill();
// 绘制管道
pipes.forEach(pipe => {
ctx.fillStyle = 'green';
ctx.fillRect(pipe.x, pipe.top.y, PIPE_WIDTH, pipe.top.height);
ctx.fillRect(pipe.x, pipe.bottom.y, PIPE_WIDTH, pipe.bottom.height);
});
// 绘制分数
ctx.fillStyle = 'white';
ctx.font = '20px Arial';
ctx.fillText('Score: ' + pipes.length, 10, 30);
}
document.addEventListener('keydown', () => {
if (!gameOver) {
bird.velocity = -8;
}
});
function gameLoop() {
if (!gameOver) {
update();
draw();
}
requestAnimationFrame(gameLoop);
}
gameLoop();
</script>
</body>
</html>六、源码解析
1. 游戏主循环
function gameLoop() {
if (!gameOver) {
update();
draw();
}
requestAnimationFrame(gameLoop);
}- 使用
requestAnimationFrame实现流畅动画 - 每帧更新游戏状态和重绘画面
- 在游戏结束时停止循环
2. 碰撞检测优化
function checkCollisions() {
// 鸟的碰撞框
const birdRect = {
x: bird.x - bird.radius,
y: bird.y - bird.radius,
width: bird.radius * 2,
height: bird.radius * 2
};
// 管道的碰撞框
const pipeTop = {
x: pipe.x,
y: pipe.top.y,
width: PIPE_WIDTH,
height: pipe.top.height
};
const pipeBottom = {
x: pipe.x,
y: pipe.bottom.y,
width: PIPE_WIDTH,
height: pipe.bottom.height
};
// 碰撞检测
if (isColliding(birdRect, pipeTop) || isColliding(birdRect, pipeBottom)) {
gameOver = true;
}
}- 使用轴对齐包围盒(AABB)检测算法
- 通过分离碰撞框的坐标计算判断是否发生碰撞
- 优化了碰撞检测的性能
3. 管道生成逻辑
function generatePipes() {
const topHeight = Math.random() * (canvas.height - PIPE_GAP - 100) + 50;
const bottomHeight = canvas.height - topHeight - PIPE_GAP - 100;
return {
x: canvas.width,
top: {
y: 0,
height: topHeight
},
bottom: {
y: topHeight + PIPE_GAP,
height: bottomHeight
}
};
}- 管道高度随机生成,确保有合理空隙
- 管道宽度固定为 50 像素
- 管道之间保持固定间距 120 像素
七、进阶使用
1. 添加音效
const flapSound = new Audio('flap.mp3');
const hitSound = new Audio('hit.mp3');
document.addEventListener('keydown', () => {
if (!gameOver) {
flapSound.play();
bird.velocity = -8;
}
});
function checkCollisions() {
// ...原有代码
if (gameOver) {
hitSound.play();
}
}2. 添加分数记录
let score = 0;
function update() {
// ...原有代码
// 检测管道是否通过
pipes.forEach((pipe, index) => {
if (pipe.x + PIPE_WIDTH < bird.x && !pipe.passed) {
score++;
pipe.passed = true;
}
});
}3. 添加游戏结束界面
function drawGameOver() {
ctx.fillStyle = 'black';
ctx.font = '40px Arial';
ctx.fillText('Game Over', 80, 300);
ctx.fillText('Score: ' + score, 80, 350);
}八、性能与工程实践
1. 性能优化方法
- 使用
requestAnimationFrame而不是setInterval - 减少不必要的 DOM 操作
- 使用 canvas 的
clearRect而不是重绘整个画面 - 对管道进行对象池复用
- 使用防抖处理按键事件
2. 异常处理
try {
// 游戏逻辑
} catch (e) {
console.error('Game crash:', e);
gameOver = true;
}3. 安全性考虑
- 避免直接使用
eval()等危险函数 - 对用户输入进行校验
- 使用
Audio对象时注意内存管理 - 避免在 canvas 中直接渲染敏感信息
九、常见问题与踩坑
1. 碰撞检测错误
// 错误示例:未考虑碰撞框的偏移
function isColliding(a, b) {
return a.x < b.x && a.x + a.width > b.x &&
a.y < b.y && a.y + a.height > b.y;
}问题:未考虑物体中心点与坐标系的偏移
解决:需要计算碰撞框的正确位置
2. 重力模拟不自然
// 错误示例:重力加速度过大
const gravity = 2.0;问题:导致小鸟下落过快
解决:调整重力值为 0.5,增加速度变化的平滑度
3. 管道生成漏洞
// 错误示例:未考虑管道高度边界
const topHeight = Math.random() * (canvas.height) + 50;问题:可能导致管道超出画布范围
解决:设置合理的高度范围限制
十、最佳实践
- 使用对象池:复用管道对象减少内存分配
- 分离更新与渲染:保持逻辑和视觉分离
- 使用 TypeScript:增加类型安全和代码可维护性
- 添加状态管理:明确游戏状态(运行、暂停、结束)
- 使用模块化结构:将不同功能拆分为独立模块
十一、总结
Flappy Bird 游戏的实现展示了前端开发中的核心技术:动画渲染、物理模拟和碰撞检测。通过深入分析其工作原理,我们可以掌握如何用 JavaScript 实现简单的游戏逻辑。
在实际开发中,这种方案适用于:
- 教学演示类项目
- 简单的互动小游戏
- 原型验证快速开发
但需要避免在:
- 高性能需求场景(如实时战斗游戏)
- 复杂物理模拟场景(需使用专业引擎)
- 需要高精度渲染的场景(需使用 WebGL)
通过合理的设计和优化,这种方案可以扩展为更复杂的游戏系统,为开发者提供良好的学习路径和实践机会。