[Oculus] 簡単なゲーム作成とパフォーマンス改善について

前回の記事で、OculusRiftとOculusRift Development Kit2について、簡単なご紹介をしました。
今回は、Oculusを使った簡単なゲームを作りながら、パフォーマンスの改善について触れていきます。

■これから作るゲーム
・自分に対して向かってくる物体(ダメージブロック)を避けるゲーム
・物体に当たると自分がどんどんフィールドから押し出され、最後は床から落下する
・Oculusを装着した状態で、首を傾けることでブロックを避ける

■ベースPJを作成
UnityでPJを新規に作成します。
部品として、「Oculusのカメラ」「床」「ダメージブロック」を配置しています。

配置した部品を元にプログラムで、ダメージブロックを動的に複製して、プレイヤーに向って飛ばす処理を以下のように作りました。

using UnityEngine;
using System.Collections;

using VR = UnityEngine.VR;

public class DamageBlockController : MonoBehaviour
{
	//複製元となるブロック
	public Transform myCube;
	
	//プレイヤー
	public OVRPlayerController playerObject;
	
	//現在画面上に存在するブロックを管理するための配列
	private Object[] myCubeArrray;
	
	//このプログラムを動作させるために期待するFPS値
	static private float FPS = 60;
	
	//Update関数を何秒間隔で実行するのか
	static private float EXEC_INTERVAL = 1.0f / 24.0f;
	
	//ブロックが画面上に出る最大値
	static private int MAX_BLOCK = 100;
	
	//ブロックが出てくる間隔
	static private float APPEAR_BLOCK_SPEED = 1.0f / EXEC_INTERVAL;
	
	//ブロックがプレイヤーに対して、どの程度離れた位置に出現するか
	static private float APPEAR_BLOCK_POSITION = 10.0f;
	
	//ブロックがプレイヤーに対して、どの程度離れたら消えるか
	static private float REMOVE_BLOCK_POSITION = 100.0f;
	
	//ブロックがプレイヤーに向ってくる速度
	static private float BLOCK_SPEED = -1 * 5.0f * EXEC_INTERVAL;
	
	//カメラをどの程度傾けたときに、カメラを移動するか
	static private float MOVE_CAMERA_ROTATE = 15.0;
	
	//以下はプログラム内で利用する演算用変数
	private int num = 0;
	private int count = 0;
	private GameObject prefab;
	public float moveScale;
	
	//元々はUpdateの実行回数で処理していた部分を、経過時間で処理するようにするため、変数を修正
	private float now_interval = 0;
	
	//FPS関連
	int frameCount;
    float prevTime;
	
	// Use this for initialization
	void Start()
	{
		//FPS
		Application.targetFrameRate = FPS;
		frameCount = 0;
		prevTime = 0.0f;
		
		//配列を初期化する
		myCubeArrray = new Object[MAX_BLOCK];
	}
	
	// Update is called once per frame
	void Update()
	{
		count++;
		execBlock();

		//FPS関連
		frameCount++;
		float time = Time.realtimeSinceStartup - prevTime;

		if (time >= 0.5f) {
			Debug.LogFormat("{0}fps", frameCount / time);
			Debug.Log("Profiler.GetTotalAllocatedMemory():::" + Profiler.GetTotalAllocatedMemory());

			frameCount = 0;
			prevTime = Time.realtimeSinceStartup;
		}
	}
	
