特訓'99 タイマーの扱い

ゲームにおける時間管理

この手のゲームでは、時間の管理が重要なポインツだ。
自機や敵を一定間隔で動かさないと動きがぎこちなくなるからである。
この一定間隔を取り出すために割り込み処理を利用する。リアルタイムクロックを利用するものもあれば、画面の垂直同期を利用するものもある。一般的には画面の垂直同期を利用するものが多い。
これは、垂直同期というのがハードウエアが画面の内容をメモリからディスプレイに転送したタイミングであるからである。メモリからディスプレイへの転送中にメモリの内容を書き換えてしまうと表示内容が乱れてしまう。ソレを避けるには転送中にはメモリの内容は書き換えないようにしなければならない。実際には転送が終わったと同時にメモリの内容を変更することで、

間に合うことを祈っている。

しかしハードウエアの性能によりどうしても同間隔で動かすことができないことがある。このとき発生するのがコマ落ち、または処理落ちだ。


処理落ちとコマ落ち

この手のゲームに限らず、コンピュータのシステム上は入力と出力がある。
特訓'99系のゲームだと、入力はレバー入力、出力は画面描画、という形で行われる。

コマ落ちというのは途中の画面が抜けてしまう現象で、

途中でテレポーテーションしたかのような演出



が心にしみる。
これは入力部分と出力部分が平行して動作しているプログラムで発生する。入力を受けて出力結果を生成している間に次の入力を受け取ってしまうとこのようなことになる。
良く言うと入力をバッファリングして、まとめて出力することで出力の効率をあげる。これで時間的には誤差が無いようになる。
入力入力入力入力入力
処理処理処理処理処理
蓄積蓄積蓄積
出力出力

このキャンセルされた分が最後の出力に回される。
この間に行われた移動などの処理はその移動の途中が出力されないの我々の目にはで見えず、いきなりそこに現れたかのようにみえるのだ。

実際には描画中に垂直同期割り込みが発生してしまった場合に発生する。この時に今行っている描画を優先し、入力内容はバッファリングしておく。
次の描画時に、バッファリングした内容も加算して描画する。

処理落ちというのは、あたかも自分が

加速装置を使ってしまったかのような演出



が行われる。つまり世界すべてのものがすろうもぉしょんに、ハエが止まってみえるようになる。
これは入力と出力を同期的に、交互に行っているようなプログラムで発生する。つまり、出力に時間がかかるような場合でもソレをキャンセルせずに最後まで出力する。
このためテレポーテーションするようなことは発生しない。
シューティングゲームだと、自機や敵がテレポーテーションするとゲームにならないのでコマ落ちさせるのではなくて処理落ちさせるようになっているものがほとんどである。
デスクリムゾンはテレポートしてくるという話なので、もしかしたらコマ落ちしているのかもしれない。

基本的に、これらの事象は性能が充分であれば発生しない。これらが発生するゲームは性能に対してデザインが大きすぎるという、致命的なバグである。


パソコンの場合

パソコンでゲームを作る場合、専用機と違って動作環境を特定できない。そのためどうしてもある環境では処理落ちが発生するなどの現象からは逃げることができない。
そのため、動作環境としてCPUを指定したり、ビデオカードを指定することがある。
よくある、CPU Pentium 200MHz以上推奨、とかいうのがソレだ。こうでもしないと処理速度を保証できないし、それでもビデオカードが遅かったりするとどうしようもない。
パソコンの場合は処理落ちが発生しても性能に対してデザインが大きすぎる、とは一概に言えないかもしれない。

Windowsの場合

これらは原則的に専用機(家庭用ゲーム機、アーケードマシンなど)での話であり、Windowsでは事情が異なる。
まず、一定間隔で動作するタイマというのが基本的に存在しない。
一応、Windowsには以下のような仕組みがある。 下のほうが精度が高い。
一応、これらによって一定時間が経過したことを知ることができる。
が、これは「一定時間が経過した」ということしか解らないのである。たとえば、「3秒経ったら教えてね」とかやった場合、教えてくれるのが3秒後かというとそうじゃない。
6秒後に

「そういえばもう3秒たったよ」



と教えてくれるかもしれない。

コレじゃゲームには使えない



のである。

メッセージタイマ

