テスト駆動でゲーム開発はできるのか?

ゲームの開発にテスト駆動開発を取り入れてみる検証のブログです

プロジェクトが中止になったので、方向転換をします

かなり更新期間が空いてしまいました。

TDDでゲーム開発を行う事が出来るかの検証プロジェクトとして使用させてもらっていたCROSSプロジェクトですが
自分を含めプロジェクトメンバー全員が十分な開発時間をとる事ができないため、中止となりました。

ですので、今後は実際のプロジェクトでTDDを実践していく検証から
どのようにすればゲームで実際に有りそうな問題をテストできるのかという検証に方向転換する事にしました。

今後の検証には、今までのコードを使うのか、新規で作り出すのかなど
まだ決めていない事が多いので
実際に動き出すまで少々お待ちください。

std::shared_ptrにポインタを置き換える

std::shared_ptr が使えるようになったので、今まで普通にポインタを使っていた箇所をstd::shared_ptrに置き換える事にします。

CollisionAreaクラスの場合

std::shared_ptr<CollisionArea> area = std::shared_ptr<CollisionArea>(new CollisionArea(x, y, w, h));

と書くのは読みづらいので

CollisionArea::Ptr area = CollisionArea::newSharedPtr(x, y, w, h);

と書けるようにクラスに型の定義と生成用のstatic関数を追加しました。
この newSharedPtr 関数は Effective Java の "Static Factory Method" パターンを参考にしています。

今後は std::shared_ptr を使用するクラスは、全てこの書き方が出来るように対応するつもりです。


そして、設計からかなり時間が空いてしまいましたが、ようやく衝突処理の実装に手をつけていきます。

まずはコリジョン同士が衝突した場合に、その衝突の通知を受け取るテストコードを書きます。
設計では、CollisionAreaからCharacterが衝突の通知を受け取るとしましたが
Characterは後に大きくなる可能性の高いクラスですので、通知を受け取るための CollisionEventListener クラスを用意しました。
これは GoF の "Observer" パターンですが、自分は Listener という名前の方が好きなので CollisionEventListener という名前にしています。

まだ、空のクラスのみで処理は未実装なので、テストコードには DISABLED_ を付けています。

TEST(Collision, DISABLED_Collide) {
	CollisionDetection collisionDitection;

	bool isCollide1 = false;
	bool isCollide2 = false;

	CollisionArea::Ptr area1 = CollisionArea::newSharedPtr(0.0f, 0.0f, 10.0f, 10.0f);
	CollisionArea::Ptr area2 = CollisionArea::newSharedPtr(5.0f, 5.0f, 10.0f, 10.0f);
	area1->addEventListener(CollisionEventListener::newSharedPtr());
	area2->addEventListener(CollisionEventListener::newSharedPtr());

	collisionDitection.add(area1);
	collisionDitection.add(area2);

	// 衝突判定を行う
	collisionDitection.ditect();

	ASSERT_TRUE(isCollide1);
	ASSERT_TRUE(isCollide2);
}

最終的にはラムダ関数を使用して、衝突が発生したら CollisionEventListener が isCollide 変数に true を入れる処理をイメージしています。

今回のタグは以下になります。
blog_20140414

今度はshared_ptrが使えない

仕事が忙しくしばらく間が空いてしまいましたが、一段落したので開発の続きを行います。

次はCollisionDitectionクラスにコリジョンの登録と削除のテストを書く事にしたのですが
登録するコリジョンをshared_ptrで扱おうとしたところ、またビルドエラーになってしまいました。
どうやら標準ライブラリがC++11に対応しておらず、shared_ptrを扱えなかった様です。

ですが、C++ Standerd Libraryの項目を libc++(LLVM C++ standard library with C++ 11 support) に変えると
shared_ptrは使えますが、今度は以前に出たASSERT_EQのリンクエラーが発生してしまいます。

ASSERT_EQのリンクエラーを回避したときは根本原因はわかっておらず、とりあえずエラーが出ない状態にしただけでしたので
きちんと根本原因を解決しないと、ASSERT_EQとshared_ptrの両方を使う事は出来ない様です。

原因を調べていくとgootle testのビルド設定に原因が有りました。
どうやら古いOSにも対応するため、C++11はサポートされていなかった様です。
ですので、C++11に対応できるように

