文章首发于我的个人博客
前言
上一篇文章:《Chrome 小恐龙游戏源码探究六 — 记录游戏分数》实现了游戏分数、最高分数的记录和绘制。这一篇文章中将实现昼夜模式交替的的效果。
夜晚模式
定义夜晚模式类 NightMode:
/**
* 夜晚模式
* @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();}
相关的配置参数:
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];
补充本篇文章中会用到的一些数据:
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 原型链上的方法:
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 来进行调用。
修改 Horizon 类:
function Horizon(canvas, spritePos, dimensions, gapCoefficient) {
// ...
// 夜晚模式
+ this.nightMode = null;
}
初始化 NightMode 类:
Horizon.prototype = {init: function () {
// ...
+ this.nightMode = new NightMode(this.canvas, this.spritePos.MOON,
+ this.dimensions.WIDTH);
},
};
更新夜晚模式:
Horizon.prototype = {- update: function (deltaTime, currentSpeed, updateObstacles) {+ update: function (deltaTime, currentSpeed, updateObstacles, showNightMode) {
// ...
+ this.nightMode.update(showNightMode);
},
};
然后修改 Runner 的 update
方法:
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 米触发一次。效果如下:
查看添加的代码,戳这里
Demo 体验地址:https://liuyib.github.io/pages/demo/games/google-dino/night-mode/
上一篇 | 下一篇 | Chrome 小恐龙游戏源码探究六 — 记录游戏分数 | Chrome 小恐龙游戏源码探究八 — 奔跑的小恐龙 |