Chrome 小恐龍遊戲源碼探究七 -- 晝夜模式交替

文章首發於個人 GitHub 博客

前言

上一篇文章:《Chrome 小恐龍遊戲源碼探究六 -- 記錄遊戲分數》實現了遊戲分數、最高分數的記錄和繪製。這一篇文章中將實現晝夜模式交替的的效果。css

夜晚模式

定義夜晚模式類 NightModegit

/**
 * 夜晚模式
 * @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();
}

相關的配置參數:github

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];

補充本篇文章中會用到的一些數據:canvas

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 原型鏈上的方法:segmentfault

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 來進行調用。dom

修改 Horizon 類:動畫

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

初始化 NightMode 類:this

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

+   this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
+     this.dimensions.WIDTH);
  },
};

更新夜晚模式:google

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

+   this.nightMode.update(showNightMode);
  },
};

而後修改 Runnerupdate 方法:spa

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 米觸發一次。效果以下:

clipboard.png

查看添加或修改的代碼, 戳這裏

Demo 體驗地址:https://liuyib.github.io/blog/demo/game/google-dino/night-mode/

上一篇 下一篇
Chrome 小恐龍遊戲源碼探究六 -- 記錄遊戲分數 Chrome 小恐龍遊戲源碼探究八 -- 奔跑的小恐龍
相關文章
相關標籤/搜索