ゲームループによる連続的なメソッドの呼び出し

XNA Franework では、Windows フォームで開発するような Windows アプリケーションとは異なりイベント駆動ではありません。一般的な GUI 用のフレームワークは描画が必要になると Paint イベントが発生し、マウスボタンが押されると MouseDown イベントや Click イベントが発生するという仕組みですが、XNA Framework では基本的にプログラム主導で制御することになります。

ゼロからはじめるXNAプログラミング - C#の自作ゲームがXbox 360でも動く「XNA Game Studio」
ゼロからはじめるXNAプログラミング - ゲームの起動とウィンドウ制御

Game クラスの Run() メソッドを実行してゲームが起動されると、XNA Framework はゲームループと呼ばれる反復処理に入ります。これは、Windows アプリケーションにおけるメッセージループのようなものですが、処理の内容は大まかにゲームデータの更新とゲーム画面の描画を行う 2 つのメソッドを永遠と繰り返すだけです。イベントを待機するのではなく、短い間隔でメソッドを実行してゲームの状態を更新し続けるのです。

まず最初に、ゲームのデータを更新するために Update() メソッドが呼び出され、その後に更新されたデータに基づいてゲーム画面を構築するために Draw() メソッドが呼び出されます。Game クラスを継承したクラスで Update() メソッドと Draw() メソッドをオーバーライドし、Update() メソッド内にデータの更新処理を、Draw() メソッドに描画処理を記述します。

Game クラス Update() メソッド

protected virtual void Update (
         GameTime gameTime
)

Game クラス Draw() メソッド

protected virtual void Draw (
         GameTime gameTime
)

これらのメソッドに渡される gameTime パラメータには、メソッドが呼び出された時点のゲーム時間と実時間に関連する情報を提供する GameTime クラスのオブジェクトです。これらの値を使って、ゲームの進捗率やアニメーションを管理できます。

Microsoft.Xna.Framework.GameTime クラス

public class GameTime

このクラスは、ゲーム時間と実時間の 2 つに分けて経過時間を提供します。ゲーム時間は Update() メソッドの呼び出しによる更新回数から現在の時間を求めたもので、ゲームが停止している時間は含まれません。ゲーム起動後からの経過をゲーム時間で取得するには TotalGameTime プロパティを、実時間を取得するには TotalRealTime プロパティを使います。

GameTime クラス TotalGameTime プロパティ

public TimeSpan TotalGameTime { get; set; }

GameTime クラス TotalRealTime プロパティ

public TimeSpan TotalRealTime { get; set; }

例えば、ゲームが毎秒 60 回更新と描画が繰り返される固定ステップの場合、Update() メソッドが呼び出された回数からゲーム時間が求められます。逆も同様です。ゲーム時間が 2 秒であれば、Update() メソッドが 120 回呼び出された状態となります。強い負荷などによってゲームが停止している間、ゲーム時間も合わせて停止します。 これに対して、実時間はゲームの進行には無関係に現実の時間経過を表します。

まず最初に、Game クラスを継承した新しいクラスを作成し、そこで Update() メソッドと Draw() メソッドをオーバーライドしてください。これらのメソッドはゲームが起動している間、短い間隔で繰り返し呼び出され続けるという点に注意してください。これらのメソッドの中で負荷のかかる処理を行えば、それだけゲームが遅くなってしまいます。

Update() メソッドと Draw() メソッドが正常に呼び出されているかどうかをテストしたいのですが、まだ画面にテキストを描画する方法を紹介していません。テキストを表示するには、ゲームで使用するフォントを登録して初期化工程で読み込むなどの手続きが必要です。この場では、簡単にウィンドウのタイトルバーにゲーム時間を表示してテストします。また、同時に Debug クラスを利用して開発環境のトレースリスナに文字列を表示しましょう。通常、デバッグ時にのみ Visual Studio の「出力」ウィンドウに出力されます。

コードを実行するには

