ホーム < ゲームつくろー! < ゲーム製作技術編

その6 シーン構成・遷移は肩の力を抜いて


 ゲーム製作の規模が大きくなってくると、場面を分割して開発する必要に迫られてきます。いわゆる「シーン(Scene)」の導入です。

 シーンの初期化、終了、及びシーン遷移と、シーンを取り巻く振る舞いには何だか共通項がある気がして、色々と凝ったクラス設計にしたくなりがちです。ごたぶにもれず、私もそうでした。でも、なぜだかシーン関連クラスは凝れば凝るほど使いにくくなってしまいます。凝った挙句1つのシーンを再生するために沢山のクラスを外部から与える必要が生じ、その分シーンの独立再生がどんどん面倒になってきます。これは特にデバッグ作業で致命的な手間となって跳ね返ってくることがあります。シーンを独立に再生させるというのはゲーム開発においてとても大切な事なんです。

 そこで、難しく考えるのをやめてみましょう。この章ではシーンクラスをどう実装してどう繋げていけば良いかを、難しく考えすぎずに肩の力を抜いた状態で試行錯誤してみたいと思います。



@ シーンクラスの再利用性を捨ててみる

 1つのダンジョンシーンを例に挙げます。

 あるダンジョンには色々な仕組み(イベント)があります。また一番下に行くとダンジョンのボスがいて、倒して地上に戻ると特定のイベントが発動する。RPGのお決まりの仕組みですね。

 長いRPGの開発において、このダンジョンだけを切り取って開発しデバッグできる仕組みが絶対に必要になります。そこでダンジョンを1つの大きなシーンクラス(DungeonScene)ですべて管理するようにします。ダンジョンシーンには各階の構成とか、ボスシーンとか、他に細かい部分が沢山含まれますが、それすべての要素がDungeonSceneクラスに盛り込まれます。そしてそのダンジョンを再生するにはDungeonScene::exec()を実行するだけと究極にシンプルな仕組みにします。これにより、そのダンジョンはRPGの大局の一部として開発できるようになります。

 ダンジョンクラスの中身は必然的に大変複雑になります。でもRPG全体の複雑性よりもはるかにシンプルです。複雑度にもよりますが、ある状況に特化した複雑なクラスは通常再利用性に乏しくなります。これがゲームの1シーンを担うクラスとなりますと「再利用性なんて最初から捨てる」くらいで望んだ方が気楽です。

 シーンクラスは再利用しない。そう考えれば「クラスを小汚くしても良い」わけでして、かなり気楽に実装できます。ま、とは言うものの、大規模なシーンではシーンクラスの中身があまりに煩雑になり過ぎる場合もあります。そういう時には管理クラスの中でサブシーンクラスのオブジェクトを持つようにします。サブシーンはある小さなシーンを再生してくれます(ダンジョン1Fとか2Fとか)。サブシーンが集まって1つの大きなシーンを構成する。こうすると、ゲーム開発はどんどん細分化されていきます。そして1つのサブシーンの開発はどんどん小さく楽になっていきます(その分クラスの数は増えます)。

 シーンはまずもって難しく考え過ぎない。これが、サクサクゲームを作っていくコツな気がします。



A シーンクラスの初期化

 シーンは初期化が必要です。初期化というのは実は曖昧な言葉でして、初期化と再初期化(再開)に垣根はありません。要はシーンクラスに値を与えることでどの状態からでも再開できればいいわけです。

 このことから、シーン管理クラスのオブジェクトには生成時に必要な初期化情報を全部与えます。与える情報は大きく2つに分類されます。1つは描画・入力・サウンドデバイスです。これはゲームの骨肉にあたり、唯一どのシーンにも共通します。私は1つ1つのデバイスを与えるのが面倒なのでDeviceSetという構造体を作って渡しています。もう1つはシーン固有の初期化情報です。これは多分1つのシーンクラスに1つ対応する構造体を定義することになると思います。

 デバイスと初期化情報はコンストラクタで与えても良いですしinitメソッドを通しても良いでしょう。とにかく、シーンクラスが再開するのに必要な情報を一度にすべて与えます。初期化メソッドを細かく分ける実装も考えられますが、あまりお勧めしません。メソッドを分割するほど「クラスの使い方」が複雑になり、また「初期化が終わらない状態で再開する」という状況が発生しやすくなるため、整合性を保つのが難しくなります。

 初期化(再開)で何でも与えると言っても、テクスチャやマップ情報などのリソース(データ)は与えてはいけません。これらはシーンクラスのみが知れば良い情報です。@でシーンクラスの再利用性は考えないようにと述べました。その理念にのっとれば、シーンクラスに固有のリソース名を刻印しても別に構いません。外部がリソースを与えるとすると、外部がリソースを作らなければならなくなるわけで、その手間はシーンの独立再生に大きくマイナス作用をします。

 ダンジョンシーンのinitメソッドは、例えば次のようになります:

