Mobileアフィリエイト

FPSを安定させる

 前回作ったループ構造では、秒間に400000回ほどの計算を行ってくれます。ええ、そんなに必要ありません。描画処理を加えたら、管理者の環境でもおそらく200程度に落ちるとは思いますが…

 まぁ、そんなことはどうでもいいのです。問題は、作ったゲームが色々な環境で実行されることです。管理者の環境は「Athlon64 3200+ メモリー 1GB RADEON9600XT」というなかなか高速な環境です。こんな環境に速度を合わせて作っていては大半の人はゲームにならないでしょう。その逆も同じです。

 プレイヤーが「←」キーを1秒間押しました。ある環境では100移動。別の環境では50移動。これでは一般公開できるゲームのクオリティには程遠いですね。(まぁ、そのまま公開しているフリーソフトもありますが…)早いマシンでも、遅いマシンでも50移動するようにプログラムを組みたいわけです。それが今回実装するタイマー処理(ウェイト処理という人もいます)です。

 それだけではありません。ゲームを作る場合、FPSは画面の更新周期に合わせて作ります。パソコンの画面は大体1秒間に60〜75回くらい書き換わります。(まぁ、これも環境によりけりですが)画面の更新頻度に同期させる理由は、ティアリングという現象を避けるためです。(ティアリングについては他のページで調べてみてください)このように、様々な理由からゲームを作るうえでタイマー処理は必要なのです。

安定させるアイディア

 タイマー処理の必要性はこんなもので。実装のために、まずはアイディアです。ここでは、一般的なディスプレイ周期の60FPSで話を進めていきます。また、どんなにがんばってもFPSが60出ない環境は考えないことにします。つまり、動作保障外ってことにしてしまいましょう。(これに対する改善策は後ほど)タイマー処理を行わない場合、処理は最低でも秒間に60回以上行われる事を前提にします。

 以上のような環境下で、FPSを60に固定するには…

 まぁ、大して難しい問題ではないですね。単純に「1/60秒に一回処理を行う」ようにプログラムしてやればいいだけのことです。他にも「処理時間になるまで待つ」方式がありますが、管理者が実験した感じだと前者(「1/60秒に一回処理を行う」)の方式のほうが安定していたので、こちらを採用することにします。

とりあえず実装

 アイディアも確定したので、実装に取り掛かります。FPSを安定させる「FPStimer」クラスを作りましょう。

時間を計るには?

 とりあえず、コードを書き始める前にひとつ考えなくてはならないことがあります。.NET Frameworkでどうやって時間を計るのか?1/60は大体、0.0167つまり16.7ms(ミリ秒)を計らなくてはなりません。まず、Timerオブジェクトを使って計る方式ですが、はっきり言って精度が低すぎます…MSDNによると50ms程度の精度しか保障されていないそうです。この他に調べてみると、時間を計れる以下のような関数・クラスが見つかりました。

1..NETの「System.Environment.TickCount」プロパティ
2.WinAPIの「timeGetTime」関数
3.WinAPIのパフォーマンスカウンター

 この他にDateTimeクラスのTicksプロパティという手もありますが、秒の切り出しが面倒なので使うのをやめます。とりあえず、手軽に実装してみます。

 1は.NET標準のクラスです。ネームスペースさえ使っていれば簡単に使えます。2、3はWindowsのAPI関数なのでDLLインポートを行う必要があります。また、3は実行環境によっては使用できないそうです。

 次に、タイマーの精度です。MSDNによると1.TickCountは500 ミリ秒分解能を超える精度では保証されていないそうですが、内部的にGetTickCount()を呼び出しているため、実際はそれ以上の精度が出るそうです。次のtimeGetTime関数は最大で1msの分解能を持つそうです。3のパフォーマンスカウンターは非常に高い精度の分解能を持っていますが、その分負荷がかかります。

管理者が16.7秒に一回呼び出すよう以上の3つのタイマーを使って実験してみたところ、1と2はFPS 59〜61程度、3は常に60と非常に正確な値を出してくれました。マシンによって多少のばらつきはありますが、この結果から、今回は1の.NETの「System.Environment.TickCount」プロパティを使うことにします。この程度の精度ならプレイしていても違和感のないものが作れるでしょう。

 ちなみに、Managed DirectXのクラスの中に時間を計るクラスが存在しています。それを使えばパフォーマンスカウンターと他のAPI関数を自動的に切り替えて、最適な時間を計測してくれます。今回はDirectXを使っていないので、これは選択肢から外してあります。実際にゲームを作る場合には、このタイマーを使うのも良いと思います。

コード化

 必要な条件がそろったところで、プログラムのコードを書いていきます。



