blog
blog copied to clipboard
Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙
前言
上一篇文章:《Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替》实现了游戏昼夜模式的交替,这一篇文章中,将实现:1、小恐龙的绘制 2、键盘对小恐龙的控制 3、页面失焦后,重新聚焦会重置小恐龙的状态。
绘制静态的小恐龙
定义小恐龙类 Trex
:
/**
* 小恐龙类
* @param {HTMLCanvasElement} canvas 画布
* @param {Object} spritePos 图片在雪碧图中的坐标
*/
function Trex(canvas, spritePos) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.spritePos = spritePos;
this.xPos = 0;
this.yPos = 0;
this.groundYPos = 0; // 小恐龙在地面上时的 y 坐标
this.currentFrame = 0; // 当前的动画帧
this.currentAnimFrames = []; // 存储当前状态的动画帧在雪碧图中的 x 坐标
this.blinkDelay = 0; // 眨眼间隔的时间(随 机)
this.blinkCount = 0; // 眨眼次数
this.animStartTime = 0; // 小恐龙眨眼动画开始时间
this.timer = 0; // 计时器
this.msPerFrame = 1000 / FPS; // 帧率
this.status = Trex.status.WAITING; // 当前的状态
this.config = Trex.config;
this.jumping = false; // 是否跳跃
this.ducking = false; // 是否闪避(俯身)
this.jumpVelocity = 0; // 跳跃的速度
this.reachedMinHeight = false; // 是否达到最低高度
this.speedDrop = false; // 是否加速下降
this.jumpCount = 0; // 跳跃的次数
this.jumpspotX = 0; // 跳跃点的 x 坐标
this.init();
}
相关的配置参数:
查看内容
Trex.config = {
GRAVITY: 0.6, // 引力
WIDTH: 44, // 站立时的宽度
HEIGHT: 47,
WIDTH_DUCK: 59, // 俯身时的宽度
HEIGHT_DUCK: 25,
MAX_JUMP_HEIGHT: 30, // 最大跳跃高度
MIN_JUMP_HEIGHT: 30, // 最小跳跃高度
SPRITE_WIDTH: 262, // 站立的小恐龙在雪碧图中的总宽度
DROP_VELOCITY: -5, // 下落的速度
INITIAL_JUMP_VELOCITY: -10, // 初始跳跃速度
SPEED_DROP_COEFFICIENT: 3, // 下落时的加速系数(越大下落的越快)
INTRO_DURATION: 1500, // 开场动画的时间
START_X_POS: 50, // 开场动画结束后,小恐龙在 canvas 上的 x 坐标
};
Trex.BLINK_TIMING = 7000; // 眨眼最大间隔的时间
// 小恐龙的状态
Trex.status = {
CRASHED: 'CRASHED', // 撞到障碍物
DUCKING: 'DUCKING', // 正在闪避(俯身)
JUMPING: 'JUMPING', // 正在跳跃
RUNNING: 'RUNNING', // 正在奔跑
WAITING: 'WAITING', // 正在等待(未开始游戏)
};
// 为不同的状态配置不同的动画帧
Trex.animFrames = {
WAITING: {
frames: [44, 0],
msPerFrame: 1000 / 3
},
RUNNING: {
frames: [88, 132],
msPerFrame: 1000 / 12
},
CRASHED: {
frames: [220],
msPerFrame: 1000 / 60
},
JUMPING: {
frames: [0],
msPerFrame: 1000 / 60
},
DUCKING: {
frames: [264, 323],
msPerFrame: 1000 / 8
},
};
补充本篇文章中会用到的一些数据:
Runner.config = {
// ...
BOTTOM_PAD: 10, // 小恐龙距 canvas 底部的距离
MAX_BLINK_COUNT: 3, // 小恐龙的最大眨眼次数
};
Runner.spriteDefinition = {
LDPI: {
// ...
TREX: {x: 848, y: 2}, // 小恐龙
},
};
然后来看下 Trex
原型链上的方法。我们首先来绘制静态的小恐龙:
Trex.prototype = {
// 初始化小恐龙
init: function() {
// 获取小恐龙站在地面上时的 y 坐标
this.groundYPos = Runner.defaultDimensions.HEIGHT - this.config.HEIGHT -
Runner.config.BOTTOM_PAD;
this.yPos = this.groundYPos; // 小恐龙的 y 坐标初始化
this.draw(0, 0); // 绘制小恐龙的第一帧图片
},
/**
* 绘制小恐龙
* @param {Number} x 当前帧相对于第一帧的 x 坐标
* @param {Number} y 当前帧相对于第一帧的 y 坐标
*/
draw: function(x, y) {
// 在雪碧图中的坐标
var sourceX = x + this.spritePos.x;
var sourceY = y + this.spritePos.y;
// 在雪碧图中的宽高
var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?
this.config.WIDTH_DUCK : this.config.WIDTH;
var sourceHeight = this.config.HEIGHT;
// 绘制到 canvas 上时的高度
var outputHeight = sourceHeight;
// 躲避状态.
if (this.ducking && this.status != Trex.status.CRASHED) {
this.ctx.drawImage(
Runner.imageSprite,
sourceX, sourceY,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
this.config.WIDTH_DUCK, outputHeight
);
} else {
// 躲闪状态下撞到障碍物
if (this.ducking && this.status == Trex.status.CRASHED) {
this.xPos++;
}
// 奔跑状态
this.ctx.drawImage(
Runner.imageSprite,
sourceX, sourceY,
sourceWidth, sourceHeight,
this.xPos, this.yPos,
this.config.WIDTH, outputHeight
);
}
this.ctx.globalAlpha = 1;
},
};
前面进入街机模式那一章中,用到了 Trex 类中的数据,临时定义了 Trex 类,别忘了将其删除。
接下来需要通过 Runner
类调用 Trex
类。添加属性用于存储小恐龙类的实例:
function Runner(containerSelector, opt_config) {
// ...
+ this.tRex = null; // 小恐龙
}
初始化小恐龙类:
Runner.prototype = {
init: function () {
// ...
+ // 加载小恐龙类
+ this.tRex = new Trex(this.canvas, this.spriteDef.TREX);
},
};
这样在游戏初始化时就绘制出了静态的小恐龙,如图:
实现眨眼效果
游戏初始化之后,小恐龙会随机眨眼睛。默认的是最多只能眨三次。下面将实现这个效果。
添加更新小恐龙的方法:
Trex.prototype = {
/**
* 更新小恐龙
* @param {Number} deltaTime 间隔时间
* @param {String} opt_status 小恐龙的状态
*/
update: function(deltaTime, opt_status) {
this.timer += deltaTime;
// 更新状态的参数
if (opt_status) {
this.status = opt_status;
this.currentFrame = 0;
this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;
this.currentAnimFrames = Trex.animFrames[opt_status].frames;
if (opt_status == Trex.status.WAITING) {
this.animStartTime = getTimeStamp(); // 设置眨眼动画开始的时间
this.setBlinkDelay(); // 设置眨眼间隔的时间
}
}
if (this.status == Trex.status.WAITING) {
// 小恐龙眨眼
this.blink(getTimeStamp());
} else {
// 绘制动画帧
this.draw(this.currentAnimFrames[this.currentFrame], 0);
}
if (this.timer >= this.msPerFrame) {
// 更新当前动画帧,如果处于最后一帧就更新为第一帧,否则更新为下一帧
this.currentFrame = this.currentFrame ==
this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
// 重置计时器
this.timer = 0;
}
},
// 设置眨眼间隔的时间
setBlinkDelay: function() {
this.blinkDelay = Math.ceil(Math.random() * Trex.BLINK_TIMING);
},
// 小恐龙眨眼
blink: function (time) {
var deltaTime = time - this.animStartTime;
// 间隔时间大于随机获取的眨眼间隔时间才能眨眼
if (deltaTime >= this.blinkDelay) {
this.draw(this.currentAnimFrames[this.currentFrame], 0);
// 正在眨眼
if (this.currentFrame == 1) {
console.log('眨眼');
this.setBlinkDelay(); // 重新设置眨眼间隔的时间
this.animStartTime = time; // 更新眨眼动画开始的时间
this.blinkCount++; // 眨眼次数加一
}
}
},
};
然后将小恐龙初始更新为等待状态:
Trex.prototype = {
init: function () {
// ...
this.update(0, Trex.status.WAITING); // 初始为等待状态
},
};
最后在 Runner
的 update
方法中调用 Trex
的 update
方法来实现小恐龙眨眼:
Runner.prototype = {
update: function () {
// ...
// 游戏变为开始状态或小恐龙还没有眨三次眼
- if (this.playing) {
+ if (this.playing || (!this.activated &&
+ this.tRex.blinkCount < Runner.config.MAX_BLINK_COUNT)) {
+ this.tRex.update(deltaTime);
// 进行下一次更新
this.scheduleNextUpdate();
}
},
};
效果如下:
可以看到,眨眼的代码逻辑触发了 3 次,但是实际小恐龙只眨眼了 1 次。这就是前面说的,小恐龙默认最多只能眨三次眼。具体原因如下:
先来看下 Trex
的 update
方法中的这段代码:
if (this.timer >= this.msPerFrame) {
// 更新当前动画帧,如果处于最后一帧就更新为第一帧,否则更新为下一帧
this.currentFrame = this.currentFrame ==
this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;
// 重置计时器
this.timer = 0;
}
这段代码会将当前动画帧不断更新为下一帧。对于小恐龙来说就是不断切换睁眼和闭眼这两帧。如果当前帧为 “睁眼”,那么执行 blink
函数后小恐龙还是睁眼,也就是说实际小恐龙没眨眼;同理,只有当前帧为 “闭眼” 时,执行 blink
函数后,小恐龙才会真正的眨眼。
至于这样做的目的,就是为了防止小恐龙不停的眨眼睛。例如,将 blink
函数修改为:
// 小恐龙眨眼
blink: function () {
this.draw(this.currentAnimFrames[this.currentFrame], 0);
},
这样小恐龙会不停的眨眼睛。所以需要对其进行限制,这里 Chrome 开发人员的做法就是:设置一个间隔时间,当小恐龙眨眼的间隔时间大于这个设置的间隔时间,并且当前动画帧为 “闭眼” 时,才允许小恐龙眨眼睛。然后每次眨完眼后,重新设置眨眼间隔(默认设置为 0~7 秒),就实现了小恐龙的随机眨眼。
小恐龙的开场动画
下面来实现小恐龙对键盘按键的响应。
首先,当触发游戏彩蛋后,小恐龙会跳跃一次,并向右移动 50 像素(默认设置的是 50 像素)。
添加让小恐龙开始跳跃的方法:
Trex.prototype = {
// 开始跳跃
startJump: function(speed) {
if (!this.jumping) {
// 更新小恐龙为跳跃状态
this.update(0, Trex.status.JUMPING);
// 根据游戏的速度调整跳跃的速度
this.jumpVelocity = this.config.INITIAL_JUMP_VELOCITY - (speed / 10);
this.jumping = true;
this.reachedMinHeight = false;
this.speedDrop = false;
}
},
};
进行调用:
Runner.prototype = {
onKeyDown: function (e) {
if (!this.crashed && !this.paused) {
if (Runner.keyCodes.JUMP[e.keyCode]) {
e.preventDefault();
// ...
+ // 开始跳跃
+ if (!this.tRex.jumping && !this.tRex.ducking) {
+ this.tRex.startJump(this.currentSpeed);
+ }
}
}
},
};
这样,按下空格键后,小恐龙仍然会静止在地面上。接下来还需要更新动画帧才能实现小恐龙的奔跑动画。
添加更新小恐龙动画帧的方法:
Trex.prototype = {
// 更新小恐龙跳跃时的动画帧
updateJump: function(deltaTime) {
var msPerFrame = Trex.animFrames[this.status].msPerFrame; // 获取当前状态的帧率
var framesElapsed = deltaTime / msPerFrame;
// 加速下落
if (this.speedDrop) {
this.yPos += Math.round(this.jumpVelocity *
this.config.SPEED_DROP_COEFFICIENT * framesElapsed);
} else {
this.yPos += Math.round(this.jumpVelocity * framesElapsed);
}
// 跳跃的速度受重力的影响,向上逐渐减小,然后反向
this.jumpVelocity += this.config.GRAVITY * framesElapsed;
// 达到了最低允许的跳跃高度
if (this.yPos < this.minJumpHeight || this.speedDrop) {
this.reachedMinHeight = true;
}
// 达到了最高允许的跳跃高度
if (this.yPos < this.config.MAX_JUMP_HEIGHT || this.speedDrop) {
this.endJump(); // 结束跳跃
}
// 重新回到地面,跳跃完成
if (this.yPos > this.groundYPos) {
this.reset(); // 重置小恐龙的状态
this.jumpCount++; // 跳跃次数加一
}
},
// 跳跃结束
endJump: function() {
if (this.reachedMinHeight &&
this.jumpVelocity < this.config.DROP_VELOCITY) {
this.jumpVelocity = this.config.DROP_VELOCITY; // 下落速度重置为默认
}
},
// 重置小恐龙状态
reset: function() {
this.yPos = this.groundYPos;
this.jumpVelocity = 0;
this.jumping = false;
this.ducking = false;
this.update(0, Trex.status.RUNNING);
this.speedDrop = false;
this.jumpCount = 0;
},
};
其中 minJumpHeight
的属性值为:
Trex.prototype = {
init: function() {
+ // 最低跳跃高度
+ this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT;
// ...
},
}
然后进行调用:
Runner.prototype = {
update: function () {
// ...
if (this.playing) {
this.clearCanvas();
+ if (this.tRex.jumping) {
+ this.tRex.updateJump(deltaTime);
+ }
this.runningTime += deltaTime;
var hasObstacles = this.runningTime > this.config.CLEAR_TIME;
// 刚开始 this.playingIntro 未定义 !this.playingIntro 为真
- if (!this.playingIntro) {
+ if (this.tRex.jumpCount == 1 && !this.playingIntro) {
this.playIntro(); // 执行开场动画
}
// ...
}
// ...
},
};
这样在按下空格键后,小恐龙就会跳跃一次并进行奔跑动画。如图:
下面来实现效果:小恐龙第一次跳跃后,向右移动 50 像素。
修改 Trex
的 update
方法。当判断到正在执行开场动画时,移动小恐龙:
Trex.prototype = {
update: function(deltaTime, opt_status) {
this.timer += deltaTime;
// 更新状态的参数
if (opt_status) {
// ...
}
// 正在执行开场动画,将小恐龙向右移动 50 像素
+ if (this.playingIntro && this.xPos < this.config.START_X_POS) {
+ this.xPos += Math.round((this.config.START_X_POS /
+ this.config.INTRO_DURATION) * deltaTime);
+ }
// ...
},
};
可以看出当 playingIntro
属性为 true
时,小恐龙就会向右移动。所以需要通过控制这个属性的值来控制小恐龙第一次跳跃后的移动。
修改 Runner
上的 playIntro
方法,将小恐龙标记为正在执行开场动画:
Runner.prototype = {
playIntro: function () {
if (!this.activated && !this.crashed) {
+ this.tRex.playingIntro = true; // 小恐龙执行开场动画
// ...
}
},
};
然后需要在开始游戏后也就是执行 startGame
方法时,结束小恐龙的开场动画:
Runner.prototype = {
startGame: function () {
this.setArcadeMode(); // 进入街机模式
+ this.tRex.playingIntro = false; // 小恐龙的开场动画结束
// ...
},
};
效果如下:
可以很明显的看到,小恐龙在第一次跳跃后向右移动了一段距离(默认 50 像素)。
使用键盘控制小恐龙
在这个游戏中,当按下 ↓
键后,如果小恐龙正在跳跃,就会快速下落,如果小恐龙在地上,就会进入躲闪状态,下面来实现这些效果。
加速下落:
Trex.prototype = {
// 设置小恐龙为加速下落,立即取消当前的跳跃
setSpeedDrop: function() {
this.speedDrop = true;
this.jumpVelocity = 1;
},
};
设置小恐龙是否躲闪:
Trex.prototype = {
// 设置小恐龙奔跑时是否躲闪
setDuck: function(isDucking) {
if (isDucking && this.status != Trex.status.DUCKING) { // 躲闪状态
this.update(0, Trex.status.DUCKING);
this.ducking = true;
} else if (this.status == Trex.status.DUCKING) { // 奔跑状态
this.update(0, Trex.status.RUNNING);
this.ducking = false;
}
},
};
在 onKeyDown
方法中调用:
Runner.prototype = {
onKeyDown: function () {
if (!this.crashed && !this.paused) {
if (Runner.keyCodes.JUMP[e.keyCode]) {
// ...
+ } else if (this.playing && Runner.keyCodes.DUCK[e.keyCode]) {
+ e.preventDefault();
+
+ if (this.tRex.jumping) {
+ this.tRex.setSpeedDrop(); // 加速下落
+ } else if (!this.tRex.jumping && !this.tRex.ducking) {
+ this.tRex.setDuck(true); // 进入躲闪状态
+ }
+ }
}
},
};
这样就实现了前面所说的效果。但是小恐龙进入躲闪状态后,如果松开按键并不会重新站起来。因为现在还没有定义松开键盘按键时响应的事件。下面来定义:
Runner.prototype = {
onKeyUp: function(e) {
var keyCode = String(e.keyCode);
if (Runner.keyCodes.DUCK[keyCode]) { // 躲避状态
this.tRex.speedDrop = false;
this.tRex.setDuck(false);
}
},
};
然后调用,修改 handleEvent
方法:
Runner.prototype = {
handleEvent: function (e) {
return (function (eType, events) {
switch (eType) {
// ...
+ case events.KEYUP:
+ this.onKeyUp(e);
+ break;
default:
break;
}
}.bind(this))(e.type, Runner.events);
},
};
效果如下:
第一次跳是正常下落,第二次跳是加速下落
处理小恐龙的跳跃
小恐龙的跳跃分为大跳和小跳,如图:
要实现这个效果,只需要在 ↑
键被松开时,立即结束小恐龙的跳跃即可。
修改 onKeyUp
方法:
Runner.prototype = {
onKeyUp: function(e) {
var keyCode = String(e.keyCode);
+ var isjumpKey = Runner.keyCodes.JUMP[keyCode];
+ if (this.isRunning() && isjumpKey) { // 跳跃
+ this.tRex.endJump();
} else if (Runner.keyCodes.DUCK[keyCode]) { // 躲避状态
this.tRex.speedDrop = false;
this.tRex.setDuck(false);
}
},
};
其中 isRunning
方法定义如下:
Runner.prototype = {
// 是否游戏正在进行
isRunning: function() {
return !!this.raqId;
},
};
这样就实现了小恐龙的大跳和小跳。
最后是要实现的效果是:如果页面失焦时,小恐龙正在跳跃,就重置小恐龙的状态(也就是会立即回到地面上)。这个效果实现很简单,直接调用前面定义的 reset
方法即可:
Runner.prototype = {
play: function () {
if (!this.crashed) {
// ...
+ this.tRex.reset();
}
},
};
效果如下:
查看添加或修改的代码,戳这里
Demo 体验地址:https://liuyib.github.io/demo/game/google-dino/dino-gogogo/
上一篇 | 下一篇 | Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替 | Chrome 小恐龙游戏源码探究九 -- 游戏碰撞检测 |