深入解析canvas画布游戏机制
canvas使用了一个特别特殊的模式,上屏的元素,立刻被像素化。也就是说,上屏幕的元素,你将得不到这个“对象”的引用。比如,一个圆形画到了ctx上面,此时就是一堆像素点,不是一个整体的对象了,你没有任何变量能够得到这个圆形,改变这个圆形的x、y。也就是说,这种“改变”的思路在canvas中是行不通的。
所以canvas的画图的原理就是:
清屏 → 重绘 → 清屏 → 重绘 → 清屏 → 重绘 → 清屏 → 重绘 →清屏 → 重绘 → 清屏 → 重绘 →清屏 → 重绘 → 清屏 → 重绘 →清屏 → 重绘 → 清屏 → 重绘 →清屏 → 重绘 → 清屏 → 重绘 →清屏 → 重绘 → 清屏 → 重绘 →……
下面是我写的flappBird,采用了中介者模式参考了白鹭引擎中的场景管理
首先一个游戏类负责资源的加载与渲染,以及定时器的添加
// 中介者类 class Game { // 构造函数 constructor(dataJson) { // 获取画布 this.canvas = document.getElementById(dataJson.canvasId); this.ctx = this.canvas.getContext("2d"); this.jsonUrl = dataJson.jsonUrl; // 数据对象 this.R = {}; // 定时器 this.timer = null; // 帧数 this.fno = 0; // 管子数组 this.pipeArr = []; this.init(); // 游戏logo坐标 this.logoY = -48; // 开始按钮坐标 this.buttonY = this.canvas.height; // 提示板透明度 this.tutorialOpacity = 1; // 提示板闪烁间隔 this.tutorialNum = 0.1; // 作者介绍从上而下 this.autorY = -330; // 坠地动画序列 this.boomNum = 1; // 碰撞闪屏 this.screenFlash = 1; // 闪屏是否开启 this.flashLook = true; // 分数 this.score = 0; // 资源加载完毕 this.allResourceLoadEnd(() => { // 开始按钮 this.button_play = this.R['button_play']; // 游戏logo this.logo = this.R['logo']; // 游戏提示板 this.tutorial = this.R['tutorial']; // 作者介绍 this.autor = this.R['autor']; // 坠地动画资源 this.boomImg; // 结束标志 this.text_game_over = this.R["text_game_over"]; // 绑定场景管理器 this.sceneManager = new SceneManager(); // 开始游戏 this.start(); }); }; // 初始化画布 init() { let windowX = document.documentElement.clientWidth; let windowY = document.documentElement.clientHeight; // 设置画布最小宽度 windowX = windowX < 320 ? 320 : windowX; windowX = windowX > 414 ? 414 : windowX; windowY = windowY < 500 ? 500 : windowY; windowY = windowY > 812 ? 812 : windowY; this.canvas.width = windowX; this.canvas.height = windowY; }; // 资源管理器函数 allResourceLoadEnd(callback) { let that = this; // 图片加载计数 let imageLoadNum = 0; // 异步加载资源 let xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { // 获取数据数组 let imageArr = JSON.parse(xhr.responseText).images; // 遍历对象 imageArr.forEach(value => { that.R[value.name] = new Image(); that.R[value.name].src = value.url; // 所有资源加载完毕开始游戏 that.R[value.name].onload = function() { imageLoadNum++ // 渲染进度 that.ctx.clearRect(0, 0, that.canvas.width, that.canvas.height); that.ctx.font = "20px Microsoft YaHei" that.ctx.textAlign = "center" // 资源加载提醒 that.ctx.fillText("正在加载资源" + imageLoadNum + "/" + imageArr.length + "请稍后...", that.canvas.width / 2, that.canvas.height * (1 - 0.618)) if (imageLoadNum == imageArr.length) { callback && callback() } } }); } } } xhr.open("get", this.jsonUrl); xhr.send(null); }; // 分数渲染 scoreRender() { // 渲染分数 let scoreStr = this.score.toString(); for (let i = 0; i < scoreStr.length; i++) { this.ctx.drawImage(this.R['shuzi' + scoreStr[i]], this.canvas.width / 2 - scoreStr.length / 2 * 28 + i * 28, this.canvas.height * 0.2); } }; // 游戏开始函数 start() { this.timer = setInterval(() => { // 清屏 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 渲染场景 this.sceneManager.update(); this.sceneManager.render(); // 帧数自增 this.fno++; // 打印帧数 this.ctx.font = "14px Arial"; this.ctx.fillStyle = "#333" this.ctx.textAlign = "left"; this.ctx.fillText("FNO:" + this.fno, 10, 20); // 打印场景编号 this.ctx.font = "550 14px Microsoft YaHei"; this.ctx.fontWeight = "700"; this.ctx.fillText("场景号:" + this.sceneManager.sceneNumber, 10, 40) }, 10) } }
JSON文件
{ "images": [ { "name": "bg_day", "url": "images/bg_day.png" }, { "name": "bird0_0", "url": "images/bird0_0.png" }, { "name": "bird0_1", "url": "images/bird0_1.png" }, { "name": "bird0_2", "url": "images/bird0_2.png" }, { "name": "bird1_0", "url": "images/bird1_0.png" }, { "name": "bird1_1", "url": "images/bird1_1.png" }, { "name": "bird1_2", "url": "images/bird1_2.png" }, { "name": "bird2_0", "url": "images/bird2_0.png" }, { "name": "bird2_1", "url": "images/bird2_1.png" }, { "name": "bird2_2", "url": "images/bird2_2.png" }, { "name": "land", "url": "images/land.png" }, { "name": "pipe_down", "url": "images/pipe_down.png" }, { "name": "pipe_up", "url": "images/pipe_up.png" }, { "name": "shuzi0", "url": "images/font_048.png" }, { "name": "shuzi1", "url": "images/font_049.png" }, { "name": "shuzi2", "url": "images/font_050.png" }, { "name": "shuzi3", "url": "images/font_051.png" }, { "name": "shuzi4", "url": "images/font_052.png" }, { "name": "shuzi5", "url": "images/font_053.png" }, { "name": "shuzi6", "url": "images/font_054.png" }, { "name": "shuzi7", "url": "images/font_055.png" }, { "name": "shuzi8", "url": "images/font_056.png" }, { "name": "shuzi9", "url": "images/font_057.png" }, { "name": "logo", "url": "images/title.png" }, { "name": "button_play", "url": "images/button_play.png" }, { "name": "tutorial", "url": "images/tutorial.png" }, { "name": "b1", "url": "images/b1.png" }, { "name": "b2", "url": "images/b2.png" }, { "name": "b3", "url": "images/b3.png" }, { "name": "b4", "url": "images/b4.png" }, { "name": "b5", "url": "images/b5.png" }, { "name": "b6", "url": "images/b6.png" }, { "name": "b7", "url": "images/b7.png" }, { "name": "b8", "url": "images/b8.png" }, { "name": "b9", "url": "images/b9.png" }, { "name": "b10", "url": "images/b10.png" }, { "name": "b11", "url": "images/b11.png" }, { "name": "b12", "url": "images/b12.png" }, { "name": "b13", "url": "images/b13.png" }, { "name": "b14", "url": "images/b14.png" }, { "name": "b15", "url": "images/b15.png" }, { "name": "b16", "url": "images/b16.png" }, { "name": "text_game_over", "url": "images/text_game_over.png" }, { "name": "autor", "url": "images/autor.png" } ] }
场景管理类负责各个场景下的渲染以及更新指向
// 场景管理器类game class SceneManager { // 构造函数 constructor() { // 绑定天空个背景 game.backGround = new BackGround(); // 绑定大地背景 game.land = new Land(); // 绑定小鸟 game.bird = new Bird(); // 场景标识符 this.sceneNumber = 1; this.bindEvenet(); }; // 更新函数 update() { switch (this.sceneNumber) { case 1: // 更新logo和开始按钮坐标 game.logoY += 10; game.buttonY -= 10; // logo距离天空200 if (game.logoY > 120) { game.logoY = 120; } // 按钮距离大地140 if (game.buttonY < game.land.y - 150) { game.buttonY = game.land.y - 150 } break; case 2: // 煽动翅膀 game.bird.wingUpdate(); // 背景板闪烁 if (game.fno % 3 == 0) { if (game.tutorialOpacity < 0.1 || game.tutorialOpacity > 0.9) { game.tutorialNum *= -1; } game.tutorialOpacity += game.tutorialNum; } break; case 3: // 煽动翅膀 game.bird.wingUpdate(); // 作者介绍 game.autorY += 10; if (game.autorY > 118) { game.autorY = 118 } break; case 4: // 更新小鸟 game.bird.update(); game.backGround.update(); game.land.update(); // 每150帧创建管子 game.fno % 150 == 0 && new Pipe(); // 遍历管子数组更新 game.pipeArr.forEach(ele => { ele.update(); }); break; case 5: if (game.bird.y > game.land.y) { game.bird.isDropFloor = true; } // 碰撞后小鸟失去能量 game.bird.hasEnergy = false; // 闪屏更新 if (game.flashLook) { game.screenFlash -= 0.1; } if (game.screenFlash < 0) { game.flashLook = false; game.screenFlash = 1; } // 没落地更新小鸟 if (!game.bird.isDropFloor) { // 更新小鸟 game.fno % 3 == 0 && (game.bird.birdFon++); game.bird.y += game.bird.downSpeed * game.bird.birdFon; } else { game.fno % 3 == 0 && (game.boomNum++); // 坠地动画结束后进入场景6 if (game.boomNum >= 16) { this.enter(6); }; game.boomImg = game.R["b" + game.boomNum]; } break; case 6: } } render() { switch (this.sceneNumber) { case 1: // 场景一静态天空大地 game.backGround.render(); game.land.render(); // 渲染小鸟 game.bird.render(); // 渲染logo和button game.ctx.drawImage(game.logo, game.canvas.width / 2 - 89, game.logoY); game.ctx.drawImage(game.button_play, game.canvas.width / 2 - 58, game.buttonY); break case 2: // 场景二静态天空大地 game.backGround.render(); game.land.render(); // 渲染小鸟 game.bird.render(); // 渲染提示板 game.ctx.save(); game.ctx.globalAlpha = game.tutorialOpacity game.ctx.drawImage(game.tutorial, game.canvas.width / 2 - 57, game.land.y - 180); game.ctx.restore(); break; case 3: // 场景三静态天空大地 game.backGround.render(); game.land.render(); // 渲染小鸟 game.bird.render(); game.ctx.drawImage(game.autor, game.canvas.width / 2 - 175, game.autorY); break; case 4: // 场景四静态天空大地 game.backGround.render(); game.land.render(); // 遍历管子数组渲染 game.pipeArr.forEach(ele => { ele.render(); }); // 渲染小鸟 game.bird.render(); // 渲染分数 game.scoreRender(); break; case 5: game.ctx.globalAlpha = game.screenFlash; // 场景五静态天空大地 game.backGround.render(); game.land.render(); // 遍历管子数组渲染 game.pipeArr.forEach(ele => { ele.render(); }); // 坠地动画 if (!game.bird.isDropFloor) { // 渲染小鸟 game.bird.y <= game.land.y && game.bird.render(); } else { game.boomImg && game.ctx.drawImage(game.boomImg, game.bird.x - 93, game.land.y - 350) } // 渲染分数 game.scoreRender(); break; case 6: // 场景五静态天空大地 game.backGround.render(); game.land.render(); game.ctx.drawImage(game.text_game_over, game.canvas.width / 2 - 102, game.canvas.height * (1 - 0.618)) // 渲染分数 game.scoreRender(); break; } } enter(num) { this.sceneNumber = num; switch (this.sceneNumber) { case 1: // 每次进入场景一瞬间还原位置 game.fno = 0; game.logoY = -48; game.buttonY = game.canvas.height; game.bird = new Bird(); game.score = 0; break; case 2: // 场景二位置重设 game.bird.y = 150; break; case 3: // 场景三位置重设 game.autorY = -330; break; case 4: game.pipeArr = []; game.bird.birdFon = 0; break; case 5: game.boomNum = 0; game.screenFlash = 1; game.flashLook = true; game.bird.isDropFloor = false; } } bindEvenet() { // document.onclick = (event) => { // clickHandler.call(this, event.clientX, event.clientY); // }; document.addEventListener("touchstart", (event) => { clickHandler.call(this, event.touches[0].clientX, event.touches[0].clientY); }) function clickHandler(mousex, mousey) { switch (this.sceneNumber) { case 1: if (mousey > game.buttonY && mousey < game.buttonY + 70 && mousex > game.canvas.width / 2 - 58 && mousex < game.canvas.width / 2 + 58) { this.enter(2) } break; case 2: this.enter(3); break; case 3: this.enter(4) break; case 4: game.bird.fly(); break; case 6: this.enter(1) break; } } } }
接着就是各个演员类,例:小鸟,管子,天空,大地
// 天空背景类 class BackGround { constructor() { this.bg_day = game.R['bg_day']; this.x = 0; this.y = game.canvas.height * 0.73 - 412; this.h = 512; this.w = 288; this.speed = 1.8; }; // 更新函数 update() { this.x -= this.speed; if (this.x < -this.w) { this.x = 0; } }; // 渲染函数 render() { game.ctx.fillStyle = "#4ec0ca"; game.ctx.fillRect(0, 0, game.canvas.width, this.y); game.ctx.fillStyle = "#5ee270"; game.ctx.fillRect(0, this.y + this.h - 1, game.canvas.width, game.canvas.height - this.y - this.h); game.ctx.drawImage(this.bg_day, this.x, this.y); game.ctx.drawImage(this.bg_day, this.x + this.w, this.y); game.ctx.drawImage(this.bg_day, this.x + this.w * 2 - this.speed, this.y); } }
class Bird { constructor() { // 鸟颜色随机 this.color = parseInt(Math.random() * 3); // 小鸟翅膀状态 this.birdWing = 0; // 小鸟资源 this.birdArr = [ game.R["bird" + this.color + "_0"], game.R["bird" + this.color + "_1"], game.R["bird" + this.color + "_2"], ]; // 小鸟坐标 this.x = game.canvas.width / 2; this.y = game.canvas.height * (1 - 0.618); // 小鸟的旋转角度 this.d = 0; // 小鸟每帧旋转角度 this.framD = 0.023; // 小鸟帧 this.birdFon = 0; // 下落速度 this.downSpeed = 0.53; // 上升速度 this.upSpeed = 0.18; // 是否有能量 this.hasEnergy = false; // 能量数量 this.energyNum = 16; // 是否落地 this.isDropFloor = false; }; // 更新 update() { // 小鸟翅膀切换 this.wingUpdate(); // 小鸟帧数自增 game.fno % 4 == 0 && this.birdFon++; // 没有能量下落加速度 if (!this.hasEnergy) { this.y += this.downSpeed * this.birdFon; // 旋转 this.d += this.framD; } else { // 有能量上升加速度 this.y += this.upSpeed * parseInt((this.birdFon - this.energyNum)); // 上升时旋转放缓 game.fno % 4 == 0 && (this.d += this.framD); // 当能量耗尽关掉能量开关 if (this.birdFon > this.energyNum) { this.hasEnergy = false; this.birdFon = 0; } } // 触碰大地结束 if (this.y > game.land.y) { this.isDropFloor = true; game.sceneManager.enter(5); } // 不能飞过天空 if (this.y < 0) { this.y = 0; } // 计算小鸟上右下左位置 this.T = this.y - 12; this.R = this.x + 17; this.B = this.y + 12; this.L = this.x - 17; }; // 渲染 render() { game.ctx.save(); game.ctx.translate(this.x, this.y); game.ctx.rotate(this.d); game.ctx.drawImage(this.birdArr[this.birdWing], -24, -24) game.ctx.restore(); }; // 翅膀更新 wingUpdate() { game.fno % 8 == 0 && this.birdWing++; if (this.birdWing > 2) { this.birdWing = 0; } }; // 小鸟飞翔 fly() { // 点击屏幕时赋予小鸟能量 // 赋予能量 this.hasEnergy = true; // 重新计算帧数 this.birdFon = 0; this.d = -0.6 }; }
class Land { constructor() { this.land = game.R['land']; this.x = 0; this.y = game.canvas.height * 0.73; this.w = 336; this.h = 112; this.speed = 3; }; // 更新函数 update() { this.x -= this.speed; if (this.x < -this.w) { this.x = 0; } }; // 渲染函数 render() { game.ctx.fillStyle = "#ded895"; game.ctx.fillRect(0, this.y + this.h - 1, game.canvas.width, game.canvas.height - this.y - 112); game.ctx.drawImage(this.land, this.x, this.y); game.ctx.drawImage(this.land, this.x + this.w, this.y); game.ctx.drawImage(this.land, this.x + this.w * 2, this.y); } }
class Pipe { constructor() { this.pipe_down = game.R["pipe_down"]; this.pipe_up = game.R["pipe_up"]; this.w = 52; this.allH = 320; this.x = game.canvas.width; this.speed = 3; // 管子间隔 this.spacing = 160; // 上管子高度随机 this.height1 = parseInt(Math.random() * 121) + 100; console.log(this.height1) // 下管子高度自适应 this.height2 = game.land.y - this.height1 - this.spacing; game.pipeArr.push(this); // 小鸟是否越过当前管子 this.leapOver = false; }; // 更新 update() { this.x -= this.speed; if (this.x <= -52) { this.removeSelf(); } // 碰撞检测 if (game.bird.R > this.x && game.bird.L < this.x + 52) { if (game.bird.T < this.height1 || game.bird.B > this.height1 + this.spacing) { // 死亡进入场景5 game.sceneManager.enter(5) } } // 小鸟是否跃过当前管子 if (!this.leapOver) { if (game.bird.L > this.x + 52) { game.score++; this.leapOver = true; } } }; // 渲染 render() { game.ctx.drawImage(this.pipe_down, 0, this.allH - this.height1, this.w, this.height1, this.x, 0, this.w, this.height1); game.ctx.drawImage(this.pipe_up, 0, 0, this.w, this.height2, this.x, this.height1 + this.spacing, this.w, this.height2) }; // 删除自己 removeSelf() { game.pipeArr.forEach((ele, i) => { if (ele === this) { game.pipeArr.splice(i, 1) } }); } }
最后调用中介者类
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0,user-scalable=no" id="viewport" /> <meta autor="xiaBin"> <title>xiaBin_flappBird</title> <style> * { padding: 0; margin: 0; } body { overflow: hidden; } </style> </head> <body> <canvas id="xBinCanvas"> </canvas> <script src="js/game.js"></script> <script src="js/sceneManager.js"></script> <script src="js/backGround.js"></script> <script src=js/land.js></script> <script src=js/pipe.js></script> <script src=js/bird.js></script> <script> let game = new Game({ "canvasId": "xBinCanvas", "jsonUrl": "R.json" }) </script> <script> </script> </body> </html>