読者です 読者をやめる 読者になる 読者になる

【SMFL非公式和訳】チュートリアル: ベーシックゲームエンジン

C++ SFML ゲーム開発

この記事は僕がSFMLを学習する為に
Tutorial: [C++] Basic Game Engine
https://github.com/SFML/SFML/wiki/Tutorial%3A-Basic-Game-Engine

を無断で和訳したものです。訳者がSMFL、英語どちらも学習中の為意訳・誤訳ご了承のほど。

チュートリアル: ベーシックゲームエンジン

ベーシックゲームエンジンの制作

 素晴らしいゲームたちのコアはゲームエンジンだ。初心者が自分のゲームエンジンを初めて開発しようとした場合、かなりの確率で挫折することになる。キレのあるゲームエンジンを制作する際には、多くの検討が必要になるだろう。全てのグラフィックの管理、サウンド、ゲームオブジェクトの処理などは、多くの労力を必要とし、それらの開発はあなたがゲーム制作で実際にやりたい事と比べたらかなり見劣りするだろう。

 多くのゲームプログラミングに興味がある人はゲームのアイデア部分から取り掛かるが、実際はゲームのアイデアを実現するための具体的な手順を理解していない。付け加えていうならば、彼らのゲームのアイデアは自体もゴミだ。こういったもののほとんどは、世に出てすでにプレイされている多くのゲームと、ほとんど同じ代物である。

 だからといってゲーム制作を辞めるべきだとは私は思わない。何故なら製作途中で投げ出されたゲーム開発であっても、多くの経験と、制作していくにしたがって自分のゲームがリアルになっていく、その過程自体の楽しみを得る事ができる(良くも悪くも)。私の場合もそのゲームが制作され実現される過程自体を目的にゲーム制作をしている。

 こういった理由から、私の将来のアイデアに使いまわせるようベーシックゲームエンジンの制作をする事にした。これによりゲーム制作の楽しい部分、ゲームメカニズム制作をより簡単に楽しむことができる。このゲームエンジンを皆さんにシェアする前に、いくつかの難しいレッスン、長年に渡って私が学習してきた事をまずは名前空間に関してから共有させてほしい。

ネームスペース(名前空間)

 多くのゲームエンジンは他者から借りられた、盗まれた(そうでないことを願う!)または書かれた、基本的な要素から始まる。SFMLをサンプルとすれば、基本要素は、グラフィック、サウンド、ネットワーク、システム、画像サポートなどである。

 これらのSFMLクラスは、sf.という名前空間でラップして格納されている。だから全てのクラス、列挙型、定数、変数の前にsf::が置かれている。

 我々はゲームエンジンの名前空間をGQEとする(私のニックネームGatorQueからGatorQue Englineの頭文字を取った)。これは自分のプロジェクトではこれを好きに変えたら良い。ただしタイプの手間を考えて出来るだけシンプルにすること。

 このように.hppファイルにて名前空間の内側にクラスをラップする。

namespace MyStuff
{
  class MyClass {
    public:
      MyClass();
      virtual ~MyClass();
  };
} // namespace MyStuff

…で.cppはこのようにする。

namespace MyStuff
{
  MyClass::MyClass()
  {
  }

  MyClass::~MyClass()
  {
  }
} // namespace MyStuff

もう1つの名前空間の素晴らしい機能は、同じ名前が付けられた2つのクラスの干渉を防ぐ事である。例を挙げると、SFMLにはClockクラスがある。そこで君がsf::Clockクラスと全く別の自分用のClockクラスを作りたいとする。その時君のClockクラスを名前空間でラップしてしまえば、以下のようにどのClockクラスが呼び出されたか、コンパイラに指示を与える事ができる。

class MyClass {
  public:
    MyClass();
    virtual ~MyClass();

    // 変数
    sf::Clock mClock1;
    MyStuff::Clock mClock2;
};

 どのように名前空間タグをもちい、各Clockクラスの変数を作っているかに注目して欲しい。変数の前に毎回MyStuffと名前空間を書くのは少し嫌な気持ちにさせるかもしれない。そんな時は以下のように1行を君の.cppか.hppに追加すれば、コンパイラに各クラスの名前空間を伝える事ができる。

