文章首发于个人 GitHub 博客
上一篇文章:《Chrome 小恐龙游戏源码探究六 -- 记录游戏分数》实现了游戏分数、最高分数的记录和绘制。这一篇文章中将实现昼夜模式交替的的效果。css
定义夜晚模式类 NightMode
:git
/** * 夜晚模式 * @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(); }
相关的配置参数:github
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];
补充本篇文章中会用到的一些数据:canvas
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
原型链上的方法:segmentfault
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
来进行调用。dom
修改 Horizon
类:动画
function Horizon(canvas, spritePos, dimensions, gapCoefficient) { // ... + // 夜晚模式 + this.nightMode = null; }
初始化 NightMode
类:this
Horizon.prototype = { init: function () { // ... + this.nightMode = new NightMode(this.canvas, this.spritePos.MOON, + this.dimensions.WIDTH); }, };
更新夜晚模式:google
Horizon.prototype = { - update: function (deltaTime, currentSpeed, updateObstacles) { + update: function (deltaTime, currentSpeed, updateObstacles, showNightMode) { // ... + this.nightMode.update(showNightMode); }, };
而后修改 Runner
的 update
方法:spa
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/blog/demo/game/google-dino/night-mode/
上一篇 | 下一篇 |
Chrome 小恐龙游戏源码探究六 -- 记录游戏分数 | Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙 |