プログラム5 メインループ

ウィンドウズのプログラムが、イベントドリブン型だという事は以前にも説明しました。
何らかのイベントが発生したとき、それに対応する処理を行い、それ以外の時は特に何
もしないというのであれば特に問題は無いのですが、常に何らかの処理を行いつつ、イ
ベントに対応した処理を行うというのは良くあることです。
最も簡単な実装は、Timerコントロールを使用することですが、HELPを読むとTimerコン
トロールの分解能は1/18秒が最大とあります。
つまり、大体55.5msです。
この1/18秒という数字は、一見それなりに早そうに見えますが、よく使われるリフレッ
シュレートである1/60秒の1/3以下という数値で、とても使えたものではありません。
データーの送受信を行うアプリケーションなどでは1/100秒の精度を要求されることも
ありますから、Timerコントロールはゲームのような用途には不向きといえます。

では、どうするのか、ということになるのですが、大きく分けて2つの方法があります。
一つはWin32APIを使うという方法で、もう一つがメインタイマで待ちを入れるという
方法です。
Win32APIを使う方法は、かなり正確なタイミングで処理を行うことが出来るのですが、
FrameWork上でWin32APIを直接使うというのはあまりお勧めできる方法ではありません。
コールバック関数を使うタイプは特にそうです。

もう一つの方法を説明する前に、FrameWorkでプログラミングをして、不思議に思った
ことは無いでしょうか。
何で動いているの?、と。
Cではmain関数なるものがありました。
コードはまずこのmain関数から実行されます。
C#では??
フォームのコードを記述すればプログラムが動くように自動生成してくれているだけで、
実は、C#にもmain関数があります。

表示→ソリューションエクスプローラーを実行してソリューションエクスプローラーを
開き、Program.csを開いてみましょう。

       static void Main()
       {
           Application.EnableVisualStyles();
           Application.SetCompatibleTextRenderingDefault(false);
           Application.Run(new Form1());
       }

こんなコードがあるはずです。
これが、C#のmain関数です。
表示属性を有効にして、標準的なメッセージループの実行を開始しています。
メッセージループ処理は、その名の通りWindowsのメッセージを処理するループのこと
で、リサイズ時にフォームを再描画するなどといったことが含まれます。
メッセージループ、というからにはぐるぐると回っているわけです。
このメッセージ処理を手動で行えれば、ループの中で規定時間待ってから処理を行う
ようにすれば、任意の時間で繰り返し処理が行えるという事になります。
ありがたいことに、アプリケーションクラスではキューに残っているメッセージを処
理するというメソッドが用意されています。
となるとやることは簡単です。
フォームのオブジェクトを生成して、表示して、メッセージの処理をしつつ、必要な
処理を行えばよいのです。

   static class Program
   {
       private const double waitTime = 1000.0f / 60.0f;
   
       [STAThread]
       static void Main()
       {
           double targetTime;
           Form1 mainForm = new Form1();
           mainForm.Show();
     
           targetTime = (double)System.Environment.TickCount;
           targetTime += waitTime;
           while (mainForm.Created)
           {
               if ((double)System.Environment.TickCount >= targetTime)
               {
                   //メインの処理
                   mainForm.RenderFps();
                   targetTime += waitTime;
               }
               System.Threading.Thread.Sleep(1);
               Application.DoEvents();
           }
       }
   }

例によって、フォームにラベルを貼って、フォームクラスを以下のようにします。
   public partial class Form1 : Form
   {
       private int fps;
       private int oldtime;
         
       public Form1()
       {
           fps = 0;
           oldtime = System.Environment.TickCount;
       }
   
       public void RenderFps()
       {
           fps++;
           if (System.Environment.TickCount >= oldtime + 1000 )
           {
               oldtime = System.Environment.TickCount;
               label1.Text = fps.ToString();
               fps = 0;
           }
       }
   }


targetTimeのオーバーフローを考慮していませんが、これでtargetTime ms毎にFormの
Rendererメソッドを実行します。
このメソッドの実行、ゲームでの多くは画面の更新になりますが、をフレームと呼び
ます。
フレームの更新周期をfpsと読みます。これはFrame per secondの略で、1秒間に何回
フレームを更新するかという意味で、60fpsならば1秒間に60回更新されることになり
ます。
この例では60fpsを出すのを目的としていますが、ここを10.0f/1000.0fとかすれば、
10ms毎の処理になります。
また、dowbleを使っているのは、端数による時間のずれをなるべく減らすためです。
60fpsだと、16.6666....秒ですから、整数だとずれが出てしまうのです。
つまり、整数だと恐らくは59か61fpsになるところを、浮動小数を使うことにより平均
して60fpsを出せるようにしています。
特に気にしない、或いは端数が出ないのであればintでも良いでしょう。
59fpsと60fpsの違いが分かる人間など極限られているでしょうからね(出来る人は出来
るのが、人間という生き物の恐ろしいところです)
Application.DoEventsが、キューに残っているメッセージを処理するメソッドです。
これを呼ばないと、メッセージ処理を行わなくなる為システム全体がフリーズした
ようになってしまうので注意が必要です。
DoEventの前に、Sleepを入れているのは、CPUの占有率を下げる為です。
CPUの占有率が100%のままで良いというのであれば、特に入れる必要はありません。
フォームのFPS表示メソッドは、呼び出されるたびにカウンタをインクリメントし、
1秒経過したらカウンタを表示しているだけです。
1秒間に何回カウントしたか……つまり、フレームレートを表示しているわけですね。

さて。
ここで問題が一つあります。
すべてのPCの性能が一定であれば特に考慮する必要は無いのですが、悲しいかな、世の
中のPCには性能の違いがかなり有ります。
非力なPCで重い処理を行わせると、メインの処理を終えるのに1/60秒以上を使ってしま
う可能性があります。
そうなると、wait時間は意味を成さなくなります。

それを防ぐ為の考え方がフレームスキップです。
画像描画などの重い処理を2回に1回、或いは3回に1回行い、軽い処理例えば位置の計算
等は毎回行うといった処理です。
あくまでメインの処理は通常のフレームで処理する場合に有効ですが、当然のことなが
ら、描画のほうはかくかくした動きになります(動きのある処理ならばの話です。動きの
無いようなものであれば、見た目に変化は無いでしょう)
これは通常のアプリケーションにも有効で、たとえば通信自体は10msの間隔で動かし、
通信データは3回に1回の割合で(つまり30msに1回)更新するといった方法を取れます。
このフレームスキップは簡単で、単純にフレームスキップ用のカウンタを設け、指定回
数につき1回実行すれば良いわけです。
これについては例を上げるまでもないでしょう。

で、これを前回の紙芝居プログラムに組み込んでみたところ……
25FPS
遅っ!
透過処理使ってるからかなぁ、ものすごく遅い。
このループを使うかはともかくとして、もう少し調べてみることにします。
ちゃんと作るとしたらDirectGraphicsを使わないといけないのかな、やっぱ。
最終更新:2008年01月17日 13:00
ツールボックス

下から選んでください:

新しいページを作成する
ヘルプ / FAQ もご覧ください。