在上一篇《Chrome自帶恐龍小游戲的源碼研究(三)》中實現了讓游戲晝夜交替,這一篇主要研究如何繪制障礙物。
障礙物有兩種:仙人掌和翼龍。仙人掌有大小兩種類型,可以同時並列多個;翼龍按高、中、低的隨機飛行高度出現,不可並行。仙人掌和地面有着相同的速度向左移動,翼龍則快一些或慢一些,因為添加了隨機的速度修正。我們使用一個障礙物列表管理它們,當它們移出屏幕外時則將其從列表中移除。同時再用一個列表記錄它們的類型:
1 Obstacle.obstacles = []; //存儲障礙物的數組 2 Obstacle.obstacleHistory = []; //記錄障礙物數組中障礙物的類型
障礙物的出現不能太頻繁,也不能太稀少,太頻繁立刻就gameover了,太稀少則沒有挑戰性,因此需要一定的規則來生成障礙物。每組障礙物之間應該有一段間隔作為落腳點,新生成的障礙物在這個間隔之外生成。如示意圖所示:
因此,先定義一個最大間距系數,下面會用這個系數生成隨機間距:
Obstacle.MAX_GAP_COEFFICIENT = 1.5; //障礙物最大間距系數
另外,還需要對障礙物進行一些約束及配置:
1 //每組障礙物的最大數量 2 Obstacle.MAX_OBSTACLE_LENGTH = 3; 3 //相鄰的障礙物類型的最大重復數 4 Obstacle.MAX_OBSTACLE_DUPLICATION = 2; 5 6 Obstacle.types = [ 7 { 8 type: 'CACTUS_SMALL', //小仙人掌 9 width: 17, //寬 10 height: 35, //高 11 yPos: 105, //在畫布上的y坐標 12 multipleSpeed: 4, 13 minGap: 120, //最小間距 14 minSpeed: 0 //最低速度 15 }, 16 { 17 type: 'CACTUS_LARGE', //大仙人掌 18 width: 25, 19 height: 50, 20 yPos: 90, 21 multipleSpeed: 7, 22 minGap: 120, 23 minSpeed: 0 24 }, 25 { 26 type: 'PTERODACTYL', //翼龍 27 width: 46, 28 height: 40, 29 yPos: [ 100, 75, 50 ], //有高、中、低三種高度 30 multipleSpeed: 999, 31 minSpeed: 8.5, 32 minGap: 150, 33 numFrames: 2, //有兩個動畫幀 34 frameRate: 1000/6, //動畫幀的切換速率,這里為一秒6幀 35 speedOffset: .8 //速度修正 36 } 37 ];
障礙物的所有實現由構造函數Obstacle完成,下面是它的實現代碼:

