JavaScript使用canvas實(shí)現(xiàn)flappy bird全流程詳解
簡(jiǎn)介
canvas 是HTML5 提供的一種新標(biāo)簽,它可以支持 JavaScript 在上面繪畫,控制每一個(gè)像素,它經(jīng)常被用來(lái)制作小游戲,接下來(lái)我將用它來(lái)模仿制作一款叫flappy bird的小游戲。flappy bird(中文名:笨鳥先飛)是一款由來(lái)自越南的獨(dú)立游戲開發(fā)者Dong Nguyen所開發(fā)的作品,于2013年5月24日上線,并在2014年2月突然暴紅。
游戲規(guī)則
玩家只需要用一根手指來(lái)操控,點(diǎn)擊或長(zhǎng)按屏幕,小鳥就會(huì)往上飛,不斷的點(diǎn)擊就會(huì)不斷的往高處飛。放松手指,則會(huì)快速下降。所以玩家要控制小鳥一直向前飛行,然后注意躲避途中高低不平的管子。小鳥安全飛過(guò)的距離既是得分。當(dāng)然撞上就直接掛掉,只有一條命。
游戲素材
鏈接: https://pan.baidu.com/s/1ro1273TeIhhJgCIFj4vn_g?pwd=7vqh
提取碼: 7vqh
開始制作
初始化canvas畫布
這里主要是創(chuàng)建畫布,并調(diào)整畫布大小,畫布自適應(yīng)屏幕大小。
<!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">
當(dāng)前瀏覽器不支持canvas,請(qǐng)更換瀏覽器查看。
</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>
加載資源
圖片等資源的加載是異步的,只有當(dāng)所有的資源都加載完了才能開始游戲,所以這里需要對(duì)圖片等資源進(jìn)行統(tǒng)一的監(jiān)控和管理。 將圖片資源用json進(jìn)行描述,通過(guò)fetch進(jìn)行統(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();
背景
為了適應(yīng)不同尺寸的屏幕尺寸和管子能正確渲染到對(duì)應(yīng)的位置,不能將背景圖片拉伸,要定一個(gè)基準(zhǔn)線固定背景圖片所在屏幕中的位置。我們發(fā)現(xiàn)背景圖并不能充滿整個(gè)畫面,上右下面是空缺的,這個(gè)時(shí)候需要使用小手段填充上,這里就用矩形對(duì)上部進(jìn)行填充。接下來(lái),需要讓背景有一種無(wú)限向左移動(dòng)的效果,就要并排繪制3張背景圖片,這樣在渲染的時(shí)候,當(dāng)背景向左移動(dòng)的距離dx等于一張背景圖的寬度時(shí),將dx=0,這樣就實(shí)現(xiàn)了無(wú)限向左移動(dòng)的效果,類似于輪播圖。


// 背景
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()
// 渲染動(dòng)畫
render()
}
地面
地面要在背景的基礎(chǔ)上將地面圖上邊對(duì)齊基準(zhǔn)線(canvas.height * 0.8),并把下面空缺的部分通過(guò)和填補(bǔ)背景上半部分一致的方式填上。同時(shí)使用與背景無(wú)限向左移動(dòng)一樣的方法實(shí)現(xiàn)地面的無(wú)限向左移動(dòng)。

// 地面
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()
// 渲染動(dòng)畫
render()
}
管道
管道有上下兩部分,上部分管道需要貼著屏幕的頂部渲染,下部分要貼著地面也就是基準(zhǔn)線渲染,上下兩部分的管道長(zhǎng)度要隨機(jī)生成,且兩部分之間的距離不能小于80(我自己限制的);管道渲染速度為2s一次,并且也需要無(wú)限向左移動(dòng),這個(gè)效果和背景同理。

// 管道
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;// 記錄管道四個(gè)點(diǎn)的坐標(biāo),在碰撞檢測(cè)的時(shí)候使用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());
// 清理移動(dòng)過(guò)去的管道對(duì)象,一屏最多展示3組,所以這里取大于3
if (pipeList.length > 3) {
pipeList.shift();
}}, 2000);
// 渲染動(dòng)畫
render()
} 笨鳥
小鳥要有飛行的動(dòng)作,這個(gè)通過(guò)不斷重復(fù)渲染3張小鳥不同飛行姿勢(shì)的圖片來(lái)實(shí)現(xiàn);還要通過(guò)改變小鳥的在Y軸的值來(lái)制作上升下墜的效果,并且能夠通過(guò)點(diǎn)擊或長(zhǎng)按屏幕來(lái)控制小鳥的飛行高度。

