首页 > 代码库 > Chrome自带恐龙小游戏的源码研究(五)

Chrome自带恐龙小游戏的源码研究(五)

  在上一篇《Chrome自带恐龙小游戏的源码研究(四)》中实现了障碍物的绘制及移动,从这一篇开始主要研究恐龙的绘制及一系列键盘动作的实现。

会眨眼睛的恐龙

  在游戏开始前的待机界面,如果仔细观察会发现恐龙会时不时地眨眼睛。这是通过交替绘制这两个图像实现的: 

技术分享

可以通过一张图片来了解这个过程: 

 技术分享

  为实现图片的切换,需要一个计时器timer,并且需要知道两张图片切换的时间间隔msPerFrame。当计时器timer的时间大于切换的时间间隔msPerFrame时,将图片切换到下一张,到达最后一张时又从第一张开始,如此反复。下面是实现代码:

技术分享
 1 Trex.config = { 2     BLINK_TIMING:3000,  //眨眼间隔 3     WIDTH: 44,        //站立时宽度 4     WIDTH_DUCK: 59,    //闪避时宽度 5     HEIGHT: 47,    //站立时高度 6     BOTTOM_PAD: 10, 7     MIN_JUMP_HEIGHT: 30 //最小起跳高度 8 }; 9 //状态10 Trex.status = {11     CRASHED: ‘CRASHED‘,    //与障碍物发生碰撞12     DUCKING: ‘DUCKING‘,    //闪避13     JUMPING: ‘JUMPING‘,    //跳跃14     RUNNING: ‘RUNNING‘,    //跑动15     WAITING: ‘WAITING‘    //待机16 };17 //元数据(metadata),记录各个状态的动画帧和帧率18 Trex.animFrames = {19     WAITING: {//待机状态20         frames: [44, 0],//动画帧x坐标在44和0之间切换,由于在雪碧图中的y坐标是0所以不用记录21         msPerFrame: 1000 / 3    //一秒3帧22     },23     RUNNING: {24         frames: [88, 132],25         msPerFrame: 1000 / 1226     },27     CRASHED: {28         frames: [220],29         msPerFrame: 1000 / 6030     },31     JUMPING: {32         frames: [0],33         msPerFrame: 1000 / 6034     },35     DUCKING: {36         frames: [262, 321],37         msPerFrame: 1000 / 838     }39 };40 41 function Trex(canvas,spritePos){42     this.canvas = canvas;43     this.ctx = canvas.getContext(‘2d‘);44     this.spritePos = spritePos; //在雪碧图中的位置45     this.xPos = 0;  //在画布中的x坐标46     this.yPos = 0;  //在画布中的y坐标47     this.groundYPos = 0;    //初始化地面的高度48     this.currentFrame = 0;  //初始化动画帧49     this.currentAnimFrames = [];    //记录当前状态的动画帧50     this.blinkDelay = 0;    //眨眼延迟(随机)51     this.animStartTime = 0; //动画开始的时间52     this.timer = 0; //计时器53     this.msPerFrame = 1000 / FPS;   //默认帧率54     this.config = Trex.config;  //拷贝一个配置的副本方便以后使用55     this.jumpVelocity = 0;  //跳跃的初始速度56 57     this.status = Trex.status.WAITING;  //初始化默认状态为待机状态58 59     //为各种状态建立标识60     this.jumping = false;    //角色是否处于跳跃中61     this.ducking = false;    //角色是否处于闪避中62     this.reachedMinHeight = false;  //是否到达最小跳跃高度63     this.speedDrop = false; //是否加速降落64     this.jumpCount = 0;     //跳跃次数65 66     this.init();67 }
View Code

  首先还是和以往一样,对Trex这个构造函数进行基本的配置,然后在原型链中添加操作方法:

技术分享
 1 Trex.prototype = { 2     init:function() { 3         this.groundYPos = DEFAULT_HEIGHT - this.config.HEIGHT - this.config.BOTTOM_PAD; 4         this.yPos = this.groundYPos; 5         //计算出最小起跳高度 6         this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; 7  8         this.draw(0,0); 9         this.update(0,Trex.status.WAITING);10     },11     setBlinkDelay:function () {12         //设置随机眨眼间隔时间13         this.blinkDelay = Math.ceil(Math.random() * Trex.config.BLINK_TIMING);14     },15     update:function (deltaTime,opt_status) {16         this.timer += deltaTime;17 18         if(opt_status) {19             this.status = opt_status;20             this.currentFrame = 0;21             //得到对应状态的帧率 e.g. WAITING 1000ms / 3fps = 333ms/fps22             this.msPerFrame = Trex.animFrames[opt_status].msPerFrame;23             //对应状态的动画帧 e.g. WAITING [44,0]24             this.currentAnimFrames = Trex.animFrames[opt_status].frames;25 26             if(opt_status === Trex.status.WAITING) {27                 //开始计y时28                 this.animStartTime = getTimeStamp();29                 //设置延时30                 this.setBlinkDelay();31             }32         }33 34         //计时器超过一帧的运行时间,切换到下一帧35         if (this.timer >= this.msPerFrame) {36             this.currentFrame = this.currentFrame === this.currentAnimFrames.length - 1 ?37                  0 : this.currentFrame + 1;38             this.timer = 0; //重置计时器39         }40 41         //待机状态42         if(this.status === Trex.status.WAITING) {43             //执行眨眼动作44             this.blink(getTimeStamp());45         }46     },47     blink:function (time) {48         var deltaTime = time - this.animStartTime;49 50         if(deltaTime >= this.blinkDelay) {51             this.draw(this.currentAnimFrames[this.currentFrame],0);52 53             if (this.currentFrame === 1) {//0闭眼 1睁眼54                 //设置新的眨眼间隔时间55                 this.setBlinkDelay();56                 this.animStartTime = time;57             }58         }59     },60     draw:function (x,y) {61         var sourceX = x;62         var sourceY = y;63         var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ?64             this.config.WIDTH_DUCK : this.config.WIDTH;65         var sourceHeight = this.config.HEIGHT;66         sourceX += this.spritePos.x;67         sourceY += this.spritePos.y;68 69         this.ctx.drawImage(imgSprite,70             sourceX, sourceY,71             sourceWidth, sourceHeight,72             this.xPos, this.yPos,73             this.config.WIDTH, this.config.HEIGHT);74     }75 };
View Code

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

1 if (this.timer >= this.msPerFrame) {2     this.currentFrame = this.currentFrame === this.currentAnimFrames.length - 1 ?3          0 : this.currentFrame + 1;4     this.timer = 0;5 }

  这段代码实现了两个帧之间的切换,但如果只是单纯地以相同时间间隔来切换两张图片,那么得到的效果是不正确的,会出现频繁眨眼的情况。而实际情况是,闭眼只是一瞬间,睁开眼睛的时间则比较长。Chrome开发人员非常巧妙地解决了这个问题:

技术分享
 1 blink:function (time) { 2     var deltaTime = time - this.animStartTime; 3  4     if(deltaTime >= this.blinkDelay) { 5         this.draw(this.currentAnimFrames[this.currentFrame],0); 6  7         if (this.currentFrame === 1) {//0闭眼 1睁眼 8             //设置新的眨眼间隔时间 9             this.setBlinkDelay();10             this.animStartTime = time;11         }12     }13 }
View Code

  只要计时器没有超过blinkDelay就不绘制新的图片,这样图片就会停留在上一次绘制的状态,恐龙此时是睁着眼睛的。当时间超过了blinkDelay,即执行眨眼的时间到了,这时会绘制this.currentFrame这一帧。如果这一帧是0(闭眼),由于之前设置了this.timer >= this.msPerFrame时会切换帧,当时间再次超过blinkDelay时,这时就会绘制帧1(睁眼),我们看到的效果就是眼睛闭上只有一瞬然后立刻睁开了。 如果当前帧是1(睁眼),重新设置blinkDelay,于是在deltaTime没有超过重新设置blinkDelay的情况下,都不会绘制新图片(始终保持在帧1(睁眼)),这样我们看到的效果就是睁眼的时间稍长。

下面是运行后的效果:

 

<script type="text/javascript">// this.bumpThreshold ? this.dimensions.WIDTH : 0; }, draw:function() { this.ctx.drawImage(imgSprite, this.sourceXPos[0], this.spritePos.y, this.dimensions.WIDTH, this.dimensions.HEIGHT, this.xPos[0],this.yPos, this.dimensions.WIDTH,this.dimensions.HEIGHT); this.ctx.drawImage(imgSprite, this.sourceXPos[1], this.spritePos.y, this.dimensions.WIDTH, this.dimensions.HEIGHT, this.xPos[1],this.yPos, this.dimensions.WIDTH,this.dimensions.HEIGHT); }, updateXPos:function(pos,increment) { var line1 = pos, line2 = pos === 0 ? 1 : 0; this.xPos[line1] -= increment; this.xPos[line2] = this.xPos[line1] + this.dimensions.WIDTH; if(this.xPos[line1] <= -this.dimensions.WIDTH) { this.xPos[line1] += this.dimensions.WIDTH * 2; this.xPos[line2] = this.xPos[line1] - this.dimensions.WIDTH; this.sourceXPos[line1] = this.getRandomType() + this.spritePos.x; } }, update:function(deltaTime,speed) { var increment = Math.floor(speed * (FPS / 1000) * deltaTime); if(this.xPos[0] <= 0) { this.updateXPos(0, increment); } else { this.updateXPos(1, increment); } this.draw(); }, reset:function() { this.xPos[0] = 0; this.xPos[1] = this.dimensions.WIDTH; } }; //endregion //region utils function getRandomNum(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function getTimeStamp() { return performance.now(); } //状态 Trex.status = { CRASHED: ‘CRASHED‘, //与障碍物发生碰撞 DUCKING: ‘DUCKING‘, //闪避 JUMPING: ‘JUMPING‘, //跳跃 RUNNING: ‘RUNNING‘, //跑动 WAITING: ‘WAITING‘ //待机 }; //元数据(metadata),记录各个状态的动画帧和帧率 Trex.animFrames = { WAITING: {//待机状态 frames: [44, 0],//动画帧x坐标在44和0之间切换,由于在雪碧图中的y坐标是0所以不用记录 msPerFrame: 1000 / 3 //一秒3帧 }, RUNNING: { frames: [88, 132], msPerFrame: 1000 / 12 }, CRASHED: { frames: [220], msPerFrame: 1000 / 60 }, JUMPING: { frames: [0], msPerFrame: 1000 / 60 }, DUCKING: { frames: [262, 321], msPerFrame: 1000 / 8 } }; function Trex(canvas,spritePos){ this.canvas = canvas; this.ctx = canvas.getContext(‘2d‘); this.spritePos = spritePos; //在雪碧图中的位置 this.xPos = 0; //在画布中的x坐标 this.yPos = 0; //在画布中的y坐标 this.groundYPos = 0; //初始化地面的高度 this.currentFrame = 0; //初始化动画帧 this.currentAnimFrames = []; //记录当前状态的动画帧 this.blinkDelay = 0; //眨眼延迟(随机) this.animStartTime = 0; //动画开始的时间 this.timer = 0; //计时器 this.msPerFrame = 1000 / FPS; //默认帧率 this.config = Trex.config; //拷贝一个配置的副本方便以后使用 this.jumpVelocity = 0; //跳跃的初始速度 this.status = Trex.status.WAITING; //初始化默认状态为待机状态 //为各种状态建立标识 this.jumping = false; //角色是否处于跳跃中 this.ducking = false; //角色是否处于闪避中 this.reachedMinHeight = false; //是否到达最小跳跃高度 this.speedDrop = false; //是否加速降落 this.jumpCount = 0; //跳跃次数 this.init(); } Trex.prototype = { init:function() { this.groundYPos = DEFAULT_HEIGHT - this.config.HEIGHT - this.config.BOTTOM_PAD; this.yPos = this.groundYPos; //计算出最小起跳高度 this.minJumpHeight = this.groundYPos - this.config.MIN_JUMP_HEIGHT; //设置眨眼间隔的时间 //this.blinkDelay = this.setBlinkDelay(); this.draw(0,0); this.update(0,Trex.status.WAITING); }, setBlinkDelay:function () { //设置随机眨眼间隔时间 this.blinkDelay = Math.ceil(Math.random() * Trex.config.BLINK_TIMING); }, update:function (deltaTime,opt_status) { this.timer += deltaTime; if(opt_status) { this.status = opt_status; this.currentFrame = 0; //得到对应状态的帧率 e.g. WAITING 1000ms / 3fps = 333ms/fps this.msPerFrame = Trex.animFrames[opt_status].msPerFrame; //对应状态的动画帧 e.g. WAITING [44,0] this.currentAnimFrames = Trex.animFrames[opt_status].frames; if(opt_status === Trex.status.WAITING) { //开始计y时 this.animStartTime = getTimeStamp(); //设置延时 this.setBlinkDelay(); } } //计时器超过一帧的运行时间,切换到下一帧 if (this.timer >= this.msPerFrame) { this.currentFrame = this.currentFrame === this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1; this.timer = 0; } //待机状态 if(this.status === Trex.status.WAITING) { //执行眨眼动作 this.blink(getTimeStamp()); } }, blink:function (time) { var deltaTime = time - this.animStartTime; if(deltaTime >= this.blinkDelay) { this.draw(this.currentAnimFrames[this.currentFrame],0); if (this.currentFrame === 1) {//0闭眼 1开眼 //设置新的眨眼间隔时间 this.setBlinkDelay(); this.animStartTime = time; } } }, draw:function (x,y) { var sourceX = x; var sourceY = y; var sourceWidth = this.ducking && this.status != Trex.status.CRASHED ? this.config.WIDTH_DUCK : this.config.WIDTH; var sourceHeight = this.config.HEIGHT; sourceX += this.spritePos.x; sourceY += this.spritePos.y; this.ctx.drawImage(imgSprite, sourceX, sourceY, sourceWidth, sourceHeight, this.xPos, this.yPos, this.config.WIDTH, this.config.HEIGHT); } };window.onload = function () { var h = new HorizonLine(c,spriteDefinition.HORIZON); var trex = new Trex(c,spriteDefinition.TREX); var startTime = 0; var deltaTime; var speed = 3; (function draw(time) { time = time || 0; deltaTime = time - startTime; trex.update(deltaTime); startTime = time; requestAnimationFrame(draw,c); })(); };// ]]></script>

Chrome自带恐龙小游戏的源码研究(五)