7.1 タイマー
7.1.1 一定間隔で処理を繰り返す
ゲームのようなマルチメディア系アプリケーションでは、画面上に表示する図形やイメージを短い間隔で連続して更新する、いわゆるアニメーション処理が要求されます。アニメーションはビジネス系アプリケーションやツールでは特に必要とされない技術ですが、ゲームアプリケーションの場合はアニメーション処理がプログラムの大部分を占める重要なコードとなります。また、アニメーションはイメージを連続して更新するため、コンピュータへの負荷がもっとも大きい部分でもあります。
これまでは、OnPaint() メソッドが呼び出されるタイミングでコントロールに図形を描画したり、マウスやキーボードイベントなどから画面を更新することによって描画する図形を更新することができました。マウスやキーボードの入力によって描画する画像の座標を変更すれば、ユーザーがキャラクターを操作することができます。しかし、これはユーザーがマウスやキーボードを使って操作するまで、プログラムは待機し続けなければなりません。
アニメーションを行うには、ユーザーの操作とは関係なく、一定間隔で積極的に描画更新を行う必要があります。もしかすると、for 文を使ったループで定期的に画面を更新するコードを思いつくかもしれませんが、OnPaint() メソッドの中でそのようなコードを書いてしまった場合、イベントを監視するメッセージループに制御が戻らなくなってしまうため、アニメーションを行っている間、アプリケーションがビジー状態になってしまいます。
何らかの描画プログラムを一定間隔で繰り返し実行しながら画面を更新し続ける最も簡単な方法はタイマーを利用することです。タイマーはイベントの一種で、指定したミリ秒間隔でイベントとして登録したメソッドを呼び出します。タイマーを利用するには System.Windows.Forms.Timer クラスを使います。
System.Object System.MarshalByRefObject System.ComponentModel.Component System.Windows.Forms.Timer
public class Timer : Component
Timer クラスには、パラメータを受け取らない単純なコンストラクタが定義されているのでコンストラクタからインスタンスを生成します。インスタンスを生成しただけでは、まだイベントは何も発生しません。
タイマーを起動する前に、タイマーイベントを発生させる感覚を設定しなければなりません。Interval プロパティでイベントを発生させる感覚を設定したり取得することができます。
public int Interval { get; set; }
Interval プロパティに設定する値は、ミリ秒単位(1/1000 秒)でイベントを発生させる間隔を指定します。ただし、必ずここで指定したミリ秒間隔でイベントが発生することを期待するべきではありません。イベントの処理が遅れるなどの原因で、次に発生するタイマーイベントの間隔が遅延する可能性があります。そのため、タイマーイベントを使って時間を計測するべきではありません。
Interval プロパティに希望する値を設定したら、次に Timer オブジェクトのイベントにデリゲートを登録する必要があります。Interval に指定した時間が経過すると Timer オブジェクトは Tick イベントを発生させます。
public event EventHandler Tick
Tick に設定するのは特にイベントデータを持たない単純な EventHandler デリゲート型のオブジェクトです。
Interval プロパティで適切なイベント間隔を設定し、Tick イベントに呼び出してほしいメソッドへのデリゲートを設定すれば、後はタイマーを起動するだけです。タイマーの起動は Start() メソッドを呼び出します。起動したタイマーを停止させるには Stop() メソッドを使います。
public void Start()
public void Stop()
Start() メソッドを呼び出してタイマーを起動しても、処理がメッセージループに戻らなければイベントは発生しません。System.Windows.Forms.Timer クラスは、Windows フォームアプリケーションで、単一のスレッドで動作するように設計されている。並行処理を行うマルチスレッドについてはこの後で解説します。
using System; using System.Drawing; using System.Windows.Forms; public class Test : Form { private int count; private void Test_Tick(object sender, EventArgs e) { count++; Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); Font font = new Font(Font.Name, 20); Brush brush = new SolidBrush(ForeColor); e.Graphics.DrawString("Count=" + count, font, brush, 0, 0); } public Test() { Timer timer = new Timer(); timer.Interval = 100; timer.Tick += new EventHandler(Test_Tick); timer.Start(); } static void Main() { Application.Run(new Test()); } }
コード1は、100 ミリ秒ごとに Test_Tick() メソッドを呼び出すように Timer オブジェクトを設定しています。Test_Tick() メソッドでは int 型の count フィールドをインクリメントしてフォームを再描画しています。これによって、フォームに表示される数値がタイマーイベントが発生するたびにインクリメントされます。タイマーイベントが定期的に繰り返し発生していることを視覚的に確認できます。
この機能を利用して、例えば表示する図形の座標をタイマーイベントが発生するたびに移動させれば、表示している図やイメージが自動的に動いているように見えます。ゲームの敵キャラクターが移動するなど、ユーザーの操作に関係なくゲームが進行させる方法にタイマーを使うことができます。画像や図形が移動するプログラムは、例えば次のようなコードになるでしょう。
using System; using System.Drawing; using System.Windows.Forms; public class Test : Form { private bool addh, addv; private Rectangle rect = new Rectangle(0, 0, 32, 32); private void Test_Tick(object sender, EventArgs e) { rect.X += addh ? 5 : -5; rect.Y += addv ? 5 : -5; if (rect.X + rect.Width >= ClientSize.Width) addh = false; else if(rect.X <= 0) addh = true; if (rect.Y + rect.Height >= ClientSize.Height) addv = false; else if(rect.Y <= 0) addv = true; Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); Brush brush = new SolidBrush(ForeColor); e.Graphics.FillEllipse(brush, rect); } public Test() { Timer timer = new Timer(); timer.Interval = 10; timer.Tick += new EventHandler(Test_Tick); timer.Start(); } static void Main() { Application.Run(new Test()); } }
コード2は、タイマーイベントが発生するたびに楕円を描画する矩形の座標を変更しています。OnPinat() メソッドでは rect フィールドの値にしたがって FillEllipse() メソッドで楕円を描画するだけです。この楕円の座標を操作しているのは、タイマーイベントで呼び出される Test_Tick() メソッドです。楕円は、フォームのクライアント領域の隅に到達すると、移動方向を反転させます。