using namespace MyStuff;
// OR
using namespace sf;

 このやり方の問題点は、全てのクラスが同じ名前空間の中で名前空間タグ無しに作られることだ。もし君がローカルクラスで同じ名前を使ったら、コンパイラは混乱してしまう。私は以下のように使いたいクラスのみ指定して使うのをお勧めする。

using MyStuff::MyClass;
using sf::Clock;

 この方法であれば、このファイルに名前空間の衝突の可能性をなくしつつ、特定のクラスを名前空間タグ無しで使用できる。

 名前空間について理解したところで次のトピック、ゲーム制作のような大きなプロジェクトで沢山のファイルをどう取り扱う必要があるかを考えていこう。大したことはない。前方宣言とポインタを使うのだ。

前方宣言

 ゲームプログラミングにおいては、しばしば全ゲームオブジェクトが他のゲームオブジェクトを参照するために親クラスへアクセスできる必要がある。しばしば親クラスはこれらの全ゲームオブジェクトが作られた場所のコンテナとなるからだ。たとえば、全ゲームオブジェクトを持っているレベルクラスがあるとする。そしてこれらのゲームオブジェクトは、範囲変数によってそのレベルクラスの内側にとどめておく必要がある。そのうえ、他のレベルクラス内にあるゲームオブジェクトを移動できる必要も、現在の状態もどうにかして保持する必要がある。コードでこの問題を表現していこう。

#include "GameObject.hpp"
class Level {
  public:
    GameObject mObjects[100];
};

Level.hppファイルとGameObject.hppがどうやって結び付けられているかに注目してほしい。次に、GameObjectを親クラスLevelに結びつけようとするとどうなるか見て欲しい。

#include "Level.hpp"
class GameObject {
  public:
    Level& mParent;
};

 このサンプルコードをコンパイルしようとするとLevel.hpp、GameObject.hppどちらかが先に読み込まれることになる。どちらの場合も、include命令によって無限ループを引き起こすことになる。コンパイラは最終的に強制終了するか、もっと酷いことになり、当初求めていた結果は得ることができない。

 しかし心配は無用だ。簡単な解決策がこれにはある。.hppファイルでクラスの前方宣言で使用し、cppファイルではinclude命令だけを行う。

 以下は上記と同じ例だ。ただし代わりに前方参照を利用している。

// GameObjectクラスの前方宣言
class GameObject; // クラスがどういうものか定義していないのに注目 それはcppファイルの最後に登場する

class Level {
  public:
    GameObjects* mObjects; // GameObjectsがポインタで完全なオブジェクトでない事に注目
};

次にGameObject.hpp の方にはどういう変化があったか見ていこう。

// レベルクラスの前方宣言
class Level; // cppファイルで完了しているのでLevel.hppをincludeしていないのに注目

class GameObject {
  public
    Level& mParent;
}

 
 こうするとコンバイラがGameObjectやLevelクラスを見ようとしたとき、それらの定義をしようとしたときに、定義が書かれている部分を自分で探そうとする。同じく変数内のポインタやアドレスリファレンスについてもコンパイラはこれらのサイズを自動的に識別し、作成する(32bitか64bit。CPUアーキテクチャにより異なる)。

 前方参照はこのような場合に大変便利だ。ただしポインタやリファレンスオブジェクト、その他必要なポインターに充分な注意を払うひつようがある。上記の事柄をまとめて、HPPファイルで前方参照を使うべきかどうかの助けとなるシンプルなルールを以下に紹介しよう。

  • 呼び出そうとしているクラスは呼び出し元クラスののメソッドの引数としてだけ使っているか。呼び出しをクラスからポインタに変更できないか?
  • このクラスのコントラクション(サンプルの場合GameObject)で独立したクラス(サンプルの場合Levelクラス)のアドレスが分かるようになるか? その独立クラス(Levelクラス)を参照で使い、ポインタ利用ではないか
  • 2つのクラスはそれぞれのクラスのコードを利用するか? このクラスの使用は全てポインタか参照か?
  • これはクラスへのポインタ変数または参照アドレスか?

