JavaScript使用canvas實現(xiàn)flappy bird全流程詳解
簡介
canvas 是HTML5 提供的一種新標簽,它可以支持 JavaScript 在上面繪畫,控制每一個像素,它經(jīng)常被用來制作小游戲,接下來我將用它來模仿制作一款叫flappy bird的小游戲。flappy bird(中文名:笨鳥先飛)是一款由來自越南的獨立游戲開發(fā)者Dong Nguyen所開發(fā)的作品,于2013年5月24日上線,并在2014年2月突然暴紅。
游戲規(guī)則
玩家只需要用一根手指來操控,點擊或長按屏幕,小鳥就會往上飛,不斷的點擊就會不斷的往高處飛。放松手指,則會快速下降。所以玩家要控制小鳥一直向前飛行,然后注意躲避途中高低不平的管子。小鳥安全飛過的距離既是得分。當然撞上就直接掛掉,只有一條命。
游戲素材
鏈接: https://pan.baidu.com/s/1ro1273TeIhhJgCIFj4vn_g?pwd=7vqh
提取碼: 7vqh
開始制作
初始化canvas畫布
這里主要是創(chuàng)建畫布,并調(diào)整畫布大小,畫布自適應屏幕大小。
<!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>
加載資源
圖片等資源的加載是異步的,只有當所有的資源都加載完了才能開始游戲,所以這里需要對圖片等資源進行統(tǒng)一的監(jiān)控和管理。 將圖片資源用json進行描述,通過fetch進行統(tǒng)一加載。

// 資源管理器
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();
背景
為了適應不同尺寸的屏幕尺寸和管子能正確渲染到對應的位置,不能將背景圖片拉伸,要定一個基準線固定背景圖片所在屏幕中的位置。我們發(fā)現(xiàn)背景圖并不能充滿整個畫面,上右下面是空缺的,這個時候需要使用小手段填充上,這里就用矩形對上部進行填充。接下來,需要讓背景有一種無限向左移動的效果,就要并排繪制3張背景圖片,這樣在渲染的時候,當背景向左移動的距離dx等于一張背景圖的寬度時,將dx=0,這樣就實現(xiàn)了無限向左移動的效果,類似于輪播圖。


// 背景
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();
// 渲染函數(shù)
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),并把下面空缺的部分通過和填補背景上半部分一致的方式填上。同時使用與背景無限向左移動一樣的方法實現(xiàn)地面的無限向左移動。

// 地面
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();
// 渲染函數(shù)
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(我自己限制的);管道渲染速度為2s一次,并且也需要無限向左移動,這個效果和背景同理。

// 管道
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("http:///小于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()
} 笨鳥
小鳥要有飛行的動作,這個通過不斷重復渲染3張小鳥不同飛行姿勢的圖片來實現(xiàn);還要通過改變小鳥的在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()
} 我們發(fā)現(xiàn)小鳥好像是只美國鳥,有點太freedom了~,不符合我們的游戲規(guī)則,要想辦法控制一下。
碰撞檢測
碰撞檢測的原理就是不斷檢測小鳥圖四個頂點坐標是否在任一管道所占的坐標區(qū)域內(nèi)或小鳥圖下方的點縱坐標小于地面縱坐標(基準線),在就結束游戲。上面管道和小鳥類中記錄的坐標就是為了實現(xiàn)碰撞檢測的。

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實現(xiàn)flappy bird全流程詳解的文章就介紹到這了,更多相關JS flappy bird內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JavaScript跳出循環(huán)的三種方法(break, return, continue)
這篇文章主要介紹了JavaScript跳出循環(huán)的三種方法(break, return, continue),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07
js實現(xiàn)動態(tài)改變radio狀態(tài)的方法
下面小編就為大家分享一篇js實現(xiàn)動態(tài)改變radio狀態(tài)的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02
JavaScript實現(xiàn)封裝一個快速生成目錄樹的全局腳本
目錄樹可以很好的介紹項目中各文件目錄的用途,幫助讀者了解整個項目結構。本文就來用JavaScript封裝一個快速生成目錄樹的全局腳本,希望對大家有所幫助2023-03-03
javascript 構造函數(shù)強制調(diào)用經(jīng)驗總結
本文將介紹javascript構造函數(shù)調(diào)用方面的案例應用,需要了解的朋友可以參考下2012-12-12

