资讯专栏INFORMATION COLUMN

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

tomorrowwu / 1052人阅读

摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究四随机绘制云朵实现了云朵的随机绘制,这一篇文章中将实现仙人掌翼龙障碍物的绘制游戏速度的改变障碍物的类型有两种仙人掌和翼龙。

文章首发于我的 GitHub 博客
前言

上一篇文章:《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;
  },
};

然后定义更新障碍物的方法:

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);
    }
  },
};

调用 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 方法来调用 Horizonupdate 方法:

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);
      }
    }

    // ...
  },
};

到这里,就实现了障碍物的基本绘制。不过由于速度一直恒定并且较小,所以不会绘制较大的障碍物。下面我们给游戏加上加速度来实现速度的不断加快(有最大值)。

修改 Runnerupdate 方法:

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

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

+     if (this.currentSpeed < this.config.MAX_SPEED) {
+       this.currentSpeed += this.config.ACCELERATION; // 速度增加一个加速度的值
+     }
    }

    // ...
  },
};

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

查看添加或修改的代码,戳这里

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103881.html

相关文章

  • Chrome 恐龙游戏源码探究一 -- 绘制静态地面

    摘要:首先是绘制静态的地面。上一篇下一篇无小恐龙游戏源码探究二让地面动起来 文章首发于我的 GitHub 博客 目录 Chrome 小恐龙游戏源码探究一 -- 绘制静态地面 Chrome 小恐龙游戏源码探究二 -- 让地面动起来 Chrome 小恐龙游戏源码探究三 -- 进入街机模式 Chrome 小恐龙游戏源码探究四 -- 随机绘制云朵 Chrome 小恐龙游戏源码探究五 -- 随机绘...

    lixiang 评论0 收藏0
  • Chrome 恐龙游戏源码探究六 -- 记录游戏分数

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究五随机绘制障碍实现了障碍物仙人掌和翼龙的绘制。在游戏中,小恐龙移动的距离就是游戏的分数。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究五 -- 随机绘制障碍》 实现了障碍物仙人掌和翼龙的绘制。这一篇将实现当前分数、最高分数的记录和绘制。 在游戏中,小恐龙移动的距离就是游戏的分数。分数每达 1...

    Jingbin_ 评论0 收藏0
  • Chrome 恐龙游戏源码探究四 -- 随机绘制云朵

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究三进入街机模式实现了开场动画和街机模式。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究三 -- 进入街机模式》 实现了开场动画和街机模式。这一篇文章中,将实现云朵的随机绘制。 云朵类 Cloud 定义云朵类 Cloud: /** * 云朵类 * @param {HTMLCanvasEle...

    svtter 评论0 收藏0
  • Chrome 恐龙游戏源码探究九 -- 游戏碰撞检测

    摘要:文章首发于我的博客前言上一篇文章小恐龙游戏源码探究八奔跑的小恐龙实现了小恐龙的绘制以及键盘对小恐龙的控制,这一篇文章中将实现游戏的碰撞检测。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙》实现了小恐龙的绘制以及键盘对小恐龙的控制,这一篇文章中将实现游戏的碰撞检测。 碰撞检测原理 这个游戏采用的检测方法是盒子碰撞,这种检...

    cpupro 评论0 收藏0
  • Chrome 恐龙游戏源码探究八 -- 奔跑的恐龙

    摘要:例如,将函数修改为小恐龙眨眼这样小恐龙会不停的眨眼睛。小恐龙的开场动画下面来实现小恐龙对键盘按键的响应。接下来还需要更新动画帧才能实现小恐龙的奔跑动画。 文章首发于我的 GitHub 博客 前言 上一篇文章:《Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替》实现了游戏昼夜模式的交替,这一篇文章中,将实现:1、小恐龙的绘制 2、键盘对小恐龙的控制 3、页面失焦后,重新聚焦会重置...

    paulquei 评论0 收藏0

发表评论

0条评论

tomorrowwu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<