43 章
今回はシューティングゲームには特に重要なFPSを保つ処理をやってみましょう。
この章で拘っているのは、重くならずにきちんと待機させる事です。
ここで少し前提知識の話をします。
だいたいゲームは60fpsで動作しています。
fpsというのはframe per secondの略で、1秒間に何フレームかという意味です。
1秒間に60回描画するなら60FPSです。
ここで1フレームは何秒でしょうか?60FPSならば
(ミリ秒=1/1000秒、msと略す)
1000ms / 60fps = 16.666666....(ms)
となります。つまり1フレームで保つべきミリ秒はint型では表現出来ないのです。
仮に毎フレーム16msで待機したとすると、
1000ms / 16ms = 62.5fps
17msで待機したとすると
1000ms / 17ms = 58.823...fps
になってしまい、ちゃんと60FPSになりません。
ここで16.666666...という数字は(16+17+17)/3だなということに気づいた人は、
「16msで1回待機させ、17msで2回待機させる繰り返しでよさそうだな」と思うかもしれません。
しかし、待機は恐らくSleep関数を皆さんお使いになるでしょうけど、
(WaitTimer関数は余計な処理が多いので、短い待機には向きません)
引数に渡すこの
Sleep( int time );
timeは「待機するミリ秒数」ではありません。
この関数は指定したミリ秒ほど待機するというものではなく、
「timeミリ秒以上処理を返さない」
という関数です。windowsのようなマルチタスクOSでは正確な時間を測定する事は無理で、
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になるまで
待機させればよいことがわかります。
こうして、1秒間での誤差はSleep関数の誤差だけということになりました。
これをプログラムで実装してみましょう。
---- function.h に以下を追加 ---- //fps.cpp GLOBAL void fps_wait(); GLOBAL void draw_fps(int x, int y); ---- main.cpp の メイン関数内の以下を修正・追加 ---- 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();//自機ショットメイン enemy_main();//敵処理メイン boss_shot_main(); shot_main(); //ショットメイン out_main(); //当たり計算 effect_main();//エフェクトメイン calc_main();//ゲームタイトル表示計算 graph_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;//エスケープが入力されたらブレイク 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 //待つべき時間=現在あるべき時刻-現在の時刻 term = (int)(count0t+fps_count*(1000.0/FLAME))-GetNowCount(); if(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以上にはなりませんので、
ご注意下さい。
実行結果
- Remical Soft -