思考日記:シーン切替の実装方法

もくじへ戻る


どうやらゲームを作る界隈(?)では,大きな場面の単位のことを「シーン」と呼ぶみたいだね.
【ゲームを起動したら最初は「タイトル画面シーン」になっていて,そこでゲームプレイを開始する操作が成されたら次のシーン(何シーンかはわからんけど)に遷移する】みたいな.
「シーン」とは,要は「状態(State)」だけど,その中でも特に大きなやつのことを指す言葉なのかな?

まぁ言葉の正確な定義はよくわからないけど,本稿ではざっくりこれくらい↓の粒度のものを「シーン」と呼ぶということにしてみよう.

//ゲームの実装
class MyGame
{
public:
  //これがメインループから呼ばれるとして……
  void Update( 操作入力情報 Input )
  {
    //更新対象は「現在のシーン」
    m_pCurrentScene->Update( Input );
    //描画対象も「現在のシーン」
    m_pCurrentScene->Draw();
  }
  
private:
  IScene *m_pCurrentScene;  //ポインタかどうかはわからないけども,どうにかして「現在のシーン」を指す
}

こんな形の実装にするなら,場面転換を可能にするには,「現在のシーン」をどうにかして切り替える方法が必要だ.
シーンの切り替えとはどう実装するのかな?


シーン型

まず「シーン」を,必要そうなメソッドがあるインタフェースクラスとして考えておく.こんなもんか?:

// シーン:ゲーム状態の排他的な切り替え単位
struct IScene
{
  virtual void OnEnter(){}  //カレントのシーンになった際に呼ばれる
  virtual void OnLeave(){}  //カレントシーンではなくなった(他のシーンがカレントになった)際に呼ばれる

  //更新
  //引数は何か操作入力の情報を得られるやつ
  //戻り値は未定
  virtual SceneUpdateResult Update( const IController &Controller ) = 0;
  
  //描画
  virtual void Draw() = 0;
};

具体的な実装には,シーンの保持/管理者である MyGame への参照を ctor で教えておくことにしよう.
各シーンの側から「別のシーンに遷移したいんですが?」っていう要求を MyGame に対して出すことができるように.

//何らかのシーンの実装
class TitleScene : public IScene
{
public:
  TitleScene( MyGame &Owner ) : m_Owner(Owner) {}  //ctorで指定されるので覚えておく
private:
  m_Owner;
};

シーンをどう保持するのか?

常に複数種類のシーンのうちのいずれか1つをカレントとする,という話だが,そしたら MyGame はその「複数種類のシーン」というのを…

みたいな.
これは,シーンのオブジェクトの寿命に関係してくる.

後者側の場合,シーン側から「別のシーンに遷移してください」って要求したら自身が破壊されるかもしれないわけだ.
例えばシーン遷移を要求する手段というのが以下のメソッドである場合……

//話をわかりやすくするために newとdelete を用いて書けば,こんなことになっているかもしれない
void MyGame::ChangeSceneTo( 何かカレントにすべきシーンを指定する情報 )
{
  m_pCurrentScene->OnLeave();
  delete m_pCurrentScene;  //←このメソッドを呼び出したオブジェクトを破壊
  
  m_pCurrentScene = new 新たにカレントとするシーン();
  m_pCuurentScene->OnEnter();
}

これを呼ぶ側はちょっと注意が必要だ.

void TitleScene::SomePrivateMethod()
{
  /* ... */
  
  //このメソッドの呼び出しから処理が返ってくるよりも前に,自身(*this)は解体されているかもしれない.
  m_Onwer.ChangeSceneTo( 次のシーンを指定する何らかの値 );
  
  //……ということは,これ以降 (*this) に触る記述があってはならない
  //(このメソッド内だけでなく,このメソッドから返った先も含まれる)
}

↑の疑似コード内のコメントを書いている間にも「そんな危ねぇ仕組みはノーサンキュー」と思えてくる.なにそれこわい.避けたい.