1 /** 2 * 繪制障礙物構造函數 3 * @param canvas 4 * @param type 障礙物的類型 5 * @param spriteImgPos 雪碧圖坐標 6 * @param dimensions 屏幕尺寸 7 * @param gapCoefficient 障礙物間隙 8 * @param speed 障礙物移動速度 9 * @param opt_xOffset 障礙物水平偏移量 10 * @constructor 11 */ 12 function Obstacle(canvas,type,spriteImgPos,dimensions,gapCoefficient,speed,opt_xOffset) { 13 this.ctx = canvas.getContext('2d'); 14 this.spritePos = spriteImgPos; 15 //障礙物類型(仙人掌、翼龍) 16 this.typeConfig = type; 17 this.gapCoefficient = gapCoefficient; 18 //每個障礙物的數量(1~3) 19 this.size = getRandomNum(1,Obstacle.MAX_OBSTACLE_LENGTH); 20 this.dimensions = dimensions; 21 //表示該障礙物是否可以被移除 22 this.remove = false; 23 //水平坐標 24 this.xPos = dimensions.WIDTH + (opt_xOffset || 0); 25 this.yPos = 0; 26 this.width = 0; 27 this.gap = 0; 28 this.speedOffset = 0; //速度修正 29 30 //障礙物的動畫幀 31 this.currentFrame = 0; 32 //動畫幀切換的計時器 33 this.timer = 0; 34 35 this.init(speed); 36 } 37 ``` 38 39 實例方法: 40 ```javascript 41 Obstacle.prototype = { 42 init:function(speed) { 43 //如果隨機障礙物是翼龍,則只出現一只 44 //翼龍的multipleSpeed是999,遠大於speed 45 if (this.size > 1 && this.typeConfig.multipleSpeed > speed) { 46 this.size = 1; 47 } 48 //障礙物的總寬度等於單個障礙物的寬度乘以個數 49 this.width = this.typeConfig.width * this.size; 50 51 //若障礙物的縱坐標是一個數組 52 //則隨機選取一個 53 if (Array.isArray(this.typeConfig.yPos)) { 54 var yPosConfig = this.typeConfig.yPos; 55 this.yPos = yPosConfig[getRandomNum(0, yPosConfig.length - 1)]; 56 } else { 57 this.yPos = this.typeConfig.yPos; 58 } 59 60 this.draw(); 61 62 //對翼龍的速度進行修正,讓它看起來有的飛得快一些,有些飛得慢一些 63 if (this.typeConfig.speedOffset) { 64 this.speedOffset = Math.random() > 0.5 ? this.typeConfig.speedOffset : 65 -this.typeConfig.speedOffset; 66 } 67 68 //障礙物之間的間隙,與游戲速度有關 69 this.gap = this.getGap(this.gapCoefficient, speed); 70 }, 71 //障礙物之間的間隔,gapCoefficient為間隔系數 72 getGap: function(gapCoefficient, speed) { 73 var minGap = Math.round(this.width * speed + 74 this.typeConfig.minGap * gapCoefficient); 75 var maxGap = Math.round(minGap * Obstacle.MAX_GAP_COEFFICIENT); 76 return getRandomNum(minGap, maxGap); 77 }, 78 //判斷障礙物是否移出屏幕外 79 isVisible: function() { 80 return this.xPos + this.width > 0; 81 }, 82 draw:function() { 83 //障礙物寬高 84 var sourceWidth = this.typeConfig.width; 85 var sourceHeight = this.typeConfig.height; 86 87 //根據障礙物數量計算障礙物在雪碧圖上的x坐標 88 //this.size的取值范圍是1~3 89 var sourceX = (sourceWidth * this.size) * (0.5 * (this.size - 1)) + 90 this.spritePos.x; 91 92 // 如果當前動畫幀大於0,說明障礙物類型是翼龍 93 // 更新翼龍的雪碧圖x坐標使其匹配第二幀動畫 94 if (this.currentFrame > 0) { 95 sourceX += sourceWidth * this.currentFrame; 96 } 97 this.ctx.drawImage(imgSprite, 98 sourceX, this.spritePos.y, 99 sourceWidth * this.size, sourceHeight, 100 this.xPos, this.yPos, 101 sourceWidth * this.size, sourceHeight); 102 }, 103 //單個障礙物的移動 104 update:function(deltaTime, speed) { 105 //如果障礙物還沒有移出屏幕外 106 if (!this.remove) { 107 //如果有速度修正則修正速度 108 if (this.typeConfig.speedOffset) { 109 speed += this.speedOffset; 110 } 111 //更新x坐標 112 this.xPos -= Math.floor((speed * FPS / 1000) * deltaTime); 113 114 // Update frame 115 if (this.typeConfig.numFrames) { 116 this.timer += deltaTime; 117 if (this.timer >= this.typeConfig.frameRate) { 118 //在兩個動畫幀之間來回切換以達到動畫效果 119 this.currentFrame = 120 this.currentFrame == this.typeConfig.numFrames - 1 ? 121 0 : this.currentFrame + 1; 122 this.timer = 0; 123 } 124 } 125 this.draw(); 126 127 if (!this.isVisible()) { 128 this.remove = true; 129 } 130 } 131 }, 132 //管理多個障礙物移動 133 updateObstacles: function(deltaTime, currentSpeed) { 134 //保存一個障礙物列表的副本 135 var updatedObstacles = Obstacle.obstacles.slice(0); 136 137 for (var i = 0; i < Obstacle.obstacles.length; i++) { 138 var obstacle = Obstacle.obstacles[i]; 139 obstacle.update(deltaTime, currentSpeed); 140 141 //移除被標記為刪除的障礙物 142 if (obstacle.remove) { 143 updatedObstacles.shift(); 144 } 145 } 146 Obstacle.obstacles = updatedObstacles; 147 148 if(Obstacle.obstacles.length > 0) { 149 //獲取障礙物列表中的最后一個障礙物 150 var lastObstacle = Obstacle.obstacles[Obstacle.obstacles.length - 1]; 151 152 //若滿足條件則添加障礙物 153 if (lastObstacle && 154 lastObstacle.isVisible() && 155 (lastObstacle.xPos + lastObstacle.width + lastObstacle.gap) < 156 this.dimensions.WIDTH) { 157 this.addNewObstacle(currentSpeed); 158 } 159 } else {//若障礙物列表中沒有障礙物則立即添加 160 this.addNewObstacle(currentSpeed); 161 } 162 }, 163 //隨機添加障礙 164 addNewObstacle:function (currentSpeed) { 165 //隨機選取一種類型的障礙 166 var obstacleTypeIndex = getRandomNum(0,Obstacle.types.length - 1); 167 var obstacleType = Obstacle.types[obstacleTypeIndex]; 168 169 //檢查隨機取到的障礙物類型是否與前兩個重復 170 //或者檢查其速度是否合法,這樣可以保證游戲在低速時不出現翼龍 171 //如果檢查不通過,則重新再選一次直到通過為止 172 if(this.duplicateObstacleCheck(obstacleType.type) || currentSpeed < obstacleType.minSpeed) { 173 this.addNewObstacle(currentSpeed); 174 } else { 175 //檢查通過后,獲取其雪碧圖中的坐標 176 var obstacleSpritePos = this.spritePos[obstacleType.type]; 177 //生成新的障礙物並存入數組 178 Obstacle.obstacles.push(new Obstacle(c,obstacleType,obstacleSpritePos,this.dimensions, 179 this.gapCoefficient,currentSpeed,obstacleType.width)); 180 //同時將障礙物的類型存入history數組 181 Obstacle.obstacleHistory.unshift(obstacleType.type); 182 } 183 184 //若history數組的長度大於1,則清空最前面的兩個 185 if (Obstacle.obstacleHistory.length > 1) { 186 Obstacle.obstacleHistory.splice(Obstacle.MAX_OBSTACLE_DUPLICATION); 187 } 188 }, 189 //檢查障礙物是否超過允許的最大重復數 190 duplicateObstacleCheck:function(nextObstacleType) { 191 var duplicateCount = 0; 192 //與history數組中的障礙物類型比較,最大只允許重得兩次 193 for(var i = 0; i < Obstacle.obstacleHistory.length; i++) { 194 duplicateCount = Obstacle.obstacleHistory[i] === nextObstacleType ? duplicateCount + 1 : 0; 195 } 196 return duplicateCount >= Obstacle.MAX_OBSTACLE_DUPLICATION; 197 } 198 };
最后在此前的基礎上添加一段測試代碼:

1 window.onload = function () { 2 var h = new HorizonLine(c,spriteDefinition.HORIZON); 3 var cloud = new Cloud(c,spriteDefinition.CLOUD,DEFAULT_WIDTH); 4 var night = new NightMode(c,spriteDefinition.MOON,DEFAULT_WIDTH); 5 var obstacle = new Obstacle(c,Obstacle.types[0],spriteDefinition,{WIDTH:600},0.6,1); 6 var startTime = 0; 7 var deltaTime; 8 var speed = 3; 9 (function draw(time) { 10 gameFrame++; 11 if(speed < 13.5) { 12 speed += 0.01; 13 } 14 ctx.clearRect(0,0,600,150); 15 time = time || 0; 16 deltaTime = time - startTime; 17 h.update(deltaTime,speed); 18 cloud.updateClouds(0.2); 19 night.invert(deltaTime); 20 obstacle.updateObstacles(deltaTime,speed); 21 startTime = time; 22 window.requestAnimationFrame(draw,c); 23 })(); 24 };
最終得到的效果: