介绍
这是一个非常精简的游戏引擎,它仅仅包含了一般游戏所必须拥有的功能。
游戏引擎
主要功能如下
1. 游戏循环
2. 绘制精灵
3. 基于时间运动
4. 碰撞检测
5. 帧速率更新
4. 暂停游戏
5. 事件处理
6. 图片加载
动画循环 /游戏循环
其实游戏循环就是依赖动画循环实现的。
window.requestAnimationFrame()
传统的是window.setTimeout()
核心: 只需在播放动画时持续更新并绘制就行了。 持续更新和重绘:动画循环。
它是所有动画的核心逻辑。
setInterval setTimeout的缺点
使用setInterval实现动画循环,只需要调用一次,而setTimeOut() 则需要持续调用。
他们的问题: 对于setTimeout()它要明确告诉浏览器下一次执行动画循环的时间。所以,每次调用它都要把下次执行动画循环的时间点计算出来。
它不提供精确计时机制,它们只是让程序能在某个大致时间点上运行代码的通用方法而已。
“强制规定时间间隔的下限”。 况且浏览器也是在这么做。
比如FIREFOX 允许最小时间间隔是10ms。 后续调用的最小间隔是5ms。 这也就是说如果你以3ms 为参数来调用setTimeout()方法,浏览器就会根据规则认定参数无效。
不应主动命令浏览器何时去绘制下一frame动画,这应该由浏览器告诉你
虽然setTimeout() setInterval() 时间间隔机制不精确,不过调用的时候,会主动告知绘制下一frame的时间。然而调用者并不知道下一frame动画最佳时机,你可能根本不了解浏览器绘制动画内部机制。 我们应该让浏览器在它觉得可以绘制下一frame动画时通知你。我们用requestAnimationFrame() 实现。
function animate(time){
requestAnimationFrame(animate);
}
function animate() { ...}
实现动画效果:
animate:function(time,that){
//let self=this;
if(this.paused){
//check if the game is still paused , in PAUSE_TIMEOUT. no need to check
//more frequently
setTimeout(()=>{
window.requestNextAnimationFrame((time)=>{
//this.animate.call(this,time);
this.animate(time,that);
}) ;
},this.PAUSE_TIMEOUT);
}else{ //game is not paused
this.updateSprites(time); //Invoke sprite behavirus
//call this method again when it's time for the next animation frame
window.requestNextAnimationFrame((time)=>{
this.animate.call(this,time);
});
}
},
这个就是游戏循环,每次requestNextAnimationFrame都会重绘 ,绘制的内容就是这个动画中会动的所有对象,包括精灵。
动画重绘的问题
动画重绘是非常重要的一个地方,实现动画不一定是借助requestAnimationFrame()
绘制动画有3种方法:
1. 将所有内容擦除,并重新绘制(requestAnimationFrame)
2. 仅重绘内容发生变化的那部分区域
3. 从offscreen缓冲区中将内容变化的那部分背景图像复制到屏幕上
是否需要全部重绘?
有时全部重绘,反倒可以获得最佳性能。 如果背景很简单,而且你要绘制的运动物体也比较简单的话,那么将所有内容都擦掉并重新绘制是个好办法。
剪辑区域实现动画
function draw(){
var numDisc,disc,il
for(i=0;i<numDiscs;++i){
drawXXXBackground(disc[i]);
}
for(i=0;i<numDiscs;i++){
drawDisc(disc[i]);
}
}
function drawDiscBackground(disc){
context.save();
context.beginPath();
context.arc(disc.lastX,disc.lastY,disc.radius+1,0,Math.PI*2,false);
eraseBackground();
drawBackground();
context.restore();
}
图块实现动画
基本思想是多开一个offcanvas,离屏canvas对象来避免每帧动画都要重绘整个背景
然后将离屏canvas绘制到屏幕上,主要的动画循环函数仍然是draw()
只是drawDiscBackground不同了
function drawDiscBackground(context,disc){
var x=disc.lastX,
y=disc.lastY,
r=disc.radius,
w=r*2,
h=r*2;
context.save();
context.beginPath();
context.clip()
context.arc(disc.lastX,disc.lastY,disc.radius+1,0,Math.PI*2,false);
eraseBackground();
drawBackground();
context.restore();
}
绘制精灵
精灵:
我们把一些基本功能封装为JS对象,我们赋予精灵各种行为
精灵 就是集成到动画之中的图形对象。
一个精灵的定义如下:
var Sprite=function(name,painter,behaviors){
if(name!==undefined) this.name=name;
if(painter!==undefined) this.painter=painter;
if(behaviors!==undefined) this.behaviors=behaviors;
return this;
};
//Prototype
Sprite.prototype={
top:0, //全部都是默认值,不同的实例拥有同样的属性值
left:0,
width:10,
height: 10,
velocityX: 0,
velocityY: 0,
fps:60,
trap:false,
freeze:false,
tapTimes:0,
color:[],
visible: true,
animating: false,
painter: undefined, // object with paint(sprite, context)
behaviors: [], // objects with execute(sprite, context, time)
//绘制方法
paint:function(context){
if(this.painter!==undefined&&this.visible){
this.painter.paint(this,context);
}
},
//update执行所有精灵行为
update:function(context,time){
for(var i=0;i<this.behaviors.length;++i){
this.behaviors[i].execute(this,context,time);
}
}
}
这个精灵对象的类和它的绘制器以及行为都是解耦的。
实际上是一个策略模式,因为精灵和绘制器是解耦的,
策略模式
实现一个功能有多种方案可以选择。 这些算法灵活多样可以随意互相替换。
定义: 定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换!
使用策略模式就是 把算法的使用和 算法的实现 分离!
也就是说我这里的paint方法可以选择多种绘制算法,这些算法是可以互相替代的,我们
调用painter的paint方法即可。
绘制器与策略模式:
精灵对象不需要自己完成绘制,相反,它会将绘制操作委托给另一个对象完成。 Painter对象就是一些可以互相替换着使用的绘制算法。
例如
let ballSprite=new Sprite('ball',ballPainter,[Behavior.moveGravity,Behavior.fallOnLedge]);
我定义了一个ballSprite ,传入的ballPainter就是它的绘制器,
然后在游戏引擎中:
//Paint all visible sprites
paintSprites:function(time){
for(let i=0;i<this.sprites.length;++i){
let sprite=this.sprites[i];
if(sprite.visible){
sprite.paint(this.context);
}
}
},
我们会在一个动画循环中不断调用这个paintSprite方法绘制所有的设置为可见的精灵~
精灵对象的行为
只要实现了execute方法的对象,都可以叫做行为。
该方法一般会以某种方式来修改精灵的属性,比如移动其位置,或者是修改其外观。
精灵含有一个行为对象数组,它的update()方法会遍历该数组,使每个行为对象都得以执行一次。
我们可以把行为封装为对象,在程序运行的时候将它添加到多个精灵之中!
精灵的行为对象运用了命令模式:
行为对象能够将某种命令封装起来,它是命令模式的一种实例。 行为对象可以被执行也可以被放在某个队列中。
应用程序在创建精灵对象时,把这一个对象的行为数组传递给精灵的构造器。
像这样创建一个精灵:
sprite=new Sprite('runner',new SpriteSheetPainter(runningCells),[runInPlace,moveLeftToRight])
将多个行为组合起来
精灵有一个行为对象数组,可以根据需要向任何精灵对象中添加惹你数量的行为对象。
精灵的update()方法会从数组的第一个行为对象开始,一直遍历到最后一个对象依次调用execute方法。
//Update all sprites . The sprite update() method invokes all of a
//sprite 's behaviors ,命令模式: update && paint
updateSprites:function(time){
for(let i=0;i<this.sprites.length;++i){
let sprite=this.sprites[i];
sprite.update(this.context,time); //(context,time)
};
},
简单介绍一下目前引擎支持的行为对象
- 将精灵从左往右移动,水平平移
- 普通依据重力的下落行为
- 上抛行为
- 下降到Ledge的行为
举例
看下面这个从右移动精灵到左边的行为
moveRightToLeft
moveRightToLeft={
lastMove:0,
reset:function(){
this.lastMove=0;
},
execute:function(sprite,context,time){
//
let elapsed=animationTimer.getElapsedTime(),
advanceElapsed=elapsed-this.lastMove;
if(this.lastMove===0){//skip first time
this.lastMove=elapsed;
}else{
sprite.left-=(advanceElapsed/1000)*sprite.velocityX;
this.lastMove=elapsed;
}
}
},
它有一个execute方法,这是一个命令模式,
精灵的update()方法会从数组的第一个行为对象开始,一直遍历到最后一个对象依次调用execute方法。
命令模式
将“请求”封装成对象,以便使用不同的请求、队列或日志来参数化其他对象。 命令模式支持可撤销的操作。
一个命令对象通过在特定接收者上绑定一组动作来封装一个请求。 命令对象将动作和接收者包含进对象中。 这个对象只暴露出一个execute方法,当此方法被调用时,接收者会进行这些动作。
//sprite对象中的update方法,会对每一个行为对象执行execute方法
//命令对象就是执行update方法的对象(即sprite精灵对象),而behavior对象就是这组动作。
update:function(context,time){
for(var i=0;i<this.behaviors.length;++i){
this.behaviors[i].execute(this,context,time);
}
}
好处 : 不需要知道请求的接收者是谁,也不知道被请求的操作是什么。松耦合的方式设计软件。
但是对于JS
命令模式的由来,其实是回调callback的一个面向对象的替代品。
解释
JS作为将函数作为一等公民的语言,跟策略模式一样,命令模式也是早就融入语言中。 运算块不一定要封装在command.execute方法中,也可以封装在普通函数中。 函数本身就可以被四处传递!!!!!!!
第一篇先讲到这里