個人的なメモ

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

Xamarin + Cocos Sharp で iOS, Android 対応のゲームを開発する手順 (9) 加速度センサーの値でキャラクターを動かす。

今回は、加速度センサーの値を利用してキャラクターを動かしてみます。

画面を傾けた方向に自機が動きます。
これでいよいよゲームっぽくなります。

まずは、加速度センサーの値の取得を調整します。

・画面を横向きにしているのでX軸、Y軸がてれこになります。
・ちょうど手に持った時の角度をニュートラルにするために Math.Atan2 で角度にオフセットを入れます。

この処理はネイティブ部分で行なっているので、iOS, Android それぞれのセンサーの値取得部分を修正します。

iOS は下記の通りです。

CocosSharpGameSample.iOS プロジェクトの AppDelegate.cs

// センサー更新時のハンドラの設定
cMMotionManager.StartAccelerometerUpdates(NSOperationQueue.CurrentQueue, (data, error) =>
{
	// センサーの値を書き込む
	lock (this.accelerometerSyncLock)
	{
		// 横向きなのでX,Yを入れ替え
		// X,Yの感度を調整するためセンサーのX,Y軸の取得値を調整
		AccelerometerInfo.Default.AccelerationX = data.Acceleration.Y * 8d;
		// 角度のオフセット
		double angleY = Math.Atan2(data.Acceleration.Z, data.Acceleration.X);
		AccelerometerInfo.Default.AccelerationY = Math.Sin(angleY - 30.6d) * 6d;
		AccelerometerInfo.Default.AccelerationZ = data.Acceleration.Z;
	}
});

Android は下記の通りです。

CocosSharpGameSample.Android プロジェクトの MainActivity.cs

// センサーの値が変更されたときのハンドラ
public void OnSensorChanged(SensorEvent e)
{
	lock (this.accelerometerSyncLock)
	{
		// 横向きなのでX,Yを入れ替え
		// X,Yの感度を調整するためセンサーのX,Y軸の取得値を調整
		AccelerometerInfo.Default.AccelerationX = e.Values[1] * 1.5f;
		// 角度のオフセット
		double angleY = Math.Atan2(e.Values[2], e.Values[0]);
		AccelerometerInfo.Default.AccelerationY = Math.Sin(angleY - 29.1d) * 12f;
		AccelerometerInfo.Default.AccelerationZ = e.Values[2];
	}
}


自機の画像を配置します。

f:id:hiro128:20160415201729p:plain

※自機の画像はハムコロ様の素材を利用させていただきました。

・制作者:ハムコロ様
・配付元:http://homepage2.nifty.com/hamcorossam


iOS は下記の通りです。

/Resources/Images/Background/GameBackground.png

f:id:hiro128:20160415201818p:plain

Android は下記の通りです。

/Assets/Images/Background/GameBackground.png

f:id:hiro128:20160415201924p:plain

ゲーム内の設定値を管理するクラスを作ります。

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

f:id:hiro128:20160415201947p:plain

自機のサイズ, 画面のサイズ, 加速度センサーの取得値の増幅倍率を設定します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CocosSharpGameSample.Core
{
	public class GameSettings
	{
		// 自機のサイズ
		public const float PlayerWidth = 32f;
		public const float PlayerHeight = 32f;

		// 画面のサイズ
		public const float ScreenWidth = 1280f;
		public const float ScreenHeight = 720f;

		// 加速度センサーの取得値の増幅倍率
		public const float AccelerationRatio = 3f;
	}
}

CocosSharpGameSample.Core プロジェクトの SampleGameApplicationDelegate.cs の ApplicationDidFinishLaunching メソッドの中、

f:id:hiro128:20160415202017p:plain

デザイン解像度設定部分を定数(GameSettings.ScreenWidth, GameSettings.ScreenHeight)に置き換えます。

public override void ApplicationDidFinishLaunching(CCApplication application, CCWindow mainWindow)
{
	// デザイン解像度設定
	CCScene.SetDefaultDesignResolution(GameSettings.ScreenWidth, GameSettings.ScreenHeight, CCSceneResolutionPolicy.ShowAll);
	// シーンにタイトル画面の作成、セット
	var scene = new CCScene(mainWindow);
	var titleLayer = new TitleLayer();
	scene.AddChild(titleLayer);
	// シーン開始
	mainWindow.RunWithScene(scene);
}

CocosSharpGameSample.Core プロジェクトの GameLayer.cs のフィールドに、自機用の CCNode player を定義します。

f:id:hiro128:20160415202040p:plain

public class GameLayer : LayerBase
{

	private CCLabel accelerationX;
	private CCLabel accelerationY;
	private CCLabel accelerationZ;

	// 自機用の CCNode
	private CCNode player;