メッセージタイマとは、一定時間が経過した後にOSがウインドウに対して通知する仕組みである。
これは手紙みたいなもので、プログラムが自分からメッセージが届いていないか確認しないと届いていることが解らない。
つまり、届いてもその時にテレビに夢中だったりしてメッセージの確認をしていないと無駄になる。
また、そもそもが一定時間が経過した後に届くというだけでソレがいつ届くかという保証が無い。なので、入力のタイムアウトなどには有効だがリアルタイムゲームの時間基準とするには不向きである。

BKEYのタイムアウト機能はコレを使用している。

コールバックタイマ

メッセージタイマと同じAPIで生成するが、こちらはウインドウに通知を送るのではなくて指定した関数を実行するパターンである。
そのため、メッセージの確認をしなくても、届いたことに気づいた時点で動く。
しかしやっぱり気づかないと動かない。

マルチメディアタイマ

精度的にはかなりのものがあるのでかなり使える。
コレもコールバックタイマのように指定した関数が勝手に動く。
ちょー優先的なスレッドを作り出し、その中で時間待ちをして時間が来たら指定した関数を呼び出す、というような動作をしているため、指定した関数はアプリケーションの優先レベルではなく、OS並み(か、それ以上)の優先レベルで動作する。
そのため、このタイマで時間のかかる処理を動かすと、その間はPC全体が重くなる。
あと、よくわからないがときどき精度が不安定になる。
が、何しろ重いというのが致命的。
あと、タイマのコールバックからでは特定の関数を呼び出してはいけないことになっている。これが意外と不便で細かな制御を行う際に苦労する。

Windowsのプログラムでは、メッセージループというものを用意して、そこでメッセージを受け取り、配送する、という仕組みをもっている。
これがないと、先程のメッセージタイマや、コールバックタイマは使用できない。というのもこれらはメッセージベースで動作するタイマだからである。メッセージを受信する仕組みが無いとこれらのタイマーは動作しない。

しかし、メッセージを受信して、そのメッセージを処理している間はメッセージを受信できない。つまり、キー入力メッセージを受け取って其の処理を行っている間はタイマーの処理はできないし、その逆もできない。この事もメッセージタイマの精度を下げる要因にもなっている。

仕方がないので別の手段も考える。
要は、独立して動いていればいいわけだ。

タイマーループ

ありがちなのがコレ、時間稼ぎにループを使うやり方だ。

for(i=0; i<1000; i++);
とか書いて時間稼ぎしてごまかす。
この間、こちらのプログラムは停止しているわけだが、CPUを親の敵のように消費する。
そのため他に動作しているプログラム(OS含む)がはみ出るほど重くなる。
対応としてはコレ、
for(i=0; i<1000; i++)Sleep(0);
というところか。
もっとも1000回のインクリメントとジャンプ、というのもナンセンスなんで、普通は時間を調べて、時間が経つのを計測する。
for(dwTime=GetTickCount()+10; GetTickCount()<dwTime; Sleep(0));
とか。
これらは、CPUを消費するがかなり精度が高い結果を得ることができる。

マルチスレッド

Windows 9x/NTなどの32ビットベースのWindowsではスレッドという単位でCPUを割り当てる。一つのプログラム(=プロセス)は複数のスレッドを生成することができる。
時間計測用のスレッドを用意することも一つの手段である。
ちなみに、瞑想修行では別スレッドを生成してその中でマルチメディアタイマを使用している。
これは、アプリケーションのメインスレッド内でマルチメディアタイマを使用すると、そっちに制御が奪われてしまい、キー入力と描画が追従しないという問題が発生したためである。このため、マルチメディアタイマから呼び出される関数は、タイマをセットしたスレッドのコンテキストで動作しているのだと誤解していたのだが、今回この特訓'99を開発している時にそうではないことが発覚してしまった。
こうなると、なぜ瞑想修行はタイマが不安定になったのか理由が推測できない。
とにかく、ウインドウを所有し、メッセージループを処理するスレッドでタイマを使うとタイマとメッセージループが干渉し精度を確保できない。

もっともマルチスレッドにしてしまえばタイマを使って定期的に関数を実行するよりも

while(1){
    OnTimer();
    Sleep(interval);
}
という手段もある。
これは某「花火」で使用されているテクニックである。
Sleep()が一定時間後に確実に復帰する関数では無いためいくつか問題があるが、問題にならない場合もある。

メッセージループのコーディング

