Spritekitを利用したゲームAIもどきの実装

今流行りのAI、即ち人工知能とタイトルすれば甚だ釣り気味ですが、 ゲーム作成に於いてはコンピュータにプレイヤーの相手をさせる場合、 大にしろ小にしろ孰れ其の為のアルゴリズムを用意しなければなりません。 其れは手元の開発iPhone専用ゲームアプリ バルーンズオキュパイ でも一般です。

略してバルオキュでは プレイヤーが画面上に風船を膨らませて自陣を拡張しているのに対し 此のゲームでは敵キャラクターとなる うさ犬 兄弟の弟、 はなまる が画面上を所狭しとジャンプしまくりプレイヤーが膨張させつつある風船を割って回る、 と言うゲーム内容になっています。 此の際、はなまるはプレイヤー陣地の確定した部分は避けて飛ぶ必要があります。 また膨張中の風船にはなまるが飛び込めば其の風船は割れてプレイヤーの負けが確定しなければなりません。 此の両者の解決を同時に図る上手い方法はないものかと模索して、 どうやら共に衝突判定が使えるだろうと思い付きました。 ゲームAIに衝突事件を組み込んで実装すればはなまるは確定風船を避けつつ時には膨張中の風船を割りに掛かるだろうと言う目論見です。

Hanamaru and uPanda in iPhone Game Balloons Occpy

衝突判定の実装は一般に難しいと言われる処ですが アップル社がXcodeに用意する2Dゲーム開発フレームワークである SpriteKit には最初から其の機能が用意されているので其れを用いることにします。

スポンサーリンク
日付:2014年4月16日
開発機:MacBook Air(11-inch, Mid 2013)
MacOSバージョン:OS X 10.9.2
Xcodeバージョン:5.1
言語:Objective-C
主関連アプリ:Balloons Occupy(邦題:バルーンズ・オキュパイ、バルオキュ)

風船が徐々に膨らむのは此れもSpritekitに当初用意される関数 -(void)update:(CFTimeInterval)currentTime
を用います。 時間が経つごとに風船の半径が広がっていく、と言う塩梅です。

SpriteKitではまた2D制限された物理エンジンを搭載しており 衝突判定は其の一部で当該メソッドは -(void)didBeginContact:(SKPhysicsContact *)contact となっています。 キャラクターはなまるとは別にはなまるのジャンプ先オブジェクトを生成し、 其れが確定したプレイヤー陣地と衝突すればジャンプ先オブジェクトの生成場所を変更してやります。 此れをプレイヤー確定陣地と接触しなくなる迄繰り返せば目論見は達成されます。

しかし以下の実装では目論見は達成出来ませんでした。

if (collisionDetect) {
	[appearPoints removeObjectAtIndex:tempAPNum];
	collisionDetect = NO;
} else {
	/* はなまるの移動 */
	[hamanaru runAction:[SKAction sequence:@[
		[SKAction moveTo:aptemp.position duration:0.5],
		[SKAction waitForDuration:1.0f]
		]]];
	[aptemp removeFromParent];
}

此のコードではフラグとして赤く示した (BOOL)collisionDetect を用意し其れを変化させて衝突判定を条件に処理を分岐させる目論見なのですが 衝突判定メソッドの実行タイミングが遅く、従って衝突の可否が判明せず 結局オブジェクト生成のタイミングで分岐させても常に collisionDetect=NO で前の処理はスルーされてしまうのが原因です。

また此のコードの一番の問題は衝突しない場合に其の検出が出来ないのにあります。 衝突した時だけアルゴリズムが進行するのでは噺になりません。 衝突してもしなくてもゲームは進行しなければならないのであって 従って衝突しなかったのなら衝突しなかった旨がメインアルゴリズムに通知されねばなりません。

以上を踏まえて collisionDetect を画面全体との衝突フラグBOOL値に変更し、 兎に角キャラクター出現ポイントオブジェクトが生成されれば此れがYESとなって衝突が発生するようにした訳です。 加えて後段のコード内に緑字で示す別フラグ (BOOL)collisionBalloonOccupyDetect を設定し此れを風船との衝突検知に利用するようにしました。 そうすればキャラクター出現ポイントがプレイヤー確定陣地と衝突する時、しない時が検出出来る勘定になります。

