読者です 読者をやめる 読者になる 読者になる

個人的なメモ 〜Cocos Sharp 情報を中心に‥

Tomohiro Suzuki @hiro128_777 のブログです。Cocos Sharp の事を中心に書いています。 Microsoft MVP for Visual Studio and Development Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

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

今回は、ここまでです。

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

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