JavaScript Canvas 实现 Flappy Bird 游戏全流程详解
本文详细解析如何使用 HTML5 Canvas 和 JavaScript 从零开发 Flappy Bird 游戏。内容涵盖画布初始化、异步资源加载、背景与地面的无限滚动渲染、小鸟飞行物理逻辑以及基于坐标的碰撞检测机制,提供完整的代码实现思路。
简介
Canvas 是 HTML5 引入的新标签,允许 JavaScript 直接操作像素进行绘图,常用于开发小型网页游戏。本文将利用该技术复刻一款名为 Flappy Bird(中文名:笨鸟先飞)的游戏。该游戏由越南独立开发者 Dong Nguyen 制作,于 2013 年 5 月 24 日发布,并在 2014 年 2 月迅速走红。
游戏规则
游戏操作极为简单,玩家只需通过点击或长按屏幕控制小鸟上升,松开手指则小鸟受重力影响快速下落。玩家需操控小鸟持续向前飞行并避开途中高低错落的管道。小鸟安全飞过的管道数量即为得分,一旦撞击管道或地面,游戏立即结束,且仅有一条生命。
游戏素材
链接: https://pan.baidu.com/s/1ro1273TeIhhJgCIFj4vn_g?pwd=7vqh
提取码: 7vqh
开始制作
初始化canvas画布
主要任务是创建 Canvas 元素并调整其尺寸,使其能够自适应屏幕大小,确保在不同设备上均能正常显示。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style> body {
margin: 0;
padding: 0;
overflow: hidden;
} </style>
</head>
<body>
<canvas id="canvas">
当前浏览器不支持canvas,请更换浏览器查看。
</canvas>
<script> /** @type {HTMLCanvasElement} */
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')
canvas.width = window.innerWidth
canvas.height = window.innerHeight
window.addEventListener('resize', () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}) </script>
</body>
</html>
加载资源
图片和其他游戏资源的加载是异步进行的。只有当所有资源加载完毕后才能启动游戏,因此需要对图片等资源进行统一的监控与管理。具体做法是使用 JSON 文件描述图片资源,并通过 fetch API 进行统一加载。

// 资源管理器
class SourceManager {
static images = {};
static instance = new SourceManager();
constructor() {
return SourceManager.instance;}
loadImages() {
return new Promise((resolve) => {
fetch("./assets/images/image.json")
.then((res) => res.json())
.then((res) => {
res.forEach((item, index) => {
const image = new Image();
image.src = item.url;
image.onload = () => {
SourceManager.images[item.name] = image;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = "24px 黑体";
ctx.textAlign = "center";
ctx.fillText(`资源加载中${index + 1}/${res.length}...`, canvas.width / 2, (canvas.height / 2) * 0.618);
if (index === res.length - 1) {
console.log(index, "加载完成");
resolve();
}
};
});
});
});}
}
async function main() {
// 加载资源
await new SourceManager().loadImages();
}
main();
背景
为了适配不同屏幕尺寸并确保管道正确渲染,不能简单拉伸背景图片,而需设定一条基准线以固定背景在屏幕中的位置。由于背景图无法填满整个画面,上、右、下侧存在空缺,需使用矩形填充上部区域。为实现背景无限向左滚动的效果,需并排绘制三张背景图。当背景向左移动的距离 dx 等于一张背景图的宽度时,将 dx 重置为 0,从而模拟出类似轮播图的无限循环移动效果。


// 背景
class GameBackground {
constructor() {
this.dx = 0
this.image = SourceManager.images.bg_day
this.dy = 0.8 * (canvas.height - this.image.height)
this.render()}
update() {
this.dx -= 1
if (this.dx + this.image.width <= 0) {
this.dx = 0
}
this.render()}
render() {
ctx.fillStyle = '#4DC0CA'
ctx.fillRect(0, 0, canvas.width, 0.8 * (canvas.height - this.image.height) + 10)
ctx.drawImage(this.image, this.dx, this.dy)
ctx.drawImage(this.image, this.dx + this.image.width, this.dy)
ctx.drawImage(this.image, this.dx + this.image.width * 2, this.dy)}
}
let gameBg = null
main();
// 渲染函数
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
gameBg.update();
requestAnimationFrame(render)
}
async function main() {
// 加载资源
await new SourceManager().loadImages();
// 背景
gameBg = new GameBackground()
// 渲染动画
render()
}
地面
地面的渲染需基于背景,将地面上边缘对齐基准线(即 canvas.height * 0.8),并采用与填充背景上部相同的方法填补下方空缺。同时,使用与背景相同的原理实现地面的无限向左滚动效果。

// 地面
class Land {
constructor() {
this.dx = 0;
this.dy = canvas.height * 0.8;
this.image = SourceManager.images.land;
this.render();}
update() {
this.dx -= 1.5;
if (this.dx + this.image.width <= 0) {
this.dx = 0;
}
this.render();}
render() {
ctx.fillStyle = "#DED895";
ctx.fillRect(
0,
canvas.height * 0.8 + this.image.height - 10,
canvas.width,
canvas.height * 0.2 - this.image.height + 10
);
ctx.drawImage(this.image, this.dx, this.dy);
ctx.drawImage(this.image, this.dx + this.image.width, this.dy);
ctx.drawImage(this.image, this.dx + this.image.width * 2, this.dy);}
}
let land = null
main();
// 渲染函数
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
gameBg.update();
requestAnimationFrame(render)
}
async function main() {
// 加载资源
await new SourceManager().loadImages();
// 此处省略其他元素
// 地面
land = new Land()
// 渲染动画
render()
}
管道
管道分为上下两部分:上部分紧贴屏幕顶部渲染,下部分紧贴地面(基准线)渲染。上下管道的长度需随机生成,且两部分之间的垂直间距不得小于 80(此处为自定义限制值)。管道以每 2 秒一次的频率生成,并同样采用无限向左滚动的渲染方式,逻辑与背景一致。

// 管道
class Pipe {
constructor() {
this.dx = canvas.width;
this.dy = 0;
this.upPipeHeight = (Math.random() * canvas.height * 0.8) / 2 + 30;
this.downPipeHeight = (Math.random() * canvas.height * 0.8) / 2 + 30;
if (canvas.height * 0.8 - this.upPipeHeight - this.downPipeHeight <= 80) {
console.log("///小于80了///");
this.upPipeHeight = 200;
this.downPipeHeight = 200;
}
this.downImage = SourceManager.images.pipe_down;
this.upImage = SourceManager.images.pipe_up;}
update() {
this.dx -= 1.5;// 记录管道四个点的坐标,在碰撞检测的时候使用this.upCoord = {tl: {x: this.dx,y: canvas.height * 0.8 - this.upPipeHeight,},tr: {x: this.dx + this.upImage.width,y: canvas.height * 0.8 - this.upPipeHeight,},bl: {x: this.dx,y: canvas.height * 0.8,},br: {x: this.dx + this.upImage.width,y: canvas.height * 0.8,},};this.downCoord = {bl: {x: this.dx,y: this.downPipeHeight,},br: {x: this.dx + this.downImage.width,y: this.downPipeHeight,},};
this.render();}
render() {
ctx.drawImage(
this.downImage,
0,
this.downImage.height - this.downPipeHeight,
this.downImage.width,
this.downPipeHeight,
this.dx,
this.dy,
this.downImage.width,
this.downPipeHeight
);
ctx.drawImage(
this.upImage,
0,
0,
this.upImage.width,
this.upPipeHeight,
this.dx,
canvas.height * 0.8 - this.upPipeHeight,
this.upImage.width,
this.upPipeHeight
);}
}
let pipeList = []
main();
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 此处省略其他元素渲染步骤
pipeList.forEach((item) => item.update());
requestAnimationFrame(render)
}
async function main() {
// 此处省略其他元素渲染步骤
// 管道
setInterval(() => {
pipeList.push(new Pipe());
// 清理移动过去的管道对象,一屏最多展示3组,所以这里取大于3
if (pipeList.length > 3) {
pipeList.shift();
}}, 2000);
// 渲染动画
render()
} 笨鸟
小鸟需具备飞行动画,通过不断循环渲染三张不同飞行姿态的图片来实现。同时,通过改变小鸟在 Y 轴上的坐标来模拟上升和下坠的物理效果,并通过监听屏幕点击或长按事件来控制小鸟的飞行高度。

