canvas实现飞机打怪兽射击小游戏的示例代码
接触 canvas 也只有一个多月,第一次完整实现一个游戏流程,收获还是挺大的。
射击游戏截图
先上 demo
游戏规则
要求玩家控制飞机发射子弹,消灭会移动的怪兽,如果全部消灭了则游戏成功,如果怪兽移动到底部则游戏失败。
- 使用 ← 和 → 操作飞机
- 使用空格(space)进行射击
- 需有暂停功能
- 多关卡
场景切换
游戏分为几个场景
- 开始游戏(.game-intro)
- 游戏中(#canvas)
- 游戏失败(.game-failed)
- 游戏成功(.game-suess)
- 游戏通关(.game-all-suess)
- 暂停(.game-s)
实现场景切换,其实是先把所有场景 display: none , 然后通过 js 控制 data-status 分别为 start 、playing 、failed 、suess 、all-suess 、s 来实现对应场景 display: block 。
HTML 和 CSS 如下
<div id="game" data-status="start"> <div class="game-panel"> <section class="game-intro game-ui"> <h1 class="section-title">射击游戏</h1> <p class="game-desc">这是一个令人欲罢不能的射击游戏,使用 ← 和 → 操作你的飞机,使用空格(space)进行射击,使用回车(enter)暂停游戏。一起来消灭宇宙怪兽吧!</p> <p class="game-level">当前Level: 1</p> <button class="js-play button">开始游戏</button> </section> <section class="game-failed game-ui"> <h1 class="section-title">游戏结束</h1> <p class="game-info-text">最终得分: <span class="score"></span></p> <button class="js-replay button">重新开始</button> </section> <section class="game-suess game-ui"> <h1 class="section-title">游戏成功</h1> <p class="game-next-level game-info-text"></p> <button class="js-next button">继续游戏</button> </section> <section class="game-all-suess game-ui"> <h1 class="section-title">通关成功</h1> <p class="game-next-level game-info-text">你已经成功地防御了怪兽的所有攻击。</p> <button class="js-replay button">再玩一次</button> </section> <section class="game-s game-ui"> <h1 class="section-title">游戏暂停</h1> <button class="js-s button">游戏继续</button> </section> </div> <div class="game-info game-ui"> <span class="title">分数:</span> <span class="score"></span> </div> <canvas id="canvas" width="700" height="600"> <!-- 动画画板 --> </canvas> </div>
#game{ width: 700px; height: 600px; position: relative; left: 50%; : 40px; margin: 0 0 0 -350px; background: linear-gradient(-180deg, #040024 0%, #07165C 97%); } .game-ui{ display: none; padding: 55px; box-sizing: border-box; height: 100%; } [data-status="start"] .game-intro { display: block; padding-: 180px; background: url(./img/bg.png) no-repeat 430px 180px; background-size: 200px; } [data-status="playing"] .game-info { display: block; position: absolute; :0; left:0; padding:20px; } [data-status="failed"] .game-failed, [data-status="suess"] .game-suess, [data-status="all-suess"] .game-all-suess, [data-status="s"] .game-s{ display: block; padding-: 180px; background: url(./img/bg-end.png) no-repeat 380px 190px; background-size: 250px; }
面向对象
整个游戏可以把怪兽(Enemy)、飞机(Plane)、子弹(Bullet)都当作对象,还有配置对象(CONFIG)和控制游戏逻辑的游戏对象(GAME)。
游戏相关配置
/ 游戏相关配置 @type {Object} / var CONFIG = { status: 'start', // 游戏开始默认为开始中 level: 1, // 游戏默认等级 totalLevel: 6, // 总共6关 numPerLine: 7, // 游戏默认每行多少个怪兽 canvasPadding: 30, // 默认画布的间隔 bulletSize: 10, // 默认子弹长度 bulletSpeed: 10, // 默认子弹的移动速度 enemySpeed: 2, // 默认敌人移动距离 enemySize: 50, // 默认敌人的尺寸 enemyGap: 10, // 默认敌人之间的间距 enemyIcon: './img/enemy.png', // 怪兽的图像 enemyBoomIcon: './img/boom.png', // 怪兽死亡的图像 enemyDirection: 'right', // 默认敌人一开始往右移动 planeSpeed: 5, // 默认飞机每一步移动的距离 planeSize: { width: 60, height: 100 }, // 默认飞机的尺寸, planeIcon: './img/plane.png' };
定义父类
因为怪兽(Enemy)、飞机(Plane)、子弹(Bullet)都有相同的 x, y, size, speed 属性和 move() 方法,所以可以定义一个父类 Element,通过子类继承父类的方式实现。
/父类包含x y speed move() draw()/ var Element = function (opts) { this.opts = opts || {}; //设置坐标、尺寸、速度 this.x = opts.x; this.y = opts.y; this.size = opts.size; this.speed = opts.speed; }; Element.prototype.move = function (x, y) { var addX = x || 0; var addY = y || 0; this.x += addX; this.y += addY; }; //继承原型的函数 function inheritPrototype(subType, superType) { var proto = Object.create(superType.prototype); proto.constructor = subType; subType.prototype = proto; }
move(x, y) 方法根据传入的 (x, y) 值自叠加。
定义怪兽
怪兽包含特有属性怪兽状态、图像、控制爆炸状态持续的 boomCount ,和 draw()、down()、direction()、booming() 方法。
/敌人/ var Enemy = function (opts) { this.opts = opts || {}; //调用父类属性 Element.call(this, opts); //特有属性状态和图像 this.status = 'normal';//normal、booming、noomed this.enemyIcon = opts.enemyIcon; this.enemyBoomIcon = opts.enemyBoomIcon; this.boomCount = 0; }; //继承Element方法 inheritPrototype(Enemy, Element); //方法绘制敌人 Enemy.prototype.draw = function () { if (this.enemyIcon && this.enemyBoomIcon) { switch (this.status) { case 'normal': var enemyIcon = new Image(); enemyIcon.src = this.enemyIcon; ctx.drawImage(enemyIcon, this.x, this.y, this.size, this.size); break; case 'booming': var enemyBoomIcon = new Image(); enemyBoomIcon.src = this.enemyBoomIcon; ctx.drawImage(enemyBoomIcon, this.x, this.y, this.size, this.size); break; case 'boomed': ctx.clearRect(this.x, this.y, this.size, this.size); break; default: break; } } return this; }; //方法down 向下移动 Enemy.prototype.down = function () { this.move(0, this.size); return this; }; //方法左右移动 Enemy.prototype.direction = function (direction) { if (direction === 'right') { this.move(this.speed, 0); } else { this.move(-this.speed, 0); } return this; }; //方法敌人爆炸 Enemy.prototype.booming = function () { this.status = 'booming'; this.boomCount += 1; if (this.boomCount > 4) { this.status = 'boomed'; } return this; }
- draw() 主要是根据怪兽的状态绘制不同的图像。
- down() 调用父类 move() 方法,传入 y 值控制怪兽向下移动。
- direction() 根据传入的方向值控制左/右移动。
- booming() 让爆炸状态持续4帧,4帧后再消失。
定义子弹
子弹有 fly() 、draw() 方法。
/子弹/ var Bullet = function (opts) { this.opts = opts || {}; Element.call(this, opts); }; inheritPrototype(Bullet, Element); //方法让子弹飞 Bullet.prototype.fly = function () { this.move(0, -this.speed); return this; }; //方法绘制子弹 Bullet.prototype.draw = function () { ctx.beginPath(); ctx.strokeStyle = '#fff'; ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y - CONFIG.bulletSize); ctx.closePath(); ctx.stroke(); return this; };
- fly() 调用父类 move() 方法,传入 y 值控制子弹向上移动。
- draw() 因为子弹其实就是一条长度为 10 的直线,通过绘制路径的方式画出子弹。
定义飞机
飞机对象包含特有属性状态、宽高、图像、横坐标最大最小值,有 hasHit()、draw()、direction()、shoot()、drawBullets() 方法。
/飞机/ var Plane = function (opts) { this.opts = opts || {}; Element.call(this, opts); //特有属性状态和图像 this.status = 'normal'; this.width = opts.width; this.height = opts.height; this.planeIcon = opts.planeIcon; this.minX = opts.minX; this.maxX = opts.maxX; //子弹相关 this.bullets = []; this.bulletSpeed = opts.bulletSpeed || CONFIG.bulletSpeed; this.bulletSize = opts.bulletSize || CONFIG.bulletSize; }; //继承Element方法 inheritPrototype(Plane, Element); //方法子弹击中目标 Plane.prototype.hasHit = function (enemy) { var bullets = this.bullets; for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; var isHitPosX = (enemy.x < bullet.x) && (bullet.x < (enemy.x + enemy.size)); var isHitPosY = (enemy.y < bullet.y) && (bullet.y < (enemy.y + enemy.size)); if (isHitPosX && isHitPosY) { this.bullets.splice(i, 1); return true; } } return false; }; //方法绘制飞机 Plane.prototype.draw = function () { this.drawBullets(); var planeIcon = new Image(); planeIcon.src = this.planeIcon; ctx.drawImage(planeIcon, this.x, this.y, this.width, this.height); return this; }; //方法飞机方向 Plane.prototype.direction = function (direction) { var speed = this.speed; var planeSpeed; if (direction === 'left') { planeSpeed = this.x < this.minX ? 0 : -speed; } else { planeSpeed = this.x > this.maxX ? 0 : speed; } console.log('planeSpeed:', planeSpeed); console.log('this.x:', this.x); console.log('this.minX:', this.minX); console.log('this.maxX:', this.maxX); this.move(planeSpeed, 0); return this;//方便链式调用 }; //方法发射子弹 Plane.prototype.shoot = function () { var bulletPosX = this.x + this.width / 2; this.bullets.push(new Bullet({ x: bulletPosX, y: this.y, size: this.bulletSize, speed: this.bulletSpeed })); return this; }; //方法绘制子弹 Plane.prototype.drawBullets = function () { var bullets = this.bullets; var i = bullets.length; while (i--) { var bullet = bullets[i]; bullet.fly(); if (bullet.y <= 0) { bullets.splice(i, 1); } bullet.draw(); } };
- hasHit() 判断飞机发射的子弹是否击中怪兽,主要是判断子弹的横坐标是否在[怪兽横坐标,怪兽横坐标+怪兽高度]范围内,子弹的纵坐标在[怪兽纵坐标,怪兽纵坐标+怪兽宽度]范围内,击中返回 true,并移除该子弹。
- draw() 绘制子弹和飞机。
- direction() 因为飞机移动范围有左右边界,需要判断飞机横坐标是否到达边界,如果到达边界 planeSpeed 为 0,不再移动。
- shoot() 创建子弹对象,保存到 bullets 数组,子弹横坐标为飞机横坐标加上飞机宽度的一半。
- drawBullets() 绘制子弹,从数组往回遍历子弹对象数组,调用子弹 fly() 方法,如果子弹向上飞出屏幕,则移除这颗子弹。
定义键盘事件
键盘事件有以下几种状态
- keydown用户在键盘上按下某按键时发生。一直按着某按键则会不断触发(opera 浏览器除外)。
- keypress用户按下一个按键,并产生一个字符时发生(也就是不管类似 shift、alt、ctrl 之类的键,就是说用户按了一个能在屏幕上输出字符的按键 keypress 事件才会触发)。一直按着某按键则会不断触发。
- keyup用户释放某一个按键是触发。
因为飞机需要按下左键(keyCode=37)右键(keyCode=39)时(keydown)一直移动,释放时 keyup 不移动。按下空格(keyCode=32)或上方向键(keyCode=38)时(keydown)发射子弹,释放时 keyup 停止发射。按下回车键(keyCode=13)暂停游戏。所以,需要定义一个 KeyBoard 对象监听 onkeydown 和 onkeyup 是否按下或释放某个键。
因为左右键是矛盾的,为保险起见,按下左键时需要把右键 设为 false。右键同理。
//键盘事件 var KeyBoard = function () { document.onkeydown = this.keydown.bind(this); document.onkeyup = this.keyup.bind(this); }; //KeyBoard对象 KeyBoard.prototype = { pressedLeft: false, pressedRight: false, pressedUp: false, heldLeft: false, heldRight: false, pressedSpace: false, pressedEnter: false, keydown: function (e) { var key = e.keyCode; switch (key) { case 32://空格-发射子弹 this.pressedSpace = true; break; case 37://左方向键 this.pressedLeft = true; this.heldLeft = true; this.pressedRight = false; this.heldRight = false; break; case 38://上方向键-发射子弹 this.pressedUp = true; break; case 39://右方向键 this.pressedLeft = false; this.heldLeft = false; this.pressedRight = true; this.heldRight = true; break; case 13://回车键-暂停游戏 this.pressedEnter = true; break; } }, keyup: function (e) { var key = e.keyCode; switch (key) { case 32: this.pressedSpace = false; break; case 37: this.heldLeft = false; this.pressedLeft = false; break; case 38: this.pressedUp = false; break; case 39: this.heldRight = false; this.pressedRight = false; break; case 13: this.pressedEnter = false; break; } } };
游戏逻辑
游戏对象(GAME)包含了整个游戏的逻辑,包括init(初始化)、bindEvent(绑定按钮)、setStatus(更新游戏状态)、play(游戏中)、s(暂停)、end(结束)等,在此不展开描述。也包含了生成怪兽、绘制游戏元素等函数。
// 整个游戏对象 var GAME = { //一系列逻辑函数 //游戏元素函数 }
1、初始化
初始化函数主要是定义飞机初始坐标、飞机移动范围、怪兽移动范围,以及初始化分数、怪兽数组,创建 KeyBoard 对象,只执行一次。
/ 初始化函数,这个函数只执行一次 @param {object} opts @return {[type]} [description] / init: function (opts) { //设置opts var opts = Object.assign({}, opts, CONFIG);//合并所有参数 this.opts = opts; this.status = 'start'; //计算飞机对象初始坐标 this.planePosX = canvasWidth / 2 - opts.planeSize.width; this.planePosY = canvasHeight - opts.planeSize.height - opts.canvasPadding; //飞机极限坐标 this.planeMinX = opts.canvasPadding; this.planeMaxX = canvasWidth - opts.canvasPadding - opts.planeSize.width; //计算敌人移动区域 this.enemyMinX = opts.canvasPadding; this.enemyMaxX = canvasWidth - opts.canvasPadding - opts.enemySize; //分数设置为0 this.score = 0; this.enemies = []; this.keyBoard = new KeyBoard(); this.bindEvent(); this.renderLevel(); },
2、绑定按钮事件
因为几个游戏场景中包含开始游戏(playBtn)、重新开始(replayBtn)、下一关游戏(nextBtn)、暂停游戏继续(sBtn)几个按钮。我们需要给不同按钮执行不同事件。
定义 var self = this; 的原因是 this 的用法。在 bindEvent 函数中, this 指向 GAME 对象,而在 playBtn.onclick = function () {}; 中 this 指向了 playBtn ,这显然不是我们希望的,因为 playBtn 没有 play() 事件,GAME 对象中才有。需要把GAME 对象赋值给一个变量 self ,然后才能在 playBtn.onclick = function () {}; 中调用 play() 事件。
需要注意的是 replayBtn 按钮在闯关失败和通关场景都有出现,获取的是所有 .js-replay 的集合。然后 forEach 遍历每个 replayBtn 按钮,重置关卡和分数,调用 play() 事件。
bindEvent: function () { var self = this; var playBtn = document.querySelector('.js-play'); var replayBtn = document.querySelectorAll('.js-replay'); var nextBtn = document.querySelector('.js-next'); var sBtn = document.querySelector('.js-s'); // 开始游戏按钮绑定 playBtn.onclick = function () { self.play(); }; //重新开始游戏按钮绑定 replayBtn.forEach(function (e) { e.onclick = function () { self.opts.level = 1; self.play(); self.score = 0; totalScoreText.innerText = self.score; }; }); // 下一关游戏按钮绑定 nextBtn.onclick = function () { self.opts.level += 1; self.play(); }; // 暂停游戏继续按钮绑定 sBtn.onclick = function () { self.setStatus('playing'); self.updateElement(); }; },
3、生成飞机
createPlane: function () { var opts = this.opts; this.plane = new Plane({ x: this.planePosX, y: this.planePosY, width: opts.planeSize.width, height: opts.planeSize.height, minX: this.planeMinX, speed: opts.planeSpeed, maxX: this.planeMaxX, planeIcon: opts.planeIcon }); }
4、生成一组怪兽
因为怪兽都是成组出现的,每一关的怪兽数量也不同,两个 for 循环的作用就是生成一行怪兽,根据关数(level)增加 level 行怪兽。或者增加怪兽的速度(speed: speed + i,)来提高每一关难度等。
//生成敌人 createEnemy: function (enemyType) { var opts = this.opts; var level = opts.level; var enemies = this.enemies; var numPerLine = opts.numPerLine; var padding = opts.canvasPadding; var gap = opts.enemyGap; var size = opts.enemySize; var speed = opts.enemySpeed; //每升级一关敌人增加一行 for (var i = 0; i < level; i++) { for (var j = 0; j < numPerLine; j++) { //综合元素的参数 var initOpt = { x: padding + j (size + gap), y: padding + i (size + gap), size: size, speed: speed, status: enemyType, enemyIcon: opts.enemyIcon, enemyBoomIcon: opts.enemyBoomIcon }; enemies.push(new Enemy(initOpt)); } } return enemies; },
5、更新怪兽
获取怪兽数组的 x 值,判断是否到达画布边界,如果到达边界则怪兽向下移动。也要监听怪兽状态,正常状态下的怪兽是否被击中,爆炸状态下的怪兽,消失的怪兽要从数组剔除,得分。
//更新敌人状态 updateEnemeis: function () { var opts = this.opts; var plane = this.plane; var enemies = this.enemies; var i = enemies.length; var isFall = false;//敌人下落 var enemiesX = getHorizontalBoundary(enemies); if (enemiesX.minX < this.enemyMinX || enemiesX.maxX >= this.enemyMaxX) { console.log('enemiesX.minX', enemiesX.minX); console.log('enemiesX.maxX', enemiesX.maxX); opts.enemyDirection = opts.enemyDirection === 'right' ? 'left' : 'right'; console.log('opts.enemyDirection', opts.enemyDirection); isFall = true; } //循环更新敌人 while (i--) { var enemy = enemies[i]; if (isFall) { enemy.down(); } enemy.direction(opts.enemyDirection); switch (enemy.status) { case 'normal': if (plane.hasHit(enemy)) { enemy.booming(); } break; case 'booming': enemy.booming(); break; case 'boomed': enemies.splice(i, 1); this.score += 1; break; default: break; } } },
getHorizontalBoundary 函数的作用是遍历数组每个元素的 x 值,筛选出更大或更小的值,从而获得数组最大和最小的 x 值。
//获取数组横向边界 function getHorizontalBoundary(array) { var min, max; array.forEach(function (item) { if (!min && !max) { min = item.x; max = item.x; } else { if (item.x < min) { min = item.x; } if (item.x > max) { max = item.x; } } }); return { minX: min, maxX: max } }
6、更新键盘面板
按下回车键执行 s() 函数,按下左键执行飞机左移,按下右键执行飞机右移,按下空格执行飞机发射子弹,为了不让子弹连成一条直线,在这里设置 keyBoard.pressedUp 和 keyBoard.pressedSpace 为 false。
updatePanel: function () { var plane = this.plane; var keyBoard = this.keyBoard; if (keyBoard.pressedEnter) { this.s(); return; } if (keyBoard.pressedLeft || keyBoard.heldLeft) { plane.direction('left'); } if (keyBoard.pressedRight || keyBoard.heldRight) { plane.direction('right'); } if (keyBoard.pressedUp || keyBoard.pressedSpace) { keyBoard.pressedUp = false; keyBoard.pressedSpace = false; plane.shoot(); } },
7、绘制所有元素
draw: function () { this.renderScore(); this.plane.draw(); this.enemies.forEach(function (enemy) { //console.log('draw:this.enemy',enemy); enemy.draw(); }); },
8、更新所有元素
判断怪兽数组长度是否为 0 ,为 0 且 level 等于 totalLevel 说明通关,否则显示下一关游戏准备画面;如果怪兽数组 y 坐标大于飞机 y 坐标加怪兽高度,显示游戏失败。
canvas 动画的原理就是不断绘制、更新、清除画布。
游戏暂停的原理就是阻止 requestAnimationFrame() 函数执行,但不重置元素。判断 status 的状态为 s 时跳出函数。
//更新所有元素状态 updateElement: function () { var self = this; var opts = this.opts; var enemies = this.enemies; if (enemies.length === 0) { if (opts.level === opts.totalLevel) { this.end('all-suess'); } else { this.end('suess'); } return; } if (enemies[enemies.length - 1].y >= this.planePosY - opts.enemySize) { this.end('failed'); return; } //清理画布 ctx.clearRect(0, 0, canvasWidth, canvasHeight); //绘制画布 this.draw(); //更新元素状态 this.updatePanel(); this.updateEnemeis(); //不断循环updateElement requestAnimationFrame(function () { if(self.status === 's'){ return; }else{ self.updateElement(); } }); },
写在
通过以上几个步骤,游戏的基本功能就完成了,其他一些游戏流程控制,包括开始、结束、得分计算等在此就不叙述了。
可以优化的地方在按住空格键的时候,可以连续发射子弹。,这时再按一下方向键,发现无法再发射子弹了。最好是能移动的时候,也能保持着子弹的发射。
canvas 做游戏还是比较有趣的,还可以把这个游戏加以扩展,改成手机版,画布尺寸通过获取屏幕宽高确定,键盘部分改成触摸事件(touchstart、touchmove、touchend),怪兽出现方式也可以改成从屏幕顶端随机下落,怪兽增加血量(如射击4次才消失)等。
下载地址
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持狼蚁SEO。
长沙网站设计
- 如何自己建一个网站 自己想建个网站,怎么建
- 如何制作网站免费建站 创建网站免费注册
- html简单网页代码 html简单网页代码超链接
- dreamweaver网页制作 dreamweaver网页制作模板
- 上海网站建设 上海网站建设制作微信
- 如何制作网站和网页 如何制作一个网页
- html网页制作代码大全 端午节html网页制作代码大
- app开发公司 app开发公司前十名
- html网页制作 html网页制作文字居中
- app制作一个需要多少钱 请人制作一个app多少钱
- 成都网站制作 成都网站制作维护
- 百度建一个网站多少钱 百度做个公司网站要多少
- html+css网页制作成品 web网页制作成品css+javascrip
- html网页制作案例 html网页设计案例
- html+css网页制作成品 web网页制作成品css+javascrip
- 个人网站模板 个人网站模板HTML