文章首发于我的个人博客

前言

上一篇文章:《Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替》实现了游戏昼夜模式的交替,这一篇文章中,将实现:1、小恐龙的绘制 2、键盘对小恐龙的控制 3、页面失焦后,重新聚焦会重置小恐龙的状态

绘制静态的小恐龙

定义小恐龙类:

/** * 小恐龙类 * @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/pages/demo/games/google-dino/dino-gogogo/

上一篇下一篇
Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替TODO