画面遷移構造雑考(一応完結)

Bonjour!前回の更新からもう200日近くたっているようです。あれまあ。最近はC#+XNAでいそいそゲーム開発駆け出しをしていて、今はライブラリ整備をしているのですが、遷移構造のところで霧中になってしまったので、書いて整理をしたいと思います。

定義

「画面遷移」という言葉は割りとポピュラーなようですが一応自分の確認のためにも定義を確認してみます。ゲームにおいては、大きく画面の見た目と機能が変わることがあります。たとえば、タイトル画面で"Press Any Key"と出ているときに何らかのキーを押せばたぶんメインメニュー画面に飛ぶでしょう。タイトル画面においては画面上で何か動かす処理やら、キーに反応してメインメニュー画面に飛ぶ処理やらが要求されますし、メインメニュー画面においては矢印キーで項目を選択したり、項目を表示する機能が要求されるでしょう。このように、タイトル画面の処理とメインメニュー画面の機能には明らかに関連が薄いので、これらの処理を一箇所にまとめて書くのはコードの可読性からして問題だと思われます。そこで、画面の状態で明らかに違うように分けたとき、それらの画面を互いに(何らかの規則にしたがって)移り変わる構造のことをこの記事の中では「画面遷移」と呼ぶことにします。

なぜこれのことで悩む必要があるのか。いい加減に作ればいいのではないか。

確かにいい加減に作って問題が出てから直せばいい、悩んだって徒に時間を消費するだけだというのはそんな気もするし、今のモットーとして「できるだけ小柄に」を採用しているのではっきり言って真理なのですが、ここであえてじっくり悩んだほうが良いと考える理由は次のとおりです。

画面遷移構造はどんなゲームにも出てくる

ミニゲームにしてもタイトル画面、ゲーム画面、リザルト画面の3つは出てきますし、何を作るにしても画面遷移から目を背けている間ずっと苦しむことになります。

可読性にたぶん効いてくる

「困難は分割せよ」は理系の基本ですが、画面遷移を上手に作れば画面ごとに分けてプログラムを書くことをプログラマに推奨することが出来る、というわけで眠いときにプログラムを書いてもそこそこ可読性の高いものを書くことが出来る、はず。

pygame時代の反省

pygame時代に安直に組んだら(あれはあれでそこそこ使えたのですが)もっと複雑なものを作ろうとすると面倒なことになるということが当時わかったのですがその問題点については後述。

考えてみる

ざっくり考えてみる

まず前提として、ゲームは主に描画をつかさどるDrawメソッドとロジックをつかさどるUpdateメソッドとから成っています。というわけで、SceneクラスにDrawメソッドとUpdateメソッドを持たせて、メインのところにSceneの変数CurrentSceneを持たせ、そのSceneを次々取り替えていき、メインのDrawとUpdateからCurrentSceneのDrawとUpdateを呼び出せば作れそうです。

どう実現するか

