前回の記事で、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秒の改善となりました。
もっとスペックの高いマシンで、たくさんの部品を配置するようなケースでは、更なる改善が見込めます。
■最後に
ここまでご覧いただいてありがとうございました。