文章首發於個人 GitHub 博客
上一篇文章:《Chrome 小恐龍遊戲源碼探究四 -- 隨機繪製雲朵》 實現了雲朵的隨機繪製,這一篇文章中將實現:一、仙人掌、翼龍障礙物的繪製 二、遊戲速度的改變git
障礙物的類型有兩種:仙人掌和翼龍。翼龍每次只能有一隻,高度隨機,仙人掌一次能夠繪製多個,一次繪製的數目隨機。對於繪製障礙物的關鍵是:保證合適的大小和間隔。例如:不能在遊戲剛開始速度很慢的時候就繪製一個很寬的障礙物,不然是跳不過去的。也不能在遊戲速度較快的狀況下,兩個障礙物間隔生成的很窄,不然當跳過第一個障礙物後,必定會撞到下一個障礙物。github
有關障礙物的碰撞檢測部分這裏先不實現,會放在後面的單獨一章來說。
定義障礙物類 Obstacle
:canvas
/** * 障礙物類 * @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); }
相關的配置參數:segmentfault
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
原型鏈上添加方法:dom
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
類時傳的參數:this
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); }, };
定義添加障礙物的方法:google
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; }, };
而後定義更新障礙物的方法:spa
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
方法來調用 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; // 速度增長一個加速度的值 + } } // ... }, };
這樣就完整實現了障礙物的繪製和移動。效果以下:
查看添加或修改的代碼, 戳這裏
Demo 體驗地址:https://liuyib.github.io/blog/demo/game/google-dino/add-obstacle/
上一篇 | 下一篇 |
Chrome 小恐龍遊戲源碼探究四 -- 隨機繪製雲朵 | Chrome 小恐龍遊戲源碼探究六 -- 記錄遊戲分數 |