	//ブロックに関連する処理を実行
	void execBlock() {
		Transform playerTransform = playerObject.transform;
		
		//新しいブロックを生成するタイミングになったかどうか
		if (count % APPEAR_BLOCK_SPEED == 0)
		{
			//配列の空いている位置を探す
			for (num = 0; num < MAX_BLOCK; num++)
			{
				if (myCubeArrray[num] == null)
				{
					//ブロックを複製して画面上に配置する
					myCubeArrray[num] = Instantiate(myCube, transform.position, transform.rotation);

					//ブロックの初期座標を設定する
					Transform work = ((Transform)myCubeArrray[num]);
					work.localPosition = new Vector3(playerTransform.position.x, playerTransform.position.y + 0.5f, playerTransform.position.z + APPEAR_BLOCK_POSITION);
					break;
				}
			}
		}
		
		//現在画面上に存在するブロックについて、Updateのたびに座標を移動する
		for (num = 0; num < MAX_BLOCK; num++)
		{
			if (myCubeArrray[num] == null)
			{
				continue;
			}
			
			//座標の移動
			Transform work = ((Transform)myCubeArrray[num]);
			work.Translate(0, 0, BLOCK_SPEED);
			
			//プレイヤーに対して一定量、離れた位置まで移動完了したらブロックを消す
			if (work.position.z + REMOVE_BLOCK_POSITION < playerTransform.position.z)
			{
				Destroy(work.gameObject);
				Destroy(work);
				myCubeArrray[num] = null;
				System.GC.Collect();
			}
		}
		
		//カメラの傾きに応じて、カメラの座標を移動する
		//回転角取得
		//回転角は、水平時0度で取得できるので、少し傾けると360近辺の値となってしまい、計算するのに都合が悪い
		//このため、180度分数値を操作して、水平時180になるように補正している
		Quaternion headRotFrom = VR.InputTracking.GetLocalRotation(VR.VRNode.CenterEye);
		Vector3 localRot = headRotFrom.eulerAngles;
		float rotZ = (localRot.z + 360 + 180) % 360;
		
		//今自分が向いている方向取得
		Vector3 localFwd = playerTransform.forward;
		
		//z軸の回転に応じて移動する
		//水平時180で、マイナス方向に傾いていた場合(首を右に倒した場合)は、右に移動する
		if (rotZ < 180 - MOVE_CAMERA_ROTATE) {
			playerTransform.Translate(0.05f, 0, 0);
		}
		//水平時180で、プラス方向に傾いていた場合(首を左に倒した場合)は、左に移動する
		else if (rotZ > 180 + MOVE_CAMERA_ROTATE) {
			playerTransform.Translate(-0.05f, 0, 0);
		}
	}
}

このプログラムで行っていることは、以下の通りです。
・一定時間毎に、DamageBlockを複製してゲーム画面に配置します
・時間経過とともに、プレイヤーに向ってブロックを移動します
・ブロックがプレイヤーの横を通過して、さらに進んだら、不要となったブロックを消します
・首が傾いている場合には、その方向に自分を移動します

カメラの傾きは、VR.InputTracking.GetLocalRotation(VR.VRNode.CenterEye);で取得できます。
取得できる値は、カメラが水平であるときに0で、首を右に倒した場合はマイナス値に、左に倒した場合はプラス値になります。
今回計算する際に不便でしたので、180度分補正をかけたうえで、左右にどの程度傾いているのか判断して、カメラを移動しています。

■プログラムを実行する
プログラムを実行すると、定期的に自分に向ってダメージブロックが飛んできます。
ブロックがカメラに衝突すると、カメラが外に押し出されていき、最終的には床から落下します。


■チューニング
無事にダメージブロックが自分に向って飛んでくるようになりましたが、このプログラムにはいくつかの問題があります。
・環境によって、Updateが実行される間隔が異なるので、挙動が実行環境依存である
・ブロックの複製方法が、上述のプログラムではメモリ効率が悪い

そこで、上述のプログラムを改良し、問題点を解消してみます。

■Update周りの改善
Updateでは実行される間隔が環境等に依存します。具体的には、毎フレーム実行される関数ですので、
FPS=20の環境と、60の環境では、Updateが実行されるタイミングが3倍も変わってしまいます。

このため、プログラム側で経過時間を元に、処理するタイミングを決定するように修正します。
この変更を行うために、2つの改善を行います。

・FixedUpdate を利用する(デフォルトでは 0.01秒毎に実行されます)
 ⇒ただし厳密に等間隔で実行されるということではなく、その時々で若干経過時間が異なります

・さらに、FixedUpdate 内で経過時間を見て、自分で処理すべきタイミングかどうかを判断する
 ⇒FixedUpdateの実行タイミングが、その時々で若干変わる部分を、プログラムにて吸収します

Update周りについて、改善したプログラムは以下の通りです。
※無関係となる部分は省略しています

//改善前
void Update()
{
	count++;
	execBlock();
}