Deployment 項目の OS X Deployment Target をC++11に対応できるOS X 10.7に変更して
Apple LLVM 5.1 - Language - C++ 項目の C++ Language Dialect を C++11に変更
同じ項目の C++ Standerd Library を libc++(LLVM C++ standard library with C++ 11 support) に変更しました。

適当なコードを書いて確認しましたが、これでASSERT_EQとshared_ptrの両方を使うことが出来るようになりました。

今回のビルドエラーの一番の原因はgoogle testのデフォルトの設定から変更していないから正しいはずと思い込み
ASSERT_EQのリンクエラーの時にgoogle test側をきちんと調べなかった事ですね。

今回のタグは以下になります。
blog_20140316

Google Testのリンクエラーの対応

Google Testでよくわからないリンクエラーにハマってしまったので、備忘録を残します。

開発環境は
Xcode 5.0.2
Gootle Test 1.7.0

この環境で、ASSERT_EQを呼び出してビルドすると以下のようなリンクエラーが出てしまいました。

Undefined symbols for architecture x86_64:
  "testing::internal::EqFailure(char const*, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, bool)", referenced from:
      testing::AssertionResult testing::internal::CmpHelperEQ<int, int>(char const*, char const*, int const&, int const&) in CollisionTest.o

Google Testのライブラリをビルドした環境は、ダウンロードしたファイルのまま何も変更しておらず
サンプルコードも実行できています。

しばらく試行錯誤してみたのですが、最終的にこちらのサイトを参考にさせていただいて、解決しました。

自分の場合は、C++ Standerd Libraryの値をComplier Defaultではなく、libstdc++にしたらリンクが通りました。

これでようやく開発の続きができます。

密結合になってしまった設計

これから作成したい機能は以下になりますが

  1. キャラクタが持つ自分の衝突範囲を衝突判定処理クラスに登録する(2体分)
  2. 衝突判定処理クラスがキャラクタ同士の衝突を検知したら、衝突したキャラクタに衝突情報を通知する

テストコードを書く前に、シーケンス図も描いて大まかな流れも確認します。

f:id:YoBiya:20140206002036p:plain

これを見るとCharacterが持つCollisionAreaをColisionDitectionに登録するために、CollisionDitectionを知っていなければならない事と
CollisionAreaも衝突をCharacterに通知するために、Characterを知っていなければならず、相互参照の状態になってしまっています。

一度登録するためだけの参照や、相互参照の強い結びつきはテストコードを書くのを難しくしてしまうため、できるだけ粗結合な作りにするのが理想的です。

これを解決するアイディアはあるのですが、この設計を実装した場合よりもコードが複雑になってしまいます。
ですのでYAGNIに従い今はこの設計で実装を行い
必要になったら、疎結合になるようにリファクタリングをする事にします。

次はこの設計のユニットテストを書いていきます。

今回のタグは以下になります。
blog_20140206

衝突処理の設計

衝突判定処理ができたので、次は実際にそれを呼び出す処理を作って行きます。

アクションゲームなので、キャラクタと攻撃の衝突があります。
それと、キャラクタ同士も衝突をします。

ですので、先ずはキャラクタ同士の衝突処理を作成する事にします。


前回の衝突判定のテストは設計を考えるようなものではなかったので設計を行いませんでしたが
今回は複数のクラスが関係してきますので設計を行います。

設計はUMLで行います。
ツールはastahのコミュニティ版を使わせて頂く事にしました。


UMLを描く前にやりたい事を整理すると

  1. キャラクタが持つ自分の衝突範囲を衝突判定処理クラスに登録する(2体分)
  2. 衝突判定処理クラスがキャラクタ同士の衝突を検知したら、衝突したキャラクタに衝突情報を通知する

このようになります。

そしてこのやりたい事からクラスを抽出すると

  • キャラクタ : Character
  • 衝突範囲 : CollisionArea
  • 衝突判定処理クラス : CollisionDetection

こうなりました。

CollisionAreaとCollisionDetectionは既に作ってありますね。

そして、これらをクラス図にすると以下のようになりました。
f:id:YoBiya:20140203220924p:plain

次はどのように処理を行うのかを考えようかと思います。

今回のタグは以下になります。
blog_20140202

※クラス図が表示されていなかったので、上げ直しました。