ホーム < ゲームつくろー! < オブジェクト指向設計編 < シーンを切り替え初期化し再生する

その8 CS3: シーンを切り替え初期化し再生する


 中〜大規模のゲームになると、ゲームの内容を分けなければ収拾がつかなくなります。この分割単位がシーンです。1つ1つのシーンを作り上げ、それを連結することにより、ゲームは実質どこまででも続けられる事になります。また、シーン単位で分岐させることにより、ゲームの内容に深みが出てきます。いずれにせよ、ゲームを分割するという考えは遅かれ早かれ必要になってきます。

 分割することによる恩恵の裏返しとして、いくつか難点も発生します。1つは「データの引継ぎ」です。あるシーンから別のシーンに移る時、前のシーンから状態を引き継ぐ必要があります。つまりデータの共有が発生します。また、分割したシーンをどんどん繋いでいく仕組みを整える必要があります。これらは、ある程度形式化しておかないといけない部分です。この章では、そのようなシーンの取り扱いについて考えてみる事にします。



@ シーンの役割と永続性

 シーンオブジェクトはある1つのゲームシーンの再生を司ります。普通シーンは千差万別であり、同じものは2つとありません。しかし似たシーンと言うのは、特に1つのゲーム内について良くあります。例えばSTGならば、自機があり敵が複数いてスクロールしてボスがいる。各ステージはそういうステージテンプレートに従っています。ある型にはまるシーン、そうでないシーン、それらはゲームの一部となり、ある場面を演出します。このシーンが沢山繋がっていく事によって、大きな1つのゲームが成り立っていきます。

 シーンは正しく初期化されればそれ単独でも再生できる能力を持ちます。そうするように実装した方が逆に面倒が発生しにくくなります。そこで問題になるのは「初期化(再初期化)」です。千差万別のシーンには共通項がほぼありません。つまり単純に仮想関数を設計できないんです。特に初期化については、各シーンで共通するメソッドとそうでないメソッドがあり過ぎます。ですから、シーンの初期化というのはかなり特別な事をしなければならなくなります。

 まずは、次の図をご覧下さい。


 この図はシーンに関係するデータや変数の所在と動きをまとめたものです。大分にごちゃごちゃしておりますので、少しずつ説明していきます。まず右側のオレンジの角丸四角形に注目してください。これはシーンオブジェクトを表しています。これを見ますと、シーン内部でのみ使用する「ローカル変数群」と、他のシーンと共通に使用する「グローバル変数群へのポインタ」がある事がわかります。シーンを正しく再生(再開)するには、これら2つの変数群に適切な値が設定されている必要があります。
 シーンオブジェクトは、自身の持つローカル変数群の情報を適切な形(メモリブロック)にして外部に出力する能力を持ちます。いわゆる「セーブ」です。これは、単にローカル変数の値をごっそり吐き出すのではなくて、ちゃんと「ロード」を見込んだ必要な情報のみを出力します。出力したローカル変数のメモリブロックは、そのまま外部ファイルとして保存されますが、この時「シーンID」も一緒に保存しておく事が重要です。シーンIDというのは、1つのシーンにつけられた固有番号で、ゲーム内で一意とします。そうすることで、今度はシーンIDからシーンオブジェクトを逆に動的に生成し、対応するデータを流し込み、シーンのロードが出来るようになります。
 一方グローバル変数群のデータはシーンオブジェクトではなくてシーン管理者が出力します。グローバルなものを持っているのは、常に上位のオブジェクトなんです。この出力はローカル変数群のそれとほぼ一緒です。ただ、ここでもシーンIDを保存する事が重要になります。これにより、ロード時にシーン管理者は「どのシーンを動的に生成してどのファイルを読み込めば良いか」を適切に判断できます。

 以上から、シーンに永続性を持たせるには、初期化時にグローバル変数群とローカル変数群をドン!と与えれば、シーン単独で再生ができる事、そして、それら変数群が適切なタイミングでセーブロードできる事が必要であることがわかると思います。これは、単純ではないんです。詳しい事は後述しますが、まずはシーンを再生したり切り替えたりするのに必要なインターフェイス群から見ていきましょう。



A シーン生成者(シーンファクトリ)

 シーンオブジェクトはどこかで生成される必要があります。それを担うのがシーンファクトリです。シーンファクトリはシーンIDから生成すべきシーンを識別し外部に渡します。受け取るのは多分シーン管理者です(シーンファクトリの役目は比較的単純なので、上の図ではシーンファクトリの表記を省いています)。

 シーンファクトリクラスの実装は非常に簡単です。シーンIDを引数に取る生成関数を設けて、その内部でシーンを動的に生成します。生成したシーンの初期化をファクトリ自身が行うか否かは悩みどころですが、今回は生成のみに単純化します。初期化は次に説明するシーン管理者が担う事にしましょう。



B シーン管理者(シーンマネージャ)

 シーンマネージャは、シーンファクトリから出力される空っぽのシーンの初期化を行います。@で述べましたように、シーンの初期化には「ローカル変数」と「グローバル変数」の2つの変数群を設定する必要がありまして、シーンマネージャが持ちたいのはその内のグローバル変数です。しかし、シーンと言うのは千差万別であり、シーン管理者が持つべき具体的なグローバル変数と言うのは何だか良くわかりません。また、具体的なグローバル変数を持った瞬間に、シーン管理者は特定シーンに依存してしまいますので、持つ事自体も良くないかもしれません。このように具体的な変数を持つのがはばかれるので、代わりとしてコレクタオブジェクトという「抽象的な変数」を持つ事にします。これは、いわば何でも格納できる箱です。「何を持って良いかわからないので持たない」のではなくて、「何でも持ってしまおう」と考えたわけです。シーンに与えるべき具体的な変数は、コレクタオブジェクトというパックにまとめて、ドンと渡してしまう事にします。

 1つのシーンは、自身の持つ更新関数が連続的に呼び出される事によってちょっとずつ動きます。それを呼ぶのもシーンマネージャの役目です。よって、シーンマネージャの中には「ゲームループ」が存在します。毎回シーンを更新する度に、シーンマネージャはシーンのメッセージを聞きます。シーンが終了し、次のシーンの生成要求が出されていたら、シーンマネージャは生成すべきシーン(シーンIDで識別)をシーンファクトリに渡し、今のシーンから保存すべき情報を抜き取ってセーブし、シーンを消去すると共に、新しいシーンの初期化を行います。初期化が正常に終了したら、そのシーンを再生します。



