XNA Game Studio Expressで遊んでいるうちに頭が痛くなってきた(2)

■ プログラム Posted by ひぐま (Higmmer) on 2006-09-08 at 09:25:59

◆共有テーマ: ゲーム製作 関連 [ゲーム]

さて、そもそもXNAフレームワークでゲームプログラムが実行されるとき、内部では一体どのような順番で処理が行われているのでしょうか? そして、リソースの初期化/再作成は一体どのタイミングで行えばいいのでしょう?? ざっと考えられるだけでもこれだけあります。正直多すぎてワケ分かりません┐(´ー`)┌

  • Gameクラスのコンストラクタ
  • Game.OnStartingメソッド
  • Game.Startingイベント
  • GraphicsComponent.DeviceCreatedイベント
  • GraphicsComponent.DeviceResettingイベント
  • GraphicsComponent.DeviceResetイベント
  • GraphicsComponent.DeviceDisposingイベント
  • GraphicsDevice.DeviceResettingイベント
  • GraphicsDevice.DeviceResetイベント
  • GraphicsDevice.DeviceLostイベント
  • GraphicsDevice.Disposingイベント

XNAのチュートリアル(Getting Started with XNA)ではOnStartingメソッドとGraphicsDevice.ResetイベントハンドラでLoadResourcesメソッドを呼んでいますし、「Spacewar Starter Kit」ではコンストラクタ、Startingイベントハンドラ、そしてDeviceCreatedイベントハンドラのそれぞれで初期化処理を行っているようです。両方ともMicrosoft提供なのに全く一貫性がありません。

というわけで、リフレクションを駆使(?)してXNAの中を追っかけてみた結果、以下の順番で処理が行われていることが分かりました。MyGameクラスが自分が作成したGameクラスだとして…。

XNAにおけるプログラム起動からゲーム開始までの主な処理の流れ

  1. MyGameをnewする。 (Program.csのMainメソッド内)
  2. GameのコンストラクタがWindowsGameHostをnewする。
  3. WindowsGameHostのコンストラクタがWindowsGameWindowをnewする。更にWindowsGameWindowのコンストラクタがWindowsGameFormをnew(これはおなじみSystem.Windows.Forms.Formのサブクラス)。
  4. MyGameのコンストラクタがInitializeComponent()を呼ぶ (MyGame.cs内)。
  5. InitializeComponent()でGraphicsComponentをnewする。 (MyGame.Designer.cs内)
  6. InitializeComponent()でGameComponents.Add()した瞬間、GameComponent.OnGameChanging()が発動。
  7. GraphicsComponent.OnGameChanging()からChangeDevice()→CreateDevice()と呼ばれる。この段階で新しいGraphicsDeviceが作成されるGraphicsComponent.DeviceCreatedが発生
  8. MyGame.Run()を実行。 (Program.csのMainメソッド内)
  9. 各GameComponentのStart()が呼ばれる(GraphicsComponentでは何も起こらない)
  10. その後、Game.OnStartingを呼ぶ。OnStartingがオーバーライドされていればここで実行される。
  11. 次にGame.OnStartingがStartingイベントを駆動する。
  12. WindowsGameHostが3で作ったWindowsGameFormインスタンスを引数としてApplication.Runを実行(その際、Application.Idleイベントハンドラを登録)。
  13. Application.IdleイベントハンドラからGameHost.OnIdle()→Game.HostIdle()→Game.Tick()と呼ばれ、その中で時間の計測を行って適切なタイミングでGame.Update()及びGame.Draw()を呼ぶ。
  14. MyGame.Draw()の最初に呼ぶGraphicsDevice.EnsureDevice()はGraphicsDevice.GraphicsDeviceStatusプロパティの値をチェックする。Lost(消失/リセット不可)の場合は何もせずfalseを返す。NotReset(消失/リセット可)の場合はGraphicsDevice.Reset()を試みる(DeviceResettingとDeviceResetが発生)。失敗した場合はCreateDevice()を呼んで再作成(GraphicsDevice.DisposingとGraphicsComponent.DeviceCreatedが発生)。
  15. MyGame.Draw()の最後に呼ぶGraphicsDevice.Present()が失敗した場合はDeviceLostが発生。それに続いて更にDeviceLostException例外がスローされる→例外ハンドラを書いていない場合は突然ゲームが落ちることに(´・ω・`)