(※訳注 c++に不慣れな為訳に自信なし)

 これらの質問がイエスなら、前方参照を利用することを検討すべきだろう。全てのコードをCPPクラスに入れ、HPPファイルは宣言だけにしたのもこれが理由だ。

 これで前方参照についての話題は終わった。次は私が作ったベーシックゲームエンジンと基礎的な機能を議論していくことにしよう。

Main関数

 私の場合、main.cppファイルをシンプルで素直な状態を維持しようとしている。これは1つのゲームアプリケーションクラスとそれをインスタンス化した私ので作成されたMain関数だ。(GQE Projectに全ソースあり)

/**
 * ここが新しいプロジェクトのスタート地点。このファイルの処理は大したことがないが重要である
 * ここにメインのゲームループとアプリケーションを作成する
 *
 * @file main.cpp
 * @author Ryan Lindeman
 * @date 20100707 - Initial Release
 * @date 20110611 - Added new logging capabilities using macros and c++ classes.
 */

#include <assert.h>
#include <stddef.h>
#include <GQE/Core.hpp>
#include <MyApplication.hpp>

int main(int argc, char* argv[])
{
  // anExitCodeを具体値で初期化
  int anExitCode = GQE::StatusNoError;

  // アプリケーション生成前にロガーを作成
  GQE::FileLogger anLogger("output.txt", true);

  // アクションアプリケーションを生成
  GQE::IApp* anApp = new(std::nothrow) GQE::MyApplication();
  assert(NULL != anApp && "main() Can't create Application");

  // コマンドライン引数の処理
  anApp->ProcessArguments(argc, argv);

  // アクションアプリケーション稼働開始
  // アクションアプリケーションの初期化
  // アプリケーションが終了するまで持続されるゲームループの中へ
  // アプリケーションのリセット
  // Exitはここに戻ってくる
  anExitCode = anApp->Run();

  // deleteでアクションアプリケーション自身をリセット
  delete anApp;

  // オブジェクトへのポインタを保持させない
  anApp = NULL;

  // exitコードを返す
  return anExitCode;
}

 上記のメイン関数は基本的な形にとどめておいている。トラブルを避けるためにあなたのプロジェクトにこのファイルをそのままコピーすると良い。その上でゲーム毎の違いをアプリケーションファイルに書くと良いだろう。

 続いてAppクラスの下でどんな部品が動いているか見ていこう。