	public GameLayer()
		: base()
	{
	}

GameLayer.cs に画面に自機を配置するメソッドを追加します。
自機や敵キャラなどの画面上のオブジェクトは、CCSprite を使用します。

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);
}

GameLayer.cs に自機の位置を更新するメソッドを追加します。
加速度センサーの取得値に、増幅倍率の値 GameSettings.AccelerationRatio を乗算します。

また、画面の端に来たときにはそれ以上進まないように位置の値の上限、下限を設定します。

private void UpdatePlayer()
{
	float accelerationX = (float)AccelerometerInfo.Default.AccelerationX * GameSettings.AccelerationRatio;
	float accelerationY = -(float)AccelerometerInfo.Default.AccelerationY * GameSettings.AccelerationRatio;

	this.player.PositionX = this.player.PositionX + accelerationX;
	this.player.PositionY = this.player.PositionY + accelerationY;

	// 画面からはみ出ないように、位置の値の上限、下限を設定します。
	// 左
	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;
	}
}

GameLayer.cs のAddedToSceneメソッド内に、自機を配置するメソッド、AddPlayerメソッド を追加します。

protected override void AddedToScene()
{
	base.AddedToScene();

	(省略)

	AddChild(this.accelerationZ);

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

	this.StartScheduling();
}

GameLayer.cs のStartSchedulingメソッド内に、自機の位置を更新するメソッド、UpdatePlayerメソッド を追加します。
0.02秒の更新頻度なのでだいたい 50fps です。

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

これで、ゲーム画面の中で自機が動くようになりました。

f:id:hiro128:20160415202130p:plain

今回はここまでです。

以上までをまとめると、ソースコードは以下の通りとなります。

CocosSharpGameSample.iOS プロジェクトの AppDelegate.cs

using System;
using Foundation;
using UIKit;
using CoreMotion;
using CocosSharp;
using CocosSharpGameSample.Core;

namespace CocosSharpGameSample.iOS
{
	[Register("AppDelegate")]
	public partial class AppDelegate : UIApplicationDelegate
	{
		// 加速度センサー
		private CMMotionManager cMMotionManager;
		// ロックキー
		private object accelerometerSyncLock = new object();

		public override bool FinishedLaunching(UIApplication app, NSDictionary options)
		{
			// 加速度センサーのセットアップ
			this.cMMotionManager = new CMMotionManager();
			// センサーの更新間隔
			cMMotionManager.AccelerometerUpdateInterval = 0.1d;
			// センサー更新時のハンドラの設定
			cMMotionManager.StartAccelerometerUpdates(NSOperationQueue.CurrentQueue, (data, error) =>
			{
				// センサーの値を書き込む
				lock (this.accelerometerSyncLock)
				{
					// 横向きなのでX,Yを入れ替え
					// X,Yの感度を調整するためセンサーのX,Y軸の取得値を調整
					AccelerometerInfo.Default.AccelerationX = data.Acceleration.Y * 8d;
					// 角度のオフセット
					double angleY = Math.Atan2(data.Acceleration.Z, data.Acceleration.X);
					AccelerometerInfo.Default.AccelerationY = Math.Sin(angleY - 30.6d) * 6d;
					AccelerometerInfo.Default.AccelerationZ = data.Acceleration.Z;
				}
			});

			// ゲームをを起動する。
			var application = new CCApplication();
			application.ApplicationDelegate = new SampleGameApplicationDelegate();
			application.StartGame();
			return true;
		}

		// 画面の向きを LandscapeLeft に固定
		[Export("application:supportedInterfaceOrientationsForWindow:")]
		public UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, IntPtr forWindow)
		{
			return UIInterfaceOrientationMask.LandscapeLeft;
		}

	}
}

CocosSharpGameSample.Android プロジェクトの MainActivity.cs

using System;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Hardware;
using Android.OS;
using CocosSharp;
using CocosSharpGameSample.Core;
using Microsoft.Xna.Framework;

namespace CocosSharpGameSample.Android
{
	[Activity(Label = "CocosSharpGameSample.Android"
		, MainLauncher = true
		, Icon = "@drawable/icon"
		, AlwaysRetainTaskState = true
		, LaunchMode = LaunchMode.SingleInstance
		, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden
	)]
	public class MainActivity : AndroidGameActivity, ISensorEventListener
	{
		// センサーマネージャ
		private SensorManager sensorManager;
		// ロックキー
		private object accelerometerSyncLock = new object();

		protected override void OnCreate(Bundle bundle)
		{
			base.OnCreate(bundle);
			// センサーのセットアップ
			this.sensorManager = (SensorManager)GetSystemService(Context.SensorService);
			// ゲームを起動する。
			var application = new CCApplication();
			application.ApplicationDelegate = new SampleGameApplicationDelegate();
			SetContentView(application.AndroidContentView);
			application.StartGame();
		}

