Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙 #11

Open
liuyib opened this issue Apr 24, 2019 · 0 comments
Open

Chrome 小恐龙游戏源码探究八 -- 奔跑的小恐龙 #11

liuyib opened this issue Apr 24, 2019 · 0 comments

Comments

@liuyib
Copy link
Owner

liuyib commented Apr 24, 2019

前言

上一篇文章:《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);
  },
};

这样在游戏初始化时就绘制出了静态的小恐龙,如图:

static_dino

实现眨眼效果

游戏初始化之后,小恐龙会随机眨眼睛。默认的是最多只能眨三次。下面将实现这个效果。

添加更新小恐龙的方法:

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); // 初始为等待状态
  },
};

最后在 Runnerupdate 方法中调用 Trexupdate 方法来实现小恐龙眨眼:

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();
    }
  },
};

效果如下:

test

可以看到,眨眼的代码逻辑触发了 3 次,但是实际小恐龙只眨眼了 1 次。这就是前面说的,小恐龙默认最多只能眨三次眼。具体原因如下:

先来看下 Trexupdate 方法中的这段代码:

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(); // 执行开场动画
      }

      // ...
    }

    // ...
  },
};

这样在按下空格键后,小恐龙就会跳跃一次并进行奔跑动画。如图:

dino-first-jump

下面来实现效果:小恐龙第一次跳跃后,向右移动 50 像素。

修改 Trexupdate 方法。当判断到正在执行开场动画时,移动小恐龙:

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; // 小恐龙的开场动画结束
    
    // ...
  },
};

效果如下:

test

可以很明显的看到,小恐龙在第一次跳跃后向右移动了一段距离(默认 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);
  },
};

效果如下:

jump-duck

第一次跳是正常下落,第二次跳是加速下落

处理小恐龙的跳跃

小恐龙的跳跃分为大跳和小跳,如图:

trex-jump

要实现这个效果,只需要在 键被松开时,立即结束小恐龙的跳跃即可。

修改 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();
    }
  },
};

效果如下:

reset-trex

查看添加或修改的代码,戳这里

Demo 体验地址:https://liuyib.github.io/demo/game/google-dino/dino-gogogo/

上一篇 下一篇
Chrome 小恐龙游戏源码探究七 -- 昼夜模式交替 Chrome 小恐龙游戏源码探究九 -- 游戏碰撞检测
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant