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

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

グリーンの状態でリファクタリング

衝突判定のユニットテストグリーンになりましたが、テストコードにdeleteが抜けているのでメモリリークを起こしてしまいます。
以前Objective-Cで書いていたコードを持ってきたので、ついdeleteを忘れてしまっていました。
単純にdeleteを追加すれば修正が終了しますが、テストコードをよく見るとCollisionDetection::isCollide関数の引数はポインタではなく参照の方が良い気がしてきました。
ですので、引数の型を参照にするようにリファクタリングを行う事にします。

テストファーストなので、先ずはテストコードの修正から。

TEST(Collision, isCollide) {
	CollisionArea area1(0.0f, 0.0f, 10.0f, 10.0f);
	CollisionArea area2(5.0f, 5.0f, 10.0f, 10.0f);
	CollisionArea area3(-15.0f, -5.0f, 10.0f, 10.0f);
	CollisionArea area4(-15.0f, -5.0f, 10.0f, 10.0f);
	CollisionArea area5(10.0f, 10.0f, 20.0f, 20.0f);
	CollisionArea area6(12.0f, 12.0f, 10.0f, 10.0f);

	ASSERT_TRUE(CollisionDetection::isCollide(area1, area2));	// 一部が重なっている
	ASSERT_FALSE(CollisionDetection::isCollide(area1, area3));	// 重なっていない
	ASSERT_TRUE(CollisionDetection::isCollide(area3, area4));	// 同じ位置と大きさの範囲が重なっている
	ASSERT_TRUE(CollisionDetection::isCollide(area5, area6));	// 大きな範囲の内側に小さな範囲が入っている
	ASSERT_TRUE(CollisionDetection::isCollide(area6, area5));	// 小さな範囲が大きな範囲の内側に入っている
}


単純にコンパイルエラーになるので、プロダクションコードも修正します。

bool CollisionDetection::isCollide(CollisionArea &lhs, CollisionArea &rhs) {
	const float selfLeft = lhs.x;
	const float selfTop = lhs.y;
	const float selfRight = selfLeft + lhs.w;
	const float selfBottom = selfTop + lhs.h;
	const float otherLeft = rhs.x;
	const float otherTop = rhs.y;
	const float otherRight = otherLeft + rhs.w;
	const float otherBottom = otherTop + rhs.h;
	
	if(selfLeft > otherRight) {	return false;	}	// 判定対象の範囲は自分の範囲よりも左側にある
	if(selfTop > otherBottom) {	return false;	}	// 判定対象の範囲は自分の範囲よりも上側にある
	if(selfRight < otherLeft) {	return false;	}	// 判定対象の範囲は自分の範囲よりも右側にある
	if(selfBottom < otherTop) {	return false;	}	// 判定対象の範囲は自分の範囲よりも下側にある
	
	// 判定対象の範囲は自分と重なっている部分がある
	return true;
}

簡単な修正なので、問題なく終了してテストも通りました。

これでテスト駆動開発レッドグリーンリファクタリングのサイクルが一度終了しました。

このテストはユニットのサンプルでよく見る、引数の値を処理して結果を戻り値として返す関数のシンプルなテストでした。
次はコリジョン判定を呼び出す処理周りを作っていこうかと思います。

今回のリビジョンのタグはこちらです。
blog_20140129

レッドのテストをグリーンにする

衝突判定のテストがレッドの状態なので、実装を行ってグリーンにしていきます。