Visual C# 2008 Express Editonで新規プロジェクトを作成した後(参照:ゼロからはじめるXNAプログラミング - C#の自作ゲームがXbox 360でも動く「XNA Game Studio」)、自動生成されたProgram.csとGame1.csを削除、[プロジェクト]メニューの[クラスの追加]で表示される「新しい項目の追加」からコードファイルを選択し(図A)、Test.csの名前でコードを追加し、そこにコードを記述し実行させてください(図B)。

図A.自動生成されたProgram.csとGame1.csを削除、[プロジェクト]メニューの[クラスの追加]でコードファイルを作成

図B.作成したTest.csにサンプルのコードを記述して実行

コード 01

using System.Diagnostics;
using Microsoft.Xna.Framework;

public class Test : Game
{
    static void Main(string[] args)
    {
        using (Test game = new Test()) game.Run();
    }

    protected override void Update(GameTime gameTime)
    {
        Window.Title = "Update GameTime=" + gameTime.TotalGameTime +
              ", RealTime=" + gameTime.TotalRealTime;

        Debug.Print("Update GameTime={0}, RealTime={1}", gameTime.TotalGameTime, gameTime.TotalRealTime);

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        Debug.Print("Draw GameTime={0}, RealTime={1}", gameTime.TotalGameTime, gameTime.TotalRealTime);
        base.Draw(gameTime);
    }
}

実行結果

コード01の実行結果

コード 01 を実行すると、オーバーライドした Update() メソッドと Draw() メソッドが定期的に呼び出されていることが確認できます。通常、ゲーム内のデータを Update() メソッドで更新し、Draw() メソッドでデータに基づいて描画するという設計になるでしょう。ゲームデータと描画処理が依存しないよう適切に分離することがコツとなります。

デフォルトの設定では Windows や Xbox 360 は毎秒 60 回(約 16 ミリ秒に 1 回)のペースで Update() メソッドと Draw() メソッドを呼び出します。しかし、負荷の大きい場面で更新が遅れた場合 Draw() メソッドの呼び出しが省略されます。Draw() メソッドの呼び出しが省略されたかどうかは IsRunningSlowly プロパティで確認できます。

Game クラス IsRunningSlowly プロパティ

public bool IsRunningSlowly { get; set; }

このプロパティが true の場合、処理が遅れて Draw() メソッドの呼び出しが省略されています。Draw() メソッドが省略されると、1 フレーム分の画面が飛ばされるフレーム落ちと呼ばれる現象が発生します。一般的なゲームは 1 秒間に 30 ~ 60 回のペースで描画します。

コード 02

using System.Threading;
using System.Diagnostics;
using Microsoft.Xna.Framework;

public class Test : Game
{
    static void Main(string[] args)
    {
        using (Test game = new Test()) game.Run();
    }

    protected override void Update(GameTime gameTime)
    {
        Window.Title = "Slowly=" + gameTime.IsRunningSlowly;
        Debug.Print("Update GameTime={0}, RealTime={1}", gameTime.TotalGameTime, gameTime.TotalRealTime);

        base.Update(gameTime);
     }

    protected override void Draw(GameTime gameTime)
    {
        Debug.Print("Draw GameTime={0}, RealTime={1}", gameTime.TotalGameTime, gameTime.TotalRealTime);

        if (!gameTime.IsRunningSlowly)
        {
            Debug.Print("Sleep!!");
            Thread.Sleep(100);
        }
        base.Draw(gameTime);
    }
}

実行結果

コード02の実行結果

コード 02 は、Draw() メソッドの中で IsRunningSlowly プロパティの値を調べ、値が false であれば意図的にスレッドを停止させて負荷の大きい描画処理が発生した状態を再現します。その結果、ゲームの更新が遅れ IsRunningSlowly プロパティが true になり、一時的に Draw() メソッドが呼び出されなくなることを確認できます。

