Chrome 小恐龙游戏源码探究五 -- 随机绘制障碍

栏目: 后端 · 发布时间: 4年前

内容简介:上一篇文章:《障碍物的类型有两种:仙人掌和翼龙。翼龙每次只能有一只,高度随机,仙人掌一次可以绘制多个,一次绘制的数目随机。对于绘制障碍物的关键是:有关障碍物的碰撞检测部分这里先不实现,会放在后面的单独一章来讲。

前言

上一篇文章:《 Chrome 小恐龙游戏源码探究四 -- 随机绘制云朵 》 实现了云朵的随机绘制,这一篇文章中将实现:1、仙人掌、翼龙障碍物的绘制 2、游戏速度的改变

障碍物的类型有两种:仙人掌和翼龙。翼龙每次只能有一只,高度随机,仙人掌一次可以绘制多个,一次绘制的数目随机。对于绘制障碍物的关键是: 保证合适的大小和间隔 。例如:不能在游戏刚开始速度很慢的时候就绘制一个很宽的障碍物,否则是跳不过去的。也不能在游戏速度较快的情况下,两个障碍物间隔生成的很窄,否则当跳过第一个障碍物后,一定会撞到下一个障碍物。

有关障碍物的碰撞检测部分这里先不实现,会放在后面的单独一章来讲。

障碍物类 Obstacle

定义 Obstacle 类:

/**
 * 障碍物类
 * @param {HTMLCanvasElement} canvas 画布
 * @param {String} type 障碍物类型
 * @param {Object} spriteImgPos 在雪碧图中的位置
 * @param {Object} dimensions 画布尺寸
 * @param {Number} gapCoefficient 间隙系数
 * @param {Number} speed 速度
 * @param {Number} opt_xOffset x 坐标修正
 */
function Obstacle(canvas, type, spriteImgPos, dimensions,
  gapCoefficient, speed, opt_xOffset) {
  this.canvas = canvas;
  this.ctx = canvas.getContext('2d');

  this.typeConfig = type;               // 障碍物类型
  this.spritePos = spriteImgPos;        // 在雪碧图中的位置
  this.gapCoefficient = gapCoefficient; // 间隔系数
  this.dimensions = dimensions;

  // 每组障碍物的数量(随机 1~3 个)
  this.size = getRandomNum(1, Obstacle.MAX_OBSTACLE_LENGTH);

  this.xPos = dimensions.WIDTH + (opt_xOffset || 0);
  this.yPos = 0;

  this.remove = false;   // 是否可以被删除
  this.gap = 0;          // 间隙
  this.speedOffset = 0;  // 速度修正

  // 非静态障碍物的属性
  this.currentFrame = 0; // 当前动画帧
  this.timer = 0;        // 动画帧切换计时器

  this.init(speed);
}

障碍物的有关参数:

Obstacle.MAX_GAP_COEFFICIENT = 1.5; // 最大间隙系数
Obstacle.MAX_OBSTACLE_LENGTH = 3;   // 每组障碍物的最大数量

Obstacle.types = [{
  type: 'CACTUS_SMALL',  // 小仙人掌
  width: 17,
  height: 35,
  yPos: 105,             // 在 canvas 上的 y 坐标
  multipleSpeed: 4,
  minGap: 120,           // 最小间距
  minSpeed: 0,           // 最低速度
}, {
  type: 'CACTUS_LARGE',  // 大仙人掌
  width: 25,
  height: 50,
  yPos: 90,
  multipleSpeed: 7,
  minGap: 120,
  minSpeed: 0,
}, {
  type: 'PTERODACTYL',   // 翼龙
  width: 46,
  height: 40,
  yPos: [ 100, 75, 50 ], // y 坐标不固定
  multipleSpeed: 999,
  minSpeed: 8.5,
  minGap: 150,
  numFrames: 2,          // 两个动画帧  
  frameRate: 1000 / 6,   // 帧率(一帧的时间)
  speedOffset: 0.8,      // 速度修正
}];