bool DungeonScene::init( DeviceSet *dev, DungeonInfo *info ) {
   // 各種デバイスの登録
   dev_ = dev;

   // ダンジョン情報の保持
   info_ = info;

   // リソース読み込み
   if( !readyResouce ) {
      // リソースの準備が出来ていないので新規に読み込む
      LoadResource();
   }

   // 引数の初期化情報に合わせた初期化処理
   if( info->dungeonclear ) {
      // クリア時の状態にする呼び出しをここで
   }
   else {
      // クリア前の状態にする呼び出しをここで
   }

   return true;
}

bool DungeonScene::LoadResource() {
   // ダンジョンマップの読み込み
   dngMap_ = (DngMap)LoadMap( DUNGEON_MAP_B1_FILE );
   // ダンジョン構成オブジェクトの読み込み
   dngWall_ = (Object)LoadObj( DUNGEON_OBJ_WALL_FILE );
   return true;
}

 上のような流れは他のシーンでも似たり寄ったりなのですが、かといってinitメソッドを簡単に仮想関数にはできません。シーンを初期化するための引数が違うためです。無理やり構造体をvoid*型などで統一すれば仮想化できなくはないですが、内部ではダウンキャストが必要です。第一呼び出す外部は結局型のまったく違う構造体を渡すわけで、わざわざメソッドを仮想化する意味が殆どありません。難しく考えずに「各シーン固有な初期化メソッドが1つあるんだ」と割り切った方が気楽です。



B シーンクラスの実行

 シーンクラスの初期化(再開)が終わったら、シーンクラスが持つexecメソッドを毎回呼び出します。これでシーンが1ステップ進みます。execメソッドを呼び出している大局側は、中で何が起こっているか知りません。知らないけどただひたすらに呼び出す。これは究極に簡単なシーンの再生方法です。ちなみに、どのシーンもexecメソッドを持てますので、execメソッドは仮想メソッドになれます。

 exec仮想メソッドの中身はもう好きに書いてもらって結構です。シーンは普通複雑に遷移するので、execメソッドの中身はそういう遷移を繋ぐような実装になると思います。クラス内遷移についてはクラス構築編「クラス内メソッド遷移からswitch〜caseを消すMethodExecテンプレート」がかなり使えると思います。なんだか難しそうならばswitch〜caseでえいやーとやってしまってもきっと構いません。要はシーンがクラスに包まれて完全にパッケージ化していれば何も問題無いんです。



C シーンが終わった!さてどうする?

 シーンが進み、ダンジョンのボスも倒し、やったぜと外に出ました。この瞬間にダンジョンシーンの役目は終わります。

 どのシーンも「今のexecメソッドの呼び出しから抜けるとシーンが終わる」という瞬間を知ることができます。その状態になったら終了処理に移行します。終了処理では「再度シーンが読み込まれる時の状態」に戻します。これはとても大切なプロセスです。例えばダンジョンシーンならば次の呼び出し(ダンジョン再突入)でちゃんと地下一階に来るように再設定するわけです。終了処理の一元化は初期化よりも面倒だったりするのですが、シーン終了時に内部で呼び出すfinishメソッドくらいは用意して良いかもしれません。またリソースを解放するreleaseResouceメソッドは欲しいところです。

 ただ再設定の時の注意事項があります。シーンが終了したからといってシーン自身がリソースを解放してはいけません。どうしてか?シーンを引き続いて取っておきたい場合があるからです。例えば、街のシーンからフィールドシーンへ移行した時に「あ、買い物し忘れた」とすぐ街に戻る事があります。この時街シーンのリソースを街シーン自身が解放してしまうと、街に入った時に激しくリソースの再読み込みが発生します。プレイヤーはその間待たされることになるわけです。街シーンのリソースが残っていれば、シーン移行はたぶん一瞬で終わります。プラットフォームによってはメモリの余裕が無くてシーンごとにリソースを解放しなければならない場合もあるでしょう。逆にそうでないプラットフォームもあります(昨今のPCは超贅沢です)。そういうリソースをそのまま残すか、それともメモリ確保のために消してしまうかは、そのシーンを使うもっと大局のクラスが管理したい部分なんです。ですから、リソースの解放は内部ではせずに、releaseResourceメソッドをpublicとして公開し、これを外部が呼び出す事でシーンのリソース解放が行われるようにします。