いきなり閑話休題なのですが、これをXNAでどう実現するかが不安なので少し考えてみます。いまいちXNAのフィロソフィー*1がよくわからないのですが、基本的にゲームで扱うもの全般はGameComponentというものらしく、それをGame.Components(ComponentsはGameComponentのCollection)にAddすると有効になるみたいな雰囲気みたいです。DrawableGameComponentというGameComponentを継承したクラスはUpdateとDrawを持っていて、Addしておくと自動的に呼んでくれます。というわけで登場人物的なものはGameComponentを主軸に書いていくことを考えています。もっといい方法(というかXNA開発者の意図した方法)を知っている人がいたら教えて下しあ><。問題はGame.Componentsがデフォルトでは1個しかなく、これに階層構造を持たせる方法も見たらないので、これをどう扱っていくかというところになりそうです。(そしてこの基本方針が一番怪しい。

そういうわけで、各SceneにIsDeadみたいなメソッドを持たせて、メインのUpdateからそのIsDeadを呼び出し、そのSceneがすでに終了したことがわかったらそのSceneに所属するGameComponentをSceneから取得してそれらをGame.Componentsから削除し、またそのSceneから次のSceneを受け取ってそれをメインのCurrentSceneに代入して作ればよさげです。*2

遷移エフェクトについて考えてみる

ところでこのまま実装するとたぶん画面が一瞬で切り替わってしまいますが、普通画面が切り替わるときは画面がゆっくり暗転するなりの画面の移り変わりのエフェクトがかかるものです。パワーポイントをご存知の方は「ワイプアウト」などを想像していただければそれです。というわけでそれを組み込むことを考えてみます。画面遷移時に遷移前のSceneと遷移後のSceneの両方にアクセスすることが考えられるので、先ほどあったCurrentSceneのほかにPreviousSceneも必要になりそうです。またSceneは、今自分が遷移中でCurrentSceneであるのか遷移が終わってCurrentSceneであるのかPreviousSceneであるのかを区別する方法が必要になります。うち、PreviousSceneであるかどうかは、そのSceneを終わらせるかの裁量はそのSceneにあるわけで、それを示すのがIsDeadでしたから明らかですが、遷移中でCurrentSceneであるのか遷移が終わってCurrentSceneであるのかの情報は遷移をつかさどるものに裁量があるはずです。というわけで、いったんこの「遷移をつかさどるもの」をどのように作るべきかを考えるのがよさそうです。

と、一瞬思ったのですが

CurrentSceneだけでなんとかなる気がしてきました。つまり、Scene甲がその役目を終えたときに、そのScene甲を次のScene乙に渡してしまい、CurrentSceneをScene乙にして、遷移エフェクトはScene乙に担わせればそれでいいような気がしてきました。そして、Scene乙は遷移が終了したときにScene甲を破棄します。このほうがすっきりしていますし、「ざっくり考えてみる」で考えたところからたいした変更なしに実現できそうです。一応、Scene甲はScene乙を生み出して参照せずにメインに渡しますし、Scene乙はScene甲を参照するだけなので、相互参照にはなっていないはずですが、破棄タイミングは不安です。そういうわけで、SceneがPreviousSceneを持つことになりそうです。

どう実現するか

二つの画面を扱うということは、それぞれのSceneについて、その画面を直接にスクリーンにアウトプットするわけには行きません。具体的に考えればすぐにわかる話で、たとえば簡単そうなワイプアウト、つまりScene甲の画面が徐々に左に押し出されていき、Scene乙の画面が右からScene甲を押し出すように入ってくるような遷移エフェクトを考えれば、直接にScene甲やScene乙の画面がスクリーンに直接アウトプットされると、そのアウトプット位置が制御できないためにワイプアウトは実現できなくなってしまうことがわかると思います。というわけで、少なくとも画面遷移中はScene甲や乙の画面は画像としてアウトプットされ、その画像を配置するという風にしないと実現しえません。画像の出力先を2DテクスチャであるTexture2Dにするためには、
http://stackoverflow.com/questions/8425922/draw-a-string-to-texture-in-xna
この辺のことを使えばいいと思うのですが、この辺で煮詰まったのでまた後で。

一度Componentsについて考えてみる(そして昨日煮詰まった原因)

なんかComponentsのなれない仕様に惑わされている気がするので、一度Componentsについてまとめなおしてみます。Game.ComponentsはGameComponentCollectionであって、MSDNhttp://msdn.microsoft.com/ja-jp/library/microsoft.xna.framework.game.components%28v=xnagamestudio.40%29.aspxによれば、

ゲームに対してゲーム コンポーネントを登録するには、対象のコンポーネントを Game.Components.Add に渡します。登録されたコンポーネントは、Game.Initialize、Game.Update、および Game.Draw メソッドから呼び出される描画、更新、および初期化メソッドを持ちます。

とのことなので、描画、更新のするされないはComponentsに含まれているいないで決まるわけです。(本当はGameComponentのVisibleをfalseにしてしまうととまるのですが、使わないものがコレクションに居座っているのも気持ちが悪いので、これに関しては心の片隅に止めつつなかったものとして考えて見ます。)このことを意識しておくと、(抽象的な言葉遣いになってしまうのですが)Sceneは基本的にはGameComponentを包むものであって、SceneがComponentsへSceneの保有するGameComponentを出し入れするという使い方が基本になりそうです。

おそらく(自分のことなんですけどね)昨日煮詰まった原因はここにあります。CurrentSceneがPreviousSceneSceneを保有する形にしてしまうと、CurrentSceneがアクティブなわけですからCurrentSceneのGameComponentはComponentsに登録されているわけですが、その内部でPreviousSceneを生かしておくからにはCurrentScene.PreviousSceneのGameComponentもComponentsに登録されてしまっていて、2画面を描き分けることが出来なくなってしまいます。つまり、機能考えた「と、一瞬思ったのですが」の「どう実現するか」は結構気持ち悪いということがわかりました。

で、どうするか

Game.Componentsの仕組みから言って、どう考えても1フレームには1Sceneにつき1回しかDrawやUpdateしか呼べないことになりますから、割と振り出しに戻った観はありますがComponentsをSceneにも持たせるということになりそうです。なんかすごい回り道でしたが(ここまでで約4500文字)、まあこれしかないという証明が出来てよかった…

とりあえず遷移なしのSceneの再構成

SceneはGameComponentを継承したクラスで、Scene.Componentsを持つ。Scene.DrawではScene.Componentsのすべてに対してDrawが呼ばれ、Scene.UpdateではScene.Componentsのすべてに対してUpdateが呼ばれ、Scene.InitializeではScene.Componentsのすべてに対してInitializeがよばれる。Sceneがアクティブなとき、そのSceneはメインの(つまりGameの)CurrentSceneに入っていて、またGame.Componentsにも入っている。基本的にGame.Componentsの中は1つのSceneだけが入っていることになる。デバッグ用とかで2つ以上にはなるかも

画面遷移ありのSceneの構成

やっぱり遷移のコードとSceneのコードは切り分けておいたほうが良い気がするのでそういう方針で作る。遷移もある意味Scene的な要素を持っているので、むしろ遷移もSceneの一種として捉えたほうがいいような気がしてきた。つまり、Sceneはゲーム中の画面を担うGameSceneクラスと遷移を担うTransSceneクラスに派生し(インターフェースでもいい、というかSceneはインターフェースのほうがいいような気が…)、TransSceneクラスは2つのGameSceneである、遷移前のGameScene甲と遷移後のGameScene乙をとる。遷移中はメインのCurrentSceneはTransSceneになる。TransScene.DrawメソッドでGameScene甲や乙のDrawを呼び出してその画面をTextureに振り向け、適当に処理して描画するということになる。遷移が終わったらTransSceneにもIsDeadがあって、GameScene同様Scene切り替えの手続きでメインのCurrentSceneをScene乙に切り替える。

各GameSceneはやはり遷移中でアクティブであるのか遷移が終わってアクティブであるのかすでに役目を終えたのかの3状態を持つことになる。遷移中かどうかはメインから(あるいはTransSceneから)与えら得る情報なので、その窓口としてStartMainPhaseみたいな遷移が終わったときに呼ばれるvoidのメソッドが必要になる。

SceneはIsDeadとGetNextSceneを持つことになる。メインはUpdateでCurrentScene.IsDeadを呼んで、trueならGetNextSceneで次のSceneを取得しCurrentSceneとする。

だんだん文章がひどいことに…

Sceneは階層構造を持つかもしれない

いままでは2状態の遷移だけを考え、遷移したら遷移した前のことはさっぱり忘れてしまっていたのですが、それだけではすまないかもしれません。たとえば、シューティングゲームでのポーズ画面は一種の状態かもしれません。(最初の定義の「明らかに違う状態」かどうかは怪しいですが。)そのとき、ポーズ中のSceneはゲーム画面にオーバーラップして表示されることになるでしょう。つまり、ゲームのSceneにアクセスできないと困るわけです。

どう実現するか

すると、SceneがSceneを持つことになるかもしれませんが、これを実現するためにいままでの構造を変更する必要はなさそうです。
f:id:sle:20120919234005p:plain
たとえば上のような状態を考えてみたいと思います。TitleからGameってなんやねんという気がするのですが、間にMenuを挟んでもあまり本質的ではないので省きました。鍵は右二つのMainMenuとSystemMenuで、想定としてはゲーム中にEscか何かを押すとSystemMenuに入ってゲームの中断(Titleへ戻る)ができ、さらに左右キーでItemMenuとSystemMenuを行き来できるというところです。二つのMenuはGame画面にオーバーラップして描かれるわけですからGameへのアクセス権が必要です。

今までと主に違うのはGameからSystemMenuへの遷移ですが、これは遷移時にSystemMenuがGameを要求するようにしておけばいいのではないでしょうか。そして、CurrentSceneはSystemMenuのSceneになります。また、ItemMenuに移るときもやはりGameが要求され、これはSystemMenuから渡されます。


なんか満足したのでこの辺で。そのうち推敲するかも。

*1:不安になってぐぐってみたのですが、"Philosophy of Programming Languages"という用法があったので良かった。「その言語はどう使われることを意図しているか」ぐらいの意味のつもりです。

*2:相互参照でどうこうする方法もあるとは思うのですが、一応避けられる場面なら避けておこうかなーと。