补充本篇文章中会用到的一些数据:

function Runner(containerSelector, opt_config)  {
  // ...

+ this.runningTime = 0;    // 游戏运行的时间
}

Runner.config = {
  // ...

+ GAP_COEFFICIENT: 0.6,        // 障碍物间隙系数
+ MAX_OBSTACLE_DUPLICATION: 2, // 障碍物相邻的最大重复
+ CLEAR_TIME: 3000,            // 游戏开始后,等待三秒再绘制障碍物
+ MAX_SPEED: 13,               // 游戏的最大速度
+ ACCELERATION: 0.001,         // 加速度
};

Runner.spriteDefinition = {
  LDPI: {
    // ...

+   CACTUS_SMALL: {x: 228, y: 2}, // 小仙人掌
+   CACTUS_LARGE: {x: 332, y: 2}, // 大仙人掌
+   PTERODACTYL: {x: 134, y: 2},  // 翼龙
  },
};

在 Obstacle 原型链上添加方法:

Obstacle.prototype = {
  init: function (speed) {
    // 这里是为了确保刚开始游戏速度慢时,不会生成较大的障碍物和翼龙
    // 否则速度慢时,生成较大的障碍物或翼龙是跳不过去的
    if (this.size > 1 && this.typeConfig.multipleSpeed > speed) {
      this.size = 1;
    }

    this.width = this.typeConfig.width * this.size;

    // 检查障碍物是否可以被放置在不同的高度
    if (Array.isArray(this.typeConfig.yPos)) {
      var yPosConfig = this.typeConfig.yPos;
      // 随机高度
      this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)];
    } else {
      this.yPos = this.typeConfig.yPos;
    }

    this.draw();
    
    // 对于速度与地面不同的障碍物(翼龙)进行速度修正
    // 使得有的速度看起来快一些,有的看起来慢一些
    if (this.typeConfig.speedOffset) {
      this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset :
        -this.typeConfig.speedOffset;
    }

    // 障碍物的间隙随游戏速度变化而改变
    this.gap = this.getGap(this.gapCoefficient, speed);
  },
  /**
    * 获取障碍物的间隙
    * @param {Number} gapCoefficient 间隙系数
    * @param {Number} speed 速度
    */
  getGap: function(gapCoefficient, speed) {
    var minGap = Math.round(this.width * speed +
          this.typeConfig.minGap * gapCoefficient);
    var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT);
    return getRandomNum(minGap, maxGap);
  },
  draw: function () {
    var sourceWidth = this.typeConfig.width;
    var sourceHeight = this.typeConfig.height;

    // 根据每组障碍物的数量计算障碍物在雪碧图上的坐标
    var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) +
      this.spritePos.x;
    
    // 如果存在动画帧,则计算当前动画帧在雪碧图中的坐标
    if (this.currentFrame > 0) {
      sourceX += sourceWidth * this.currentFrame;
    }

    this.ctx.drawImage(
      Runner.imageSprite,
      sourceX, this.spritePos.y,
      sourceWidth * this.size, sourceHeight,
      this.xPos, this.yPos,
      this.typeConfig.width * this.size, this.typeConfig.height
    );
  },
  update: function (deltaTime, speed) {
    if (!this.remove) {
      // 修正速度
      if (this.typeConfig.speedOffset) {
        speed += this.speedOffset;
      }
      
      this.xPos -= Math.floor((speed * FPS / 1000) * Math.round(deltaTime));

      // 如果有动画帧,则更新
      if (this.typeConfig.numFrames) {
        this.timer += deltaTime;

        if (this.timer >= this.typeConfig.frameRate) {
          // 第一帧 currentFrame 为 0,第二帧 currentFrame 为 1
          this.currentFrame =
            this.currentFrame == this.typeConfig.numFrames - 1 ?
            0 : this.currentFrame + 1;
          this.timer = 0;
        }
      }
      this.draw();

      // 标记移出画布的障碍物
      if (!this.isVisible()) {
        this.remove = true;
      }
    }
  },
  // 障碍物是否还在画布中
  isVisible: function () {
    return this.xPos + this.width > 0;
  },
};