//改善後
void FixedUpdate()
{
	//経過時間に応じて処理を行う
	//前回からの経過時間を加算
	now_interval += Time.deltaTime;

	//処理すべきタイミングではない場合には、処理終了
	if (now_interval < EXEC_INTERVAL) {
		return;
	}

	//処理すべきタイミングであれば、処理を実行する
	while (now_interval >= EXEC_INTERVAL) {
		now_interval -= EXEC_INTERVAL;
		count++;
		execBlock();
	}
}

■ブロックの生成方法の改善
Unityにおいて、Instantiate(ダメージブロックを動的に生成するために利用しているもの)は処理が遅いです。
これは、Instantiateを行うと、テクスチャ読み込み処理が毎回実行されるからです。

これを改善するために、[Prefab]という機能を利用します。
この機能は、あらかじめ部品データをキャッシュしておいて、実際に使うタイミングで利用するというものです。
テクスチャの読み込みが毎回走らなくなり、高速化を図れます。

では、さっそく Prefab を追加してみます。
画面左下の、[Create]-[Assets]配下に、[Resources]-[Prefabs]というフォルダを作成します。
※[Resources]フォルダ配下のものは、プログラムから動的に読み込むことができますので、上述のフォルダ名にします

次に、今作ったフォルダの中に、[Hierarchy]の中に入っている、[DamageBlock]をドラッグ&ドロップで追加します

[Hierarchy]には、[DamageBlock]が不要になるので削除します。

そして、Prefab へ追加したDamageBlockをプログラム内であらかじめロードしておいて、必要なタイミングでインスタンスを生成するように修正します。
また、DamageBlockについては頻繁に生成&削除を繰り返しますので、削除のタイミングでガベージコレクションを走らせるようにします。

この改善を行ったプログラムは、以下の通りです。
※無関係となる部分は省略しています

//改善前
//複製元となるブロックはTransform型
public Transform myCube;

//ブロックに関連する処理を実行
void execBlock() {
	//ブロックを複製して画面上に配置する
	myCubeArrray[num] = Instantiate(myCube, transform.position, transform.rotation);

	//ブロックの初期座標を設定する
	Transform work = ((Transform)myCubeArrray[num]);
	work.localPosition = new Vector3(playerTransform.position.x, playerTransform.position.y + 0.5f, playerTransform.position.z + APPEAR_BLOCK_POSITION);
}

//改善後
//複製元となるブロックはGameObject型
//最初に部品をロードする
void Start() {
	prefab = (GameObject)Resources.Load ("Prefabs/DamageBlock");
}

//ブロックに関連する処理を実行
void execBlock() {
	//プレハブからインスタンスを生成
	myCubeArrray[num] = Instantiate (prefab, new Vector3(0,0,0),Quaternion.identity) as GameObject;

	//ブロックの初期座標を設定する
	Transform work = ((GameObject)(myCubeArrray[num])).transform;
	work.localPosition = new Vector3(playerTransform.position.x, playerTransform.position.y + 0.5f, playerTransform.position.z + APPEAR_BLOCK_POSITION);
}

■改善後プログラムを実行
改善前と、改善後について、2つのFPSの状況で動作を確認したところ、
改善前がFPSによってブロックの速度が変動するのに対して、
改善後はFPSによらず、ブロックの速度が一定に改善されています。

改善前 FPS60

改善前 FPS30

改善後 FPS60

改善後 FPS30

ただし、Prefab(キャッシュ)の効果を実感するには、今回のプログラムのように
立方体の物体を、少量配置するだけでは、差がほとんどありませんでした。

Prefabの効果を実感するために、ブロックの代わりに、ユニティちゃんを大量に出してみたところ、
以下のように、Prefabを利用した方が高速に動作しました。

<改善前>
ユニティちゃんを出すときに要した時間
 3000人:3.9秒
 
<改善後>
ユニティちゃんを出すときに要した時間
 3000人:3.3秒

今回利用したマシンでは、0.6秒の改善となりました。
もっとスペックの高いマシンで、たくさんの部品を配置するようなケースでは、更なる改善が見込めます。

■最後に
ここまでご覧いただいてありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*