更に突っ込んで言えばキャラクター出現ポイントがプレイヤー確定陣地と衝突するときは 実際にジャンプするキャラクターはなまるオブジェクトを現時点の位置から移動させたくありません。 此の仕様の実現の為に最初は衝突判定メソッド内で 以下の様に

if((firstBody.categoryBitMask & categoryHanamaru ) != 0 && (secondBody.categoryBitMask & categoryWhole ) != 0 ){
	// NSLog(@"Second Body: %@", secondBody.node.name);
	collisionDetect = YES;
	/* はなまるの移動 */
	[hamanaru runAction:[SKAction sequence:@[
		[SKAction moveTo:firstBody.node.position duration:0.5],
		[SKAction waitForDuration:1.0f]
		]]];
	[firstBody.node removeFromParent];
}

はなまるオブジェクトの移動とはなまる出現ポイントオブジェクトの消去処理を実行していたものを -(void)update:(CFTimeInterval)currentTime メソッドから呼ぶ任意単位時間生用の - (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)timeSinceLast メソッド内に移し、次のはなまる出現ポイントオブジェクトを生成する前に処理する事にしました。 此れに仍ってワンテンポ遅らせて移動処理を実行出来るようになります。 此の様にして目論見通り運んだコードを以下に記します。

先ずは以下が衝突検知メソッド部分に実装したコードとなります。

-(void)didBeginContact:(SKPhysicsContact *)contact {
	SKPhysicsBody *firstBody, *secondBody;
	// 順序整理
	if ( contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) {
		firstBody = contact.bodyA;
		secondBody = contact.bodyB;
	} else {
		firstBody = contact.bodyB;
		secondBody = contact.bodyA;
	}
	// 二つの物体が意図したものであれば、衝突メソッドをcall
	if((firstBody.categoryBitMask & categoryHanamaru ) != 0 && (secondBody.categoryBitMask & categoryBallonOccupy ) != 0 ){
		collisionBalloonOccupyDetect = YES;
		[appearPoints removeObjectAtIndex:[firstBody.node.name intValue]];
		NSLog(@"Array Count: %lu", (unsigned long)appearPoints.count);
		[firstBody.node removeFromParent];
	}
	if((firstBody.categoryBitMask & categoryHanamaru ) != 0 && (secondBody.categoryBitMask & categoryWhole ) != 0 ){
		collisionDetect = YES;
	}
}

そして以下が任意単位時間毎に発生させるキャラクター処理のコードとなります。

- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)timeSinceLast {
	self.lastSpawnTimeInterval += timeSinceLast;
	if (self.lastSpawnTimeInterval > 2) {
		self.lastSpawnTimeInterval = 0;
		/* 出現ポイントの取り出し */
		if (appearPoints.count > 0) {
			if (collisionDetect) {
				if (!collisionBalloonOccupyDetect) {
					[hamanaru runAction:[SKAction sequence:@[
						[SKAction moveTo:apperaPoint.position duration:0.5],
						[SKAction waitForDuration:1.0f]
						]]];
				}
				[apperaPoint removeFromParent];
				collisionBalloonOccupyDetect = NO;
				/* 出現ポイントをランダムに設定 */
				// NSLog(@"APcound: %lu", (unsigned long)appearPoints.count);
				// NSLog(@"APindex: %d", arc4random_uniform((unsigned int)appearPoints.count));
				int tempAPNum = arc4random_uniform((unsigned int)appearPoints.count);
				SKSpriteNode *aptemp = [appearPoints objectAtIndex:tempAPNum];
				
				aptemp.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:aptemp.size];
				aptemp.physicsBody.categoryBitMask = categoryHanamaru;
				aptemp.physicsBody.contactTestBitMask = categoryBallonOccupy | categoryWhole;
				aptemp.physicsBody.affectedByGravity = NO;
				aptemp.physicsBody.dynamic = NO;
				aptemp.name = [NSString stringWithFormat:@"%d", tempAPNum];
				
				apperaPoint = aptemp;
				
				[self addChild:apperaPoint];
				
				collisionDetect = NO;
			}
		}
	}
}

以て バルオキュ に於けるゲームAIもどきアルゴリズムは目論見を達成しています。