ゲームアプリケーション

 どんなゲームでもベーシックゲームエンジンが動作するために、最も基本的なゲームアプリケーションアルゴリズムをゲームアプリケーションクラスであるAppに入れる必要がある。このために、他のオープンソースゲームエンジンやゲームエンジンチュートリアル、それに加えて、それらで動作する私が書いた最も基本的なゲームアルゴリズムを用意した。このゲームアプリケーションアルゴリズムは次のようにApp.cppファイルのアウトラインである(GQE Projectに全ソースあり)

  int App::Run(void)
  {
    SLOG(App_Run,SeverityInfo) << std::endl;

    // 最初に稼働フラグをtrueに
    mRunning = true;

    // AppポインターをStatManagerに登録
    mStatManager.RegisterApp(this);

    // AppポインタをStateManagerに登録
    mStateManager.RegisterApp(this);

    // 最初にGQEコアライブラリのIAssetHandler派生クラスを登録
    mAssetManager.RegisterHandler(new(std::nothrow) ConfigHandler());
    mAssetManager.RegisterHandler(new(std::nothrow) FontHandler());
    mAssetManager.RegisterHandler(new(std::nothrow) ImageHandler());
    mAssetManager.RegisterHandler(new(std::nothrow) MusicHandler());
    mAssetManager.RegisterHandler(new(std::nothrow) SoundHandler());

    // 派生クラスにカスタムIAssetHandlerの登録時間を渡す(※訳注 原文Give derived class a time to register custom IAssetHandler classes いつ登録するかの情報?)
    InitAssetHandlers();

    // 全体的な設定情報であるsettings.cfgファイルをConfigAsetにロードさせる
    // IDは下の "resources/settings.cfg"で登録
    InitSettingsConfig();

    // レンダラーウインドウをディスプレイグラフィック上にオープンする事を試みる
    InitRenderer();

    // IScreenFactoryクラスに派生アプリケーションクラスを登録させる
    // IScreen派生クラスの提供 (前のIState派生クラスと同じもの)をリクエスト
    InitScreenFactory();

    // StatManagerを初期化させる
    mStatManager.DoInit();

    // 稼働フラグがまだtrueならゲームループ
    GameLoop();

    // アプリケーションリセット
    HandleCleanup();

    // 内部リセットの実行
    Cleanup();

    // 終了前に稼働フラグをfalseに変更しておく
    mRunning = false;

    if(mExitCode < 0)
      SLOGR(App_Run,SeverityError) << "exitCode=" << mExitCode << std::endl;
    else
      SLOGR(App_Run,SeverityInfo) << "exitCode=" << mExitCode << std::endl;

     // 終了用の終了コードを具体的に返す。終了の意味で0の意味は使わない
    return mExitCode;
  }

 このように、アルゴリズムは以下の基本的なステップで構成されている。

  • 各マネージャクラスが自分の親クラスであるゲームアプリケーションクラスへのポインタ/参照を持っているか確認(マネージャクラスの詳細については後述)
  • AssetManagerクラスに各AssetHandlerクラスを登録(AssetHandlerクラスの詳細については後述)
  • ゲーム設定ファイルからゲーム設定をロードし、ウィンドウをRenderするのに必要な設定を取り出す。
  • SFMLレンダリングウィンドウ/ターゲットを初期化
  • ゲームの初期情報を含む具体的な情報を初期化(Gameの状態Game statesについてはのちほど)
  • ゲームループ本体の稼働開始
  • ゲーム終了前にゲームアプリケーションが必要としていたものをリセット
  • ゲームアプリケーションの終了

 これらのステップは仕上がりがどうであれどんなタイプのゲームにも通用する。変更の必要があるかも知れない部分はInitAssetHandlers, InitScreenFactory, HandleCleanupだろう。それ以外の部分は毎ゲーム同じにすべきだ(少なくとも目指すべきだ)。

 ゲームアプリケーションクラスの2つ目の目的は、ゲーム全体に大して基本的なクラスの格納場所(コンテナ)を提供することだ。 sf::RendererWindowクラス、ゲームオブジェクト、ゲームステータス、ゲームマネージャークラスその他のことだ。ゲームマネージャークラスとゲームステータスクラスの詳細に入る前に、ゲームループ内のApp::GameLoopメソッドについて補足しておきたい。