そしたら,前者側の(全部持っておく)形にするのか? っていうと,それもどうかなー,って感じだ.
だって「最初にゲームを開始したらもう二度とタイトル画面には戻らない」っていう話ならば, TitleScene を保持し続ける意味はないもの.
(まぁ,実際は「捨てるやつと,保持し続けるやつとがある」みたいな話になりそうだけど,とにかく「捨てる」場合があるならば「なにそれこわい.避けたい」っていう話があるわけだ)
さぁどうしよう?

(1)破壊されてもいいじゃない

//仕様:呼び出された時点でカレントであるシーンのオブジェクトは,このメソッドが処理を返すよりも前に破壊される.
void MyGame::ChangeSceneTo( 何かカレントにすべきシーンを指定する情報 );

ってしっかりと書いとけばOKっていう考え方.あると思います.
シーン側が「自身が解体されるかもしれない」ではなくて「必ず解体される」のだと知っているなら,相応に実装すればいい.

(2)すぐには破壊しない

シーンが処理を返すまで破壊を遅延すればこわくないよね,っていう考え方.

void MyGame::ChangeSceneTo( 何かカレントにすべきシーンを指定する情報 )
{
  m_pSceneToBeDestroyed = m_pCurrentScene;  //ここでは破棄せずに「こいつを破棄すべき」旨を覚えておき……
  m_pCurrentScene = new 新たにカレントとするシーン();
}

void MyGame::Update( 操作入力情報 Input )
{
  //更新と描画
  m_pCurrentScene->Update( Input );
  m_pCurrentScene->Draw();
  
  //このへんで不要なシーンオブジェクトを破棄する
  if( m_pSceneToBeDestroyed )
  {
    delete m_pSceneToBeDestroyed;
    m_pSceneToBeDestroyed = nullptr;
  }
}

こんなしょぼい実装だと IScene::Update() 内から MyGame::ChangeSceneTo() が2回以上呼ばれたらまずいことになるから,もうちょい真面目な実装にするか, あるいは「2回以上呼んではならない.複数回呼んだ場合の動作は不定!」とかなんとかいう決め事にでもする必要はある.
(2回呼ぶなんて状況が考えられない気もするから「不定」でいいんじゃないかな)

(3)戻り値で要求

IScene::Update() 内から MyGame::ChangeSceneTo() なんてのを呼ぶから問題が起きるというのであれば,
IScene::Update() の戻り値で「別のシーンに遷移してくれ」と言えばいいだけだよね,という考え方.

struct IScene
{
  //戻り値で遷移先を返すよ! 遷移しなくていいときには nullptr を返すよ!
  virtual std::unique_ptr<IScene> Update( const IController &Controller ) = 0;
};

とかだと,必要なだけセットアップしたオブジェクトを MyGame 側に渡すことができるけど, 「 MyGame 側でいくつかの種類のシーンについては単一のオブジェクトを保持し続けている」みたいな実装ができなくなる.

シーンオブジェクトそのものではなく,シーンの種類を示す値( enum とか )みたいなのを返すという形だと, 遷移先シーンをセットアップするにはどうすればいいのか?っていう話が生じそう.
だからといって「戻り値が過度に複雑な構造」とかは嫌だしなぁ.

(4) (2)+遷移先毎のメソッド

void MyGame::ChangeSceneTo( 何かカレントにすべきシーンを指定する情報 ) の引数は何なんだよ?
このメソッド内でその引数を見て遷移先を判断するとしたら二度手間じゃね?

void ChangeToXXXScene( XXXSceneのセットアップに必要なもの );
void ChangeToYYYScene( YYYSceneのセットアップに必要なもの );
...

って感じでシーンの種類だけ遷移用メソッドを用意すればよくね? そうすれば遷移先毎に異なる情報を渡せるし.
で,これらの実装としては (2) みたく処理を遅延させとけば.

「シーンの種類分だけメソッドを用意するのがだるい」みたいな欠点はありそうだけど,そうはいってもシーンってのはかなりでかい粒度の物なハズだから「せいぜい数種類」で済むんじゃないかな.


というわけで

みたいな事情を鑑みて決める必要がありそうだけど,とりあえずは (4) みたいな形がいろんな面でいちばん「易しい」気がするかな.