電気通信工学研究会公式ブログ

関西大学の公認サークル、電気通信工学研究会のブログです。

ゲームのシーン管理の話

4×4個のパネルを音に合わせて押すゲームを作っている会計です。

会長から、音ゲー班でもブログ記事書いてって言われたので、

ソフトウェア担当の私が適当に好き勝手書きます。

といっても、今作っているのがどんなゲームかを紹介するという

記事では著作権とか色々とまずそうなので、

私の書いてるプログラムのシーン管理システムでも紹介します。

シーン、というのは、ゲーム上の各場面のことです。

例えば、格闘ゲームなら、戦闘、キャラ選択、オープニングなどの、

それぞれの場面一つ一つを、戦闘シーン、キャラ選択シーン、等と言います。

……一般的な定義になってるかどうかはさておき、

この記事ではそういう定義です。

多数のシーンを管理するプログラムでは、

シーンを管理するIDを定義し、それをグローバル関数などに格納して、

各シーンごとの処理に分岐させる、というのが一般的なようです。

C++で書くなら、こんな感じ。

// シーンIDの定義

#define PLAY_SCENE 0

#define RESULT_SCENE 1

#define START_SCENE 2

以下IDの定義が延々と続く

int sceneID; // シーン管理変数

この辺に色々な関数がある

// WinMainのメッセージループから呼ばれる

int SwitchScene() {

  switch(sceneID) {

    case PLAY_SCENE:

      return PlayScene();

    case RESULT_SCENE:

      return ResultScene();

    case START_SCENE:

      return StartScene();

    ...以下略

  }

}

私が最初にこれを見た時、「こんなのC++じゃねえ!」と思いました。

なんか、C++っていうよりC言語っぽいですよね。

とりあえず、オブジェクト指向的ではありません。

もちろん、使っているものが高等であればいいって訳じゃありません。

問題は、保守しやすく、改変が容易であるかどうかです。

そういう意味で、この仕組みには、いくつか欠点があります。

1つ目は「シーンを追加する毎に、手を入れる箇所が3つもある」という点。

IDの定義、SwitchScene関数内、シーン自体を処理する関数、

この3つを、各シーンごとに追加しなくてはなりません。

どうせなら、一箇所にまとめたいものです。その方が、ミスも減ります。

2つ目は「使っているリソースが管理しずらい」という事。

普通、一つのシーンが終わるまでは、そこで使用する画像や音楽などは、

メモリ上に保持しておかなければなりません。

他にも、グラフィックドライバへのポインタだとか、

プログラム終了まで必要な物もあります。

では、それらの保持されたリソースは、

この仕組みでどのように管理するのでしょうか。

全部グローバル変数でポインタを保持?

もし私がそんな仕組みでプログラムを書いたなら、

間違えて、どっかで解放しちゃってて、

うっかりヌルポインタ使って強制終了とか、

そんな事態が、コンパイル一回につき6割ぐらいの確率で起こります。

出来れば、そういう敏感な部分は、

privateな場所に隔離したいものです。

そこで、こんな仕組みを考えました。

やってることは、さっきと同等です。

// シーンクラスの基底

class SceneBase {

public:

  // シーンの処理をする関数(返り値はエラーの有無)

  virtual bool Run(引数は、シーンに共通して必要なものを入れる)=0;

};

class PlayScene : public SceneBase {

private:

  PlaySceneで必要な物のみを定義する

public:

  virtual bool Run(SceneBaseと同様) {

    PlaySceneの処理

  }

};

class ResultScene : public SceneBase {

private:

  ResultSceneで必要な物のみを(ry

public:

  virtual bool Run(SceneBaseと同様) {

    ResultSceneの処理

  }

};

class StartScene : public SceneBase {

private:

  StartSceneで必要な(ry

public:

  virtual bool Run(SceneBaseと同様) {

    StartSceneの処理

  }

};

// Singletonにして、二重生成、二重解放を防ぐ

class Game {

private:

  ここにグラフィックドライバへのポインタとかを書く

  SceneBase *_nowScene, *_nextScene;

public:

  // 次に呼ばれるシーンをセット

  bool SetNextScene(SceneBase *next) {

    if (next==NULL) return true;

    _nextScene=next;

    return false;

  }

  // WinMainのメッセージループから呼ばれる

  bool Run() {

    if (_nowScene==NULL) return true;

    if (_nextScene!=_nowScene) { // シーン切り替え処理

      delete _nowScene;

      _nowScene=_nextScene;

    }

    return _nowScene->Run(シーン共通で必要なもの);

  }

};

オブジェクト指向の、仮想関数の機能を使って、

シーン追加時の書き足し箇所を、一箇所にしました。

また、リソースの管理場所は、各所に区分けしました。

コンストラクタ・デストラクタは省略していますが、

各所のリソースを確保・解放する処理が入っています。

GameクラスのSetNextSceneの引数は、

SceneBaseを継承したクラスを、newで生成したものを渡します。

一旦_nextSceneに次のシーンへのポインタを保持しているのは、

もし仮に_nowScene=next;としてしまうと、

今まで_nowSceneに入っていたオブジェクトが破棄できなくなるからです。

コードは色々と省略しましたけど、

オブジェクト指向を知ってる人なら分かる! はず!

……そう信じていますが、分かりにくかったら補足しますので、

この記事のコメントででも質問してください。

今回はこんな感じで終わります。

次回があるかどうかは未定。