定义好 Obstacle 类之后,需要通过 Horizon 类来调用。首先需要定义两个变量来存储障碍物和障碍物的类型:

- function Horizon(canvas, spritePos, dimensions) {
+ function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
    this.canvas = canvas;
    this.ctx = this.canvas.getContext('2d');
    this.spritePos = spritePos;
    this.dimensions = dimensions;
+   this.gapCoefficient = gapCoefficient;

+   this.obstacles = [];       // 存储障碍物
+   this.obstacleHistory = []; // 记录存储的障碍物的类型

    // 云的频率
    this.cloudFrequency = Cloud.config.CLOUD_FREQUENCY;

    // ...
  }

修改初始化 Horizon 类时传的参数:

Runner.prototype = {
  init: function () {
    // ...

    // 加载背景类 Horizon
-   this.horizon = new Horizon(this.canvas, this.spriteDef,
-     this.dimensions);
+   this.horizon = new Horizon(this.canvas, this.spriteDef,
+     this.dimensions, this.config.GAP_COEFFICIENT);
  },
};

定义随机添加障碍物的方法:

Horizon.prototype = {
  addNewObstacle: function(currentSpeed) {
    // 随机障碍物
    var obstacleTypeIndex = getRandomNum(0, Obstacle.types.length - 1);
    var obstacleType = Obstacle.types[obstacleTypeIndex];

    // 检查当前添加的障碍物与前面障碍物的重复次数是否符合要求
    // 如果当前的速度小于障碍物的速度,证明障碍物是翼龙(其他障碍物速度都是 0)
    // 添加的障碍物是翼龙,并且当前速度小于翼龙的速度,则重新添加(保证低速不出现翼龙)
    if (this.duplicateObstacleCheck(obstacleType.type) ||
        currentSpeed < obstacleType.minSpeed) {
      this.addNewObstacle(currentSpeed);
    } else {
      // 通过检查后,存储新添加的障碍物
      var obstacleSpritePos = this.spritePos[obstacleType.type];

      // 存储障碍物
      this.obstacles.push(new Obstacle(this.canvas, obstacleType,
          obstacleSpritePos, this.dimensions,
          this.gapCoefficient, currentSpeed, obstacleType.width));

      // 存储障碍物类型
      this.obstacleHistory.unshift(obstacleType.type);

      // 若 history 数组长度大于 1, 清空最前面两个数据
      if (this.obstacleHistory.length > 1) {
        this.obstacleHistory.splice(Runner.config.MAX_OBSTACLE_DUPLICATION);
      }
    }
  },
  /**
    * 检查当前障碍物前面的障碍物的重复次数是否大于等于最大重复次数
    * @param {String} nextObstacleType 障碍物类型
    */
  duplicateObstacleCheck: function(nextObstacleType) {
    var duplicateCount = 0; // 重复次数

    // 根据存储的障碍物类型来判断障碍物的重复次数
    for (var i = 0; i < this.obstacleHistory.length; i++) {
      duplicateCount = this.obstacleHistory[i] == nextObstacleType ?
          duplicateCount + 1 : 0;
    }
    return duplicateCount >= Runner.config.MAX_OBSTACLE_DUPLICATION;
  },
};

然后定义更新障碍物的方法,并调用上面添加障碍物的 addNewObstacle 方法:

Horizon.prototype = {
  updateObstacles: function (deltaTime, currentSpeed) {
    // 复制存储的障碍物
    var updatedObstacles = this.obstacles.slice(0);

    for (var i = 0; i < this.obstacles.length; i++) {
      var obstacle = this.obstacles[i];
      obstacle.update(deltaTime, currentSpeed);

      // 删除被标记的障碍物
      if (obstacle.remove) {
        updatedObstacles.shift();
      }
    }

    // 更新存储的障碍物
    this.obstacles = updatedObstacles;

    if (this.obstacles.length > 0) {
      var lastObstacle = this.obstacles[this.obstacles.length - 1];

      // 满足添加障碍物的条件
      if (lastObstacle && !lastObstacle.followingObstacleCreated &&
          lastObstacle.isVisible() &&
          (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) <
          this.dimensions.WIDTH) {
        this.addNewObstacle(currentSpeed);
        lastObstacle.followingObstacleCreated = true;
      }
    } else { // 没有存储障碍物,直接添加
      this.addNewObstacle(currentSpeed);
    }
  },
};

接下来修改 Horizon 的 update 方法来调用上面定义的 updateObstacles 方法:

Horizon.prototype = {
- update: function (deltaTime, currentSpeed) {
+ update: function (deltaTime, currentSpeed, updateObstacles) {
    this.horizonLine.update(deltaTime, currentSpeed);
    this.updateCloud(deltaTime, currentSpeed);

+   if (updateObstacles) {
+     this.updateObstacles(deltaTime, currentSpeed);
+   }
  },
};

最后修改 Runner 上的 update 方法来调用 Horizon 的 update 方法:

Runner.prototype = {
  update: function () {
    // ...

    if (this.playing) {
      this.clearCanvas();

+     this.runningTime += deltaTime;
+     var hasObstacles = this.runningTime > this.config.CLEAR_TIME;

      // 刚开始 this.playingIntro 未定义 !this.playingIntro 为真
      if (!this.playingIntro) {
        this.playIntro(); // 执行开场动画
      }

      // 直到开场动画结束再移动地面
      if (this.playingIntro) {
-       this.horizon.update(0, this.currentSpeed);
+       this.horizon.update(0, this.currentSpeed, hasObstacles);
      } else {
        deltaTime = !this.activated ? 0 : deltaTime;
-       this.horizon.update(deltaTime, this.currentSpeed);
+       this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);
      }
    }

    // ...
  },
};

到此,就实现了障碍物的基本绘制。不过由于速度一直恒定并且较小,所以不会绘制较大的障碍物。下面给游戏加上 加速度 来实现速度的变化。

修改 Runner 的 update 方法:

Runner.prototype = {
  update: function () {
    // ...

    if (this.playing) {
      // ...

+     if (this.currentSpeed < this.config.MAX_SPEED) {
+       this.currentSpeed += this.config.ACCELERATION;
+     }
    }

    // ...
  },
};

现在就完整实现了障碍物的绘制和移动。效果如下:

Chrome 小恐龙游戏源码探究五 -- 随机绘制障碍

查看添加的代码, 戳这里

Demo 体验地址: https://liuyib.github.io/pages/demo/games/google-dino/add-obstacle/

上一篇 下一篇 Chrome 小恐龙游戏源码探究四 -- 随机绘制云朵 Chrome 小恐龙游戏源码探究六 -- 记录游戏分数

以上所述就是小编给大家介绍的《Chrome 小恐龙游戏源码探究五 -- 随机绘制障碍》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算理论导引

计算理论导引

[美]Michael Sipser / 张立昂、王捍贫、黄雄 / 机械工业出版社 / 2000-2 / 30.00元

本书由计算理论领域的知名权威Michael Sipser撰写。他以独特的视角,综合地描述了计算机科学理论,并以清新的笔触、生动的语言给出了宽泛的数学理论,而并非拘泥于某些低层次的技术细节。在证明之前,均有“证明思路”,帮助读者理解数学形式下蕴涵的概念。同样,对于算法描述,均以直观的文字,而非伪代码给出,从而将注意力集中于算法本身,而不是某些模型。本书的内容包括三个部分:自动机与语言、可计算性理论和一起来看看 《计算理论导引》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具