Chrome 小恐龍遊戲源碼探究六 -- 記錄遊戲分數

文章首發於個人 GitHub 博客

前言

上一篇文章:《Chrome 小恐龍遊戲源碼探究五 -- 隨機繪製障礙》 實現了障礙物仙人掌和翼龍的繪製。這一篇將實現當前分數、最高分數的記錄和繪製。git

在遊戲中,小恐龍移動的距離就是遊戲的分數。分數每達 100,就會有閃動的特效。首次進行遊戲的時候,因爲沒有記錄過遊戲的歷史得分,因此不會顯示最高分,只有當第一次 game over 後才能顯示歷史最高分。github

分數記錄

定義分數類 DistanceMeterweb

/**
 * 記錄移動的距離(分數等於移動距離)
 * @param {HTMLCanvasElement} canvas 畫布
 * @param {Object} spritePos 圖片在雪碧圖中的位置
 * @param {Number} canvasWidth 畫布的寬度
 */
function DistanceMeter(canvas, spritePos, canvasWidth) {
  this.canvas = canvas;
  this.ctx = canvas.getContext('2d');

  this.config = DistanceMeter.config;
  this.spritePos = spritePos;

  this.x = 0;               // 分數顯示在 canvas 中的 x 座標
  this.y = 5;

  this.maxScore = 0;        // 遊戲分數上限
  this.highScore = [];      // 存儲最高分數的每一位數字

  this.digits = [];         // 存儲分數的每一位數字
  this.achievement = false; // 是否進行閃動特效
  this.defaultString = '';  // 遊戲的默認距離(00000)
  this.flashTimer = 0;      // 動畫計時器
  this.flashIterations = 0; // 特效閃動的次數

  this.maxScoreUnits = this.config.MAX_DISTANCE_UNITS; // 分數的最大位數

  this.init(canvasWidth);
}

有關的配置參數:canvas

DistanceMeter.config = {
  MAX_DISTANCE_UNITS: 5,          // 分數的最大位數
  ACHIEVEMENT_DISTANCE: 100,      // 每 100 米觸發一次閃動特效
  COEFFICIENT: 0.025,             // 將像素距離轉換爲比例單位的係數
  FLASH_DURATION: 1000 / 4,       // 一閃的時間(一次閃動分別兩閃:從有到無,從無到有)
  FLASH_ITERATIONS: 3,            // 閃動的次數
};

DistanceMeter.dimensions = {
  WIDTH: 10,
  HEIGHT: 13,
  DEST_WIDTH: 11, // 加上間隔後每一個數字的寬度
};

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

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

+ this.msPerFrame = 1000 / FPS; // 每幀的時間
},

Runner.spriteDefinition = {
  LDPI: {
    //...
    
+   TEXT_SPRITE: {x: 655, y: 2}, // 文字
  },
};

DistanceMeter 上添加方法:數組