Xbox 360 や Zune のような、ハードウェアスペックが固定されている環境向けのゲームであれば開発時にゲームのパフォーマンスを確認できますが、PC 向けのゲームは多様な環境で実行されます。実行時に IsRunningSlowly プロパティを調べ、ゲームの更新が遅れるようであれば自動的に品質を下げて速度を優先させるなど、動的なパフォーマンス管理が可能です。

Draw() メソッドが省略されても、ゲームデータに矛盾が発生しないようにプログラムしてください。例えば、敵や弾との衝突を調べるあたり判定を Draw() メソッドの中に書いてしまうと Draw() メソッドの呼び出しが省略されている間は判定ができないため、弾がすり抜けてしまうといったバグが発生してしまいます。ゲームデータを制御するコードは Update() メソッドに書いてください。

ゲームの初期化

XNA Framework は .NET Framework をベースにしたマネージ環境です。アプリケーションは C# 言語で記述することができ、オブジェクトの管理は共通言語ランタイムが行ってくれます。これは一見、生産的でメモリリークのリスクを減少させますが、この仕組みをよく理解していない人が不用意に書くと問題が発生することもあります。特に、メモリを自動解放するガベージコレクションが頻繁に発生すると CPU がメモリ解放処理に食い潰されパフォーマンスに影響を及ぼします。

安定したパフォーマンスを得るには、ガベージコレクションの発生を少なくする必要があります。ガベージコレクションはメモリ使用量が一定のしきい値を超えたときに発生するので、短い寿命のオブジェクトを短いサイクルで作ることを避けなければなりません。特に Update() メソッドと Draw() メソッドの中、不用意にインスタンス化やボクシングを行わないように注意してください。

通常、XNA Framework ではゲーム内で使用するデータを Initialize() メソッドでインスタンス化しフィールドに保持します。オブジェクトの寿命がゲームの寿命と同じであれば、ゲーム中にガベージコレクションが発生する回数を減少させられます。

Run() メソッドによってゲームが起動されると、ゲームは初期化処理を行うために Initialize() メソッドを一度だけ呼び出します。このメソッドをオーバーライドすることで、ゲームに必要なデータの初期化を行えます。多くの場合、コンストラクタでも代用できますが XNA Framework 関連のリソースを初期化するタイミングとしては Run() メソッドが呼び出された後に実行される Initialize() メソッドを使ったほうが安全です。例えば、コンストラクタの時点ではデバイスが初期化されていません。

Game クラス Initialize() メソッド

protected virtual void Initialize ()

このメソッドをオーバーライドしてデータを初期化してください。ゲームで利用するデータはフィールドに保持し、Update() メソッドや Draw() メソッドで共有します。より大規模なゲームを設計する場合は、データをサービス化して疎結合にする方法もありますが、カジュアルゲームの規模であればゲームデータを管理するクラスと Game の派生クラスだけでも作れます。

ここまでの流れを簡単にまとめると Game クラスを継承する新しいゲームは、次の 3 つのメソッドをオーバーライドしてゲームに必要な機能を書き加えていくことになります。

public class Test : Game
{
    protected override void Initialize()
    {
        //初期化処理をここに書きます。
        base.Initialize();
    }

    protected override void Update(GameTime gameTime)
    {
        //データ更新処理をここに書きます。
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        //描画処理をここに書きます
        base.Draw(gameTime);
    }
}

Game クラスのメソッドをオーバーライドするとき、基底クラスのメソッドを呼び出すことを忘れないでください。Game クラスは GameComponent クラスによるゲーム機能を部品化する仕組みを提供しています。オーバーライドされた基底クラスのメソッドを呼び出さなければ、Game クラスで提供されている GameComponent のシステムが機能しなくなります。

また、実際のゲームでは画面に描画するための画像や 3D モデル、サウンド、フォントなどのリソースを読み込まなければなりません。グラフィックデバイスの初期化や画像の表示などについては次回にご説明します。