ゲームループ

 多くのゲームエンジンチュートリアルは以下のゲームループアルゴリズムに沿っている。

  • 入力デバイス(キーボード、マウス、ジョイスティック他)の処理
  • ゲームロジックの処理(動くゲームオブジェクトの位置、スピードなどの更新、衝突判定やAI機能の処理)
  • 画面のリセットとゲームオブジェクトの描画
  • ゲーム終了シグナルがセットされるまで処理を繰り返す(普通は入力デバイスによりセットされる)

 しかしこのゲームループアルゴリズムには1つだけ問題点がある。ゲームロジックの処理とコンピューターがグラフィックを画面に表示できるスピードが密接に関係している点だ。もし君が遅いグラフィックカードを搭載したPCでゲームを動作させた場合、速いグラフィックカードを搭載したPCで動作させる場合とくらべて遅くなる。これを防止するために、私はインターネットで実力を磨き、問題を解決した( entropyinteractive.comを見て)。

 以下は上記の問題に対応したゲームループのコードになる(GQE Projectに全ソースあり)。

  void App::GameLoop(void)
  {
    SLOG(App_Loop, SeverityInfo) << std::endl;

    //Updateループの頻度を制限されたものに調整するためにClockを使用
    sf::Clock anUpdateClock;

#if (SFML_VERSION_MAJOR < 2)
    // Updateクロックのリスタート/リセット
    anUpdateClock.Reset();

    // 次回いつ(何秒後)更新が必要か?
    float anUpdateNext = anUpdateClock.GetElapsedTime();
#else
    // 最終フレームからの経過時間の計算のためにCockを使用
    sf::Clock anFrameClock;

    // Updateクロックのリスタート/リセット
    anUpdateClock.restart();

    // 次回いつ(何ミリ秒後)更新が必要か?
    sf::Int32 anUpdateNext = anUpdateClock.getElapsedTime().asMilliseconds();
#endif

    // 1つ以上状態がアクティブであることを確認
    if(mStateManager.IsEmpty())
    {
      // アクティブな状態のものが1つもなければエラーで終了
      Quit(StatusAppInitFailed);
    }

    // IsRunnningがtrueを戻す限りループ
#if (SFML_VERSION_MAJOR < 2)
    while(IsRunning() && mWindow.IsOpened() && !mStateManager.IsEmpty())
#else
    while(IsRunning() && mWindow.isOpen() && !mStateManager.IsEmpty())
#endif
    {
      // 現在のアクティブな状態を取得
      IState& anState = mStateManager.GetActiveState();

      // UpdateFixedループが何回連続で呼ばれたかのカウント
      Uint32 anUpdates = 0;

      // 入力されたものがあれば処理
      ProcessInput(anState);

      // 現在の更新時刻を記録
#if (SFML_VERSION_MAJOR < 2)
      float anUpdateTime = anUpdateClock.GetElapsedTime();
#else
      sf::Int32 anUpdateTime = anUpdateClock.getElapsedTime().asMilliseconds();
#endif

      // ゲームループのUpdateFixed割り当てを処理
      while((anUpdateTime - anUpdateNext) >= mUpdateRate && anUpdates++ < mMaxUpdates)
      {
        //現在のアクティブ状態の処理を次回に
        anState.UpdateFixed();

        // StatManagerの更新処理を実行
        mStatManager.UpdateFixed();

        // 次回のUpdateFixed実行タイミングをいつにすべきか計算
        anUpdateNext += mUpdateRate;
      } // while((anUpdateTime - anUpdateNext) >= mUpdateRate && anUpdates <= mMaxUpdates)

      // 現在のアクティブ状態の変数更新処理
#if (SFML_VERSION_MAJOR < 2)
      anState.UpdateVariable(mWindow.GetFrameTime());
#else
      // SFML2.0用に秒数を浮動小数点数に変換
      anState.UpdateVariable(anFrameClock.restart().asSeconds());
#endif

      // アクティブ状態のものを描画させる
      anState.Draw();

      // StatManagerの描画を処理
      mStatManager.Draw();

#if (SFML_VERSION_MAJOR < 2)
      // ウィンドウを画面に描画
      mWindow.Display();
#else
      // ウィンドウを画面に描画
      mWindow.display();
#endif

      // 必要であればここで直近で削除された状態のリセット処理 
      mStateManager.HandleCleanup(); 
    } // while(IsRunning() && !mStates.empty())
  }

 様々なコンピュータ上で同じ速度でゲームロジックを処理するためのポイントは、グラフィックカードのスピードから独立した速度を管理の元、ゲームロジックを実行することだ。このため、独自のゲームロジック速度(レート)で作成した(今回のベーシックゲームエンジンでは20hzつまり毎秒20回ゲームループしか実行されないように制限した)。それに加え、ゲームロジックが上限(最大mMaxUpdates)に到達するまで、更新が必要な分画面に描画されるようにした。

 最近のコンピューター(私のデスクトップやノートPCみたいな)であれば、ゲームロジックは20hzで、画面のFPS(フレーム毎秒)は60hzで動作する。もしこれを古いPCで動作させると、ゲームロジックは20hzで画面のFPSは15-30hzくらいになる。この方法で、どんな環境でもゲームロジックを同じにし、良いグラフィックカードが搭載されている場合にのみなめらかなアニメーションをさせるという事が出来る。

 この驚きの変化は上記のゲームループ内のmUpdateRateとmMaxUpdate変数によって行われている。これはAppコンストラクターにより設定されている。

 mUpdateRate(1.0f / 20) // 20 Hzでゲームロジックが動作する. 15, 30, などどんな値でも君の自由に変更できる

