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

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

成都创新互联公司主要从事网站设计、网站建设、网页设计、企业做网站、公司建网站等业务。立足成都服务祁东,十年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:18980820575

游戏规则

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

  • 使用 ← 和 → 操作飞机

  • 使用空格(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 如下:

   
    
      
        射击游戏
        这是一个令人欲罢不能的射击游戏,使用 ← 和 → 操作你的飞机,使用空格(space)进行射击,使用回车(enter)暂停游戏。一起来消灭宇宙怪兽吧!

        当前Level: 1

        开始游戏                       游戏结束         最终得分: 

        重新开始                       游戏成功         

        继续游戏                       通关成功         你已经成功地防御了怪兽的所有攻击。

        再玩一次                       游戏暂停         游戏继续            
           分数:            
                     
#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;
}

定义子弹

子弹有 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怎么实现一个飞机打怪兽射击小游戏的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注创新互联行业资讯频道,感谢各位的阅读!


分享名称:使用canvas怎么实现一个飞机打怪兽射击小游戏
转载注明:http://azwzsj.com/article/ghgpps.html

其他资讯