……ふぅ。とても全部は追いきれませんが、だいたいこういった処理の流れになっているようです。これらの解析結果から分かることは…。

  • MyGameクラスのコンストラクタでInitializeComponent()した後ではGraphicsDeviceが作成されているので、この段階から各種のグラフィックリソースを作成することができる。
  • 初回のDeviceCreatedイベントを捕捉したいならGameComponents.Add()の前にイベントハンドラを登録しとかないといけない→IDEデザイナのプロパティ(イベント)欄から操作するのが簡単確実。
  • DeviceResetの前に発生するイベントは実際にはDeviceLostではなくDeviceResetting(これはMDXには無かったイベント)。つまり、ResourcePool.Defaultなリソースはここで解放しなければならない。
  • DeviceLostイベント発生時はセットでDeviceLostException例外がスローされるので、これに対処するにはイベントハンドラだけでは不十分で、少なくともGrahicsDevice.Present()呼び出しをtry-catchで包んでおかないとダメ。

実際に試してみた(トリビア風)

実際に検証プログラムを書いて確認してみたところ、ウィンドウの最小化からの復帰やリサイズ時に起こるイベントは専らDeviceResettingばっかりで、DeviceLostは発生していないようでした。また、Winキー+[L]でPCをロックすると時たまDeviceLostが発生しますが、やはりその場合は必ずPresent()がDeviceLostException例外を投げるので、何もしないとハンドルされていない例外となってアプリが落ちてしまうことが分かりました。DeviceCreatedに関しては検証できる環境(マルチモニタ?)が無いので今回は確かめられませんでしたが……。

というわけで、正しく動作するコードの書き方はこんな感じになると思います。