上記を踏まえ、アルゴリズムは以下のようになる。

  • ゲーム状態(State)への参照アドレスの取得(次の項目で議論する)
  • ゲーム状態(State)を通して入力デバイス(キーボード、マウス、ジョイスティック他)の処理
  • ゲーム状態(State)を通して ゲームロジックの処理(上記のUpdateFixed)
  • ゲームロジックレート(更新頻度)に到達するまで上記2ステップを繰り返す
  • ゲームレート処理を実行(上記UpdateVariable)
  • ゲーム状態(State)に画面へのオブジェクト描画を許可
  • ゲーム終了フラグがセットされるまで上記処理を繰り返す(上記IsRunningまたは !mState.empty())
  • ここで疑問が浮上。ゲーム状態(State)ってなに? それはなにをしてるの?

ゲーム状態(State)

 最近のゲームの多く、特にカジュアルゲームはウェブ上でいろんな種類のものを目にする。それらのゲームはよくあるパターンに基づいて動作している。たとえばこんなゲームフローだ。

  • 制作者である会社やチームのためにスプラッシュスクリーンを表示(※訳注 起動時の会社ロゴの表示の事)
  • ネットやハードディスクからゲームの画像が読み込まれるまで、お待ち下さい画面を表示
  • 挨拶をしつつ名前/ニックネームを尋ねる(初回プレイ用)
  • メインメニューの表示とゲームモードの選択入力(シングルプレイヤー、対戦、タイムアタックなど)
  • 現在のゲームモードをゲームオーバーになるかプレイヤーが終了するまで継続
  • 最後に2-3ステップをゲームアプリケーション終了まで繰り返す

 この流れに君が同意してくれるなら、多くのゲームは次に沿ったゲームフローを処理する事になる。スプラッシュ→ロード→メニュー→ゲーム→ロード→レベル→ゲーム→ロードレベル→ゲーム→メニュー→終了。今の状態を終了したとき、即座に前の状態に戻れる事をゲーマーの君なら期待するだろう。これはゲームオーバー状態に達したら、メインメニューに戻らなければならない事と同義である。このゲームフローの中の各ポイントでゲーム状態(State)を呼べる必要がある。ゲーム状態(State)とは要するに、ゲームフロー全体の各ポイントをかっこ良く言っているだけだ。

 もしあなたが大学でコンピューターサイエンスの専門的な分野を学んでいたら、もしくはいくつかのゲームチュートリアルを読んだことがあるならご存知だろうが、私が話しているこれは有限オートマトンまたはFSM(Finite State Machines 有限ステートマシン)の事だ(初めて聞く単語なら是非これについてのチュートリアルをどこかでご一読いただきたい。とても役に立つ)。有限オートマトンはあるゲームの状態から違うゲームの状態へのフローを表現する簡潔で感覚的な方法だ。一般的には、円と円を線で結んだ形ようなイメージになる。上または下に付く線は、1つの円(ゲーム状態)から別の円(ゲーム状態)に変化するルールとセットで書かれる。たとえば、最初の円に「スプラッシュ」と名づけて、ゲーム開始時にスプラッシュ画面をプレイヤーに表示する。次の円は「ローディング」と名付けローディングフェーズを表現する。上の方で書いた、お待ち下さい画面の事だ。この2つの円を繋ぐラインに付く条件は「3秒待つ」といった感じだ。スプラッシュスクリーンを3秒表示したらローディングフェーズに移るという意味だ。

 3つ目の円は「メインメニュー」と名付ける。そしてローディングと名付けられた円と繋ぎ、この線には「サウンドと画像全てを読み込み終わるまで待つ」条件付けすることになるだろう。次。今のところかなり直線的であるが、メインメニューからは多くの選択肢があり、メインメニューの円からは多くの線とそれに繋がる円が出来ることになる。追加される円は「オプション画面」「シングルキャンペーン」「マルチプレイキャンペーン」「デスマッチ」といった感じになるだろう。

 当初望んていたように、このベーシックゲームエンジンはこういったフローをなにか簡単な方法で管理・実現したい。ゲーム状態(State)毎にあつらえたゲームループをそれぞれに用意する事は可能だ。しかし、コードをコピーせずに抽象化できるところはしたい。というわけで、抽象化クラスをIStateを以下のように作成した。(GQE Projectに全ソースあり)

   /**
     * DoInitはStateを初期化する機能がある。 HandleCleanupはmCleanupがTrueの時に呼ばれ
     * Derivedクラス群は常に呼ばれる
     * IState::DoInit() の管理データをまずリセット
     */
    virtual void DoInit(void)
    {
      ...
    }

    /**
     * ReInit は StateManager::ResetActiveState()  が呼ばれた時、このステータスをリセット機能
     * この方法でゲームデータのリセットや再読み込み無しでゲーム状態(State)を再起動できる
     */
    virtual void ReInit(void) = 0;

    /**
     * DeInit は この状態がリセットされたかどうかの目印を付ける機能
     */
    void DeInit(void)
    {
      ...
    }

    /**
     * Pause は 他のゲーム状態(State)にフォーカスが移った際や自分がフォーカスを失った際に、
     * このゲーム状態(State)をポーズする機能
     */
    virtual void Pause(void)
    {
      ...
    }

    /**
     * Resume は前のゲーム状態(State)が削除され、フォーカスが戻った時に再開する機能
     */
    virtual void Resume(void)
    {
      ...
    }

    /**
     * HandleEvents はこのゲーム状態(State)がアクティブになっている時に、デバイス入力を処理する機能
     * @param[in] theEvent はAppのループメソッドから来る
     */
    virtual void HandleEvents(sf::Event theEvent) = 0;

    /**
     * UpdateFixed は このゲーム状態(State)がアクティブ状態になっている時、全てのゲーム状態(Stat)fixed updateする機能
     */
    virtual void UpdateFixed(void) = 0;

    /**
     * UpdateVariable はこのゲーム状態(State)がアクティブ状態になっている時、全てのゲーム状態(Stat)variable updateする機能
     * (※訳注 fixed/variableの違いについてはコードを見ていないため不明)
     */
    virtual void UpdateVariable(void) = 0;

    /**
     * Draw はこのゲーム状態(State)がアクティブ状態になっている時、このゲーム状態が描画が必要な時に処理する機能
     */
    virtual void Draw(void) = 0;

   /**
     * HandleCleanup is はこのゲーム状態(State)が削除される前に要求されたリセットを処理する機能
     */
    virtual void HandleCleanup(void)
    {
      ...
    }

 このやり方でアプリケーションクラスのAppはその中身やゲームフローが実際どうなっているのかは知る必要がなくなり、ただ現在のゲーム状態(State)を通して呼び出される上記のIStateクラスのメソッドを実行してやればよい。これが美しいポリモーフィズムアクションというものだ(オブジェクト思考で使う格好いい単語)。別な言い方をすれば物事の共通化ということだ。これで車輪の再発明をせず、新しいゲーム開発に使う大幅な時間の節約ができる。

 現在のゲーム状態(State)を提供するクラスはManagerクラスと呼ばれている。これが次の話題となる。Managerクラスとはなんぞや?

Managerクラス

 Managerクラスはオブジェクト思考テクニックを使った、独自の機能を提供するクラスだ。Managerクラスは全てのゲーム状態(State)やゲームブジェクト
を処理するのに慣れている。それはゲームアプリケーションクラスIAppのパブリックなメンバ変数として扱われる。例えば、現在のどゲーム状態(State)が実行されているか、次のゲーム状態(State)はどう定義されているかなどを管理しいているStateManagerがある。また、画像、音、音楽などのゲームデータがロード済みかどうかを管理しているAssetManagerクラスがある。AssetManagerクラスの中でも気の利いた機能の1として挙げられるのが、複数のゲーム状態(State)で画像、音楽、サウンドなどを共有できる機能だ。

それぞれのゲーム状態(State)は、AssetManagerクラスに欲しいゲームデータを要求する。AssetManagerはいくつゲームデータが参照しているか格納しており、ゲーム状態(State)が画像、音、サウンドなどが不要になったと伝えると、AssetManagerデータ参照はその参照数を1つ減らす。どこからも参照されなくなると、メモリ上から削除する。同じくAssetManagerはゲームデータの遅延ローディングに対応する。これはローディング状態などの特殊なStateの開発に大いに役立つ。ゲームデータの読み込みが完了したら、ゲーム状態(State)はStatManagerクラスに自分自身の削除を要求し、前のゲーム状態(State)に戻す。

 そのうち追加のManagerクラスをこのベーシックゲームエンジンに追加するつもりだ。現在GUIをベーシックゲームエンジンに提供するWidgetManagerを開発中だ。

