第四十三章 准确地控制FPS吧
本章中介绍STG中非常重要的FPS控制的处理。
本章的着眼点在于轻巧利落地完成待机这一工作。
在这里先说一些作为预备知识的东西。
一般而言游戏都是以60fps进行的。
所谓fps,就是frame per second的简称,也即1秒钟进行多少帧的意思。
1秒钟如果进行60次绘制的话那就是60FPS。
那么1帧又是多少秒呢?如果是60FPS的话 (毫秒=1/1000秒,简写为ms)
1000 / 60fps = 16.666666…(ms) 也就是说1帧需要保证的毫秒数是无法用int来表示的。
假定以1帧16ms来待机的话,
1000ms / 16ms = 62.5fps 又如果以1帧17ms来待机话,
1000ms / 17ms = 58.823…fps 结果会是这样子,那就不能严格按照60FPS进行了。
如果注意到16.666666…这个数字就是(16+17+17)/3的话,可能就会想到“第1次待机为16ms,然后以17ms待机两次,然后这样循环进行不就行了?”
然而,估计大家都用Sleep函数来待机吧(WaitTimer函数会执行一些多余的处理,不适合短时间的待机),
Sleep( int time );
不过向
Sleep(int time)
的参数time传入的并不是“待机的毫秒数”。
这个函数并不是进行待机指定毫秒数的工作,而是进行“超过time毫秒再返回”的工作。在Windows这样子多任务的操作系统中,无法测定正确的时间,即便写了Sleep(100)实际上待机时间可能是101ms。虽然会认为1/1000秒的误差看起来微不足道,但是我们已经在16ms还是17ms这个问题上都很苦恼了,要是有1ms的偏差那么计算结果可就大变样了。现在我们看看以1秒为单位的计算。
以16ms为单位考虑的话1ms的误差是很大的,而如果我们以1000ms为单位来计算的话,那么就可以很好地控制误差可能造成的错误了。
在第1帧中出现的误差我们在第2帧中修正,接着在第2帧中出现的误差我们在第3帧中修正……
这样子反复进行,一直到60帧结束,这样一来全体产生的误差都可以修正了。
具体而言就是,
在第0帧的时候保存当前时刻,以此为基准进行待机。
假定现在是第1帧,那么从第0帧开始计数,进行(int)(16.6666…1)ms的待机就行了,也就是16ms。
假定现在是第2帧,那么从第0帧开始计数,进行(int)(16.6666…2)ms的待机就行了,也就是33ms。
假定现在是第3帧,那么从第0帧开始计数,进行(int)(16.6666…3)ms的待机就行了,也就是49ms。
……(略)
假定现在是第60帧,那么从第0帧开始计数,进行(int)(16.6666…60)ms的待机就行了,也就是999ms。
这样一来,1秒的误差我们就控制到0.001秒这个数量级了。
然而,既然第1秒待机1秒这一点是很清楚的,那么从这个时候开始一直到60帧前记录的时刻+1000ms为止待机就行了(译者注:事实上这里作者的说法可能会产生误导,会让成产生可能有if(lastTime + 1000ms ⇐ curTime)这种类型的判断的想法,事实上从源代码来看并不是这样的,作者直接采用的阶段等待的方法来减小误差)。
这么一来1秒之间的误差就只有Sleep函数的误差那么多了。
我们试着在游戏中实现吧。
—-在 function.h 中进行以下追加 —-
//fps.cpp
GLOBAL void fps_wait();
GLOBAL void draw_fps(int x, int y);
—-在 main.cpp 的main函数中进行以下追加/修正 —-
switch(func_state){
case 0://游戏启动时进行的处理
load(); //读入数据
first_ini();//最开始的初始化
func_state=99;
break;
case 99://STG开始前进行的初始化
ini();
load_story();
func_state=100;
break;
case 100://通常处理
calc_ch(); //角色计算
ch_move(); //控制角色的移动
cshot_main();//自机射击main
enemy_main();//敌人处理main
boss_shot_main();
shot_main(); //射击main
out_main(); //碰撞计算
effect_main();//特效main
calc_main();//游戏Title显示计算
graph_main();//绘制main
/***修改请注意***/
draw_fps(0,465);//fps显示
/***修改请注意***/
if(boss.flag==0)
stage_count++;
break;
default:
printfDx("错误的func_state\n");
break;
}
/***修改请注意***/
fps_wait();//计算帧等待
/***修改请注意***/
music_play();
if(CheckStateKey(KEY_INPUT_ESCAPE)==1)break;//如果按下ESC键则跳出循环
ScreenFlip();
—-在 fps.cpp 中进行以下追加 —-
#include "../include/GV.h"
//fps
#define FLAME 60
//fps的计数器,60帧1次记录作为基准的时刻
int fps_count,count0t;
//为了进行平均计算,记录60次1个周期的时间
int f[FLAME];
//平均fps
double ave;
//以FLAME fps为目标进行fps的计算和控制
void fps_wait(){
int term,i,gnt;
static int t=0;
if(fps_count==0){//60帧1次的话
if(t==0)//如果是最开始的话不等待
term=0;
else//基于上一次记录了的时间计算
term=count0t+1000-GetNowCount();
}
else //应该等待的时间=当前的理论时刻-当前的实际时刻(译者注:GetNowCount()是DX Library中类似GetTickCount()的函数,用于返回系统启动后到当前为止的毫秒精度的时间计数)
term = (int)(count0t+fps_count*(1000.0/FLAME))-GetNowCount();
if(term>0)//只等待应该等待的时间(译者注:如果term大于0的话,说明当前帧率应该到达的值还超过了当前的时刻,那么就等待这个差值即可;反过来则无需等待,因为说明当前帧等待的时间早就被超过了,之所以出现这种情况一般是因为绘制过程太多影响了效率)
Sleep(term);
gnt=GetNowCount();
if(fps_count==0)// 60帧进行1次基准变更
count0t=gnt;
f[fps_count]=gnt-t;//记录1个周期的时间
t=gnt;
//平均计算
if(fps_count==FLAME-1){
ave=0;
for(i=0;i<FLAME;i++)
ave+=f[i];
ave/=FLAME;
}
fps_count = (++fps_count)%FLAME ;
}
//在x,y的位置显示fps
void draw_fps(int x, int y){
if(ave!=0){
DrawFormatString(x, y,color[0],"[%.1f]",1000/ave);
}
return;
}
实际上,只要修改
#define FLAME 60
这里的数字,就可以变成您自己想要的fps了,您可以自行确认一下。
另外,如果自己使用的显示器的刷新率是60的话,就没法显示60以上了,请注意一下。
运行结果