コレも謎なのだが、試してみた感じ、GetMessage()APIはスレッドを停止させないようだ。
メッセージを受信するまで制御を返さない、というのはドキュメントに書いてある通りだがスレッドまで止めてくれるわけではないようで、スレッドの優先順位をあげてGetMessage()を呼び出すと他のスレッドの実行を疎外してしまう。
OS内部に入ってからCPUを消費するので、システムモニターなどでは測れないというのが面白い。
そのため、GetMessage()を呼び出す時には優先順位を標準にしておかないと挙動が怪しくなる。

ちなみにWaitMessage()はスレッドを停止させるようだ。
WaitMessage()を使わなくてもSleep(0);とでもしておくとかなり改善される。

対策

処理落ちやコマ落ちについては基本的に、Windowsでは回避できない。
といいうのも、動作しているプログラムがゲームだけでは無いからだ。
DOS環境のように、他に動いているプログラムが無ければ自分のことだけに注意してデザインしプログラミングすることで回避できるが、Windowsのような環境だと自分だけがCPUを使えるわけではない。処理は間に合っているが、OSがCPUを割り当ててくれないために処理できない、ということが発生する。

CPUの割り当て

32ビットのWindowsはCPUをスレッドに対して割り当ててくる。
スレッドにはそれぞれ優先順位があり、優先的なスレッドがあればソレを優先して実行し、それでも時間があまればその下位のスレッドを実行する。時間があまらなければ下位のスレッドは実行されない。

さっきのループで時間経過を待つ、という処理を実行した場合、これが優先スレッドだった場合には悲劇である。

while(play){
    /*  時間が経過するまで待つ  */
    for(dwTime=GetTickCount()+10; GetTickCount()<dwTime; Sleep(0));
    /*  メインウインドウに時間経過を通知  */
    PostMessage(MainWindow, WM_COMMAND, ADVANCE, 0);
}
これだけが実行されてしまい、ひたすらに時間が経過するのを待つことは待つのだが待ったあとの処理が動作しないことがある。
メインウインドウ側が、時間経過の通知を受け取れないのだ。

実行するスレッドの切替というのも厄介で、次にどれを動かすか指定する方法は無い。また、どれが動くかも判らない。
さきも挙げたように、優先順位の高いスレッドで時間のかかる処理をすると他のスレッドは身動きできない。
そしてマルチメディアタイマというのが最高優先順位で動作するスレッドとして実装されている(WIndows 95の場合)。そのため、マルチメディアタイマのコールバックで処理を行うとプログラム本体が動作できなくなってしまう。具体的には、

などが発生する。
これらは特訓の開発中に実際に起こっている。

Sleep()で一定時間待つ、という手法も先程挙げたが、これもマルチメディアタイマ程度の精度しか持たない。
Sleep()を使用した場合、その時間は休止することが保証されるが、その時間が経過したら即時に復旧するとは限らない。というか保証されていないし期待できない。
メッセージタイマと同じように、時間が経過したことだけが保証される。

一定時間経過後に動きたいのであれば、現状では

for(dwTime=GetTickCount()+interval; GetTickCount()<dwTime; Sleep(0));
この形が一番精度があるようだ。
問題は、コレを使用した場合のCPUサイクルの消費。
スレッドの優先順位を下げるとこのスレッドが実行されなくなるため誤差が発生しやすくなり、優先順位を挙げると他のスレッドが動作しなくなるためゲームにならない。

精度

ちなみに、特訓'99 1999-03-03バージョンにて

で精度を比較してみた。
これは実際に動いた回数と、経過した実時間(timeGetTime()で算出)との比較である。
タイマの精度は16ms。そのため活動時間は16の倍数になっている。

コールバックマルチメディアタイマスレッド内でSleepスレッド内でループ
活動時間53.80816.76817.60017.440
実時間209.45716.78328.02617.452
精度25.69%99.91%62.80%99.93%

比率が低いほど速度が遅いと考えていただいてかまわないと思う。実際、コールバックタイマ版は非常に動作が遅くかわすのが容易であった。

というわけで、今まで別スレッドで

というのを悩んでいたのだが、今回はループさせることに決定した。
ループさせると結構もたつくような気がしていたが、コレが実装してみると意外とスムーズだったのには驚いた。
しかしやっぱりSleep()でのタイミング調整は思った通り無謀というか誤差が発生しまくり。
マルチメディアタイマがここまで負荷が高いとはちょっち予想しきれていなかったため、マルチメディアタイマを使用しつつ負荷を下げる方向を考えていたのだが、今回はあきらめた。


Copyright © 1999,2001 びい

トップをねらう