コンフィグレーションファイル(設定ファイル)

 ベーシックエームエンジンの重要な側面の1つは、柔軟な設定情報をファイルからロードする機能だろう。ConfigReaderクラスは.INIスタイルのファイル読み込みに対応し、ゲーム状態(State)はファイルからどんなタイプの情報もロードする事が出来る。IApp.cppクラスにあるinitSettingsConfigメソッドは、以下のようにConfigReaderの使い方のシンプルなサンプルとなっている(GQE Projectに全ソースあり)。

  void App::InitSettings(void)
  {
    SLOG(App_InitSettingsConfig, SeverityInfo) << std::endl;
    ConfigAsset anSettingsConfig(IApp::APP_SETTINGS);
  }

コメント

素晴らしい!

1つだけ、動作しなかったときのために修正コードを記載する。

result = new ImageAsset(theFilename, theStyle);
assert(NULL != result && "AssetManager::AddImage() unable to allocate memory");

もしnewでメモリを確保できなかったら例外bad_alloc例外を投げる。戻り値にNULLが欲しいなら'(nothrow)' を追加する必要がある

result = new(std::nothrow) ImageAsset(theFilename, theStyle);
assert(NULL != result && "AssetManager::AddImage() unable to allocate memory");

(See http://www.cplusplus.com/reference/std/new/nothrow/)

//Peter Welzien//

賛辞の言葉とコメントをありがとう。あなたの提案はとても参考になりました。変更も適用しています。

//Ryan Lindeman//

Ryan: 投稿ありがとう! 僕はこの手の投稿をここ何年かいくつも読んで(そしていくつか自分でも失敗しつつ実際試してみた)いるけど、君の投稿はその中でもとても適性で明快だったよ。

Peter: サンプルに同じコードがペーストされている部分があったよ。大したことじゃなかったので、自分の頭の中で処理しちゃったけど。

//phobius//

このベーシックゲームエンジンは開発中でチュートリアルも更新の必要があると思います。みなさん是非このゲームエンジンを試してください。

//Ryan Lindeman//

ゲームロジックとレンダラーに関して質問です。これらを分離してスレッド処理するというのはどうでしょう? そうすればグラフィックカードを待たなくて良いし、逆もまたしかり。何か問題があるかな。私の理解不足?


// RebelMoogle //

良い投稿だ。でも前方宣言のところで質問。これはただプリプロセッサ命令の#pragmaで1度、または#ifndef #defineとすればすればヘッダループの問題は無くなるのでは?

//Kevin Kung//
Contact GitHub API Training Shop Blog About