double nextframe = (double)System.Environment.TickCount;
float wait = 1000f/60f;
while(frm.Created)
{
				
	if ((double)System.Environment.TickCount >= nextframe)
	{
	
		//計算処理
		//描画処理
	
		nextframe += wait;
	}

	Application.DoEvents();
}

 前回作ったメインループを改造したものです。nextframe変数に次のフレームのタイミングを記録しておき、現在の時間が次のタイミングよりも後ならば処理を行っているだけです。処理を行った後、次のフレーム時間を更新しています。

 System.Environment.TickCountはシステム起動からの経過時間(ミリ秒単位)をint型で取得できます。しかし、FPSを60に安定させるためには16.666…ミリ秒毎に処理を行わなくてはなりません。そこで、nextframeをdouble型で保持しておいて、それに16.666…を加算していくことでできるだけ正確に呼び出しを行っています。ちなみに、これをfloat型で行うと精度が足りず、うまく60に安定してくれません。

遅いパソコン対策

 とりあえず、FPSを60に固定することは成功しました。これで十分にゲームとして成り立ちます。ただ、グラフィックに凝ったハイクオリティーなものを作る場合、描画処理に非常に時間がかかってしまい、少し遅いパソコンで動作させようとするとFPSが30しか出ないなんてことがあるでしょう。こういった場合、先ほど作ったタイマー処理では全ての移動速度が半分になってしまいます。今度はこれの対策をしてみましょう。

フレームスキップ

 ゲームプログラムにおいて、一回の処理時間の大半を占めるのが、描画処理です。キャラクタの情報更新頻度が変動することは、プレイヤーに「不快感」を与えます。キャラクタがジャンプ中に速度が変わったら嫌ですよね。それに対して、描画処理の更新頻度が例えば60fps⇒40fpsに落ちたとしても気づく人は少ないと思います。そこで、描画が間に合わない場合はその処理を飛ばしてしまうのがフレームスキップです。

 上の図のように、描画処理を行う際に「本来呼び出されなければならない時間 + 1/60秒」以上遅かった場合に、その描画処理を飛ばして、次の計算処理を呼び出します。(というか、呼び出し時間を更新します)計算処理の一気に2回行ってしまうため多少のぶらつきは発生しますが、1/60秒単位のぶらつきなんて気にはならないでしょう。この方式を使って実際に計測してみると、計算の呼び出し回数は59〜62回/秒程度に落ち着きます。描画処理が重い分、FPSも正確には計測できていないでしょうが…

 これで、計算の回数を秒間60回程度に安定させることができます。上の図でも、計算処理の呼び出し回数が間に合う場合とそうでない場合で一致していますね。計算処理と描画処理を別々に書かなくてはならないので、多少違和感が出てしまうかもしれませんが、多くの人に楽しんでもらえるものが作れると言うことでよしとしましょう。

 ただ、これはあくまでも「描画が間に合わない」場合にのみ適応されます。もしも、計算処理が1/60秒以上かかってしまったら、当然まともに動作はしません。そういった場合には、計算処理を軽くするか、それとも動作保障外にしてしまうかしかないでしょう。計算が追いつかないものはどうにもなりません…

改造!!

 それでは、上のアイディアをそのままタイマー処理に適応してみましょう。



double nextframe = (double)System.Environment.TickCount;
float wait = 1000f/60f;
while(frm.Created)
{
				
	if ((double)System.Environment.TickCount >= nextframe)
	{
	
		//計算処理
	
	
		if ((double)System.Environment.TickCount < nextframe + wait)
		{
		//描画処理
		}
	
		nextframe += wait;
	}

	Application.DoEvents();
}

 アイディアの通り、現在の時刻ェ「nextframe + wait」より前であれば描画し、そうでない場合はフレームをスキップします。やってみると意外と簡単ですね。このコードでプログラムを実行し、描画部分に負荷をかけてFPSと秒間の計算回数を計測すると、FPSが26程度に落ちても計算回数は60〜62回を記録します。なぜか、計算回数が増えてしまいます…(う〜ん、原因不明…)この講座を書く前に色々と調べて、管理者なりに解釈して実装したつもりなのですが、計算が間違っている、プログラムがおかしいなどお気づきになった点がありましたら教えてください…

基礎は整った

 前回のゲームループと今回のタイマー処理で、ゲームプログラムの基本構造は出来上がりました。これで、ある程度の幅の環境下で速度を保つことができます。あとは、このタイマー処理をクラス化するなりして扱いやすい形にすればオブジェクト指向っぽいですね。この講座では、プログラムの流れを分かりやすくするためにそこまでは触れません。(というか、管理者がそういうの苦手なので…)いいクラスができたら、是非とも教えてください。

 とりあえず、基本が整ったところでひと段落…次回からは…何をやろうかまだ考えていません。シーンをやろうか…タスクをやろうか…管理者もお勉強します