		// 画面の向きを Landscape に固定
		public override ScreenOrientation RequestedOrientation
		{
			get
			{
				return ScreenOrientation.Landscape;
			}
			set
			{
				base.RequestedOrientation = ScreenOrientation.Landscape;
			}
		}

		// センサーの精度が変更されたときのハンドラ
		public void OnAccuracyChanged(Sensor sensor, SensorStatus accuracy)
		{
			// 何もしない
		}

		// センサーの値が変更されたときのハンドラ
		public void OnSensorChanged(SensorEvent e)
		{
			lock (this.accelerometerSyncLock)
			{
				// 横向きなのでX,Yを入れ替え
				// X,Yの感度を調整するためセンサーのX,Y軸の取得値を調整
				AccelerometerInfo.Default.AccelerationX = e.Values[1] * 1.5f;
				// 角度のオフセット
				double angleY = Math.Atan2(e.Values[2], e.Values[0]);
				AccelerometerInfo.Default.AccelerationY = Math.Sin(angleY - 29.1d) * 12f;
				AccelerometerInfo.Default.AccelerationZ = e.Values[2];
			}
		}

		protected override void OnResume()
		{
			base.OnResume();
			this.sensorManager.RegisterListener(this, sensorManager.GetDefaultSensor(SensorType.Accelerometer), SensorDelay.Game);
		}

		protected override void OnPause()
		{
			base.OnPause();
			this.sensorManager.UnregisterListener(this);
		}

	}
}

CocosSharpGameSample.Core プロジェクトの GameLayer.cs

using System;
using System.Diagnostics;
using CocosSharp;

namespace CocosSharpGameSample.Core
{
	public class GameLayer : LayerBase
	{

		private CCLabel accelerationX;
		private CCLabel accelerationY;
		private CCLabel accelerationZ;

		private CCNode player;

		public GameLayer()
			: base()
		{
		}

		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.accelerationX = new CCLabel(String.Empty, String.Empty, 40f, CCLabelFormat.SystemFont);
			this.accelerationX.HorizontalAlignment = CCTextAlignment.Left;
			this.accelerationX.VerticalAlignment = CCVerticalTextAlignment.Top;
			this.accelerationX.Color = CCColor3B.White;
			this.accelerationX.PositionX = 250f;
			this.accelerationX.PositionY = 120f;
			AddChild(this.accelerationX);

			this.accelerationY = new CCLabel(String.Empty, String.Empty, 40f, CCLabelFormat.SystemFont);
			this.accelerationY.HorizontalAlignment = CCTextAlignment.Left;
			this.accelerationY.VerticalAlignment = CCVerticalTextAlignment.Top;
			this.accelerationY.Color = CCColor3B.White;
			this.accelerationY.PositionX = 250f;
			this.accelerationY.PositionY = 80f;
			AddChild(this.accelerationY);

			this.accelerationZ = new CCLabel(String.Empty, String.Empty, 40f, CCLabelFormat.SystemFont);
			this.accelerationZ.HorizontalAlignment = CCTextAlignment.Left;
			this.accelerationZ.VerticalAlignment = CCVerticalTextAlignment.Top;
			this.accelerationZ.Color = CCColor3B.White;
			this.accelerationZ.PositionX = 250f;
			this.accelerationZ.PositionY = 40f;
			AddChild(this.accelerationZ);

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

			this.StartScheduling();
		}

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

		private void UpdateAccelerationLabels()
		{
			this.accelerationX.Text = "X : " + (AccelerometerInfo.Default.AccelerationX >= 0 ? "+" : String.Empty) + AccelerometerInfo.Default.AccelerationX.ToString("0.00000000");
			this.accelerationY.Text = "Y : " + (AccelerometerInfo.Default.AccelerationY >= 0 ? "+" : String.Empty) + AccelerometerInfo.Default.AccelerationY.ToString("0.00000000");
			this.accelerationZ.Text = "Z : " + (AccelerometerInfo.Default.AccelerationZ >= 0 ? "+" : String.Empty) + AccelerometerInfo.Default.AccelerationZ.ToString("0.00000000");
		}

		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()
		{
			float accelerationX = (float)AccelerometerInfo.Default.AccelerationX * GameSettings.AccelerationRatio;
			float accelerationY = -(float)AccelerometerInfo.Default.AccelerationY * GameSettings.AccelerationRatio;

			this.player.PositionX = this.player.PositionX + accelerationX;
			this.player.PositionY = this.player.PositionY + accelerationY;

			// 画面からはみ出ないように、位置の値の上限、下限を設定します。
			// 左
			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;
			}
		}

	}
}

以上です。