JavaScript使用canvas實(shí)現(xiàn)flappy bird全流程詳解
簡(jiǎn)介
canvas 是HTML5 提供的一種新標(biāo)簽,它可以支持 JavaScript 在上面繪畫(huà),控制每一個(gè)像素,它經(jīng)常被用來(lái)制作小游戲,接下來(lái)我將用它來(lái)模仿制作一款叫flappy bird的小游戲。flappy bird(中文名:笨鳥(niǎo)先飛)是一款由來(lái)自越南的獨(dú)立游戲開(kāi)發(fā)者Dong Nguyen所開(kāi)發(fā)的作品,于2013年5月24日上線,并在2014年2月突然暴紅。
游戲規(guī)則
玩家只需要用一根手指來(lái)操控,點(diǎn)擊或長(zhǎng)按屏幕,小鳥(niǎo)就會(huì)往上飛,不斷的點(diǎn)擊就會(huì)不斷的往高處飛。放松手指,則會(huì)快速下降。所以玩家要控制小鳥(niǎo)一直向前飛行,然后注意躲避途中高低不平的管子。小鳥(niǎo)安全飛過(guò)的距離既是得分。當(dāng)然撞上就直接掛掉,只有一條命。
游戲素材
鏈接: https://pan.baidu.com/s/1ro1273TeIhhJgCIFj4vn_g?pwd=7vqh
提取碼: 7vqh
開(kāi)始制作
初始化canvas畫(huà)布
這里主要是創(chuàng)建畫(huà)布,并調(diào)整畫(huà)布大小,畫(huà)布自適應(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)所有的資源都加載完了才能開(kāi)始游戲,所以這里需要對(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è)畫(huà)面,上右下面是空缺的,這個(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)畫(huà) 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)畫(huà) 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)畫(huà) render() }
笨鳥(niǎo)
小鳥(niǎo)要有飛行的動(dòng)作,這個(gè)通過(guò)不斷重復(fù)渲染3張小鳥(niǎo)不同飛行姿勢(shì)的圖片來(lái)實(shí)現(xiàn);還要通過(guò)改變小鳥(niǎo)的在Y軸的值來(lái)制作上升下墜的效果,并且能夠通過(guò)點(diǎn)擊或長(zhǎng)按屏幕來(lái)控制小鳥(niǎo)的飛行高度。
// 小鳥(niǎo) 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);} // 添加控制小鳥(niǎo)的事件 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; // 記錄小鳥(niǎo)四個(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() { // 渲染小鳥(niǎo)飛行動(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() { // 省略其他元素渲染 // 笨鳥(niǎo) bird = new Bird() // 渲染動(dòng)畫(huà) render() }
我們發(fā)現(xiàn)小鳥(niǎo)好像是只美國(guó)鳥(niǎo),有點(diǎn)太freedom了~,不符合我們的游戲規(guī)則,要想辦法控制一下。
碰撞檢測(cè)
碰撞檢測(cè)的原理就是不斷檢測(cè)小鳥(niǎo)圖四個(gè)頂點(diǎn)坐標(biāo)是否在任一管道所占的坐標(biāo)區(qū)域內(nèi)或小鳥(niǎo)圖下方的點(diǎn)縱坐標(biāo)小于地面縱坐標(biāo)(基準(zhǔn)線),在就結(jié)束游戲。上面管道和小鳥(niǎo)類中記錄的坐標(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() // 笨鳥(niǎo) bird = new Bird() // 管道 setInterval(() => { pipeList.push(new Pipe()); // 清理移動(dòng)過(guò)去的管道對(duì)象,一屏最多展示3組,所以這里取大于3 if (pipeList.length > 3) { pipeList.shift(); }}, 2000); // 渲染動(dòng)畫(huà) 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è)后,小鳥(niǎo)碰到管道或地面就會(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)支付寶畫(huà)年兔游戲
- 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)畫(huà)倒計(jì)時(shí)
- JS+Canvas實(shí)現(xiàn)滿屏愛(ài)心和文字動(dòng)畫(huà)的制作
相關(guān)文章
Nuxt.js中PC與移動(dòng)端間自動(dòng)識(shí)別跳轉(zhuǎn)
本文主要介紹了Nuxt.js中PC與移動(dòng)端間自動(dòng)識(shí)別跳轉(zhuǎn),文中根據(jù)實(shí)例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03JavaScript中防抖和節(jié)流的實(shí)戰(zhàn)應(yīng)用記錄
防抖與節(jié)流都是用來(lái)限制用戶頻發(fā)觸發(fā)事件的機(jī)制,下面這篇文章主要給大家介紹了關(guān)于JavaScript中防抖和節(jié)流的實(shí)戰(zhàn)應(yīng)用,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04JavaScript版DateAdd和DateDiff函數(shù)代碼
VBScript中有兩個(gè)非常好用的日期操作函數(shù):DateAdd用來(lái)給日期添加指定時(shí)間間隔,DateDiff用來(lái)返回兩個(gè)日期的時(shí)間間隔??上У氖荍avaScript沒(méi)有,不過(guò)我們可以寫(xiě)一個(gè)函數(shù)來(lái)實(shí)現(xiàn),一樣的,呵呵2012-03-03javascript實(shí)現(xiàn)跟隨鼠標(biāo)移動(dòng)的圖片
這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)跟隨鼠標(biāo)移動(dòng)的圖片,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09echarts數(shù)據(jù)可視化實(shí)現(xiàn)多個(gè)柱狀堆疊圖頂部顯示總數(shù)示例
這篇文章主要為大家介紹了echarts實(shí)現(xiàn)多個(gè)柱狀堆疊圖頂部顯示總數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07javascript-簡(jiǎn)單的日歷實(shí)現(xiàn)及Date對(duì)象語(yǔ)法介紹(附圖)
主要是對(duì)Date對(duì)象的使用,首先回憶一下Date對(duì)象的參數(shù)及方法,代碼如下,感興趣的朋友可以參考下哈2013-05-05如何將php數(shù)組或者對(duì)象傳遞給javascript
這篇文章主要介紹了將php數(shù)組或者對(duì)象傳遞給javascript的方法,需要的朋友可以參考下2014-03-03js 動(dòng)態(tài)添加元素(div、li、img等)及設(shè)置屬性的方法
下面小編就為大家?guī)?lái)一篇js 動(dòng)態(tài)添加元素(div、li、img等)及設(shè)置屬性的方法。小編覺(jué)得聽(tīng)不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-07-07微信小程序?qū)崿F(xiàn)頂部固定 底部分頁(yè)滾動(dòng)效果
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)頂部固定底部分頁(yè)滾動(dòng)效果,本文大概給大家分享三種解決方案,每種方案給大家詳細(xì)剖析通過(guò)代碼解析哪種方案更適合,感興趣的朋友跟隨小編一起看看吧2022-10-10IE的事件傳遞-event.cancelBubble示例介紹
關(guān)于event.cancelBubble,Bubble就是一個(gè)事件可以從子節(jié)點(diǎn)向父節(jié)點(diǎn)傳遞,下面有個(gè)不錯(cuò)的示例,大家可以感受下2014-01-01