// 小鳥
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;
// 記錄小鳥四個(gè)點(diǎn)的坐標(biāo),在碰撞檢測(cè)的時(shí)候使用
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() {
// 渲染小鳥飛行動(dòng)作
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()
// 渲染動(dòng)畫
render()
} 我們發(fā)現(xiàn)小鳥好像是只美國(guó)鳥,有點(diǎn)太freedom了~,不符合我們的游戲規(guī)則,要想辦法控制一下。
碰撞檢測(cè)
碰撞檢測(cè)的原理就是不斷檢測(cè)小鳥圖四個(gè)頂點(diǎn)坐標(biāo)是否在任一管道所占的坐標(biāo)區(qū)域內(nèi)或小鳥圖下方的點(diǎn)縱坐標(biāo)小于地面縱坐標(biāo)(基準(zhǔn)線),在就結(jié)束游戲。上面管道和小鳥類中記錄的坐標(biāo)就是為了實(shí)現(xiàn)碰撞檢測(cè)的。

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);
// 碰撞檢測(cè)-地面
if (bird.dy >= canvas.height * 0.8 - bird.image0.height + 10) {
gg();}
//碰撞檢測(cè)-管道
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());
// 清理移動(dòng)過(guò)去的管道對(duì)象,一屏最多展示3組,所以這里取大于3
if (pipeList.length > 3) {
pipeList.shift();
}}, 2000);
// 渲染動(dòng)畫
render()
}
function gg() {
const ggImage = SourceManager.images.text_game_over;
ctx.drawImage(
ggImage,
canvas.width / 2 - ggImage.width / 2,
(canvas.height / 2) * 0.618);
};
效果
增加碰撞檢測(cè)后,小鳥碰到管道或地面就會(huì)提示失敗。 此篇展示了基本的核心邏輯,完整游戲地址和源碼在下方鏈接。

到此這篇關(guān)于JavaScript使用canvas實(shí)現(xiàn)flappy bird全流程詳解的文章就介紹到這了,更多相關(guān)JS flappy bird內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- JS技巧Canvas性能優(yōu)化臟矩形渲染實(shí)例詳解
- JavaScript+Canvas模擬實(shí)現(xiàn)支付寶畫年兔游戲
- JavaScript+Canvas實(shí)現(xiàn)文字粒子流特效
- JavaScript?Canvas實(shí)現(xiàn)兼容IE的兔子發(fā)射爆破動(dòng)圖特效
- 利用JS+Canvas給南方的冬季來(lái)一場(chǎng)紛紛揚(yáng)揚(yáng)的大雪
- JavaScript利用Canvas實(shí)現(xiàn)粒子動(dòng)畫倒計(jì)時(shí)
- JS+Canvas實(shí)現(xiàn)滿屏愛心和文字動(dòng)畫的制作
相關(guān)文章
JavaScript跳出循環(huán)的三種方法(break, return, continue)
這篇文章主要介紹了JavaScript跳出循環(huán)的三種方法(break, return, continue),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07
layui遞歸實(shí)現(xiàn)動(dòng)態(tài)左側(cè)菜單
這篇文章主要為大家詳細(xì)介紹了layui遞歸實(shí)現(xiàn)動(dòng)態(tài)左側(cè)菜單,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07
js實(shí)現(xiàn)動(dòng)態(tài)改變r(jià)adio狀態(tài)的方法
下面小編就為大家分享一篇js實(shí)現(xiàn)動(dòng)態(tài)改變r(jià)adio狀態(tài)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
微信小程序?qū)崿F(xiàn)tab組件切換動(dòng)畫
tab相對(duì)而言用的還是比較多的,但是用起來(lái)并沒有難,下面這篇文章主要給大家介紹了關(guān)于微信小程序全局配置之tab的相關(guān)資料,文中通過(guò)圖文以及示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10
JavaScript實(shí)現(xiàn)封裝一個(gè)快速生成目錄樹的全局腳本
目錄樹可以很好的介紹項(xiàng)目中各文件目錄的用途,幫助讀者了解整個(gè)項(xiàng)目結(jié)構(gòu)。本文就來(lái)用JavaScript封裝一個(gè)快速生成目錄樹的全局腳本,希望對(duì)大家有所幫助2023-03-03
javascript 構(gòu)造函數(shù)強(qiáng)制調(diào)用經(jīng)驗(yàn)總結(jié)
本文將介紹javascript構(gòu)造函數(shù)調(diào)用方面的案例應(yīng)用,需要了解的朋友可以參考下2012-12-12
Javascript作用域(局部和全局作用域)詳細(xì)介紹
作用域是當(dāng)前的執(zhí)行上下文,值和表達(dá)式在其中“可見”或可被訪問(wèn),本文主要介紹了Javascript局部作用域和全局作用域,文中有詳細(xì)的代碼示例,具有一定的參考價(jià)值,需要的朋友可以借鑒一下2023-06-06