(追記) GraphicsComponent.DeviceXXX系のイベントハンドラ登録部分をIDEデザイナから操作した場合に生成されるコードと同等になるように修正しました。
class MyGame : Game { private GraphicsComponent graphics; // (通常はMyGame.Designer.cs内にある) // コンストラクタ public MyGame() { InitializeComponent(); // 真っ先に必要なオブジェクトはここで初期化する // (ここでグラフィックリソースを作成することも一応可能) } // 各GameComponent初期化 // (通常はMyGame.Designer.cs内にある。直接編集じゃなくIDEデザイナを使うべし) private void InitializeComponent() { this.graphics = new GraphicsComponent(); // イベントハンドラ登録 graphics.DeviceDisposing += new EventHandler(graphics_DeviceDisposing); graphics.DeviceResetting += new EventHandler(graphics_DeviceResetting); graphics.DeviceReset += new EventHandler(graphics_DeviceReset); graphics.DeviceCreated += new EventHandler(graphics_DeviceCreated); this.Starting += new EventHandler<GameEventArgs>(this.MyGame_Starting); // 次の操作でGraphicsDeviceが作成される→DeviceCreated発生 this.GameComponents.Add(this.graphics); } // ゲーム開始前処理 - イベントハンドラを書く場合 private void MyGame_Starting(object sender, GameEventArgs e) { // ゲーム独自の初期化処理はここで(タイトル画面の設定とか) // サウンドや入力系などの初期化が必要な場合もここで } // ゲーム終了処理 - オーバーライドする場合 protected override void OnExiting() { base.OnExiting(); // 忘れずに呼ぶこと! ReleaseAllResources(); // 全てのグラフィックリソースを解放 // イベントハンドラ削除(しなくても最後はGCが働くから実害はないけど) graphics.DeviceDisposing -= new EventHandler(graphics_DeviceDisposing); graphics.DeviceResetting -= new EventHandler(graphics_DeviceResetting); graphics.DeviceReset -= new EventHandler(graphics_DeviceReset); graphics.DeviceCreated -= new EventHandler(graphics_DeviceCreated); graphics.GraphicsDevice.DeviceLost -= new EventHandler(GaphicsDevice_DeviceLost); this.Starting -= new EventHandler<GameEventArgs>(this.MyGame_Starting); } // デバイス作成 private void graphics_DeviceCreated(object sender, EventArgs e) { CreateAllResources(); // 全てのグラフィックリソースを作成 // イベントハンドラ登録 // DeviceLostは何故かGraphicsComponentには無いのでここで登録しないとダメ // (内部にはHandleDeviceLostというハンドラがあるのに何もせずreturnしてる) graphics.GraphicsDevice.DeviceLost += new EventHandler(GaphicsDevice_DeviceLost); } // デバイス破棄 private void graphics_DeviceDisposing(object sender, EventArgs e) { ReleaseAllResources(); // 全てのグラフィックリソースを解放 // イベントハンドラ削除 graphics.GraphicsDevice.DeviceLost -= new EventHandler(GaphicsDevice_DeviceLost); } // デバイスリセット直前 private void graphics_DeviceResetting(object sender, EventArgs e) { ReleaseUnmanagedResources(); // ResourcePool.Defaultのリソースを解放 } // デバイスリセット直後 private void graphics_DeviceReset(object sender, EventArgs e) { CreateUnmanagedResources(); // ResourcePool.Defaultのリソースを作成 } // デバイスロスト private void GraphicsDevice_DeviceLost(object sender, EventArgs e) { // 何をすればいいんだろ……? } // 状態更新 protected override void Update() { float elapsed = (float)ElapsedTime.TotalSeconds; UpdateMyGameState(); // ゲーム更新処理 UpdateComponents(); // 各GameComponentを更新 } // 画面描画 protected override void Draw() { try { if(!graphics.EnsureDevice()) return; // GraphicsDeviceが有効なことを確認 graphics.GraphicsDevice.Clear(Color.Black); graphics.GraphicsDevice.BeginScene(); DrawMyGameScene(); // ゲーム画面描画 DrawComponents(); // 各GameComponentを描画 graphics.GraphicsDevice.EndScene(); graphics.GraphicsDevice.Present(); // デバイスロスト発生の可能性あり!! } catch(DeviceLostException) { // 何もできないので放置 } } }

あーしんど。これ書くだけで小一時間かかってしまいました(だめだなぁ)……。いろんな人のサンプルとかを見てもここまでちゃんと書いてあるソースは見たことないですが、これを初めてC#(.NET)やDirectXに触る人が正しく書くのは99%不可能だと思います。というかチュートリアルからして既にこうなってますし。

void LoadResources() { myTexture = Texture2D.FromFile(graphics.GraphicsDevice, "mytexture.bmp"); spriteBatch = new SpriteBatch(graphics.GraphicsDevice); }

これだと古いリソースがDisposeされないまま宙に浮いてしまうので無駄にメモリを喰うことになります。特に巨大なテクスチャでも使っていた場合は目も当てられません。正しくは、

void LoadResources() { ReleaseResources(); myTexture = Texture2D.FromFile(graphics.GraphicsDevice, "mytexture.bmp"); spriteBatch = new SpriteBatch(graphics.GraphicsDevice); } void ReleaseResources() { if(myTexture != null){ myTexture.Dispose(); myTexture = null; } if(spriteBatch != null){ spriteBatch.Dispose(); spriteBatch = null; } }

とすべきでしょう(ReleaseResourcesを分けたのはDeviceDisposingに対応するため)。

ややこしいなぁ…。本当にここまでする必要があるの?

まぁもっとも、前回も書いたようにDisposeするのを忘れたとしてもオブジェクトが参照されなくなった時点でGCが働いてファイナライザが起動→Disposeを実行するはずであり、メモリリークみたいな悲惨なことは起こらないはずなのですが、いつ回収されるかも分からない無駄なオブジェクトが暫くメモリ上に居座ったままという状態はどうにも精神衛生上よろしくないような気がするのは自分だけでしょうか。神経質になりすぎなのかなぁ? そういったことをあまり気にしなくても致命的な事態にはならないというのが.NETの利点といえば利点なんでしょうけど……。

というか、せめてXNAプロジェクトのひな形やチュートリアルには次からもう少しちゃんとしたコードを用意しておいて欲しいなぁ。もちろん、イベントや例外の発生するタイミングや正しい対処方法についてもしっかりとリファレンスに載せて欲しいところ。現状ではDirectX/MDXの知識なしにまともなコードを書くのは至難の業に思えますし、ましてやC++やC#の経験が全く無い人がいきなり使いこなすのはほぼ不可能なのではないでしょうか?

トラックバック

この記事について書く(FC2ブログユーザー)
※言及リンクの無いトラックバックは無効です

PageTop▲

コメント

PageTop▲

コメントの投稿

 
 
 
 
 
 (後で編集・削除したいなら必須)
 
  

PageTop▲