C コレクタクラス

 シーンの初期化をする上で非常に重要な役目を成すのが「コレクタクラス」です。コレクタクラスの役目は、データの塊を保持してそれを引き出すためのメソッドを提供する事です。コレクタクラスは内部にメモリブロックを持ち、そこにデータを蓄えます。データは理路整然に並べられますが、数字だけだと意味が分からなくなるので、個々を識別するための明確なIDも一緒に保持します。ある値にアクセスしたい時、クライアントは変数のIDを指示します。そのIDに一致する変数があれば、コレクタクラスはそのメモリブロックへのポインタを返します。一致する変数が無ければ、新規にメモリを確保する事になります。これにより、クライアントはコレクタクラスに保存されているデータに直接アクセスできるようになります。また、ポインタの先が無い(ダングリングポインタ)になることもありません。

 下図はコレクタクラスの振る舞いのイメージ図です。


 変数を識別するIDは、ユーザ任意でありますが、今回はGUID(16byte)としました(ちょっとやり過ぎかもしれません)。

 コレクタクラスのもう1つ大きな仕事は、自分が持っているメモリブロックのセーブとロードです。保持しているメモリブロックをそのままファイルに吐き出すと、ロード時にどのシーンの情報であるか不明になってしまいます。また確保すべきメモリサイズも判断できません。そこで、セーブする時にはシーンIDと書き込みサイズを一緒に記録するようにします。ロードする時には呼び戻して欲しいシーンIDからファイルを識別し、そこに書き込まれているサイズを元にメモリを確保し、一気にデータを復帰させます。これで、シーンの永続性が保証されます。

 余談ですが、セーブ時に暗号化と可逆圧縮を掛けてセキュリティを向上させるとなお良いでしょう。また、データ改ざん防止のためのパリティを入れることもできます(デバッグが多少面倒になりますが)。そういう動作はコレクタクラスの派生型で担えるはずです。

 この動作と似たような設計に「Mementoパターン」があります(詳しくはリンク先をご覧下さい)。Mementoクラスとフレンド関係のあるクラスにのみ自身の持つ変数へのアクセスメソッドを提供し、そうでないクラスには最低限のメソッドしか公開しないことで変数の機密性を保つパターンです。このパターンはインターフェイスを固めなければならないので、派生クラスに対してはMementoクラスも派生して再度フレンド関係を結ばなければなりません。これは結構わずらわしいものなんです。今回のコレクトクラスは多少の機密の公開(変数のIDが分かればダイレクトアクセスができる)と引き換えにその煩わしさを解消しています。



D シーン関連クラス図

 ここまでに登場したシーン、シーンファクトリ、シーンマネージャ、コレクトクラス(インターフェイス)の関連図は次のようになります。


 メッセージのやり取りをざっと説明します。まずISceneManager::SetSceneFactoryメソッドにシーンファクトリを登録します。この段階でISceneFactory::GetFirstSceneメソッドが呼び出され、最初のシーンがセッティング(再初期化)されます。次にISceneManager::UpdateSceneメソッドを呼び出すとゲームループが開始され、内部でIScene::Updateメソッドが連続的に呼ばれゲームが動き出します。IScene::Updateメソッドが返す値(SCENE_MESSAGE列挙型)が次のシーンの生成要求であった場合、Updateメソッドの引数pNextSceneにシーンIDが格納されます。シーンマネージャは、現在のシーンのローカル情報をIScene::Saveメソッドを呼び出す事で保持し(場合によってはファイルに落とす)、シーンファクトリに次のシーンIDを渡して新しいシーンの生成をしてもらいます。シーン生成後シーンマネージャは新しいコレクトオブジェクトを作成し、ICollector::LoadメソッドにシーンID(第2引数は予備用)を渡して、指定のシーンに必要なローカル情報をファイル(それ以外もあります)から作成してもらいます。後はシーンファクトリが生成したシーンのIScene::Loadメソッドに、ローカル情報が格納されたICollectorオブジェクトとシーンマネージャの持つグローバル情報をそれぞれ渡せば、シーンが必要な再初期化を行います。これで次のシーンへしっかり移行できました。IScene::Updateメソッドがセーブ要求を出した場合、シーンマネージャはローカル情報とグローバル情報の両方をセーブします。逆にロード要求があった場合は指定のシーンをロードして再生します。終了宣言をした場合、シーンマネージャは素直に終了します(セーブはしません)。・・・う〜ん、文章にするとさっぱりですね(^-^;;;


 シーンの切り替えはゲームの基本ですが、汎用性を持たせようと思うと今回の章のようにやけに面倒な事になってしまいます。ただ、一度仕組みを作ってしまうと、次の開発からはそれを「基準」とできますので、開発効率がぐんと上がります。コレクトクラスはシーンだけでなく何にでも応用が利く便利なクラスですから、是非実装してみてください。