bool CollisionDetection::isCollide(CollisionArea *lhs, CollisionArea *rhs) {
	if(lhs == 0 || rhs == 0) {
		return false;
	}
	
	const float selfLeft = lhs->x;
	const float selfTop = lhs->y;
	const float selfRight = selfLeft + lhs->w;
	const float selfBottom = selfTop + lhs->h;
	const float otherLeft = rhs->x;
	const float otherTop = rhs->y;
	const float otherRight = otherLeft + rhs->w;
	const float otherBottom = otherTop + rhs->h;
	
	if(selfLeft > otherRight) {	return false;	}	// 判定対象の範囲は自分の範囲よりも左側にある
	if(selfTop > otherBottom) {	return false;	}	// 判定対象の範囲は自分の範囲よりも上側にある
	if(selfRight < otherLeft) {	return false;	}	// 判定対象の範囲は自分の範囲よりも右側にある
	if(selfBottom < otherTop) {	return false;	}	// 判定対象の範囲は自分の範囲よりも下側にある
	
	// 判定対象の範囲は自分と重なっている部分がある
	return true;
}


実装を正しく書いたつもりですが、テストのアサートに引っかかってしまいました。
よく見るとテストの数値が間違っていたので、テストコードを修正します。

TEST(Collision, isCollide) {
	CollisionArea *area1 = new CollisionArea(0.0f, 0.0f, 10.0f, 10.0f);
	CollisionArea *area2 = new CollisionArea(5.0f, 5.0f, 10.0f, 10.0f);
	CollisionArea *area3 = new CollisionArea(-15.0f, -5.0f, 10.0f, 10.0f);
	CollisionArea *area4 = new CollisionArea(-15.0f, -5.0f, 10.0f, 10.0f);
	CollisionArea *area5 = new CollisionArea(10.0f, 10.0f, 20.0f, 20.0f);
	CollisionArea *area6 = new CollisionArea(12.0f, 12.0f, 10.0f, 10.0f);

	ASSERT_TRUE(CollisionDetection::isCollide(area1, area2));	// 一部が重なっている
	ASSERT_FALSE(CollisionDetection::isCollide(area1, area3));	// 重なっていない
	ASSERT_TRUE(CollisionDetection::isCollide(area3, area4));	// 同じ位置と大きさの範囲が重なっている
	ASSERT_TRUE(CollisionDetection::isCollide(area5, area6));	// 大きな範囲の内側に小さな範囲が入っている
	ASSERT_TRUE(CollisionDetection::isCollide(area6, area5));	// 小さな範囲が大きな範囲の内側に入っている
}

これでテストが通ってグリーンの状態になりました。
Xcode上で実行するとコンソールに色が表示されないのは残念です。

実装がほぼ終わった時に気がついたのですが、このテストは delete を呼んでいないのでメモリリークを起こしてしまっています。
すぐに直してしまいたい気持ちはありますが、一度グリーンの状態になっているのでここでコミットをします。

今回のリビジョンのタグはこちらです。
blog_20140128

衝突判定のテストコードを書く

アクションゲームなので、先ずはシンプルな衝突判定処理を作成する所から始める事にします。

テストファーストで開発する流れは、簡単に説明すると以下のようになります。

  1. テストコードを書いて失敗させる → レッド
  2. テストコードが通るように実装する → グリーン
  3. ソースコードを整理する → リファクタリング

ですので、まず衝突判定のテストコードを書いて失敗させます。

TEST(Collision, isCollide) {
	CollisionArea *area1 = new CollisionArea(0.0f, 0.0f, 10.0f, 10.0f);
	CollisionArea *area2 = new CollisionArea(5.0f, 5.0f, 10.0f, 10.0f);
	CollisionArea *area3 = new CollisionArea(10.0f, 10.0f, -15.0f, -5.0f);
	CollisionArea *area4 = new CollisionArea(10.0f, 10.0f, -15.0f, -5.0f);
	CollisionArea *area5 = new CollisionArea(20.0f, 20.0f, 10.0f, 10.0f);
	CollisionArea *area6 = new CollisionArea(10.0f, 10.0f, 12.0f, 12.0f);

	ASSERT_TRUE(CollisionDetection::isCollide(area1, area2));	// 一部が重なっている
	ASSERT_FALSE(CollisionDetection::isCollide(area1, area3));	// 重なっていない
	ASSERT_TRUE(CollisionDetection::isCollide(area3, area4));	// 同じ位置と大きさの範囲が重なっている
	ASSERT_TRUE(CollisionDetection::isCollide(area5, area6));	// 大きな範囲の内側に小さな範囲が入っている
	ASSERT_TRUE(CollisionDetection::isCollide(area6, area5));	// 小さな範囲が大きな範囲の内側に入っている
}

テストコードだけではコンパイルが通らないので、レッドの状態です。
次にコンパイルを通すため必要なクラスと空の関数を書きます。

/**
 * @brief 衝突範囲
 */
class CollisionArea {
private:
	float _x, _y, _w, _h;
public:
	CollisionArea(float x, float y, float w, float h);
};

/**
 * @brief 衝突判定クラス
 */
class CollisionDetection {
public:
	static bool isCollide(CollisionArea *lhs, CollisionArea *rhs);
};

クラスの関数の内容は未実装なので、テストはまだ通りません。
この状態もまだレッドのままです。
ですが、コンパイルは通るのでここで一度区切りをつけてコミットをする事にします。

テストが通らないままコミットをするのは良くないので、以下のようにテストケースに DISABLED_ 設定を追加して、通らないテストを無視させます。

TEST(Collision, DISABLED_isCollide)

これで、コンパイルもテストも(実行されませんが)通るようになったのでコミットします。

実際はレッドのままですが、コミットをした事で一息つく事ができました。

今回のリビジョンのタグはこちらです。
blog_20140127

開発環境の準備

GitHubにサンプルプロジェクトを準備しました。

https://github.com/yobiya/tdd_game_sample

 

公開するプロジェクトのコードなどはここにコミットしていきます。

 

今回は開発環境の準備と言う事で、cocos2d-xのプロジェクトとUnitTestの実行環境を準備しました。

UnitTestを実行するためのフレームワークはGoogleTestです。

 

まだ内容は空ですが、アプリ本体とUnitTestでビルドと実行ができる状態になっています。

 

それと、Gitは特定のリビジョンを参照するのが面倒なので、コミットがあった場合は記事に対応したリビジョンにタグを付ける事にします。

今回のリビジョンのタグはこちらです。

blog_20140119

検証に使うゲーム

TDDで開発するゲームは、今友人と一緒に作っているゲームなのですが

許可をもらって、一部のコードを公開することにしました。

 

検証なので、途中経過も重要になるかと思います。

後ほど、公開できるコードをGitHubにアップいたします。

 

iOSアプリで、ジャンルはアクションです。

始めからTDDで開発をしています。

 

今まで、cocos2dを使って作成していましたが、cocos2dの開発が終了してしまったので

cocos2d-x3.0へ移植して開発を続けます。

現時点ではcocos2d-x3.0はまだベータ版ですが、これを使って開発を進めて

リリース版が公開されたら更新する予定です。

 

ユニットテストフレームワークは、cocos2dを使っていた時はSenTesgintKitを使っていましたが

今回はC++になるので、何を使うかは検討中です。

TDDでゲーム開発はできるのか?

TDD(テスト駆動開発)について知ったのは多分10年くらい前だったと思います。

当時はまだ学生でC Magazineか何かでアジャイル開発のXPを初めて知って、その中でTDDも知りました。

 

自分はゲームが作りたかったので、その後運良くXPをやっていたゲーム会社に入る事が出来たのですが

1年程度でXPは終了してしまって、一般的な開発スタイルになってしまいました。

もちろんTDDも一緒に終了でした。

 

その当時は自分もまだ新人でプログラム自体のスキルが低かったですし

TDDも流行ってはいなかったので、流行りだしたらまた始めたいとずっと思っていました。

 

ですが、日本のゲーム開発においては一向に流行る気配がありません。

その理由はたぶんグラフィック周りなどのユニットテストではカバーできない範囲が多いからだと思います。

しかし、海外の記事ではTDDでのゲーム開発に成功しているようなものも見かけます。

 

ですので、自分もTDDでのゲーム開発をまたやってみて

いろいろと検証をしてみようかと思います。