JavaScript使用canvas實現flappy bird全流程詳解
簡介
canvas 是HTML5 提供的一種新標簽,它可以支持 JavaScript 在上面繪畫,控制每一個像素,它經常被用來制作小游戲,接下來我將用它來模仿制作一款叫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)建畫布,并調整畫布大小,畫布自適應屏幕大小。
<!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>
加載資源
圖片等資源的加載是異步的,只有當所有的資源都加載完了才能開始游戲,所以這里需要對圖片等資源進行統一的監(jiān)控和管理。 將圖片資源用json進行描述,通過fetch進行統一加載。
// 資源管理器 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ā)現背景圖并不能充滿整個畫面,上右下面是空缺的,這個時候需要使用小手段填充上,這里就用矩形對上部進行填充。接下來,需要讓背景有一種無限向左移動的效果,就要并排繪制3張背景圖片,這樣在渲染的時候,當背景向左移動的距離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(我自己限制的);管道渲染速度為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張小鳥不同飛行姿勢的圖片來實現;還要通過改變小鳥的在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ā)現小鳥好像是只美國鳥,有點太freedom了~,不符合我們的游戲規(guī)則,要想辦法控制一下。
碰撞檢測
碰撞檢測的原理就是不斷檢測小鳥圖四個頂點坐標是否在任一管道所占的坐標區(qū)域內或小鳥圖下方的點縱坐標小于地面縱坐標(基準線),在就結束游戲。上面管道和小鳥類中記錄的坐標就是為了實現碰撞檢測的。
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內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JavaScript中防抖和節(jié)流的實戰(zhàn)應用記錄
防抖與節(jié)流都是用來限制用戶頻發(fā)觸發(fā)事件的機制,下面這篇文章主要給大家介紹了關于JavaScript中防抖和節(jié)流的實戰(zhàn)應用,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-04-04JavaScript版DateAdd和DateDiff函數代碼
VBScript中有兩個非常好用的日期操作函數:DateAdd用來給日期添加指定時間間隔,DateDiff用來返回兩個日期的時間間隔??上У氖荍avaScript沒有,不過我們可以寫一個函數來實現,一樣的,呵呵2012-03-03javascript-簡單的日歷實現及Date對象語法介紹(附圖)
主要是對Date對象的使用,首先回憶一下Date對象的參數及方法,代碼如下,感興趣的朋友可以參考下哈2013-05-05js 動態(tài)添加元素(div、li、img等)及設置屬性的方法
下面小編就為大家?guī)硪黄猨s 動態(tài)添加元素(div、li、img等)及設置屬性的方法。小編覺得聽不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-07-07IE的事件傳遞-event.cancelBubble示例介紹
關于event.cancelBubble,Bubble就是一個事件可以從子節(jié)點向父節(jié)點傳遞,下面有個不錯的示例,大家可以感受下2014-01-01