• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    canvas实现飞机打怪兽射击小游戏的示例代码

    接触 canvas 也只有一个多月,第一次完整实现一个游戏流程,收获还是挺大的。

    射击游戏截图

    先上 demo:https://littleyljy.github.io/demo/shootgame/

    游戏规则

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

    场景切换

    游戏分为几个场景:

    实现场景切换,其实是先把所有场景 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">
            <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-success 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-success 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-stop game-ui">
            <h1 class="section-title">游戏暂停</h1>
            <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;
    }
    

    面向对象

    整个游戏可以把怪兽(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;
    }
    
    

    定义子弹

    子弹有 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;
    };
    
    

    定义飞机

    飞机对象包含特有属性:状态、宽高、图像、横坐标最大最小值,有 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();
        }
    };
    
    

    定义键盘事件

    键盘事件有以下几种状态:

    因为飞机需要按下左键(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 做游戏还是比较有趣的,另外还可以把这个游戏加以扩展,改成手机版,画布尺寸通过获取屏幕宽高确定,键盘部分改成触摸事件(touchstart、touchmove、touchend),怪兽出现方式也可以改成从屏幕顶端随机下落,怪兽增加血量(如射击4次才消失)等。

    下载地址:https://github.com/littleyljy/shoot

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

    上一篇:你可能不熟练的十个前端HTML5经典面试题
    下一篇:canvas绘制表情包的示例代码
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯

    时间:9:00-21:00 (节假日不休)

    地址:江苏信息产业基地11号楼四层

    《增值电信业务经营许可证》 苏B2-20120278

    canvas实现飞机打怪兽射击小游戏的示例代码 canvas,实现,飞机,打,怪兽,