共计 7917 个字符,预计需要花费 20 分钟才能阅读完成。
前言
上一篇文章:《Chrome 小恐龙游戏源码探究四 — 随机绘制云朵》实现了云朵的随机绘制,这一篇文章中将实现:1、仙人掌、翼龙障碍物的绘制 2、游戏速度的改变
障碍物的类型有两种:仙人掌和翼龙。翼龙每次只能有一只,高度随机,仙人掌一次可以绘制多个,一次绘制的数目随机。对于绘制障碍物的关键是:保证合适的大小和间隔。例如:不能在游戏刚开始速度很慢的时候就绘制一个很宽的障碍物,否则是跳不过去的。也不能在游戏速度较快的情况下,两个障碍物间隔生成的很窄,否则当跳过第一个障碍物后,一定会撞到下一个障碍物。
有关障碍物的碰撞检测部分这里先不实现,会放在后面的单独一章来讲。
障碍物类 Obstacle
定义 Obstacle 类:
/** | |
* 障碍物类 | |
* @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); | |
} |
障碍物的有关参数:
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 原型链上添加方法:
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 类时传的参数:
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); | |
}, | |
}; |
定义随机添加障碍物的方法:
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; | |
}, | |
}; |
然后定义更新障碍物的方法,并调用上面添加障碍物的 addNewObstacle
方法:
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); | |
} | |
}, | |
}; |
接下来修改 Horizon 的 update
方法来调用上面定义的 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/pages/demo/games/google-dino/add-obstacle/
上一篇 | 下一篇 |
Chrome 小恐龙游戏源码探究四 — 随机绘制云朵 | Chrome 小恐龙游戏源码探究六 — 记录游戏分数 |