個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Microsoft MVP for Developer Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

Cocos Sharp での基本的なゲームの制御の方法(2) 代表的なアクションの種類

はじめに

こんにちは、@hiro128_777です。

今回も、Cocos Sharp での基本的なゲームの制御の方法をご説明します。

前回の繰り返しになりますが、Cocos Sharp での基本的なゲームの制御の方法を理解するには以下の2つの概念を理解すれば大丈夫です。

① スケジューラ 一定間隔で繰り返し何らかの処理を行なう。
② アクション  画面上のオブジェクトに対して何らかの動きを指示する。また、オブジェクトにひも付けて何らかの処理を行なう。

今回は、②のアクションについてご説明します。

アクション

アクションとは、画面上のオブジェクトに対して何らかの動きを指示したり、オブジェクトにひも付けて何らかの処理を行なうものです。
例えば、画面上のキャラクターの移動、回転、拡大縮小、色の変更などが該当します。

Cocos Sharp のアクションの基本クラスは、CCActionであり、
すべてのアクションは CCAction の派生クラスになります。

以下に、代表的なアクションをご紹介します。

public CCMoveTo(float duration, CCPoint position);
public CCMoveBy(float duration, CCPoint position);
public CCScaleTo(float duration, float scale);
public CCScaleBy(float duration, float scale);
public CCRotateTo(float duration, float deltaAngle);
public CCRotateBy(float duration, float deltaAngle);

To と By の違いですが、
To はアクション完了時の絶対値、By は現在からの相対値になります。

それではそれぞれの使用方法を見ていきましょう。

CCMoveTo : 絶対位置指定の移動

public CCMoveTo(float duration, CCPoint position);

position で指定された画面上の座標に duration の所要時間で移動します。

(例)画面上の 50, 100 の座標に3秒の所要時間で移動します。

var moveToAction = new CCMoveTo(3f, new CCPoint(50f, 100f));
node.RunAction(moveToAction);

CCMoveBy : 相対位置指定の移動

public CCMoveBy(float duration, CCPoint position);

現在の座標から position で指定された座標分だけ duration の所要時間で移動します。

(例)現在位置より画面上の座標で、 X軸方向に 50、 Y軸方向に100の座標に3秒の所要時間で移動します。

var moveByAction = new CCMoveBy(3f, new CCPoint(50f, 100f));
node.RunAction(moveByAction);

CCScaleTo : 絶対倍率指定の拡大縮小

public CCScaleTo(float duration, float scale);

オブジェクトの当初のサイズを基準として scale で指定された倍率に duration の所要時間で拡大縮小します。

(例)オブジェクトを3秒の所要時間で当初のサイズより2倍に拡大(場合によっては縮小)します。

var scaleToAction = new CCScaleTo(3f, 2f);
node.RunAction(scaleToAction);

CCScaleBy : 相対倍率指定の拡大縮小

public CCScaleBy(float duration, float scale);

現在のサイズから scale で指定された倍率に duration の所要時間で拡大縮小します。

(例)オブジェクトを3秒の所要時間で現在のサイズより2倍に拡大します。

var scaleByAction = new CCScaleBy(3f, 2f);
node.RunAction(scaleByAction);

CCRotateTo : 絶対角度指定の回転

public CCRotateTo(float duration, float deltaAngle);

オブジェクトの0度を基準として deltaAngle で指定された角度に duration の所要時間で回転します。

(例)オブジェクトを3秒の所要時間で45度の角度に回転させます。

var rotateToAction = new CCRotateTo(3f, 45f);
node.RunAction(rotateToAction);

CCRotateBy : 相対角度指定の回転

public CCRotateBy(float duration, float deltaAngle);

オブジェクトの現在の角度を基準として deltaAngle で指定された角度分だけ duration の所要時間で回転します。

(例)オブジェクトを3秒の所要時間で当初の角度を基準として45度回転させます。
※現在の角度が45度なら当初の角度から90度の回転となります。

var rotateByAction = new CCRotateBy(3f, 45f);
node.RunAction(rotateByAction);

このほかにもたくさんのアクションが用意されていますので、ぜひ一度確認してみてください。

次回は連続したアクションの作成方法についてご説明します。

今回はここまでです。

Cocos Sharp での基本的なゲームの制御の方法(1) スケジューラ

はじめに

こんにちは、@hiro128_777です。

今回は、Cocos Sharp での基本的なゲームの制御の方法をご説明します。

スケジューラとアクション

結論から言いますと、Cocos Sharp での基本的なゲームの制御の方法をマスターするには以下の2つの概念を理解すれば十分です。

① スケジューラ 一定間隔で繰り返し何らかの処理を行なう。
② アクション  画面上のオブジェクトに対して何らかの動きを指示する。また、オブジェクトにひも付けて何らかの処理を行なう。

基本的にこれだけです。

ですが、この2つは処理の方向性がまったく違うのでそれぞれの特性をよく理解し使用しましょう。

今回は、①のスケジューラについてご説明します。

スケジューラ

スケジューラとは一定間隔で繰り返し何らかの処理を行なう機能です。
ゲームの中ではゲームを進行する上で必要な様々な処理に使用します。
例えば、自機の移動、当たり判定、スコアなどの各種表示項目の制御などです。

Cocos Sharp のスケジューラとは具体的には、CCNode クラスの Schedule メソッドです。
いくつかオーバーロードがありますが、まずは以下の2つだけを覚えれば十分です。

public void Schedule(Action<float> selector, float interval);
public void ScheduleOnce(Action<float> selector, float delay);

次にそれぞれの使用方法をご説明します。

Schedule メソッド

public void Schedule(Action<float> selector, float interval);

interval で指定された間隔ごとに selector で指定されたデリゲートを実行します。
※デリゲートはラムダ式で書くので、要はラムダ式内で指定したメソッドを実行するということになります。

例えば、以下のような場合、0.1秒ごとに、DetectCollisions メソッドを実行します。

this.Schedule(t => DetectCollisions(), 0.1f);

private void DetectCollisions()
{
	// 当たり判定の処理
}

この場合、0.1秒ごとに当たり判定が実行されます。

ScheduleOnce メソッド

public void ScheduleOnce(Action<float> selector, float delay);

selector で指定されたデリゲートを delay で指定された時間遅延させて実行します。

例えば、以下のような場合、1秒後に、PerformGameOver メソッドを実行します。

this.ScheduleOnce(t => PerformGameOver(), 1f);

private void PerformGameOver()
{
	// ゲームオーバーの処理
}

この場合、1秒後にゲームオーバーの処理が実行されます。

スケジューラは、Cocos Sharp でゲームを作成する場合最も重要な機能です。
Cocos Sharp ではスケジューラによってゲームが管理されます。

今回はここまでです。

Xamarin + Cocos Sharp で iOS, Android 対応のゲームを開発する手順 (14) 当たり判定② 敵に当たった場合のアクションの作成

今回は自機が敵キャラに当たった時のアクションを作成します。

具体的には、以下の処理を行ないます。
1. 自機に爆発するアニメーションを設定する。
2. 自機を画面上から削除する。

まず、自機が爆発するアニメーションを作成します。

アニメーションを作成する場合、それぞれのコマの画像を準備します。

CocosSharpGameSample.iOS プロジェクトに爆発する画像を追加します。

f:id:hiro128:20160920210550p:plain

/Resources/Images/Character/Explosion/Explosion00.png ~ Explosion04.png

f:id:hiro128:20160920210632p:plainf:id:hiro128:20160920210633p:plainf:id:hiro128:20160920210634p:plainf:id:hiro128:20160920210635p:plainf:id:hiro128:20160920210636p:plain

CocosSharpGameSample.Android プロジェクトに爆発する画像を追加します。

f:id:hiro128:20160920210559p:plain

/Assets/Images/Character/Explosion/Explosion00.png ~ Explosion04.png

CocosSharpGameSample.Core プロジェクトに処理を作成します。

Layers/GameLayer.cs に処理を追加します。

まずは、アニメーションの状態を把握できるフィールドを追加します。

public class GameLayer : LayerBase
{

	private ExtendedCCSprite player;
	private bool enableCollisionDetection = true;
	private CCActionState playerShotDownActionState; // <-追加

次に爆発のアニメーションを作成します。

アニメーションは以下の要領で作成できます。
1. 各コマの画像の CCSprite を作成
2. CCSprite から CCSpriteFrame を作成
3. List としてアニメーションを構築
4. ループ回数、所要時間を設定し、アニメーションを適用させたい CCSprite に設定し実行
5. アニメーションの状態を返却

この手順をコードにすると以下の通りになります。

protected CCActionState PerformExplosionAnimation(CCSprite sprite)
{
	// 各コマの画像の CCSprite を作成
	var sprite00 = new CCSprite("/Resources/Images/Character/Explosion/Explosion00.png", null);
	var sprite01 = new CCSprite("/Resources/Images/Character/Explosion/Explosion01.png", null);
	var sprite02 = new CCSprite("/Resources/Images/Character/Explosion/Explosion02.png", null);
	var sprite03 = new CCSprite("/Resources/Images/Character/Explosion/Explosion03.png", null);
	var sprite04 = new CCSprite("/Resources/Images/Character/Explosion/Explosion04.png", null);

	// CCSprite から CCSpriteFrame を作成
	var frame00 = new CCSpriteFrame(sprite01.Texture, sprite01.TextureRectInPixels);
	var frame01 = new CCSpriteFrame(sprite02.Texture, sprite02.TextureRectInPixels);
	var frame02 = new CCSpriteFrame(sprite03.Texture, sprite03.TextureRectInPixels);
	var frame03 = new CCSpriteFrame(sprite04.Texture, sprite04.TextureRectInPixels);
	var frame04 = new CCSpriteFrame(sprite03.Texture, sprite03.TextureRectInPixels);
	var frame05 = new CCSpriteFrame(sprite02.Texture, sprite02.TextureRectInPixels);
	var frame06 = new CCSpriteFrame(sprite01.Texture, sprite01.TextureRectInPixels);
	var frame07 = new CCSpriteFrame(sprite00.Texture, sprite00.TextureRectInPixels);

	// List<CCSpriteFrame> としてアニメーションを構築
	var frames = new List<CCSpriteFrame>();
	frames.Add(frame00);
	frames.Add(frame01);
	frames.Add(frame02);
	frames.Add(frame03);
	frames.Add(frame03);
	frames.Add(frame04);
	frames.Add(frame05);
	frames.Add(frame06);
	frames.Add(frame07);

	// ループ回数、所要時間を設定し、アニメーションを適用させたい CCSprite に設定し実行
	var animation = new CCAnimation(frames, 0.15f);
	animation.Loops = 1;
	var animate = new CCAnimate(animation);
	var state = sprite.RunAction(animate);

	// アニメーションの状態を返却
	return state;
}

衝突を検出した場合、爆発のアニメーションを実行するように、当たり判定検出のメソッドに下記の爆発のアニメーションを実行処理を追加します。

追加する処理

this.playerShotDownActionState = this.PerformExplosionAnimation(this.player);

追加場所は下から6行目です。

private void DetectCollisions()
{
	// プレーヤーがやられてしまった後など、プレーヤーが画面上に居ない場合
	if (this.player == null)
	{
		return;
	}
	// ゲーム開始直後、リスタート直後、ステージクリア時など、当たり判定をキャンセルする場合
	if (this.EnableCollisionDetection == false)
	{
		return;
	}
	// 当たり判定領域をプレーヤー中心部分だけにする
	var rectPlayer = player.BoundingBox;
	var rectTrimedPlayer = new CCRect(
									rectPlayer.MidX - (GameSettings.PlayerWidth / 6),
									rectPlayer.MidY - (GameSettings.PlayerHeight / 3),
									GameSettings.PlayerWidth / 3,
									GameSettings.PlayerHeight / 3);
	//// 実際の当たり判定領域の確認用コード(コメントを外すと実際の当たり判定領域を可視化できます。)
	//var acutualRect = new CCSprite();
	//acutualRect.TextureRectInPixels = rectTrimedPlayer;
	//acutualRect.Color = new CCColor3B(0, 255, 0);
	//acutualRect.Position = rectTrimedPlayer.LowerLeft;
	//acutualRect.Tag = 100000000;
	//this.RemoveChildByTag(100000000);
	//this.AddChild(acutualRect);

	// 当たり判定実施
	var enemies = new List<CCNode>();
	enemies.AddRange(this.GetChildrenByTag((int)NodeTag.Enemy));
	if (enemies.Count > 0)
	{
		foreach (var node in enemies)
		{
			// 同一エリア以外は当たり判定スキップ
			if (this.player.ScreenArea != ((ExtendedCCSprite)node).ScreenArea)
			{
				continue;
			}
			var rectEnemy = node.BoundingBox;
			if (rectTrimedPlayer.IntersectsRect(rectEnemy) == true)
			{
				Debug.WriteLine("敵キャラに当たった!");
				this.EnableCollisionDetection = false;
				this.playerShotDownActionState = this.PerformExplosionAnimation(this.player); // 爆発のアニメーションを実行処理を追加 <-変更箇所
				break;
			}
		}
	}
}

爆発のアニメーションが終了した後、自機を画面上から削除します。

private void RemovePlayer()
{
	if (this.player == null)
	{
		return;
	}
	if (this.playerShotDownActionState == null)
	{
		return;
	}
	if (this.playerShotDownActionState.IsDone == true)
	{
		this.RemoveChild(this.player);
		Debug.WriteLine("プレーヤー削除!");
		this.player = null;
	}
}

自機削除を0.5秒に1回行なうようスケジューラに登録します。

private void StartScheduling()
{
	this.Schedule(t => this.UpdatePlayer(), 0.02f);
	this.Schedule(t => this.RemoveOffScreenNodes(), 1.0f);
	this.Schedule(t => this.DetectCollisions(), 0.1f);
	this.Schedule(t => this.RemovePlayer(), 0.5f);// <-自機削除をスケジューラに登録
}

既存のメソッドで自機を削除することによる、null参照エラーが発生する部分に対策を入れます。

private void UpdatePlayer()
{
	if (this.player == null)
	{
		return;
	}
private void DetectCollisions()
{
	// プレーヤーがやられてしまった後など、プレーヤーが画面上に居ない場合
	if (this.player == null)
	{
		return;
	}


以上をまとめると、CocosSharpGameSample.Core プロジェクトの Layers/GameLayer.cs は以下の通りになります。

using System.Collections.Generic;
using System.Diagnostics;
using CocosSharp;

namespace CocosSharpGameSample.Core
{
	public class GameLayer : LayerBase
	{

		private ExtendedCCSprite player;
		private bool enableCollisionDetection = true;
		private CCActionState playerShotDownActionState;

		public bool EnableCollisionDetection
		{
			get { return this.enableCollisionDetection; }
			set { this.enableCollisionDetection = value; }
		}

		public GameLayer()
			: base()
		{
			var listener = new CCEventListenerTouchOneByOne();
			listener.OnTouchBegan = this.CCEventListener_TouchBegan;
			this.AddEventListener(listener, this);
		}

		protected bool CCEventListener_TouchBegan(CCTouch touch, CCEvent touchEvent)
		{
			Debug.WriteLine(this.ChildrenCount);
			// タッチした場所に敵キャラ配置
			var touchedLocation = touch.Location;
			// 画面の上部25%なら敵キャラ配置
			if (touchedLocation.Y > GameSettings.ScreenHeight * 3 / 4)
			{
				var addingEnemy = new ExtendedCCSprite("/Resources/Images/Character/Enemy/Enemy001.png", null);
				addingEnemy.Position = touchedLocation;
				addingEnemy.Tag = (int)NodeTag.Enemy;
				this.ApplyGoStraightFromTopDownAction(addingEnemy);
				this.AddChild(addingEnemy);
				return true;
			}
			return false;
		}

		// Y軸のみの移動で画面外まで行くアクションを適用
		public void ApplyGoStraightFromTopDownAction(CCNode enemy)
		{
			var destinationPoint = new CCPoint(enemy.PositionX, -100);
			var action = new CCMoveTo(8.0f, destinationPoint);
			enemy.RunAction(action);
		}

		// 画面外のNodeを除去
		public void RemoveOffScreenNodes()
		{
			foreach (var node in this.Children)
			{
				if (node.Position.Y < 0)
				{
					this.RemoveChild(node);
				}
			}
		}

		protected override void AddedToScene()
		{
			base.AddedToScene();
			// ゲーム画面の背景画像を配置
			var gameBackground = new CCSprite("/Resources/Images/Background/GameBackground.png", null);
			gameBackground.Position = new CCPoint(this.ContentSize.Center.X, this.ContentSize.Center.Y);
			AddChild(gameBackground);

			// 自機を配置
			this.AddPlayer();
			this.StartScheduling();
		}

		private void StartScheduling()
		{
			this.Schedule(t => this.UpdatePlayer(), 0.02f);
			this.Schedule(t => this.RemoveOffScreenNodes(), 1.0f);
			this.Schedule(t => this.DetectCollisions(), 0.1f);
			this.Schedule(t => this.RemovePlayer(), 0.5f);
		}

		private void AddPlayer()
		{
			this.player = new ExtendedCCSprite("/Resources/Images/Character/Player/Player.png", null);
			this.player.Position = new CCPoint(this.ContentSize.Center.X, this.ContentSize.Center.Y);
			this.AddChild(this.player);
		}

		private void UpdatePlayer()
		{
			if (this.player == null)
			{
				return;
			}

			this.player.PositionX = this.player.PositionX;
			this.player.PositionY = this.player.PositionY;

			// 画面からはみ出ないように、位置の値の上限、下限を設定します。
			// 左
			if (this.player.PositionX < 0 + GameSettings.PlayerWidth)
			{
				this.player.PositionX = 0 + GameSettings.PlayerWidth;
			}
			// 右
			if (this.player.PositionX > GameSettings.ScreenWidth - GameSettings.PlayerWidth)
			{
				this.player.PositionX = GameSettings.ScreenWidth - GameSettings.PlayerWidth;
			}
			// 下
			if (this.player.PositionY < 0 + GameSettings.PlayerHeight)
			{
				this.player.PositionY = 0 + GameSettings.PlayerHeight;
			}
			// 上
			if (this.player.PositionY > GameSettings.ScreenHeight - GameSettings.PlayerHeight)
			{
				this.player.PositionY = GameSettings.ScreenHeight - GameSettings.PlayerHeight;
			}
		}

		private void DetectCollisions()
		{
			// プレーヤーがやられてしまった後など、プレーヤーが画面上に居ない場合
			if (this.player == null)
			{
				return;
			}
			// ゲーム開始直後、リスタート直後、ステージクリア時など、当たり判定をキャンセルする場合
			if (this.EnableCollisionDetection == false)
			{
				return;
			}
			// 当たり判定領域をプレーヤー中心部分だけにする
			var rectPlayer = player.BoundingBox;
			var rectTrimedPlayer = new CCRect(
											rectPlayer.MidX - (GameSettings.PlayerWidth / 6),
											rectPlayer.MidY - (GameSettings.PlayerHeight / 3),
											GameSettings.PlayerWidth / 3,
											GameSettings.PlayerHeight / 3);
			//// 実際の当たり判定領域の確認用コード(コメントを外すと実際の当たり判定領域を可視化できます。)
			//var acutualRect = new CCSprite();
			//acutualRect.TextureRectInPixels = rectTrimedPlayer;
			//acutualRect.Color = new CCColor3B(0, 255, 0);
			//acutualRect.Position = rectTrimedPlayer.LowerLeft;
			//acutualRect.Tag = 100000000;
			//this.RemoveChildByTag(100000000);
			//this.AddChild(acutualRect);

			// 当たり判定実施
			var enemies = new List<CCNode>();
			enemies.AddRange(this.GetChildrenByTag((int)NodeTag.Enemy));
			if (enemies.Count > 0)
			{
				foreach (var node in enemies)
				{
					// 同一エリア以外は当たり判定スキップ
					if (this.player.ScreenArea != ((ExtendedCCSprite)node).ScreenArea)
					{
						continue;
					}
					var rectEnemy = node.BoundingBox;
					if (rectTrimedPlayer.IntersectsRect(rectEnemy) == true)
					{
						Debug.WriteLine("敵キャラに当たった!");
						this.EnableCollisionDetection = false;
						this.playerShotDownActionState = this.PerformExplosionAnimation(this.player);
						break;
					}
				}
			}
		}

		private void RemovePlayer()
		{
			if (this.player == null)
			{
				return;
			}
			if (this.playerShotDownActionState == null)
			{
				return;
			}
			if (this.playerShotDownActionState.IsDone == true)
			{
				this.RemoveChild(this.player);
				Debug.WriteLine("プレーヤー削除!");
				this.player = null;
			}
		}

		protected CCActionState PerformExplosionAnimation(CCSprite sprite)
		{
			// 各コマの画像の CCSprite を作成
			var sprite00 = new CCSprite("/Resources/Images/Character/Explosion/Explosion00.png", null);
			var sprite01 = new CCSprite("/Resources/Images/Character/Explosion/Explosion01.png", null);
			var sprite02 = new CCSprite("/Resources/Images/Character/Explosion/Explosion02.png", null);
			var sprite03 = new CCSprite("/Resources/Images/Character/Explosion/Explosion03.png", null);
			var sprite04 = new CCSprite("/Resources/Images/Character/Explosion/Explosion04.png", null);

			// CCSprite から CCSpriteFrame を作成
			var frame00 = new CCSpriteFrame(sprite01.Texture, sprite01.TextureRectInPixels);
			var frame01 = new CCSpriteFrame(sprite02.Texture, sprite02.TextureRectInPixels);
			var frame02 = new CCSpriteFrame(sprite03.Texture, sprite03.TextureRectInPixels);
			var frame03 = new CCSpriteFrame(sprite04.Texture, sprite04.TextureRectInPixels);
			var frame04 = new CCSpriteFrame(sprite03.Texture, sprite03.TextureRectInPixels);
			var frame05 = new CCSpriteFrame(sprite02.Texture, sprite02.TextureRectInPixels);
			var frame06 = new CCSpriteFrame(sprite01.Texture, sprite01.TextureRectInPixels);
			var frame07 = new CCSpriteFrame(sprite00.Texture, sprite00.TextureRectInPixels);

			// List<CCSpriteFrame> としてアニメーションを構築
			var frames = new List<CCSpriteFrame>();
			frames.Add(frame00);
			frames.Add(frame01);
			frames.Add(frame02);
			frames.Add(frame03);
			frames.Add(frame03);
			frames.Add(frame04);
			frames.Add(frame05);
			frames.Add(frame06);
			frames.Add(frame07);

			// ループ回数、所要時間を設定し、アニメーションを適用させたい CCSprite に設定し実行
			var animation = new CCAnimation(frames, 0.15f);
			animation.Loops = 1;
			var animate = new CCAnimate(animation);
			var state = sprite.RunAction(animate);

			// アニメーションの状態を返却
			return state;
		}

	}
}

以上で、自機が敵キャラにぶつかった時、爆発する動きが完成しました。

f:id:hiro128:20160920210710g:plain

今回は、ここまでです。

何かご質問などございましたらコメント頂ければご回答させていただきますのでお気軽にどうぞ!

また、間違い等ございましたら、ご指摘頂ければ幸いです。

Xamarin + Cocos Sharp で iOS, Android 対応のゲームを開発する手順 (13) 当たり判定① 当たり判定を検出できるようにする。

今回は自機が敵キャラに当たった時の当たり判定を実装します。

当たり判定を行なうには CCRect クラスの IntersectsRect メソッドを利用します。

public bool IntersectsRect(CCRect rect);

CCRect が引数に与えられた CCRect と重なっている場合、true を返します。

ですが、この処理を画面上の全ての敵キャラに対し、1/10秒といったようなインターバルで行なってしまうと、大きな負荷がかかってしまいます。
そこで、一般的には自分の近くに居る敵キャラにだけ当たり判定を行ないます。

具体的には、画面をいくつかのエリアに分割し、自機と敵キャラが同じエリアに居た場合のみ、当たり判定を行います。

今回は、下記の様に画面を15のエリアに分割します。

f:id:hiro128:20160914210709p:plain

では、最初に自機と敵キャラが今どのエリアに居るのかを簡単に判別できるような機能を追加します。

CocosSharpGameSample.Core プロジェクトに ScreenArea.cs を追加します。

f:id:hiro128:20160914210722p:plain

各エリアをあらわす enum を作成します。

using System;

namespace CocosSharpGameSample.Core
{
	public enum ScreenArea
	{
		UnKnown,
		Area00,
		Area01,
		Area02,
		Area10,
		Area11,
		Area12,
		Area20,
		Area21,
		Area22,
		Area30,
		Area31,
		Area32,
		Area40,
		Area41,
		Area42
	}
}

次に、CCSprite を拡張して現在のエリアを取得できるプロパティを追加します。

CocosSharpGameSample.Core プロジェクトに ExtendedCCSprite.cs を追加します。

f:id:hiro128:20160914210732p:plain

座標ごとのエリアを返すプロパティ ScreenArea を追加します。

using CocosSharp;

namespace CocosSharpGameSample.Core
{
	public class ExtendedCCSprite : CCSprite
	{
		public ExtendedCCSprite(string fileName, CCRect? texRectInPixels = null)
			: base(fileName, texRectInPixels)
		{ 
		}

		public ScreenArea ScreenArea
		{
			get
			{
				if (this.Position.X >= 0 && this.Position.X < 240 && this.Position.Y >= 1024 && this.Position.Y < 1280)
				{
					return Core.ScreenArea.Area00;
				}
				else if (this.Position.X >= 240 && this.Position.X < 480 && this.Position.Y >= 1024 && this.Position.Y < 1280)
				{
					return Core.ScreenArea.Area01;
				}
				else if (this.Position.X >= 480 && this.Position.X < 720 && this.Position.Y >= 1024 && this.Position.Y < 1280)
				{
					return Core.ScreenArea.Area02;
				}
				else if (this.Position.X >= 0 && this.Position.X < 240 && this.Position.Y >= 768 && this.Position.Y < 1024)
				{
					return Core.ScreenArea.Area10;
				}
				else if (this.Position.X >= 240 && this.Position.X < 480 && this.Position.Y >= 768 && this.Position.Y < 1024)
				{
					return Core.ScreenArea.Area11;
				}
				else if (this.Position.X >= 480 && this.Position.X < 720 && this.Position.Y >= 768 && this.Position.Y < 1024)
				{
					return Core.ScreenArea.Area12;
				}
				else if (this.Position.X >= 0 && this.Position.X < 240 && this.Position.Y >= 512 && this.Position.Y < 768)
				{
					return Core.ScreenArea.Area20;
				}
				else if (this.Position.X >= 240 && this.Position.X < 480 && this.Position.Y >= 512 && this.Position.Y < 768)
				{
					return Core.ScreenArea.Area21;
				}
				else if (this.Position.X >= 480 && this.Position.X < 720 && this.Position.Y >= 512 && this.Position.Y < 768)
				{
					return Core.ScreenArea.Area22;
				}
				else if (this.Position.X >= 0 && this.Position.X < 240 && this.Position.Y >= 256 && this.Position.Y < 512)
				{
					return Core.ScreenArea.Area30;
				}
				else if (this.Position.X >= 240 && this.Position.X < 480 && this.Position.Y >= 256 && this.Position.Y < 512)
				{
					return Core.ScreenArea.Area31;
				}
				else if (this.Position.X >= 480 && this.Position.X < 720 && this.Position.Y >= 256 && this.Position.Y < 512)
				{
					return Core.ScreenArea.Area32;
				}
				else if (this.Position.X >= 0 && this.Position.X < 240 && this.Position.Y >= 0 && this.Position.Y < 256)
				{
					return Core.ScreenArea.Area40;
				}
				else if (this.Position.X >= 240 && this.Position.X < 480 && this.Position.Y >= 0 && this.Position.Y < 256)
				{
					return Core.ScreenArea.Area41;
				}
				else if (this.Position.X >= 480 && this.Position.X < 720 && this.Position.Y >= 0 && this.Position.Y < 256)
				{
					return Core.ScreenArea.Area42;
				}
				return Core.ScreenArea.UnKnown;
			}
		}

	}
}

次に、CocosSharpGameSample.Core プロジェクトの GameLayer.cs に当たり判定の処理を追加していきます。

f:id:hiro128:20160914210757p:plain

まず、自機の参照を ExtendedCCSprite に変更し、当たり判定をキャンセルするかどうかを判定するフラグを追加します。

下記の部分を

public class GameLayer : LayerBase
{

	private CCNode player;

以下のように変更します。

public class GameLayer : LayerBase
{

	private ExtendedCCSprite player;
	private bool enableCollisionDetection = true;

	public bool EnableCollisionDetection
	{
		get { return this.enableCollisionDetection; }
		set { this.enableCollisionDetection = value; }
	}

当たり判定の処理は以下のような流れになります。

まず、ゲーム開始直後、リスタート直後、ステージクリア時、プレーヤーが画面上に居ない場合など当たり判定をキャンセルする場合はすぐに処理を抜けるようします。
また、当たり判定は四角の領域で行なうために、自機の中心部だけで行なわないと非常に厳しい当たり判定になってしまいますので、自機の中心部に実際の当たり判定領域を設定します。
上記で設定した当たり判定領域に対し、自機と敵キャラが同一エリアに居るときのみ、当たり判定を実施します。

当たり判定を行なうコードは以下のようになります。

private void DetectCollisions()
{
	// ゲーム開始直後、リスタート直後、ステージクリア時など、当たり判定をキャンセルする場合
	if(this.EnableCollisionDetection == false)
	{
		return;
	}
	// プレーヤーがやられてしまった後など、プレーヤーが画面上に居ない場合
	if (this.player == null)
	{
		return;
	}
	// 当たり判定領域をプレーヤー中心部分だけにする
	var rectPlayer = player.BoundingBox;
	var rectTrimedPlayer = new CCRect(
									rectPlayer.MidX - (GameSettings.PlayerWidth / 6), 
									rectPlayer.MidY - (GameSettings.PlayerHeight / 3), 
									GameSettings.PlayerWidth / 3, 
									GameSettings.PlayerHeight / 3);
	//// 実際の当たり判定領域の確認用コード(コメントを外すと実際の当たり判定領域を可視化できます。)
	//var acutualRect = new CCSprite();
	//acutualRect.TextureRectInPixels = rectTrimedPlayer;
	//acutualRect.Color = new CCColor3B(0, 255, 0);
	//acutualRect.Position = rectTrimedPlayer.LowerLeft;
	//acutualRect.Tag = 100000000;
	//this.RemoveChildByTag(100000000);
	//this.AddChild(acutualRect);

	// 当たり判定実施
	var enemies = new List<CCNode>();
	enemies.AddRange(this.GetChildrenByTag((int)NodeTag.Enemy));
	if (enemies.Count > 0)
	{
		foreach (var node in enemies)
		{
			// 同一エリア以外は当たり判定スキップ
			if (this.player.ScreenArea != ((ExtendedCCSprite)node).ScreenArea)
			{
				continue;
			}
			var rectEnemy = node.BoundingBox;
			if (rectTrimedPlayer.IntersectsRect(rectEnemy) == true)
			{
				this.EnableCollisionDetection = false;
				Debug.WriteLine("敵キャラに当たった!");
				break;
			}
		}
	}
}

当たり判定は 0.1 秒に1回行なうようスケジューラに登録します。

private void StartScheduling()
{
	this.Schedule(t => this.UpdatePlayer(), 0.02f);
	this.Schedule(t => this.RemoveOffScreenNodes(), 1.0f);
	this.Schedule(t => this.DetectCollisions(), 0.1f); // <-当たり判定スケジューラ登録
}

以上をまとめると、CocosSharpGameSample.Core プロジェクトの Layers/GameLayer.cs は以下の通りになります。

using System.Collections.Generic;
using System.Diagnostics;
using CocosSharp;

namespace CocosSharpGameSample.Core
{
	public class GameLayer : LayerBase
	{

		private ExtendedCCSprite player;
		private bool enableCollisionDetection = true;

		public bool EnableCollisionDetection
		{
			get { return this.enableCollisionDetection; }
			set { this.enableCollisionDetection = value; }
		}

		public GameLayer()
			: base()
		{
			var listener = new CCEventListenerTouchOneByOne();
			listener.OnTouchBegan = this.CCEventListener_TouchBegan;
			this.AddEventListener(listener, this);
		}

		protected bool CCEventListener_TouchBegan(CCTouch touch, CCEvent touchEvent)
		{
			Debug.WriteLine(this.ChildrenCount);
			// タッチした場所に敵キャラ配置
			var touchedLocation = touch.Location;
			// 画面の上部25%なら敵キャラ配置
			if (touchedLocation.Y > GameSettings.ScreenHeight * 3 / 4)
			{
				var addingEnemy = new ExtendedCCSprite("/Resources/Images/Character/Enemy/Enemy001.png", null);
				addingEnemy.Position = touchedLocation;
				addingEnemy.Tag = (int)NodeTag.Enemy;
				this.ApplyGoStraightFromTopDownAction(addingEnemy);
				this.AddChild(addingEnemy);
				return true;
			}
			return false;
		}

		// Y軸のみの移動で画面外まで行くアクションを適用
		public void ApplyGoStraightFromTopDownAction(CCNode enemy)
		{
			var destinationPoint = new CCPoint(enemy.PositionX, -100);
			var action = new CCMoveTo(8.0f, destinationPoint);
			enemy.RunAction(action);
		}

		// 画面外のNodeを除去
		public void RemoveOffScreenNodes()
		{
			foreach (var node in this.Children)
			{
				if (node.Position.Y < 0)
				{
					this.RemoveChild(node);
				}
			}
		}

		protected override void AddedToScene()
		{
			base.AddedToScene();
			// ゲーム画面の背景画像を配置
			var gameBackground = new CCSprite("/Resources/Images/Background/GameBackground.png", null);
			gameBackground.Position = new CCPoint(this.ContentSize.Center.X, this.ContentSize.Center.Y);
			AddChild(gameBackground);

			// 自機を配置
			this.AddPlayer();
			this.StartScheduling();
		}

		private void StartScheduling()
		{
			this.Schedule(t => this.UpdatePlayer(), 0.02f);
			this.Schedule(t => this.RemoveOffScreenNodes(), 1.0f);
			this.Schedule(t => this.DetectCollisions(), 0.1f);
		}

		private void AddPlayer()
		{
			this.player = new ExtendedCCSprite("/Resources/Images/Character/Player/Player.png", null);
			this.player.Position = new CCPoint(this.ContentSize.Center.X, this.ContentSize.Center.Y);
			this.AddChild(this.player);
		}

		private void UpdatePlayer()
		{
			this.player.PositionX = this.player.PositionX;
			this.player.PositionY = this.player.PositionY;

			// 画面からはみ出ないように、位置の値の上限、下限を設定します。
			// 左
			if (this.player.PositionX < 0 + GameSettings.PlayerWidth)
			{
				this.player.PositionX = 0 + GameSettings.PlayerWidth;
			}
			// 右
			if (this.player.PositionX > GameSettings.ScreenWidth - GameSettings.PlayerWidth)
			{
				this.player.PositionX = GameSettings.ScreenWidth - GameSettings.PlayerWidth;
			}
			// 下
			if (this.player.PositionY < 0 + GameSettings.PlayerHeight)
			{
				this.player.PositionY = 0 + GameSettings.PlayerHeight;
			}
			// 上
			if (this.player.PositionY > GameSettings.ScreenHeight - GameSettings.PlayerHeight)
			{
				this.player.PositionY = GameSettings.ScreenHeight - GameSettings.PlayerHeight;
			}
		}

		private void DetectCollisions()
		{
			// ゲーム開始直後、リスタート直後、ステージクリア時など、当たり判定をキャンセルする場合
			if (this.EnableCollisionDetection == false)
			{
				return;
			}
			// プレーヤーがやられてしまった後など、プレーヤーが画面上に居ない場合
			if (this.player == null)
			{
				return;
			}
			// 当たり判定領域をプレーヤー中心部分だけにする
			var rectPlayer = player.BoundingBox;
			var rectTrimedPlayer = new CCRect(
											rectPlayer.MidX - (GameSettings.PlayerWidth / 6),
											rectPlayer.MidY - (GameSettings.PlayerHeight / 3),
											GameSettings.PlayerWidth / 3,
											GameSettings.PlayerHeight / 3);
			//// 実際の当たり判定領域の確認用コード(コメントを外すと実際の当たり判定領域を可視化できます。)
			//var acutualRect = new CCSprite();
			//acutualRect.TextureRectInPixels = rectTrimedPlayer;
			//acutualRect.Color = new CCColor3B(0, 255, 0);
			//acutualRect.Position = rectTrimedPlayer.LowerLeft;
			//acutualRect.Tag = 100000000;
			//this.RemoveChildByTag(100000000);
			//this.AddChild(acutualRect);

			// 当たり判定実施
			var enemies = new List<CCNode>();
			enemies.AddRange(this.GetChildrenByTag((int)NodeTag.Enemy));
			if (enemies.Count > 0)
			{
				foreach (var node in enemies)
				{
					// 同一エリア以外は当たり判定スキップ
					if (this.player.ScreenArea != ((ExtendedCCSprite)node).ScreenArea)
					{
						continue;
					}
					var rectEnemy = node.BoundingBox;
					if (rectTrimedPlayer.IntersectsRect(rectEnemy) == true)
					{
						this.EnableCollisionDetection = false;
						Debug.WriteLine("敵キャラに当たった!");
						break;
					}
				}
			}
		}

	}
}

以上で、デバッグ出力に当たり判定が出力できるようになりました。

f:id:hiro128:20160914210858p:plain

今回は、ここまでです。

何かご質問などございましたらコメント頂ければご回答させていただきますのでお気軽にどうぞ!

また、間違い等ございましたら、ご指摘頂ければ幸いです。

Xamarin + Cocos Sharp で iOS, Android 対応のゲームを開発する手順 (12) 敵キャラを移動させ、画面外に出たら消去する。

今回は画面をタッチして出した敵キャラを移動させ、画面外に出たら消去します。

具体的には、画面タッチで出現させた敵キャラは、横移動はなしで画面の下に移動し、画面外に消えていく動きを作成します。

キャラを移動する場合は、CCMoveTo で移動のアクションを作成し、Node の RunAction メソッドで動作開始となります。
※CCMoveTo はNodeの移動のアクションをカプセル化したオブジェクトです。

まずは、CCMoveTo に移動に必要な時間と移動後の目的地の座標を与えてインスタンスを作成します。

移動後の目的地の座標ですが Cocos Sharp の座標は左下が原点で(0, 0)なので、まっすぐに画面の下に移動したい場合、目的地の座標は、(敵キャラの現在のX座標, -100)のようになります。

次にそのアクションを Node の RunAction で動作開始させます。

// Y軸のみの移動で画面外まで行くアクションを適用
public void ApplyGoStraightFromTopDownAction(CCNode enemy)
{
	var destinationPoint = new CCPoint(enemy.PositionX, -100);
	var action = new CCMoveTo(8.0f, destinationPoint);
	enemy.RunAction(action);
}

配置した敵キャラに上記のアクションを適用します。

protected bool CCEventListener_TouchBegan(CCTouch touch, CCEvent touchEvent)
{
	// タッチした場所に敵キャラ配置
	var touchedLocation = touch.Location;
	// 画面の上部25%なら敵キャラ配置
	if (touchedLocation.Y > GameSettings.ScreenHeight * 3 / 4)
	{
		var addingEnemy = new CCSprite("/Resources/Images/Character/Enemy/Enemy001.png", null);
		addingEnemy.Position = touchedLocation;
		addingEnemy.Tag = (int)NodeTag.Enemy;

		// 敵キャラにアクションを適用
		this.ApplyGoStraightFromTopDownAction(addingEnemy);

		this.AddChild(addingEnemy);
		return true;
	}
	return false;
}

このままだと、画面外に移動してしまった敵キャラがどんどん溜まっていってしまうので、画面外に出た敵キャラは定期的に消去します。

画面外かどうかはY座標の値で判定します。

// 画面外のNodeを除去
public void RemoveOffScreenNodes()
{
	foreach (var node in this.Children)
	{
		if (node.Position.Y < 0)
		{
			this.RemoveChild(node);
		}
	}
}

消去処理を定期的に実行します。

private void StartScheduling()
{
	this.Schedule(t => this.UpdatePlayer(), 0.02f);
	this.Schedule(t => this.RemoveOffScreenNodes(), 1.0f);// 定期的に消去
}

以上をまとめると、CocosSharpGameSample.Core プロジェクトの Layers/GameLayer.cs は以下の通りになります。

using System;
using System.Diagnostics;
using CocosSharp;

namespace CocosSharpGameSample.Core
{
	public class GameLayer : LayerBase
	{

		private CCNode player;

		public GameLayer()
			: base()
		{
			var listener = new CCEventListenerTouchOneByOne();
			listener.OnTouchBegan = this.CCEventListener_TouchBegan;
			this.AddEventListener(listener, this);
		}

		protected bool CCEventListener_TouchBegan(CCTouch touch, CCEvent touchEvent)
		{
			Debug.WriteLine(this.ChildrenCount);
			// タッチした場所に敵キャラ配置
			var touchedLocation = touch.Location;
			// 画面の上部25%なら敵キャラ配置
			if (touchedLocation.Y > GameSettings.ScreenHeight * 3 / 4)
			{
				var addingEnemy = new CCSprite("/Resources/Images/Character/Enemy/Enemy001.png", null);
				addingEnemy.Position = touchedLocation;
				addingEnemy.Tag = (int)NodeTag.Enemy;
				this.ApplyGoStraightFromTopDownAction(addingEnemy);
				this.AddChild(addingEnemy);
				return true;
			}
			return false;
		}

		// Y軸のみの移動で画面外まで行くアクションを適用
		public void ApplyGoStraightFromTopDownAction(CCNode enemy)
		{
			CCPoint destinationPoint = new CCPoint(enemy.PositionX, -100);
			CCMoveTo action = new CCMoveTo(8.0f, destinationPoint);
			enemy.RunAction(action);
		}

		// 画面外のNodeを除去
		public void RemoveOffScreenNodes()
		{
			foreach (var node in this.Children)
			{
				if (node.Position.Y < 0)
				{
					this.RemoveChild(node);
				}
			}
		}

		protected override void AddedToScene()
		{
			base.AddedToScene();
			// ゲーム画面の背景画像を配置
			var gameBackground = new CCSprite("/Resources/Images/Background/GameBackground.png", null);
			gameBackground.Position = new CCPoint(this.ContentSize.Center.X, this.ContentSize.Center.Y);
			AddChild(gameBackground);

			// 自機を配置
			this.AddPlayer();
			this.StartScheduling();
		}

		private void StartScheduling()
		{
			this.Schedule(t => this.UpdatePlayer(), 0.02f);
			this.Schedule(t => this.RemoveOffScreenNodes(), 1.0f);
		}

		private void AddPlayer()
		{
			this.player = new CCSprite("/Resources/Images/Character/Player/Player.png", null);
			this.player.Position = new CCPoint(this.ContentSize.Center.X, this.ContentSize.Center.Y);
			this.AddChild(this.player);
		}

		private void UpdatePlayer()
		{
			this.player.PositionX = this.player.PositionX;
			this.player.PositionY = this.player.PositionY;

			// 画面からはみ出ないように、位置の値の上限、下限を設定します。
			// 左
			if (this.player.PositionX < 0 + GameSettings.PlayerWidth)
			{
				this.player.PositionX = 0 + GameSettings.PlayerWidth;
			}
			// 右
			if (this.player.PositionX > GameSettings.ScreenWidth - GameSettings.PlayerWidth)
			{
				this.player.PositionX = GameSettings.ScreenWidth - GameSettings.PlayerWidth;
			}
			// 下
			if (this.player.PositionY < 0 + GameSettings.PlayerHeight)
			{
				this.player.PositionY = 0 + GameSettings.PlayerHeight;
			}
			// 上
			if (this.player.PositionY > GameSettings.ScreenHeight - GameSettings.PlayerHeight)
			{
				this.player.PositionY = GameSettings.ScreenHeight - GameSettings.PlayerHeight;
			}
		}

	}
}

以上で、タッチして出現した敵キャラが画面の下に消えていくようになりました。

f:id:hiro128:20160827170933g:plain

今回は、ここまでです。

何かご質問などございましたらコメント頂ければご回答させていただきますのでお気軽にどうぞ!