温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

使用canvas怎么实现一个飞机打怪兽射击小游戏

发布时间:2021-04-14 15:46:11 来源:亿速云 阅读:244 作者:Leah 栏目:web开发

使用canvas怎么实现一个飞机打怪兽射击小游戏?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

游戏规则

要求玩家控制飞机发射子弹,消灭会移动的怪兽,如果全部消灭了则游戏成功,如果怪兽移动到底部则游戏失败。

  • 使用 ← 和 → 操作飞机

  • 使用空格(space)进行射击

  • 需有暂停功能

  • 多关卡

场景切换

游戏分为几个场景:

  • 开始游戏(.game-intro)

  • 游戏中(#canvas)

  • 游戏失败(.game-failed)

  • 游戏成功(.game-success)

  • 游戏通关(.game-all-success)

  • 暂停(.game-stop)

实现场景切换,其实是先把所有场景 display: none , 然后通过 js 控制 data-status 分别为 start 、playing 、failed 、success 、all-success 、stop 来实现对应场景 display: block 。

HTML 和 CSS 如下:

  <div id="game" data-status="start">      <div class="game-panel">       <section class="game-intro game-ui">         <h2 class="section-title">射击游戏</h2>         <p class="game-desc">这是一个令人欲罢不能的射击游戏,使用 &larr; 和 &rarr; 操作你的飞机,使用空格(space)进行射击,使用回车(enter)暂停游戏。一起来消灭宇宙怪兽吧!</p>         <p class="game-level">当前Level: 1</p>         <button class="js-play button">开始游戏</button>       </section>       <section class="game-failed game-ui">         <h2 class="section-title">游戏结束</h2>         <p class="game-info-text">最终得分: <span class="score"></span></p>         <button class="js-replay button">重新开始</button>       </section>       <section class="game-success game-ui">         <h2 class="section-title">游戏成功</h2>         <p class="game-next-level game-info-text"></p>         <button class="js-next button">继续游戏</button>       </section>       <section class="game-all-success game-ui">         <h2 class="section-title">通关成功</h2>         <p class="game-next-level game-info-text">你已经成功地防御了怪兽的所有攻击。</p>         <button class="js-replay button">再玩一次</button>       </section>       <section class="game-stop game-ui">         <h2 class="section-title">游戏暂停</h2>         <button class="js-stop 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%;   top: 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-top: 180px;   background: url(./img/bg.png) no-repeat 430px 180px;   background-size: 200px; } [data-status="playing"] .game-info {   display: block;   position: absolute;   top:0;   left:0;   padding:20px; } [data-status="failed"] .game-failed, [data-status="success"] .game-success, [data-status="all-success"] .game-all-success, [data-status="stop"] .game-stop{   display: block;   padding-top: 180px;   background: url(./img/bg-end.png) no-repeat 380px 190px;   background-size: 250px; }

面向对象

使用canvas怎么实现一个飞机打怪兽射击小游戏

整个游戏可以把怪兽(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(游戏中)、stop(暂停)、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)、暂停游戏继续(stopBtn)几个按钮。我们需要给不同按钮执行不同事件。

首先定义 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 stopBtn = document.querySelector('.js-stop');     // 开始游戏按钮绑定     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();     };     // 暂停游戏继续按钮绑定     stopBtn.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、更新键盘面板

按下回车键执行 stop() 函数,按下左键执行飞机左移,按下右键执行飞机右移,按下空格执行飞机发射子弹,为了不让子弹连成一条直线,在这里设置 keyBoard.pressedUp 和 keyBoard.pressedSpace 为 false。

  updatePanel: function () {     var plane = this.plane;     var keyBoard = this.keyBoard;     if (keyBoard.pressedEnter) {       this.stop();       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 的状态为 stop 时跳出函数。

  //更新所有元素状态   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-success');       } else {         this.end('success');       }       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 === 'stop'){         return;       }else{         self.updateElement();       }     }); }

看完上述内容,你们掌握使用canvas怎么实现一个飞机打怪兽射击小游戏的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注亿速云行业资讯频道,感谢各位的阅读!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI