Seaside Laboratory

Posts

ゲーム開発用のスタティックライブラリを作る

自作ゲームのソースファイル数が増大して管理するのが難しくなってきたので、汎用的なゲーム処理をスタティックライブラリとして分離することにした。

スタティックライブラリを自作して使う人は少数派なのか、ネット上には最低限の作成方法しか書かれていないことが多い。まとまった情報がないので最終的な形に仕上げるまでに試行錯誤することになってしまったが、同じようなことをやろうとしている人がいたら参考にしてほしい。

作成には Visual Studio 2012 Express を使用。

スタティックライブラリプロジェクトの作成方法

古い Visual Studio だと最初のプロジェクト作成ダイアログで「スタティックライブラリ」を選択できたような記憶があるが、2012 では「Win32 プロジェクト」を選択してアプリケーションウィザードが起動してから、ダイアログ内にある「アプリケーションの種類」を「スタティックライブラリ」にすると、スタティックライブラリのプロジェクトが作成される。

プロジェクトのディレクトリ構成

DirectX SDK のような外部ライブラリを何度か使った経験から、ヘッダーファイルを格納する include ディレクトリと、ライブラリを格納する lib ディレクトリが必要なことはわかっている。これらのディレクトリにソースコードを格納する src ディレクトリを追加して、以下のようなディレクトリ構成にした。

ディレクトリ 用途
include ライブラリ使用に必要な最低限のヘッダーファイル
lib ビルドしたスタティックライブラリ
src ライブラリ内で使用しているヘッダーファイルとソースファイル

プロジェクトのディレクトリ構成をライブラリ配布時の構成と同じにしておくと、他のプロジェクトから参照する「ライブラリパス」としても使えるので、ビルドする度に lib ファイルをライブラリパスへ手動でコピーするといった手間が不要になる。

初期状態ではビルドした lib ファイルは、Debug もしくは Release ディレクトリに出力されるので、プロジェクトのプロパティにある出力先を lib ディレクトリに変更しておく。

設定項目 Debug Release
出力ディレクトリ $(SolutionDir)lib\ $(SolutionDir)lib\
ターゲット名 $(ProjectName)d $(ProjectName)

Debug と Release の出力先が同じなので、Debug 版のファイル名末尾に d を付加して上書きされないようにした。

namespace とオブジェクトファイル

ライブラリによる名前空間の汚染が極力発生しないよう、何も考えずグローバル空間に置いていたクラスをライブラリ用の名前空間に移動することにした。

クラス名の一部を名前空間として切り離すと、クラス名はより単純な名前になるので他のクラスと衝突しやすくなる。例えばクラス CFooBaz と CBarBaz があった場合、

namespace Foo
{
    class CBaz
    {
    };
}

namespace Bar
{
    class CBaz
    {
    };
}

両クラスとも CBaz になる。

ひとつのディレクトリに同じ名前のクラス (ファイル) を置くことはできないので、「Google C++ スタイルガイド」を参考にディレクトリを分けることにした。

名前空間に含まれるコードは、通常、その名前空間の名前と一致するディレクトリ (かそのサブディレクトリ) の中に置かれます。

ただ、この状態でビルドを行うと警告が出る。

warning LNK4042: オブジェクトの指定が重複しています。余分なものは無視されます。

コンパイル時に生成されるオブジェクトファイル名はソースファイル名が元になっているので、同名のソースファイルがあるとオブジェクトファイル名が衝突し、上書きされてしまうのが原因だった。

こんなことはコンパイラ側で勝手に処理してくれという感じではあるが、仕方がないのでプロジェクトのプロパティにある「オブジェクトファイル名」を、$(IntDir) から $(IntDir)%(RelativeDir) に変更して対処した。

%(RelativeDir) はソースファイルの相対ディレクトリを保持しているマクロで、これを付加するとオブジェクトファイルは別々のディレクトリに出力される。

フレームワーク化とエントリーポイント

ライブラリを使う側のプロジェクトはゲームの処理に集中させたいので、直接ゲームと関係のない処理は出来るだけライブラリに任せたい。

例えば「最低限のコードを書くだけでウィンドウが表示される」という状態にするには、エントリーポイントである WinMain を乗っ取って、ウィンドウ初期化のような前準備を終わらせた後に、ユーザーが定義したエントリーポイントを呼び出す、という構造にする必要がある。

// WinMain をライブラリ側のソースコードに定義する
// ※単純化したコードなので実際のものとは異なる
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
    // ウィンドウの初期化
    InitializeWindow();

    // ユーザー定義のエントリーポイント
    // この関数にゲームの処理を記述する
    GameMain();

    return 0;
}

このような構造を定義したライブラリをビルドした後、ライブラリを使う側のプロジェクトをビルドするとリンク時にエラーが発生した。

error LNK1561: エントリー ポイントを定義しなければなりません。

ソースコード上に直接 WinMain を書くとビルドが通るので何が原因なのかわからない。

別のプロジェクトを新規に作成し、現在のプロジェクト設定と比較しながら相違点を探ってみたところ、「サブシステム」という項目が設定されていないとエラーになるということがわかった。この項目を「Windows (/SUBSYSTEM:WINDOWS)」にするとビルドが通る。

原因はプロジェクト作成時に「空のプロジェクト」を選択したことだった。スタティックライブラリのプロジェクト作成時と同様、「Win32 プロジェクト」を選択した後の「追加オプション」で「空のプロジェクト」を指定しないと、Windows アプリケーション特有の設定が行われない模様。

