接着上一节,我们来讲一下引擎后面的部分
碰撞检测
碰撞检测分事前检测和事后检测:
priori collision detection / posteriori collision detection
提前探知是否会发生碰撞 或 事发之后才检测到碰撞
事前碰撞可能会失效,因为计算小球在下一帧的位置来检测碰撞,容易出错。它是根据当前速率估算的,而帧速率突然改变,估算的结果就不准了。
事前的准确度不够高!
外接图形判断法
以下是外接矩形检测法
ballWillHitLedge:function(sprite,ledge){
let fps=sprite.fps;
let spriteRight=sprite.left+sprite.width,
ledgeRight=ledge.left+ledge.width,
spriteBottom=sprite.top+sprite.height*2, //here is sprite.height*2!!! not so accureately!
nextSpriteBottomEstimate=spriteBottom+sprite.velocityY/fps;
//外接矩形 碰撞检测
//此处的碰撞检测包含了3种情况,sprite在ledge的左边缘上,但还没超出
//sprite在ledge的右边缘上,但还没超出,
//sprite整体在ledge里面。 后面的identify会判断出来
return spriteRight>ledge.left&&
sprite.left<ledgeRight&&
spriteBottom<ledge.top&&
nextSpriteBottomEstimate>ledge.top;
},
分离轴定理SAT与最小平移向量MTV
外接矩形 外接圆 和 光线投射法检测碰撞。 不适用于检测任意多边形之间的碰撞。
分离轴定理:只适用于凸多变形!
把受测的两个多边形置于一堵墙前面,用光线照射它们,然后根据其阴影部分是否相交来判断二者有没有相撞。
它们在任意一条轴上都不重叠就是没有碰撞。 只要能在任何一条轴上找到互相分离的投影,就说明两个多边形没有发生碰撞,一旦发生了碰撞就肯定在所有的轴上都找不到互相分离的投影。
我们要对 两个图形的 每个边都做一次投影, 也就是说 这个多边形有几条边就有多少个投影轴!
不过只要在任何一个投影轴上检测到互相分离的投影就可以结束检测!
帧速率更新
解释
动画是由一系列FRAME图像组成的。 这些图像的显示频率就叫做“frame速率”
frame rate 。
更新公式
我们要计算frame rate (每秒 frame per second fps)
上次绘制frame的时间从当前时间中减去,得到了这两frame动画的时间差,然后再用1000除以这个以ms为单位的时间差。 于是就得出了动画每秒播放的frame数。
以不同的frame rate 执行各种任务
很多动画程序是播放动画的时候还要执行其他任务。 比如,播放动画时还要执行其他任务。
所以要学把不同的任务安排在不同的frame rate上执行。
//有关帧速率的对象属性
//Time
this.startTime=0;
this.lastTime=0;
this.gameTime=0;
this.fps=0;
this.STARTING_FPS=60;
//Update the frame rate, game time, and the last time the application
//drew an animation frame
tick:function(time){
this.updateFrameRate(time);
this.gameTime=(getTimeNow())-this.startTime;
},
// Update the frame rate, based on the amount of time it took
// for the last animation frame only.
updateFrameRate:function(time){
//Here calculate the fps!
if (this.lastTime===0)
this.fps=this.STARTING_FPS;
else
this.fps=1000/(time-this.lastTime);
},
我们在游戏循环中会调用这个tick来更新游戏时间,以及更新游戏帧速率。
time-lastTime 即帧之间的相隔时间,然后用1000除以它就是每秒多少帧, fps的值
暂停游戏
//游戏引擎中关于暂停的属性
this.paused=false; //暂停的标志,true为暂停。
this.startedPauseAt=0; //
this.PAUSE_TIMEOUT=100; //暂停检查的超时时间
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);
}
当设置了暂停标志this.paused 我们的游戏引擎的animate循环就会进入
到this.paused为true的分支中,然后我们此时不再像之前未暂停前那样
频繁调用animate,现在利用setTimeout隔this.PAUSE_TIMEOUT(一般是100ms)
的时长,再去调用下一次的animate。这样可以节省资源。
toggle暂停函数
//Toggle the paused state of the game. If, after togglling
//the pause state is unpaused, the application subtracts the time
//spent during the pause from the game's start time.
//Game pick up where it left off, without a potentially large jump in time.
togglePaused:function() {
let now=getTimeNow();
this.paused=!this.paused;
if(this.paused){
this.startedPauseAt=now;
}else{//not paused
//Adjust start time, so game starts where it leff off when
//then user pause it
this.startTime=this.startTime+now-this.startedPauseAt;
this.lastTime=now;
}
},
这个函数用于切换游戏暂停状态,如果是非暂停的状态,函数会处理这个开始时间,
因为经过暂停之后,开始的时间应该更新为暂停之后开始的时间。
所以要加上now-this.startedPauseAt,这个暂停的时间间隔。
很显然每一帧就是animate被调用一次。更新lastTime为now,现在这个时刻。
如果暂停了,那么更新暂停的起点时刻。 this.startedPauseAt=now;
图片加载
加载图像需要时间, 我们最好能显示一个进度,其实就是预加载
queueImage 图像放入加载队列中
loadImage 我们需要持续调用这个方法 直到其返回100
getImage 返回图像对象。 100的时候才能调用这个方法
逻辑:
先用queueImage() 把需要加载的图像入队,然后持续调用loadImage方法直到其返回0为止
该方法的返回值也可以用来更新游戏界面。 告诉用户当前的图片加载进度
有些图像也许会加载失败, 通过imageFailedToLoad 属性确认是不是所有图像都加载好了。
imageFailedToLoad ,该属性表示未完成加载的图像数。
图像相关属性
this.imgaeLoadingProgressCallback;//图片加载进度回调
this.images={};
this.imageUrls=[];//图片url集合
this.imagesLoaded=0;//加载完的图片个数
this.imagesFailedToLoad=0; //加载失败的图片个数
this.imagesIndex=0; //加载下标
图像队列
//Call this method to add an image to the queue. The image will be loaded by
//loadImages()
queueImage:function(imageUrl){
this.imageUrls.push(imageUrl);
},
这个方法用户加入一个图片到队列中,传入img的url
队列取出图片,返回加载进度
//call this method repeatly to load images that have been queued .
//return the percent of the game images that have been processed
loadImages:function(){
//if there are images left to load
if(this.imagesIndex<this.imageUrls.length){
this.loadImage(this.imageUrls[this.imagesIndex]);
this.imagesIndex++;
}
//return the percent complete
return (this.imagesLoaded+this.imagesFailedToLoad)/
this.imageUrls.length*100;
},
重复调用这个方法来加载图片,imagesIndex表示当前加载的图片下标,调用loadImage来加载图片。
返回加载的进度,其实是图片张数的进度。
加载图片
loadImage:function(imageUrl){
let image=new Image();//self= this;
image.src=imageUrl;
image.addEventListener('load', (e)=>{
this.imageLoadedCallback(e);
});
image.addEventListener('error',(e)=>{
this.imageLoadedErrorCallback(e);
});
this.images[imageUrl]=image;
},
根据url来加载图片,其实就是预加载使用的方法new Image()
然后绑定图片的load 和error事件。加载完成的时候调用imageLoadedCallback
加载失败调用imageLoadedCallback
图片事件
//an image loads successfully
imageLoadedCallback:function(e){
this.imagesLoaded++;
},
imageLoadedErrorCallback:function(e){
this.imagesFailedToLoad++;
},
图片完成事件就是把imagesLoaded记录加一,而加载失败就把失败的记录加一。
获取图片
//Only need icons background few images!
//Given a URL return the associated image
getImage:function(imageUrl){
return this.images[imageUrl];
},
获取图片的方法。非常简单。
事件处理
事件监听器(处理函数) 列表。存放所有事件。
this.keyListeners=[];
按键监听
定义一个函数用于添加事件处理函数到 keyListeners队列中。
//Key listeners , defined event handler here
//Add a (key,listener) pair to the keyListeners array.
addKeyListener:function(keyAndListener){
this.keyListeners.push(keyAndListener);
},
给定一个按键key值,返回对应的监听器(事件处理函数)。
注意我们定义一个listener的时候,是需要定义它的key值和listener
然后才push到keyListeners队列中。
//Given a key, return the associated listener
//
findKeyListener:function(key){
let listener=undefined;
for(let i=0;i<this.keyListeners.length;++i){W
let keyAndListener=this.keyListeners[i],
currentKey=keyAndListener.key;
//find out which key it is
if(currentKey===key){//找出对应的监听器并返回
listener=keyAndListener.listener;
}
};
return listener;
},
按键处理函数
//
keyPressed:function(e){
let listener=undefined,
key=undefined;
switch(e.keyCode){//choose which key it is
//Add more keys as needed
case 32: key='space'; break;
case 68: key='d'; break;
case 75:key='k' ; break;
case 83:key='s' ; break;
case 80:key='p' ; break;
case 37:key='left arrow' ; break;
case 39:key='right arrow' ; break;
case 38:key='up arrow';break;
case 40: key='down arrow';break;
}
//find the corresponding eventhandlers
listener=this.findKeyListener(key);
if(listener){ // lisener is a function
listener(); //invoke it
}
},
按键对应的key是不同的,我们根据这个key使用findKeyListener函数
来返回监听器(处理函数)。
其他
其实还有其他的比如score分数的处理函数,
可以存到localStorage。
游戏引擎还提供了几个提供给外部实现的函数
//Override following method as desired: These four method are callbacks
//These methods are called by animate() in the order the are listed
startAnimate: function(time){
},
//this method are called before the sprite is painted.
paintUnderSprites:function(){
},
//called after the sprites are painted.
paintOverSprites:function(){
},
//called after current frame are rendered.
endAnimate:function(){
}
startAnimate是在游戏循环开始时调用的。
paintUnderSprite是在绘制精灵前调用
paintOverSprites绘制精灵后调用
endAnimate 游戏循环结束后调用
结语
游戏引擎的介绍到此结束,我会附上github地址
给各位参考。
后面我会给出一个使用这个游戏引擎的
一个极简游戏的例子,上面提到的提供给外部实现的4个方法就是可以在这个极简游戏中实现。
理解这个游戏引擎的核心就是理解动画循环
所有的逻辑都是在这个循环里面实现的。
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.tick(time); //Update fps game time
this.clearScreen(); //clear the screen
this.startAnimate(time); //Override it
this.paintUnderSprites(that); //Override
this.updateSprites(time); //Invoke sprite behavirus
this.paintSprites(time); //Paint sprites in the canvas
this.paintOverSprites(); //Override
this.endAnimate(); //Override yourself
this.lastTime=time;//update lastTime to calculate fps
//call this method again when it's time for the next animation frame
window.requestNextAnimationFrame((time)=>{
this.animate.call(this,time);
});
}
},
游戏引擎
github地址
具体代码可以看gameEngine.js