文章首发于我的个人博客

前言

上一篇文章:《Chrome 小恐龙游戏源码探究六 -- 记录游戏分数》实现了游戏分数、最高分数的记录和绘制。这一篇文章中将实现昼夜模式交替的的效果。

夜晚模式

定义夜晚模式类 NightMode:

/** * 夜晚模式 * @param {HTMLCanvasElement} canvas 画布 * @param {Object} spritePos 雪碧图中的坐标信息 * @param {Number} containerWidth 容器宽度 */function NightMode(canvas, spritePos, containerWidth) {  this.canvas = canvas;  this.ctx = this.canvas.getContext('2d');  this.spritePos = spritePos;  this.containerWidth = containerWidth;  this.xPos = containerWidth - 50; // 月亮的 x 坐标  this.yPos = 30;                  // 月亮的 y 坐标  this.currentPhase = 0;           // 月亮当前所处的时期  this.opacity = 0;                // 星星和月亮的透明度  this.stars = [];                 // 存储星星  this.drawStars = false;          // 是否绘制星星    // 放置星星  this.placeStars();}

相关的配置参数:

NightMode.config = {  WIDTH: 20,         // 半月的宽度  HEIGHT: 40,        // 月亮的高度  FADE_SPEED: 0.035, // 淡入淡出的速度  MOON_SPEED: 0.25,  // 月亮的速度  NUM_STARS: 2,      // 星星的数量  STAR_SIZE: 9,      // 星星的大小  STAR_SPEED: 0.3,   // 星星的速度  STAR_MAX_Y: 70,    // 星星在画布上的最大 y 坐标};// 月亮所处的时期(不同的时期有不同的位置)NightMode.phases = [140, 120, 100, 60, 40, 20, 0];

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

function Runner(containerSelector, opt_config) {  // ...+ this.inverted = false;         // 是否开启夜晚模式+ this.invertTimer = 0;          // 夜晚模式的时间}Runner.config = {  // ...+ INVERT_FADE_DURATION: 12000,             // 夜晚模式的持续时间+ INVERT_DISTANCE: 100,                    // 触发夜晚模式的距离};Runner.spriteDefinition = {  LDPI: {    // ...+   MOON: {x: 484, y: 2},+   STAR: {x: 645, y: 2},  },};Runner.classes = {  // ...+ INVERTED: 'inverted',};
body {  transition: filter 1.5s cubic-bezier(0.65, 0.05, 0.36, 1),              background-color 1.5s cubic-bezier(0.65, 0.05, 0.36, 1);  will-change: filter, background-color;}.inverted {  filter: invert(100%);  background-color: #000;}

来看下 NightMode 原型链上的方法:

NightMode.prototype = {  draw: function () {    // 月期为 3 时,月亮为满月    var moonSourceWidth = this.currentPhase == 3 ? NightMode.config.WIDTH * 2 :        NightMode.config.WIDTH;    var moonSourceHeight = NightMode.config.HEIGHT;    // 月亮在雪碧图中的 x 坐标    var moonSourceX = this.spritePos.x + NightMode.phases[this.currentPhase];    var moonOutputWidth = moonSourceWidth;        // 星星在雪碧图中的 x 坐标    var starSourceX = Runner.spriteDefinition.LDPI.STAR.x;    var starSize = NightMode.config.STAR_SIZE;    this.ctx.save();    this.ctx.globalAlpha = this.opacity; // 画布的透明度随之变化    // 绘制星星    if (this.drawStars) {      for (var i = 0; i < NightMode.config.NUM_STARS; i++) {        this.ctx.drawImage(          Runner.imageSprite,          starSourceX, this.stars[i].sourceY,          starSize, starSize,          Math.round(this.stars[i].x), this.stars[i].y,          NightMode.config.STAR_SIZE, NightMode.config.STAR_SIZE,        );      }    }    // 绘制月亮    this.ctx.drawImage(      Runner.imageSprite,      moonSourceX, this.spritePos.y,      moonSourceWidth, moonSourceHeight,      Math.round(this.xPos), this.yPos,      moonOutputWidth, NightMode.config.HEIGHT    );        this.ctx.globalAlpha = 1;    this.ctx.restore();  },  /**    * 更新星星和月亮的位置,改变月期    * @param {Boolean} activated 是否夜晚模式被激活    */  update: function (activated) {    // 改变月期    if (activated && this.opacity === 0) {      this.currentPhase++;      if (this.currentPhase >= NightMode.phases.length) {        this.currentPhase = 0;      }    }    // 淡入    if (activated && (this.opacity < 1 || this.opacity === 0)) {      this.opacity += NightMode.config.FADE_SPEED;    } else if (this.opacity > 0) { // 淡出      this.opacity -= NightMode.config.FADE_SPEED;    }    // 设置月亮和星星的位置    if (this.opacity > 0) {      // 更新月亮的 x 坐标      this.xPos = this.updateXPos(this.xPos, NightMode.config.MOON_SPEED);      // 更新星星的 x 坐标      if (this.drawStars) {        for (var i = 0; i < NightMode.config.NUM_STARS; i++) {          this.stars[i].x = this.updateXPos(this.stars[i].x,             NightMode.config.STAR_SPEED);        }      }      this.draw();    } else {      this.opacity = 0;      this.placeStars();    }    this.drawStars = true;  },  // 更新 x 坐标  updateXPos: function (currentPos, speed) {    // 月亮移出画布半个月亮宽度,将其位置移动到画布右边    if (currentPos < -NightMode.config.WIDTH) {      currentPos = this.containerWidth;    } else {      currentPos -= speed;    }    return currentPos;  },  // 随机放置星星  placeStars: function () {    // 将画布分为若干组    var segmentSize = Math.round(this.containerWidth /      NightMode.config.NUM_STARS);    for (var i = 0; i < NightMode.config.NUM_STARS; i++) {      this.stars[i] = {};      // 分别随机每组画布中星星的位置      this.stars[i].x = getRandomNum(segmentSize * i, segmentSize * (i + 1));      this.stars[i].y = getRandomNum(0, NightMode.config.STAR_MAX_Y);      // 星星在雪碧图中的 y 坐标      this.stars[i].sourceY = Runner.spriteDefinition.LDPI.STAR.y +          NightMode.config.STAR_SIZE * i;    }  },};

定义好 NightMode 类以及相关方法后,接下来需要通过 Horizon 来进行调用。

修改 Horizon 类:

function Horizon(canvas, spritePos, dimensions, gapCoefficient) {  // ...    // 夜晚模式+ this.nightMode = null;}

初始化 NightMode 类:

Horizon.prototype = {  init: function () {    // ...+   this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,+     this.dimensions.WIDTH);  },};

更新夜晚模式:

Horizon.prototype = {- update: function (deltaTime, currentSpeed, updateObstacles) {+ update: function (deltaTime, currentSpeed, updateObstacles, showNightMode) {    // ...+   this.nightMode.update(showNightMode);  },};

然后修改 Runner 的 update 方法:

Runner.prototype = {  update: function () {    this.updatePending = false; // 等待更新    if (this.playing) {      // ...      // 直到开场动画结束再移动地面      if (this.playingIntro) {        this.horizon.update(0, this.currentSpeed, hasObstacles);      } else {        deltaTime = !this.activated ? 0 : deltaTime;-       this.horizon.update(deltaTime, this.currentSpeed, hasObstacles);+       this.horizon.update(deltaTime, this.currentSpeed, hasObstacles,+         this.inverted);      }+     // 夜晚模式+     if (this.invertTimer > this.config.INVERT_FADE_DURATION) { // 夜晚模式结束+       this.invertTimer = 0;+       this.invertTrigger = false;+       this.invert();+     } else if (this.invertTimer) { // 处于夜晚模式,更新其时间+       this.invertTimer += deltaTime;+     } else { // 还没进入夜晚模式+       // 游戏移动的距离+       var actualDistance =+         this.distanceMeter.getActualDistance(Math.ceil(this.distanceRan));++       if(actualDistance > 0) {+         // 每移动指定距离就触发一次夜晚模式+         this.invertTrigger = !(actualDistance % this.config.INVERT_DISTANCE);++         if (this.invertTrigger && this.invertTimer === 0) {+           this.invertTimer += deltaTime;+           this.invert();+         }+       }+     }    }    if (this.playing) {      // 进行下一次更新      this.scheduleNextUpdate();    }  },};

上面用到的 invert 方法定义如下:

Runner.prototype = {  /**   * 反转当前页面的颜色   * @param {Boolea} reset 是否重置颜色   */  invert: function (reset) {    var bodyElem = document.body;    if (reset) {      bodyElem.classList.toggle(Runner.classes.INVERTED, false); // 删除 className      this.invertTimer = 0;  // 重置夜晚模式的时间      this.inverted = false; // 关闭夜晚模式    } else {      this.inverted = bodyElem.classList.toggle(Runner.classes.INVERTED,        this.invertTrigger);    }  },};

这样就是实现了昼夜交替的效果。原来的游戏中,昼夜交替每 700 米触发一次,这里为了演示,改成了 100 米触发一次。效果如下:

查看添加的代码,戳这里

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

上一篇下一篇
Chrome 小恐龙游戏源码探究六 -- 记录游戏分数Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