D シーンの遷移

 シーンが終わった後は、必ず別のシーンにつなげないといけません。シーンの遷移です。大局クラスが1つのシーンの終わりを知るには、execメソッドの戻り値を使うのが楽でしょう。単純な話で、実行中はSCENE_RUNNING、終わったらSCENE_FINISHを返すようにすれば良いだけです。

 大局クラスはSCENE_FINISHを捕まえたら、次にどのシーンを再生すべきかを判断します。この判断は「自分自身で知っているパターン」と「シーンに教えてもらうパターン」があると思います。自分自身で知っているパターンの場合は、シーンが終わったら次のシーンにさっさと遷移させてしまいます。一方シーンに教えてもらうパターンは、シーンからのフィードバックを必要とします。これは初期化時に渡した「シーン状態構造体」の中身に記録してもらっても良いでしょう。

 ダンジョンシーンクラスの例ですと、ダンジョンを途中で抜け出す時にクリアしたかどうかをinitメソッドで与えたシーン状態構造体(DungeonInfo構造体)に書き込みます。その構造体の内容を見て、大局のクラスは次のシーンの呼び出しを決めるわけです。「シーンクラスに状態を取得するメソッドを設けては?」。それでももちろんOKです。構造体の中身を見るにせよ、状態を取得するにせよ、要は大局シーンに遷移先を教える手段を設ければいいんです。

 ただ、シーンが遷移先を指定すると言う事は、そのシーンが自分自身が繋がる先を知っているという前提となります。それがまずいシーンもあるでしょう(ゲーム中のオプションメニューなど)。その辺は臨機応変で対応しましょう。



E シーン遷移の分割単位

 大抵のゲームは、会社(チーム)ロゴが出て、すばらしいムービーが始まって、タイトル画面になって、場合によってはデモがあり、ユーザのメニュー選択でゲームが始まり、色々決め事をした後ゲームがスタートし、ゲーム内で実に様々な遷移を繰り返し、ゲームが終わればメニューかタイトルかロゴに戻る、そういう流れになっていると思います。この遷移すべてについて大局クラスに全部担わせるのはちょっと酷というものです。そこで考えるのが「シーン遷移の分割」です。

 昨今のほぼすべてのゲームは「ゲームを開始する前の遷移部分」と「ゲーム本体」の2つに大別されます。これらは有効な分割単位と言えます。ゲーム本体はもちろんもっと細かくシーンが細分化されるでしょうし、ゲームを開始する前の遷移部分もプログラマの判断でもっと細分化して構いません。ただあまり再分化すると大局クラスの開発が煩雑になってきます。その良いバランスは実装しながら調節ができるはずです。



F シーンクラス宣言

 最後に本章で考えてきたゆるいシーンクラスの宣言部分の一例を挙げます:

SceneBaseクラス宣言部
enum SCENESTATE {
   SCENE_FINISH,
   SCENE_RUNNIMG
};

class SceneBase {
protected:
   virtual void finish() = 0;

public:
   virtual SCENEFLAG exec() = 0;   // 実行
   virtual UINT getNextScene() = 0;     // 次のシーン番号を取得
   virtual void releaseResouce() = 0;    // リソースの解放
};

 基本クラスはとてもシンプルです。実際のシーンはこのSceneクラスを派生させて実装します。そこで初めて初期化をするinitメソッドが定義される事になります。

 シーンを遷移させる部分は例えばこうなります:

シーン遷移例
SCENESTATE MainProcedure::exec() {
   SCENESTATE flag;
   // 現在のシーンへ飛ぶ
   switch( curScene_ ) {
  case SCENE_DUNGEON:
      flag = dungeonScene->exec();
      if ( flag == SCENE_FINISH ) {
         if( dungeonInfo.clear ) {
            // ダンジョン崩壊シーンへ
            curScene_ = SCENE_DUNGEONBROKEN;
         }
         else {
            // フィールドシーンへ
            curScene_ = SCENE_FIELD;
         }
      }
      break;

   case SCENE_FIELD:
      flag = fieldScene->exec();
      ...

   case SCENE_DUNGEONBROKEN:
      flag = dungeonBrokenScene->exec();
      ...

   }
   return SCENE_RUNNING;
}

 caseごとにメソッドを分割したりMethodExecテンプレートを使うなどすればもっと見通しが良くなります。



 シーンの遷移について、私もかなり色々と試してきました。シーンを統一すべくクラスを工夫してみたり、テンプレート化してみたり…。でも結局はクラスの使い方と規約だらけで逆に使いにくいものになってしまいました。そこで「大きいシーンクラスは再利用しない!」と決心してみると、かなり気楽にシーンを組んで繋げられる状態になりました。経験則なのでこれが必ず正しいというものでもありませんが、シーンの遷移を懲りすぎてゲーム開発が進まないのでは本末転倒です。本章で貫いてきた「難しく考えすぎない」精神で、見通しの良いゲーム開発をしていきたいもんです。