フレームワーク化とクラス

先程の例では話を単純にするために GameMain という関数を呼び出していたが、フレームワークを提供するのであればクラスの方が適してる。ライブラリ側から提供されたクラスのメソッドをカスタマイズ (オーバーライド) して使う方が楽で、クラス内に定義されたメソッドはクラス名に属するのでグローバルの名前空間が汚染されにくくなる。

// ライブラリが用意した CGameBase クラスをカスタマイズして使う
// ※単純化したコードなので実際のものとは異なる
class CGame : public CGameBase
{
public:
    void Run()
    {
        // ここにゲームの処理を記述する
    }
};

クラスのメソッドを呼び出す方式の WinMain を書いてみたら、関数にはないクラス固有の性質が影響してコード化できないことに気が付いた。

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
    // ウィンドウの初期化
    InitializeWindow();

    // ユーザー定義のクラスからインスタンスを生成
    ???? lpGame = new ????; // 未解決: ユーザーが定義したクラス名がわからない

    // ユーザー定義のエントリーポイントを呼び出す
    lpGame->Run();

    return 0;
}

クラスのメソッド (static を除く) はインスタンスがないと呼ぶことができないので、事前に new でインスタンスを生成しておく必要がある。その new を呼び出すときに必要なクラス名はユーザーが勝手につけたものなので、ライブラリ側からは知ることができない。

仕方がないので「ユーザーはライブラリが決めた名前 (CGame) でクラスを作る」というルールで妥協して問題を回避してみる。

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
    // ウィンドウの初期化
    InitializeWindow();

    // ユーザー定義のクラスからインスタンスを生成
    CGame lpGame = new CGame(); // エラー: クラスの実体がないと new を呼び出すことができない

    // ユーザー定義のエントリーポイントを呼び出す
    lpGame->Run();

    return 0;
}

new を呼び出すにはクラス名に加え、クラスの実体 (ソース) も必要になる。ライブラリをビルドする時点でライブラリを利用する側のコードが確定している状況はありえないので、この方法は一旦保留することにした。

他のフレームワークの実装を調べる

悩んだときは既にある実装を参考にする (≒パクる) のが早いので、C++ で作られた有名なフレームワークでいいものはないかと考えた結果、頭に浮かんだのが MFC。

MFC も WinMain が隠蔽されていて、CWinApp というベースクラスを継承したクラスを作り、そこにユーザー定義のコードを書いてカスタマイズするという点では同じ構造と言える。ただ、MFC の場合はアプリケーションウィザードによるコードの自動生成と、ウィンドウプロシージャを隠蔽する複雑なマクロによって実体が見えにくくなっている。

MFC がどうやってユーザー定義のクラスを検出しているのか気になる人はいるようで、「どうやって CWinApp を探している」という記事に MFC の挙動が書かれていた。

数年来、気になっていたのは、MFC アプリケーションは、どうやって対象の CWinApp の派生クラスを探しているのか、ということである。ずっと、自分のなかにあるクラスを走査して、CWinApp クラスの派生クラスが見つかったらそれをインスタンス化するもんだとばかり思っていた。

: (中略)

なんかマクロかなにか書いてあるのかな、と思ったら、それらしいのは、

CHogehogeApp theApp;

という記述。

えー

グローバル変数だったの?。。。しかも固定だし。。。

どうやら MFC はユーザーが定義したクラスのインスタンス名が、theApp であることを前提として動いている模様。先程の自作フレームワークが GameMain という関数名でつながっていたのに対し、MFC は theApp という変数名でつながっていた。

ユーザーが定義したアプリケーションインスタンスを検出する方法

クラスの振る舞いをうまく利用して数々の便利なテクニックを生み出したデザインパターンのように、この「振る舞い」を利用すればユーザー定義クラスの検出ができることに気が付いた。

// ベースのアプリケーションクラス
class CApp
{
public:
    CApp()
    {
        // 派生クラスのインスタンス生成タイミングで呼び出される
        // その時のインスタンスを static メンバ変数に保持
        m_lpApp = this;
    }

    virtual ~CApp()
    {
        // 派生クラスのインスタンス解体時に呼び出される
        // 一応インスタンスをクリアしておく
        m_lpApp = nullptr;
    }

    static CApp* GetInstance()
    {
        // 保持しておいたインスタンスを返す
        return m_lpApp;
    }

    virtual bool Run()
    {
        // 空のゲーム処理
        return true;
    }

private:
    static CApp* m_lpApp;
};

// static メンバ変数初期化
CApp* CApp::m_lpApp = nullptr;

使うときはこのクラスを継承した後、インスタンスを生成する。

class CUserApp : public CApp
{
public:
    bool Run()
    {
        // ゲーム処理
        return true;
    }
};

// インスタンスを生成
CUserApp UserApp;

子クラス生成時に親クラスのコンストラクタが呼ばれるという挙動を利用して、インスタンスのポインタを static メンバ変数に保持しておく。インスタンスが必要なタイミングで GetInstance メソッドを呼び出せば、ユーザーが定義したクラスのインスタンスが取得できる。

継承のつながりを利用して子クラスのインスタンスを取得しているのでクラス名の制限がなく、インスタンス呼び出しも親クラスの static メソッドで取得するので MFC のような変数名の制限もない。