// 小鸟
class Bird {
constructor() {
this.dx = 0;
this.dy = 0;
this.speed = 2;
this.image0 = SourceManager.images.bird0_0;
this.image1 = SourceManager.images.bird0_1;
this.image2 = SourceManager.images.bird0_2;
this.loopCount = 0;
this.control();
setInterval(() => {
if (this.loopCount === 0) {
this.loopCount = 1;
} else if (this.loopCount === 1) {
this.loopCount = 2;
} else {
this.loopCount = 0;
}
}, 200);}
// 添加控制小鸟的事件
control() {
let timer = true;
canvas.addEventListener("touchstart", (e) => {
timer = setInterval(() => {
this.dy -= this.speed;
});
e.preventDefault();
});
canvas.addEventListener("touchmove", () => {
clearInterval(timer);
});
canvas.addEventListener("touchend", () => {
clearInterval(timer);
});}
update() {
this.dy += this.speed;
// 记录小鸟四个点的坐标,在碰撞检测的时候使用
this.birdCoord = {
tl: {
x: this.dx,
y: this.dy,
},
tr: {
x: this.dx + this.image0.width,
y: this.dy,
},
bl: {
x: this.dx,
y: this.dy + this.image0.height,
},
br: {
x: this.dx + this.image0.width,
y: this.dy + this.image0.height,
},
};
this.render();}
render() {
// 渲染小鸟飞行动作
if (this.loopCount === 0) {
ctx.drawImage(this.image0, this.dx, this.dy);
} else if (this.loopCount === 1) {
ctx.drawImage(this.image1, this.dx, this.dy);
} else {
ctx.drawImage(this.image2, this.dx, this.dy);
}}
}
let bird = null
main();
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 省略其他元素渲染
bird.update();
requestAnimationFrame(render);
}
async function main() {
// 省略其他元素渲染
// 笨鸟
bird = new Bird()
// 渲染动画
render()
} 我们发现小鸟似乎是只美国鸟,显得过于自由~,这不符合游戏规则,需要对其进行限制。
碰撞检测
碰撞检测的核心原理是持续监测小鸟图形的四个顶点坐标是否落入任一管道所占的坐标区域内,或者检测小鸟图形下方的点纵坐标是否小于地面纵坐标(基准线)。若满足上述任一条件,则判定游戏结束。此前在管道和小鸟类中记录的坐标数据正是为了支持这一碰撞检测逻辑。

let gameBg = null
let land = null
let bird = null
let pipeList = []
main();
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
gameBg.update();
land.update();
bird.update();
pipeList.forEach((item) => item.update());
requestAnimationFrame(render);
// 碰撞检测-地面
if (bird.dy >= canvas.height * 0.8 - bird.image0.height + 10) {
gg();}
//碰撞检测-管道
pipeList.forEach((item) => {
if (
bird.birdCoord.bl.x >= item.upCoord.tl.x - 35 &&
bird.birdCoord.bl.x <= item.upCoord.tr.x &&
bird.birdCoord.bl.y >= item.upCoord.tl.y + 10
) {
gg();
} else if (
bird.birdCoord.tl.x >= item.downCoord.bl.x - 35 &&
bird.birdCoord.tl.x <= item.downCoord.br.x &&
bird.birdCoord.tl.y <= item.downCoord.bl.y - 10
) {
gg();
}});
}
async function main() {
// 加载资源
await new SourceManager().loadImages();
// 背景
gameBg = new GameBackground()
// 地面
land = new Land()
// 笨鸟
bird = new Bird()
// 管道
setInterval(() => {
pipeList.push(new Pipe());
// 清理移动过去的管道对象,一屏最多展示3组,所以这里取大于3
if (pipeList.length > 3) {
pipeList.shift();
}}, 2000);
// 渲染动画
render()
}
function gg() {
const ggImage = SourceManager.images.text_game_over;
ctx.drawImage(
ggImage,
canvas.width / 2 - ggImage.width / 2,
(canvas.height / 2) * 0.618);
};
效果
加入碰撞检测后,当小鸟撞击管道或地面时,系统将提示失败。本文展示了基本的核心逻辑,完整游戏的地址和源码链接见下方。

到此这篇关于JavaScript使用canvas实现flappy bird全流程详解的文章就介绍到这了,更多相关JS flappy bird内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
JS监听滚动事件:回到顶部按钮的显示与隐藏逻辑
2026-11-05 16:19:37
JavaScript怎么读写LocalStorage与SessionStorage
2026-11-05 15:40:11
anime.js 与 GSAP 对比:轻量级项目该怎么选择动画库
2026-11-05 11:24:55
flv.js 与 hls.js 在直播场景下的选型对比
2026-11-05 08:18:52
JavaScript能做什么?前端开发中的作用与应用场景解析
2026-11-04 23:17:54
