概要
C++11の勉強がてら、昔いじっていたゲームのソースを抜き出して、単独のシステムとして再利用出来ないか、色々と試してみた。
事のなれ初め
その昔、Goluah!!というフリーの2ch格ゲーがあった。
以前このゲームの改良保守をやっていたのだけど、このままソースコードを無駄にするのももったいないなあと思い、
比較的よく出来ていたタスク管理部分のソースコードを抜き出し、C++11の勉強もかねて単体のシステムとして作り直してみることにした。
完成品
あくまでゲームソフト用の、プラットフォーム非依存のタスク管理システムです。
タスクシステムというと古くさいイメージがあるけど、index付きのlist化機能、検索機能などがある、どちらかというと近代的なものです。
C++11の記法を意識した作りになっているので、良ければご参考にしてください。
需要があるかどうかは微妙。
作り直すに当たってやったこと
C++11の新仕様への対応が主だけど、いくつか特筆すべき点があったのでまとめてみる。
テストの記述
ゲームから切り離した所、単独で実行することが出来なくなったので、テストコードを記述しないといけなくなってしまった。
TEST_METHOD(実行順序)
{
// TODO: テスト コードをここに挿入します
CTaskManager task;
veve.clear();
auto ptr = task.AddNewTask< CTekitou2<int, CExclusiveTaskBase> >(1);
auto ptr2 = task.AddNewTask< CTekitou2<int, CTaskBase> >(2);
task.Execute(0);
Assert::AreEqual(2, veve[0]);
}
VS2013に付属のテスト機能をそのまま使ったけど、もっとちゃんとしたフレームワークを導入した方が良いのかもしれない。**(11/20)**有志の方にiutest対応して頂きました。多謝。
まだだいぶテストケースに抜け穴があるような気がする…
関数インタフェースの統一
このタスクシステムでは、通常タスク・常駐タスク等、扱うタスクが何種類かに分かれている。
元ソースでは以下のように、タスクの種類ごとに関数を使い分けなければならず、不便であった。
CBackgroundTaskBase* FindBGTask(DWORD id); //!< 指定IDをもつ常駐タスクゲット
CTaskBase* FindTask(DWORD id); //!< 指定IDをもつ通常タスクゲット
enable_ifを用いて型に応じて関数を振り分けることが出来るので、以下のようなテンプレート関数を追加して対処した。
template<class T, typename std::enable_if<!std::is_base_of<CBackgroundTaskBase, T>::value, std::nullptr_t>::type = nullptr>
TaskPtr FindTask_impl(unsigned int id) const
{
return FindTask(id);
}
template<class T, typename std::enable_if<std::is_base_of<CBackgroundTaskBase, T>::value, std::nullptr_t>::type = nullptr>
BgTaskPtr FindTask_impl(unsigned int id) const
{
return FindBGTask(id);
}
is_base_of
は、指定された型が、ある型の基底クラスであるかどうかを判別するメタ関数である。
これを用いて、特定の型の派生クラスが対象である場合のみ、別の関数に振り分けるようにしている。
タスク生成の自動化
元ソースでは、タスクを登録する際に、あらかじめnewしてから格納しなければならず、設計上の不安があった。
テンプレート関数で自動化出来ることが分かったので、以下のようなテンプレート関数を作ることにした。
//! タスクの自動生成(暫定)
template <class C, typename... A, class PC = std::weak_ptr<C>,
typename std::enable_if<
std::integral_constant<bool, std::is_base_of<CBackgroundTaskBase, C>::value ||
std::is_base_of<CExclusiveTaskBase, C>::value
>::value>::type *& = enabler>
PC AddNewTask(A... args)
{
return std::static_pointer_cast<C>(AddTask(new C(args...)).lock());
}
内部では相変わらずnewを使用しているものの、テンプレートの呼び出しだけで、newを使わずにタスク用クラスの生成から登録まで自動で行うことが出来る。コンストラクタ引数もちゃんと渡すことが出来る。
11/29 追記
記事投稿後、若干の修正を行ったので追記します。
//! タスクの自動生成
template <class C, typename... A, class PC = std::shared_ptr<C>,
typename std::enable_if<
std::integral_constant<bool, std::is_base_of<CBackgroundTaskBase, C>::value ||
std::is_base_of<CExclusiveTaskBase, C>::value
>::value, std::nullptr_t>::type = nullptr>
PC AddNewTask(A&&... args)
{
return std::static_pointer_cast<C>(AddTask(new C(std::forward<A>(args)...)).lock());
}
有志の方のご指摘とプルリクエストにより、enable_if_tの使用を廃止し、nullptrの使用によりenablerを使用する必要がなくなりました。
参考:std::enable_ifを使ってオーバーロードする時、enablerを使う?
ついでに引数の引き渡しにforward関数を使用しました。
リストのindex化
元ソースにはリスト内のオブジェクトを逐次検索する機能があったのだけど、それだと計算量が多いので、unordered_mapを用いてindex化してみた。
std::unordered_map<unsigned int, TaskPtr> indices;
//!指定IDの通常タスク取得
TaskPtr FindTask(unsigned int id) const
{
const auto result = indices.find(id);
return (result != indices.end()) ? result->second : TaskPtr();
}
index化した結果、検索機能の実装はコンテナを呼び出す2行のみで良くなり、検索速度も5倍程度まで上がった。
参考文献
- 近代的タスクシステムの構築 - やねうらお-ノーゲーム・ノーライフ
- プログラミング言語C++ 第4版 - Bjarne Stroustrup, 柴田望洋, SBクリエイティブ
- C++メタ関数のまとめ