DistanceMeter.prototype = {
  // 初始化分數
  init: function (width) {
    var maxDistanceStr = '';     // 遊戲的最大距離

    this.calcXPos(width);        // 計算分數顯示在 canvas 中的 x 座標

    for (var i = 0; i < this.maxScoreUnits; i++) {
      this.draw(i, 0);           // 第一次遊戲,不繪製最高分
      this.defaultString += '0'; // 默認初始分數 00000
      maxDistanceStr += '9';     // 默認最大分數 99999
    }
    
    this.maxScore = parseInt(maxDistanceStr);
  },
  // 計算 x 座標
  calcXPos: function (canvasWidth) {
    this.x = canvasWidth - (DistanceMeter.dimensions.DEST_WIDTH *
      (this.maxScoreUnits + 1));
  },
  /**
   * 將分數繪製到 canvas 上
   * @param {Number} digitPos 數字在分數中的位置
   * @param {Number} value 數字的具體值(0-9)
   * @param {Boolean} opt_highScore 是否顯示最高分
   */
  draw: function (digitPos, value, opt_highScore) {
    // 在雪碧圖中的座標
    var sourceX = this.spritePos.x + DistanceMeter.dimensions.WIDTH * value;
    var sourceY = this.spritePos.y + 0;
    var sourceWidth = DistanceMeter.dimensions.WIDTH;
    var sourceHeight = DistanceMeter.dimensions.HEIGHT;

    // 繪製到 canvas 時的座標
    var targetX = digitPos * DistanceMeter.dimensions.DEST_WIDTH;
    var targetY = this.y;
    var targetWidth = DistanceMeter.dimensions.WIDTH;
    var targetHeight = DistanceMeter.dimensions.HEIGHT;

    this.ctx.save();

    if (opt_highScore) { // 顯示最高分
      var hightScoreX = this.x - (this.maxScoreUnits * 2) *
        DistanceMeter.dimensions.WIDTH;

      this.ctx.translate(hightScoreX, this.y);
    } else {            // 不顯示最高分
      this.ctx.translate(this.x, this.y);
    }

    this.ctx.drawImage(
      Runner.imageSprite,
      sourceX, sourceY,
      sourceWidth, sourceHeight,
      targetX, targetY,
      targetWidth, targetHeight
    );

    this.ctx.restore();
  },
  /**
   * 將遊戲移動的像素距離轉換爲真實的距離
   * @param {Number} distance 遊戲移動的像素距離
   */
  getActualDistance: function (distance) {
    return distance ? Math.round(distance * this.config.COEFFICIENT) : 0;
  },
  // 更新分數
  update: function (deltaTime, distance) {
    var paint = true;      // 是否繪製分數
    var playSound = false; // 是否播放音效

    // 沒有進行閃動特效
    if (!this.achievement) {
      distance = this.getActualDistance(distance);

      // 分數超出上限時,上限增長一位數。超出上限兩位數時,分數置零
      if (distance > this.maxScore &&
        this.maxScoreUnits === this.config.MAX_DISTANCE_UNITS) {
        this.maxScoreUnits++;
        this.maxScore = parseInt(this.maxScore + '9');
      } else {
        this.distance = 0;
      }

      if (distance > 0) {
        // 觸發閃動特效
        if (distance % this.config.ACHIEVEMENT_DISTANCE == 0) {
          this.achievement = true;
          this.flashTimer = 0;
          playSound = true;
        }

        // 分數前面補零來湊位數
        var distanceStr = (this.defaultString + distance).substr(-this.maxScoreUnits);
        this.digits = distanceStr.split('');
      } else {
        // 將默認分數 00000 中的每一位數字存到數組中
        this.digits = this.defaultString.split('');
      }
    } else {
      // 控制特效的閃動次數
      if (this.flashIterations <= this.config.FLASH_ITERATIONS) {
        this.flashTimer += deltaTime;

        // 第一閃不繪製數字
        if (this.flashTimer < this.config.FLASH_DURATION) {
          paint = false;
        }
        // 進行了兩閃,閃動次數加一
        else if (this.flashTimer > this.config.FLASH_DURATION * 2) {
          this.flashTimer = 0;
          this.flashIterations++;
        }
      } else { // 閃動特效結束
        this.achievement = false;
        this.flashIterations = 0;
        this.flashTimer = 0;
      }
    }

    // 繪製當前分
    if (paint) {
      for (var i = this.digits.length - 1; i >= 0; i--) {
        this.draw(i, parseInt(this.digits[i]));
      }
    }

    // 繪製最高分
    this.drawHighScore();
    return playSound;
  },
  // 繪製最高分
  drawHighScore: function () {
    this.ctx.save();
    this.ctx.globalAlpha = 0.8;

    for (var i = this.highScore.length - 1; i >= 0; i--) {
      this.draw(i, parseInt(this.highScore[i], 10), true);
    }
    this.ctx.restore();
  },
  /**
   * 將遊戲的最高分數存入數組
   * @param {Number} distance 遊戲移動的像素距離
   */
  setHighScore: function (distance) {
    distance = this.getActualDistance(distance);
    var highScoreStr = (this.defaultString
      + distance).substr(-this.maxScoreUnits);
    
    // 分數前面字母 H、I 在雪碧圖中位於數字後面,也就是第 十、11 位置
    this.highScore = ['10', '11', ''].concat(highScoreStr.split(''));
  },
};

下面是對分數類的調用。動畫

首先給 Runner 類添加屬性:this

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

+ this.distanceMeter = null;     // 距離計數類
+ this.distanceRan = 0;          // 遊戲移動距離
+ this.highestScore = 0;         // 最高分
}

而後初始化分數類 DistanceMetergoogle

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

+   // 加載距離計數器類 DistanceMeter
+   this.distanceMeter = new DistanceMeter(this.canvas,
+     this.spriteDef.TEXT_SPRITE, this.dimensions.WIDTH);
  },
};

更新遊戲分數(移動距離):spa

Runner.prototype = {
  update: function () {
    this.updatePending = false; // 等待更新

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

+     this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;

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

+     var playAchievementSound = this.distanceMeter.update(deltaTime,
+       Math.ceil(this.distanceRan));
    }

    // ...
  },
};

這樣就實現了分數的繪製,效果以下:

clipboard.png

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

上面定義了保存、繪製遊戲最高分的方法,但尚未調用,如今只要在遊戲結束時,將分數保存下來,就能實現最高分的繪製。添加 gameOver 方法:

Runner.prototype = {
  // 遊戲結束
  gameOver: function () {
    this.stop();

    if (this.distanceRan > this.highestScore) {
      this.highestScore = Math.ceil(this.distanceRan);
      this.distanceMeter.setHighScore(this.highestScore); // 保存最高分
    }

    // 重置時間
    this.time = getTimeStamp();
  },
};

這裏爲了演示,當頁面失焦時,結束遊戲(等後面講到實現遊戲結束時,須要刪除這裏的臨時代碼):

Runner.prototype = {
  onVisibilityChange: function (e) {
    if (document.hidden || document.webkitHidden || e.type == 'blur' ||
      document.visibilityState != 'visible') {
      this.stop();
      
+     this.gameOver();
    } else if (!this.crashed) {
      this.play();
    }
  },
};

效果以下:

clipboard.png

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

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

上一篇 下一篇
Chrome 小恐龍遊戲源碼探究五 -- 隨機繪製障礙 Chrome 小恐龍遊戲源碼探究七 -- 晝夜模式交替
相關文章
相關標籤/搜索