[2019] ゲームプログラム向けのフレームワークを自作している話
tl;dr
この記事はQrunchに投稿したものサービス終了に伴いを転載したものです。
@ekuinoxの自己満記事。駄文注意。Fogo書いた話です。
ながれ
私は専門学校の4年生で、これまでDirectXを使ったゲームプログラムの開発を中心に授業を受けてきました。
DirectX9のAPIをC++で叩き、ゲームを実装するというものです。それを私を含む学生はいくつか制作してきました。
制作の度に私はゲーム内に登場するオブジェクトの扱いをどう行うか、どうすれば楽にできるのだろうかということを考えていました。
2年生の最後での制作(ekuinox/Mugicha)では、すべてのゲームオブジェクトが継承するべき仮想クラスを制作し、それのポインタ一つ配列でまとめて管理していました。
この方法だと、それぞれのゲームオブジェクトが実際、プレイヤなのか敵キャラクタなのか、ステージ上のギミックなのかがわからなくて、扱いづらいのが問題でした。
このときは、継承したクラスが何なのかを表す列挙型を一つにまとめて作成して、それを使って判定していました。
しかし、これでは継承したクラスを一つ作るたびに列挙型を定義したヘッダファイルへの変更が入り、ほぼすべてのファイルにリコンパイルが走るので大変手間でした。
3年夏の制作(ekuinox/Hikouki)では、これを解消するためにクラスのIDをunsigned intで定義して、実行時に判定しようとしました。
インターフェイスクラスにIDを取得する純粋仮想関数を定義し、継承先でオーバライドさせるのと、継承先ではstaticなIDを用意し、そこで判定を行おうとしました。
これで、新しいクラスを作る度に他のファイルまでリコンパイルが大量に走ってしまう問題は回避できました。
しかし、unsigned intを使うのはどのクラスがどのIDを既に使っているか把握する必要があり、衝突する危険性がありました。
更にわざわざstaticなIDとID取得関数をオーバライドさせるのも面倒に感じていました。
3年も終わりに近づき、我々専門学校生は就職に向けて作品を制作することになります。
私はこれまでの制作からフレームワークっぽいものを作って、オブジェクトの扱いを楽にできるものを作ってやろうと考えました。
Fogo
こいつの制作をやっていくことになりました。 -> ekuinox/Fogo
思想としては、前にも合ったオブジェクトの扱いを楽にできればなあといった感じで、ゲームに必要な描画やゲームループ処理も含めていい感じにしてやろう!というものでした。
レンダリングにはDirectX12を使ったり、FBXSDKのラッパを盛り込んだりして、ゴチャついたのでこの辺は結構後悔しています。
Unityではゲームオブジェクトに追加されたコンポーネントをGetComponent<T>()
で拾ってくることができます。
こんな感じでオブジェクトを型指定付きで取得できればなあと、私はこれが大変魅力的に感じていました。
C++にはtemplateというパワフルなやつがあります。こいつでこれを作ってやろうというのが私の目論見でした。
パワフルなC++を表す図
いくぞC++!って気持ちでこの画像持ってきたけどPythonがエグくてアレだな...
まず、任意の型に対応できる配列を用意する必要がありました。
理想としてはマネージャになるクラスのメンバにtemplate <class T> std::unordered_map<T>
な変数を宣言し、メンバ関数でうまい具合に扱うことができれば良かったのですが、メンバでそのようなことはできないみたいで私の心が折れました。
流れとして、Get<Foo*>(...)
された際に、std::unordered_map<..., Foo*>
へのアクセスが飛んでほしくて、メンバに持てないことがわかってしまったので、シングルトンなstd::unordered_map(ContainerBase)を作成しました。
これで、Get<Foo*>(...)
された際にContainerBase<..., Foo*>::shared
としてアクセスできるようになりました。
これを用いて、UUIDをキーとするComponent配列を複数、staticなマネージャクラス(Store)が管理するという具合で制作しました。
Storeでは、Get<T>(...)
やCreate<T>(...)
された際などにTがComponentを継承しているかを静的にチェックしています。関係ない型が来てコケることがないようになっています。
StoreではComponentに対して内部的に親子関係を持たせています。
Componentは自身のUUIDを用いてStoreに対してリクエストを行います。このリクエストをUnityにおけるGetComponentと同じようにできればと思い、Storeにアクセスするための関数をラップしています。
これは学内イベントの発表で使った図です。
class FooComponent : public Fogo::Component {
public:
void test() {}
};
class BarComponent : public Fogo::Component {
public:
void test1() {
const auto & foo = create<FooComponent>();
foo->test();
foo.makeIndex("foo1");
}
void test2() {
if (const auto & foo = get<FooComponent>()) {
foo->test();
}
if (const auto & foo = get<FooComponent>("foo1")) {
foo->test();
}
};
こんな具合にかけるようにしています。
Resultというクラスを用意していて、
Result<ErrorType, const char *> is_negative(int n) {
if (n < 0) {
return "OK Value";
}
return ErrorType::NotOK;
}
void main() {
const auto & result = is_negative(-10);
if (result) {
ok(*result);
} else if (result == ErrorType::NotOK) {
fail();
}
}
みたいな具合にやれるようにしています。
Component::get<T>()
などの関数もResultで結果をラップして返すようにしているので、取得に失敗したときの処理などが書きやすいかと思っています。
開発所感
- 実際書きやすくはなった
- DirectX12はクソしんどい
- FBXへの対応
- Storeがstaticの塊でクソキモい
- Fogoがそもそもstaticの嵐でしんどい
- ウルトラブックボックスを作った気持ち
- テンプレートでゴリ押すと、Visual StudioのIntelliSenseが効かなくなって、マジ何がなんだかわからん(ここでバグが生まれる)
- テストとデバッグめっちゃしんどい、テストどうやったらいいの!?
問題として、親UUIDとポインタを管理している場所が複数あって、ややこしいことになっているので、親子関係を作っているツリーをしっかり切り分けようとしています。ekuinox/Fogo/experimental/renew-handler、これをやっているとサンプルのプログラムでバグが発生したので困っています。
実際Unityがどうコンポーネントを管理しているか知らなくて、私はこういうやり方でやってみたという感じです。
RTTIはやりたくなかった。私は実行時に型をIDで判定するという柵から開放されたかったし、方向としては良かったのだと思っています。
Discussion