Oculus Goで動くVR脱出ゲームをUnityで作ろう
こんにちは、木村です。
Oculus Questが発表されましたね。わくてかです。VRのためにWindows PCを本気で買ってしまいそうなところでしたが、Questを待ちましょう。発売されたら取り上げたいと思います。
さて、第一回のVRブロック崩しに引き続きVRアプリ第二弾です。
今回は脱出ゲームを作ってみましょう。できあがると下記動画のようになります。ちょっと普通の脱出ゲームとイメージ違いますが、VRならではということで…
今回は前回使っていなかった以下のようなテクニックを使っています。
1. プロジェクトの作成まで前回と同じ
前回の記事の手順の「7. この時点で動作確認」まで進めてください(11までは無料公開しているので、前回の記事を買っていなくても大丈夫です)。
ただし、「5. Unityプロジェクトの作成」でプロジェクト名を「EscapeRoom」にしましょう。
Unityインストール済みの方は適時スキップしてください。
2. ファイル・フォルダの整理
これからいろいろなコンポーネントを作っていきます。その前にファイルとフォルダを整理します。
SampleSceneも格好悪いので、名前をMainSceneに変更します。Projectウィンドウで[Assets] > [Scene] > [SampleScene]を右クリックして[Rename]で「MainScene」に変更します。
続いてフォルダを作成します。[Assets]の空いているところを右クリックして[Create] > [Folder]から作成できます。
以下のような構成になるように[Main]フォルダ以下を作成してください。
3. Asset Storeで使用するアセットをマイアセットに追加
Unity Asset Storeから今回使用するAssetをインポートします。今回は全部無料のAssetを利用します。
以下のサイトにアクセスし、Unityアカウントでログインしてください。
以下の4つのアセットを開き、[マイアセットに追加する]ボタンを押して、マイアセットに追加します。
脱出する部屋として使います。
この中から鍵のオブジェクトだけ使います。
ドアの開く効果音に使います。
BGMに使います。
もしなくなっていたら、合いそうなアセットを探してみてください。Unity上からも探せますが、Webサイトの方が検索機能が充実しています。
4. アセットをUnityプロジェクトにインポート
Unityに戻って、[Asset Store]ウィンドウを開くと、Asset Storeを表示できます。
上部の[My Assets]ボタンをクリックすると、先程マイアセットに追加したアセットが表示されているので、全部[ダウンロード]して、[インポート]します。
[インポート]ボタンを押すとインポートするファイルの確認ウィンドウが表示されるので、[Import]ボタンを押します(必要なファイルだけ取り込むと軽くなりますが、簡便のため)。
ここまでで以下のようなフォルダ構成になっていると思います。
5. 部屋を配置する
Projectウィンドウに[Assets]>[HomeStuff]に[Demo]というシーンがあります。家具を自分で配置して部屋を作ってもいいのですが、今回はこのDemoシーンをそのまま使わせてもらいましょう。
[Demo]シーンをダブルクリックして開くと、以下のようになります。
Hierarchyウィンドウで[Demo]を右クリックして、[Save Scene As]をクリックします。
EscapeRoom > Assets > Scenes > MainScene.unityを指定して、MainSceneを上書き保存してしまいます。
管理しにくいので、部屋はEmpty Objectの下にまとめましょう。Hierarchyウィンドウで[Demo]を右クリックして、[Game Object] > [Create Empty]をクリックします。
名前を「Room」に変更し、Positionもx:0, y:0, z:0に変更しましょう。
Hierarchyウィンドウで[Plane]をクリックし、[Room]の上の[Wall]をShift+クリックして、PlaneからWallまで選択します。そのまま[Room]にドラッグ&ドロップしてください。
以下のように[Main Camera]と[Directional Light]以外が全部[Room]の下に入ったらOKです。
ちょっと家具が大きすぎるので小さくしましょう。[Room]のScaleを全部0.3に変更してください。
6. Oculus Utilities for Unityの設定を行う
こちらも前の記事の「8. Oculus Utilities for Unityを追加」から「11. 再度動作確認」までの手順を行ってください。
ここまでできると以下のような状態になると思います。
ちょっとここで今後のためにSceneビューの操作を学んで置くといいと思います。以下のマニュアルを読んで、シーンギズモ、ハンドツール、オブジェクトの移動/拡大/回転の使い方だけ軽く理解しておくとよいと思います。マニュアルを見てから、適当に触っているとなんとなく理解できると思います。
別にここで操作をマスターしなくとも、以降の操作で困ったときにまたこのマニュアルに戻ってみるくらいの心持ちで大丈夫だと思います。
7. 歩けるようにする
実はOculus RiftとかだとOVRPlayerControllerという機能で簡単に移動できるのですが、これはキーボードで操作するように作られているので、Oculus Goでは使えません。自力で実装していきます。
Hierarchyウィンドウで[OVRCameraRig]を選択し、Inspectorウィンドウで以下のように設定します。(後々の脱出手順のため、お風呂場をスタート地点にします。)
Inspectorウィンドウ下部の[Add Component]ボタンから[Rigidbody]を選択し追加します。これによって自分に物理特性を与えます。
追加したRigidbodyを以下のように設定します。
Massは質量(kg)を表します。
Freeze Position:YはY軸方向の移動を禁止。重力に従って下に落ちるのをこの設定で止めます。
Freeze Rotationは回転を禁止します。家具などにぶつかったときに勝手に回転しないようにしています。この辺もVR酔い対策でもあります。
(あとでチェックを外して再生してみるとおもしろいと思います。)
Inspectorウィンドウ下部の[Add Component]ボタンから[New script]を選択し、「PlayerController」と入力しスクリプトを作成します。
[Assets]直下に作成されるので、Projectウィンドウで[Main] > [Scripts]にPlayerControllerをドラッグ&ドロップして移動します。
スクリプトを編集するにはInspectorウィンドウの[Player Controller (Script)]の歯車アイコン > [Edit Script]をクリックします。
Visual Studioが起動し、スクリプトを編集できます。
以下のコードをコピーして、PlayerController.csのコードを置き換えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
// GetComponentがコストが大きいのでStart時に取得しておく
private Rigidbody rb;
// 移動時の速度
private float speed = 2.0F;
// PCでの移動時の回転速度
private float rotateSpeed = 2.0F;
// Use this for initialization
void Start()
{
rb = GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update()
{
// 戻るボタンが押されたらリセット
if (Input.GetKeyDown(KeyCode.Return) || OVRInput.Get(OVRInput.Button.Back))
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
void FixedUpdate()
{
// RigidbodyのAddForce以外での移動はFixedUpdateで呼ぶ
Move();
}
/**
* 他のオブジェクトに衝突したときの処理
*/
private void OnTriggerEnter(Collider other)
{
}
private void Move()
{
// Oculus Goでの移動
if (OVRManager.isHmdPresent)
{
moveOculusGo();
}
// 開発用にPCでの移動
else
{
movePc();
}
}
/**
* カメラの向きをXY平面に射影した方向を取得
*/
private Vector3 getCameraFoward()
{
// カメラの向き
Vector3 cameraDir = Camera.main.transform.forward;
// 上下移動しないようXY平面に射影
return Vector3.ProjectOnPlane(cameraDir, Vector3.up);
}
/**
* Oculus Goでの移動
*/
private void moveOculusGo()
{
if (OVRInput.Get(OVRInput.Button.One))
{
// タッチパッドのタッチ位置
Vector2 touchPadPt = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad);
//Debug.Log("touchPadPt: " + touchPadPt.ToString());
// タッチパッドの横の方を押したときは移動しない
if (Mathf.Abs(touchPadPt.y) > 0.2)
{
// カメラの向きに進む
Vector3 direction = getCameraFoward();
if (touchPadPt.y > 0)
{
// 前進
rb.MovePosition(transform.position + direction * speed * Time.fixedDeltaTime);
}
else
{
// 後進
rb.MovePosition(transform.position - direction * speed * Time.fixedDeltaTime);
}
}
}
}
/**
* PCでの移動
*/
private void movePc()
{
// 左右矢印キーが押されたら向きを回転
if (Mathf.Abs(Input.GetAxis("Horizontal")) > 0)
{
transform.Rotate(0, Input.GetAxis("Horizontal") * rotateSpeed, 0);
}
// 上下矢印キーが押されたら前後進
if (Mathf.Abs(Input.GetAxis("Vertical")) > 0)
{
if (Input.GetAxis("Vertical") > 0)
rb.MovePosition(transform.position + transform.forward * speed * Time.fixedDeltaTime);
else
rb.MovePosition(transform.position - transform.forward * speed * Time.fixedDeltaTime);
}
}
}
上部の[Play]ボタンをクリックすると動作を確認することができます。PCでは左右矢印キーで向き変更、上下矢印キーで前後進ができます。
よかったらOculus Goにもデプロイして確認してみてください。タッチパッドの上部と下部を押すと頭が向いている方向に前進と後進します。
どうでしょう?VR酔いしないのではないでしょうか?
いろいろ試した結果、VRでは基本的には以下のポイントを抑えておけばVR酔いせずに移動できるとの結論に達しました。
しかし、今の状態だと壁も家具も通り抜けてしまいますね。
8. 壁と家具をすり抜けないようにする
UnityではCollider(コライダー)とRigidbody(リジッドボディ)という機能があります。これらは基本的には以下のような動きをします。
・Colliderをつけたオブジェクト同士はすり抜けず衝突する
・Rigidbodyをつけると物理特性(重力や慣性)に従う
なので、すり抜けてほしくないプレイヤー、壁、家具、地面にColliderをつけて、つかんで投げたい家具にRigidbodyをつけます。
これだけ考えると自分や壁にはRigidbodyはいらないのですが、プレイヤーは移動にRigidbodyのMovePositionメソッドを使いたいのでつけます。壁は、後々鏡や本棚をつけるのですが、そのときにRigidbodyが必要なのでつけます。(ちなみに家具が動かなくていいならRigidbodyをつけなければいいので簡単です。)
[Room]の下の[Plane]以外の全オブジェクトを選択して、Rigidbodyを追加します。
試しにここで[Play]ボタンを押してみると、すごいことになります。壁は倒れ、家具は沈みます(笑)
この部屋のモデルの場合、壁と地面は元々Colliderがついているので沈みません。家具は重力に従って落ちていってColliderがついてないので地面を通過します。
壁はRigidbodyをつけなければ倒れないのですが、後で壁に鏡や本棚をつけたいのでRigidbodyをつけてます。
壁を倒れないようにするために、Rigidbodyをつけても物理演算の影響を受けないように[Is Kinematic]を有効にします。[Wall]をクリックとShift+クリックで全部選択して、[Rigidbody] > [Is Kinematic]を有効にします。
[Play]すると壁が倒れなくなっています。
プレイヤーが壁をすり抜けてしまうので、プレイヤーにColliderを設定します。なんとなく人っぽいCapsule Colliderにしましょう。[OVRCameraRig]の[Add Component]から[Capsule Collider]を追加し、以下のように設定します。
以下のように黄色いカプセル型の線が衝突判定に使われるエリアになります。実際はこのようにSceneウィンドウで見ながら、カメラが顔のところに来て、足元までカプセルが来るように調整して、上の数値を決めていきます。
家具が落ちないように家具にもColliderをつけましょう。[Room]下の[Plane]と[Wall]以外を全部選択し、[Add Component]から[Box Collider]を追加します。
Box Colliderは直方体の衝突判定エリアをつけるColliderです。Sceneウィンドウで見てみると、以下のようにトイレも風呂も直方体の線で囲まれていると思います。Box Colliderを使うとこれが衝突判定の境になります。
明らかに形が合っていませんが、ここの調整をすると説明が大変なので、今回はいきましょう。
Unityのマニュアルにも以下のように書かれていますし、実際も完全に同じにする必要はないです。完全に同じにすると処理も重くなってしまいます。
ここで再び[Play]ボタンを押して歩いてみましょう。家具に体当りすると家具が現実のように動くと思います。
でも、変なところが見つかりませんか?キッチンが離れていたり、ゴミ箱が倒れかけていたり、寝室の本棚が落ちていたり(笑)
せめてこの辺は直してみましょう。
9. くっついている家具がいきなり吹き飛ばないようにする
まずくっついている家具がスタート直後に離れている問題を直しましょう。この原因は以下のようにColliderが重なっていることです。
Colliderが重なっていると物理演算によって戻そうとする力が働いて、ゲームスタートした途端離れるという挙動をします。
[Sink.2]を選択し、[Box Collider] > [Edit Collider]をクリックします。すると、以下のようにSceneビューの表示が変わります。各面の中央に点があり、これをドラッグ&ドロップで動かして大きさを変更できます。
必要に応じて他のオブジェクトのColliderも変更してください。ゴミ箱は[Sink.1]のColliderをちょっと縮めると良いと思います。[OVRCameraRig]をそのオブジェクトが見える位置に移動して[Play]すると確認がしやすいと思います。
10. 壁掛け家具が落ちないようにする
寝室の本棚が落ちていましたね。これを落ちないようにするには本棚と壁をUnityのJoint(ジョイント)という機能でつなげます。今回は動かないJointなのでFixed Jointを使用します。
[Bookshelf]を選択し、[Add Component]から[Fixed Joint]を追加します。
[Connected Body]に本棚がくっついている壁[Wall]をドラッグ&ドロップします。[Wall]は全部名前が一緒なので、Sceneでどの[Wall]か確認してください。
[Break Force]と[Break Torque]はInfinityだと離れないので、両方100にします(値は動きが気に入らなかったら適当に調整してください)。
もう一つ本棚があります。[Bookshelf (1)]です。こちらもJointを設定しましょう。[Bookshelf (1)]に同じように[Fixed Joint]を追加し、同じように設定してください。
実は風呂場の鏡も洗面台のColliderに乗っているから落ちていないだけで、洗面台どかしたら落ちます。こちらも同じようにFixed Jointを設定しましょう。
再生すると本棚が落ちないことが確認できます。体当りするとちゃんと重力に従って落ちますね。
11. (補足)ダイニングテーブルと椅子が一体化している!?
ここまでの設定では実はすごく変なところがあります。
ダイニングテーブルと椅子が一体化しています。体当りすると以下のようにくっついたまま動きます。
これを直すのはちょっと面倒なので、今回の手順ではやりませんが、簡単に対応方法だけ書いておきます。この章は飛ばしてしまっても全然構いません。
まず椅子([Chair]...)が[Table]の子になっているので、フラットな関係にします。そして、各椅子に[Box Colider]と[RigidBody]を追加するのですが、そうすると開始と同時に以下のように惨事になってしまいます。
椅子のColliderとテーブルのColliderが重なっているので吹っ飛ぶんですね。
これを直すにはColliderが重ならないようにします。例えば、テーブルのColliderは以下のように天板と足4本の5つのBox Colliderをつけるようにします。
これだけではまだ椅子と天板が重なります。椅子も同じように背もたれと座面以下とColliderを分けるようにするとColliderが重ならず吹っ飛ばないようになります。
ちょっと手順をここに書くのは大変なので省略させてもらいますが、興味があったらチャレンジしてみてください。
12. レーザーを出して物をつかんで投げられるようにする
次はコントローラーからレーザーを出し、レーザーが当たった物を持って投げられるようにします。
実は投げるのもOculus RiftなどだとOVRGrabberという機能が使えるのですが、Oculus Goはボタンが足りなくてこれが使えないので、再び自力で実装していきます。
Hierarchyウィンドウの何もないところを右クリックし、[Create Empty]し、「LaserPointer」という名前にします。一応Positionも(0, 0, 0)にしておきます。
[Add Component]から[Line Renderer]を追加します。[Materials] > [Element 0]の入力フォーム右の丸を押して「Default-Line」を選択します。
[Positions]セクションで以下の手順で設定します。
1. Widthを1.000から「0.010」に変更する(下図の①)
2. グラフの右端の縦軸が0.005のところをダブルクリック(下図の②)
3. 曲線になっているグラフを直線にします。②の点を右クリックして、[Left Tangent] > [Linear]を選択。
4. グラフの左端の点も同じように[Right Tangent] > [Linear]を選択。
だんだん細くなって末端では半分の太さになるようにしています。
次に[Color]を設定します。以下の手順で①②とクリックしていき、③に「FFFE92」を入力します。
末端を半透明にします。①をクリックして、②のAlphaを128にします。
Sceneビューで以下のように表示されればOKです。
[OVRCameraRig]に[Add Component] > [New script]で「LaserPointerController」スクリプトを追加します。[Assets] > [Main] > [Scripts]にLaserPointerControllerを移動します。
[Edit Script]でLaserPointerControllerを編集します。
以下のコードでLaserPointerController.csを丸ごと置き換えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LaserPointerController : MonoBehaviour
{
[SerializeField]
private Transform rightHandAnchor;
[SerializeField]
private Transform leftHandAnchor;
[SerializeField]
private Transform centerEyeAnchor;
[SerializeField]
private LineRenderer laserPointerRenderer;
// レーザーの最大距離
private float maxDistance = 2.5f;
// レーザーの発射口
private Transform pointer;
// キャッチしたオブジェクトの1フレーム前の位置
private Vector3 lastCatchObjPosition;
// レーザーが当たったオブジェクト
private Rigidbody hitRb;
// キャッチしたオブジェクト
private Rigidbody catchRb;
private Transform Pointer
{
get
{
// 現在アクティブなコントローラーを取得
var controller = OVRInput.GetActiveController();
if (controller == OVRInput.Controller.RTrackedRemote)
{
return rightHandAnchor;
}
else if (controller == OVRInput.Controller.LTrackedRemote)
{
return leftHandAnchor;
}
// どちらも取れなければ目の間からビームが出る
return centerEyeAnchor;
}
}
void Start()
{
}
void Update()
{
UpdateLaser();
if (catchRb)
{
// キャッチしたオブジェクトをトリガー離したら投げる
if (Input.GetKeyUp(KeyCode.Space) || OVRInput.GetUp(OVRInput.Button.PrimaryIndexTrigger))
{
ThrowObject();
}
}
if (hitRb)
{
// レーザーが当たっているオブジェクトをトリガー押したら取る
if (Input.GetKeyDown(KeyCode.Space) || OVRInput.GetDown(OVRInput.Button.PrimaryIndexTrigger))
{
CatchObject();
}
}
if (catchRb)
{
// 投げたときの速度計算にキャッチしたオブジェクトの位置を保存しておく
lastCatchObjPosition = catchRb.transform.position;
}
}
private void UpdateLaser()
{
pointer = Pointer;
// Rayを作成
Ray pointerRay = GenerateRay();
// レーザーの起点
laserPointerRenderer.SetPosition(0, pointerRay.origin);
RaycastHit hitInfo;
if (Physics.Raycast(pointerRay, out hitInfo, maxDistance))
{
// Rayがヒットしたらそこまで
laserPointerRenderer.SetPosition(1, hitInfo.point);
hitRb = hitInfo.rigidbody;
}
else
{
// Rayがヒットしなかったら向いている方向にMaxDistance伸ばす
laserPointerRenderer.SetPosition(1, pointerRay.origin + pointerRay.direction * maxDistance);
hitRb = null;
}
}
/**
* Oculus GoとPCでRayの場所と方向を変えているので、それに従ってRayを生成
*/
private Ray GenerateRay()
{
if (OVRManager.isHmdPresent)
{
// コントローラー位置からRayを飛ばす
return new Ray(pointer.position, pointer.forward);
}
else
{
// PCではカメラのちょい下から斜め下に飛ばす
return new Ray(pointer.position + new Vector3(0, -1.4f, 0), pointer.forward + new Vector3(0, -0.15f, 0));
}
}
/**
* オブジェクトをキャッチする
*/
private void CatchObject()
{
FixedJoint pointerJoint = pointer.gameObject.GetComponent<FixedJoint>();
if (pointerJoint)
{
// まだキャッチしたままのオブジェクトがあったら離しておく
pointerJoint.connectedBody = null;
Destroy(pointerJoint);
}
catchRb = hitRb;
// FixedJointを使ってPointerとレーザーの当たったオブジェクトをくっつける
pointerJoint = pointer.gameObject.AddComponent<FixedJoint>();
pointerJoint.breakForce = 20000;
pointerJoint.breakTorque = 20000;
pointerJoint.connectedBody = catchRb;
}
/**
* オブジェクトを離す
*/
private void ThrowObject()
{
FixedJoint pointerJoint = pointer.gameObject.GetComponent<FixedJoint>();
if (pointerJoint)
{
// FixedJointを削除し、オブジェクトを切り離す
pointerJoint.connectedBody = null;
Destroy(pointerJoint);
// OVRInput.GetLocalControllerVelocityでやりたかったがzeroを返すので、位置情報から速度ベクトルを計算
Vector3 catchRbVelocity = (catchRb.transform.position - lastCatchObjPosition) / Time.deltaTime;
// 家具の重量を加味するようAddForceで力を加える
catchRb.AddForce(catchRbVelocity, ForceMode.Impulse);
}
catchRb = null;
}
}
このコードは以下の処理をしています。詳しくはコードを見てください。
Inspectorで以下のようにドラッグ&ドロップして設定します。
Fixed Jointを使うにはRigidbodyが必要なので、一時的につける可能性のある[CenterEyeAnchor][LeftHandAnchor][RightHandAnchor]に[Rigidbody]を追加します。追加してから[Is Kinematic]にチェックを入れてください。
これでつかんで投げまくれます。PCだとスペースキーでつかんで離すと投げます。Oculus Goだとトリガーボタンでつかんで離すと投げます。ただこれだけでも意外と楽しいですよ。
ただ、みんな1kgにしているので、紙ゴミかのように吹っ飛んでいきますけど(笑)気になる場合は家具の質量(Mass)を調整してください。
13. 鍵を配置する
今回の脱出ゲームは特に謎解きはなく、部屋の中から鍵を探してドアを開けていくという流れで作っていきます。赤青黄の3つの鍵と3つのドアを置いて、同じ色でないと開かないという仕組みにしていこうと思います。
では、まず鍵を配置しましょう。
Cartoon Props Packアセットに入っている鍵モデルを使います。Prefabを直接修正してしまいましょう。
[Assets] > [Cartoon Props Pack] > [Prefabs] > [Key_1]を選択し、InspectorビューでScaleを大きくします。これは鍵を取りやすいようにするためです。
次に[Add Component]から[Box Collider]と[Rigidbody]を追加します。設定はデフォルトのままで構いません。
そして、Projectビューから下図のように風呂場のトイレの前あたりにドラッグ&ドロップして、鍵を配置します。
ちなみにこの上から見ているビューにするにはシーンギズモのy軸をクリックします。真ん中の四角をクリックすると立体感のない平面的なビューになります。
続いて2個目と3個目の鍵も置きます。同じように[Assets] > [Cartoon Props Pack] > [Prefabs] > [Key_1]をドラッグ&ドロップします。2個目は冷蔵庫の上、3個目はベッドの横にしましょう。(どこでもいいんですが)
わかりやすいように2個目は「Key_2」、3個目は「Key_3」に名前を変更しておきましょう。
次に鍵に色をつけましょう。
[Assets] > [Materials]で右クリックし、[Create] > [Material]をクリックし、Materialを作成します。名前を「Yellow Material」に変更します。
下図のように色を黄色に変更します。「FFFF00」に設定してください。
同様に「Blue Material」「Red Material」を作成してください。
「Blue Material」は色コードを「0000FF」に、「Red Material」は「FF0000」に設定してください。
Key_1を黄色にしましょう。[Key_1]のInspectorビューの[Mesh Renderer] > [Materials] > [Element 0]に[Yellow Material]をドラッグ&ドロップしてください。
同じように[Key_2]に[Blue Material]、[Key_3]に[Red Material]を設定してください。
Sceneビューで見ると鍵の色が変わっているのが確認できると思います。
14. ドアを配置する
ドアはいい無料アセットが見つからなかったので、Cubeなどを組み合わせて作ってしまいましょう。(本当は3Dモデリングツール作るところでしょうが、手抜きです。)鍵を当てたら開くようにアニメーションも設定していきます。
まずドアを作りましょう。風呂場とキッチンの間の壁を複製してドアにします。Sceneビューで壁を選択し、Hierarchyビューで選択された[Wall]を右クリック>[Duplicate]します。この上からの平面表示ビューにするには右上のシーンギズモを使いますよ。
[Wall (1)]ができるので、「Door」に名前を変更します。
その他PositionやScaleを下図のように変更します。Sceneビューで下図のような配置になるようにマウスで調整しても構いません。なお、この時点では壁と重なっています。
壁が重なっているのを直します。Sceneビューで風呂場とキッチンの間の壁を選択し、左上の[Rect Tool]をクリックすると、壁の四隅が青丸になります。下図で「上にドラッグ」と書いてある辺を上にドラッグ&ドロップし、壁を小さくして、ドアと重ならないようにします。
同じように風呂場と寝室の間の壁もX軸方向に伸ばし、下図のような形にします。
ドアを黄色くします。[Door]を選択し、Inspectorビューで[Mesh Renderer] > [Materials] > [Element 0]に[Assets] > [Main] > [Materials] > [Yellow Material]をドラッグ&ドロップします。
ドアを回転できるようにします。そのためには回転軸としてEmpty Objectを作成し、ドアをその子オブジェクトにします。
Hierarchyビューで[Room]を右クリックし、[Create Empty]をクリックします。作成したら、以下のように変更します。上部にある[Center]となっているボタンをクリックすると下図のように[Pivot]になります。これを忘れないようにしてください。
Sceneビューではこの位置にいます。ドアの回転軸の位置ですね。
Hierarchyビューで[Door]を[DoorPivot]にドラッグ&ドロップします。[DoorPivot]の位置が蝶番の位置からずれてしまっていたら、一度元に戻して、[Pivot]ボタンになっているか確認してください。
ドアノブをつけます。Hierarchyビューで[DoorPivot]を選択し、右クリックし、[3D Object] > [Cube]をクリックします。作成したら、以下のように設定します。Cast Shadows: Offは影を無効にする設定です。(今回は後で影を全部消してしまいますが、今時点で見た目が変になるので無効にしておきます。)
Sceneビューでは以下のように表示されます。ドアの両側からぱっと見ドアノブっぽく見えるよう、直方体をドアの両側にはみ出るように突き刺しています。
鍵穴を作成します。Hierarchyビューで[DoorPivot]を選択し、右クリックし、[3D Object] > [Cylinder]をクリックします。作成したら、以下のように設定します。
ドアができました。見た目は以下のようになります。
15. ドアが開くアニメーションを設定する
ドアにアニメーションをつけます。今回は開くだけなのでスクリプトで回転させてもいいのですが、せっかくなのでAnimatorという機能を使ってみましょう。
メニューの[WIndow] > [Animation] > [Animation]をクリックし、Animationビューを開きます。(たぶん、初期インストール時はAnimationビューが表示されていなかったと思います。)
Hierarchyビューで[DoorPivot]を選択し、Animationビューで[Create]ボタンをクリックします。保存先を求められたら、[Assets] > [Main] > [Animator]の下の「DoorOpen.anim」と指定してください。
[DoorPivot]には自動的に[Animator]コンポーネントが追加されているはずです。
アニメーションを録画します。下図を見ながら設定をしてください。
1. ①の30秒の箇所をクリックします。白い線が移動すると思います。
2. ②の録画ボタンをクリックします。
3. ③の[DoorPivot]のRotation Yを「90」に変更します。Sceneビューでドアが開くことが確認できます。
4. 最後にもう一度②の録画ボタンを押します。
録画ボタンの右にある再生ボタン(▶)をクリックすると、アニメーションを確認できます。ループ再生されますが、問題ありません。[Preview]ボタンで停止することができます。
Projectビューで[Assets] > [Main] > [Animator] > [DoorOpen]をクリックします。Inspectorビューで[Loop Time]をオフにします。アニメーションがループしないようにする設定です。
Projectビューで[Assets] > [Main] > [Animator] > [DoorPivot]をクリックします。[Animator]ビューが開きます。以下のような状態になっているはずです。
これはアニメーションの状態遷移を設定することができます。今は開始と同時にいきなりDoorOpenステートになるようになっています。いきなりドアが開いては困るので、「DoorClose」というステートを作りましょう。
何もないところを右クリックし、[Create State] > [Empty]をクリックします。
[New State]というステートが作成されますので、それをクリックして、Inspectorビューで「DoorClose」に名前を変更します。
開始時は閉じた状態にしたいので、[DoorClose]ステートをデフォルトにします。[DoorClose]ステートを右クリックし、[Set as Layer Default State]をクリックします。
[Entry]ステートからの矢印が[DoorClose]ステートを指すように変更されたと思います。
[DoorClose]から[DoorOpen]に状態遷移できるようにします。[DoorClose]を右クリックし、[Make Transition]をクリックします。矢印がマウスについてくるようになるので、[DoorOpen]をクリックし、[DoorOpen]につなぎます。
遷移するための条件を設定します。Animatorビューの[Parameters]で下図のように[+] > [Trigger]をクリックします。
「New Trigger」という名前で作成されるので、「open」に変更します。
[open]パラメータがセットされた場合に[DoorClose]から[DoorOpen]に遷移するように設定します。下図の①の矢印をクリックし、Inspectorビューで②の[+]をクリックします。③で[open]が選択されているはずですが、選択されていなかったら[open]を選択してください。
これでアニメーションの設定は完了です。スクリプトから[open]パラメータをセットするとドアが開くようになりました。
[Play]ボタンでゲームを動かし、Animatorビューで[open]パラメータの右のラジオボタンを押すと[Game]ビューでドアが開くのが確認できます。[DoorClose]から[DoorOpen]に遷移したことも確認できますね。
16. 鍵でドアが開くようにする
鍵をドアにぶつけるとドアが開くようにしましょう。鍵穴だとイライラしそうなのでドア全体で開くようにしちゃいます。
Hierarchyビューで[Door](DoorPivotではない)を選択し、[Add Component] > [New Script]をクリックし、「DoorController」という名前でスクリプトを作成します。
[Assets]直下に作成されるので、Projectビューで[Assets] > [Main] > [Scripts]にドラッグ&ドロップで移動します。移動したら[DoorController]をダブルクリックしVisual Studioで[DoorController.cs]を開きます。
以下のコードで[DoorController.cs]を丸ごと書き換えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DoorController : MonoBehaviour
{
// 鍵オブジェクト
[SerializeField]
private GameObject key;
// ドアが開いた効果音
private AudioSource openSound;
// Use this for initialization
void Start()
{
openSound = GetComponent<AudioSource>();
}
// Update is called once per frame
void Update()
{
}
private void OnCollisionEnter(Collision collision)
{
// 当たったオブジェクトが鍵と一致したらドアを開ける
if (collision.gameObject == key)
{
// 効果音を鳴らす
openSound.Play();
// ドアを開くアニメーションを実行
Animator animator = transform.parent.GetComponent<Animator>();
animator.SetTrigger("open");
// 鍵は削除する
Destroy(collision.gameObject);
}
}
}
ついでにドアが空いたときの効果音も鳴らしています。鍵は残しておくと混乱しそうなのでドアを開けたら削除してしまっています。
Audio Sourceを追加します。[Door]を選択し、[Add Component] > [Audio Source]をクリックします。効果音はCasual Game Soundsアセットに入っている[DM-CGS-45]を使用します。
下図の①の丸をクリックするとAudio Clipの選択ウィンドウが表示されるので、「45」と入力し、表示された[DM-CGS-45]をクリックします。
いきなり効果音がなると困るので、[Play On Awake]もOFFにしておいてください。
ドアを開けるための鍵を設定します。黄色いドアは黄色い鍵で開けるので、[Key_1]を[Door Controller]の[Key]パラメータにドラッグ&ドロップします。
これで鍵をつかんでドアに当てるとドアが開くようになりました。試してみてください。
17. ドアを複製する
あと青と赤の2つのドアを作る必要があります。作っていきましょう。複製はPrefabを使ってもいいのですが、今回は普通にDuplicateしてしまいましょう。
Hierarchyビューで[DoorPivot]を右クリックし、[Duplicate]をクリックします。名前を「DoorPivot_2」に変更し、[Position]を変更します。寝室のドアとして配置します。
[DoorPivot_2] > [Door]を選択し、以下のように[Materials]と[Key]を変更します。青いドアにして青い鍵で開くようにしています。
壁がドアにめり込んでいるので、壁を縮めて重ならないようにしましょう。重なっている壁を選択して、[Rect Tool]に切り替えて、壁のドアの重なっている面をドラッグ&ドロップで縮めます。
上からの平面図にするにはシーンギズモのY軸と真ん中の四角をクリックです。
次に赤いドアをリビングから外に出るドアとして設置します。同じく[DoorPivot]を右クリック > [Duplicate]して、名前を[DoorPivot_3]に、Positionを以下のように変更します。
[DoorPivot_3] > [Door]を選択し、[Materials]と[Key]を変更します。
壁が重なっているので、先ほどと同じように壁を縮めましょう。
はい。これで、鍵3つを使って順々にドアを開けて、外に出られるようになりました!
18. タイトル画面とゲームスタート
Unity公式アセットのTextMesh Proを使って少しいい感じにタイトル画面を作ってみましょう。
Hierarchyビューで[OVRCameraRig]を右クリックし、[Create Empty]で空オブジェクトを作成し、「Title」という名称に変更し、以下のように[Transform]も変更します。
作成した[Title]を右クリックし、[3D Object] > [TextMeshPro - Text]をクリックします。
以下のウィンドウが表示されるので、[Import TMP Essentials]をクリックします。(もし、うまくいかなかったらUnity Asset StoreからTextMesh Proを検索してインポートしてください。)
[Assets] > [TextMesh Pro]フォルダが作成されます。
この時点では日本語が使えないので、日本語フォントアセットを作成します。
まず、日本語フォントを用意します。今回は以下のフリーフォントを使用します。以下のサイトからフォントをダウンロードしてください。
解凍したら、[GN-KillGothic-U-KanaNB.ttf]ファイルをProjectビューの[Assets] > [Main] > [Fonts]にドラッグ&ドロップします。
メニューから[Window] > [TextMeshPro] > [Font Asset Creator]をクリックします。
Font Asset Creatorでは以下の手順でフォントアセットを作成します。
1. [Font Source]に先ほど追加した[GN-KillGothic-U-KanaNB]をドラッグ&ドロップします。
2. [Character Set]を[Custom Characters]に設定。
3. [Custom Character List]に今回使う文字列を入力。
4. [Generate Font Atlas]をクリック。
5. しばらく待つと右にプレビューが表示されるので、確認して[Save]ボタンをクリック。ファイル名はそのまま[Main] > [Fonts]に保存する。
これで日本語を使う準備ができました。
[Title] > [TextMeshPro]オブジェクトが既にできているので、それを選択し、タイトル文字列を設定していきます。
まず、名前とWidth, Heightを設定します。
次に[Text Mesh Pro (Script)]セクションの設定です。
[TEXT INPUT BOX]に以下のように入力。
先ほど作成したフォントアセット[GN-KillGothic-U-KanaNB SDF]を[Font Asset]にドロップ。
Font Size: 5、Alignmentを中央揃え、中段揃えに設定します。
[GN-KillGothic-U-KanaNB SDF Material]セクションの設定です。[Face] > [Color]を「#FFFF00」に、[Outline] > [Thickness]を「0.3」に設定します。
Gameビューで見ると以下のようになります。
次にこの下に一昔前のゲームっぽく「TRIGGER TO START」の文字を追加します。
こちらの文字列用にもう一つMaterialを作成します。
Projectビューで[Assets] > [Main] > [Fonts] > [GN-KillGothic-U-KanaNB SDF]の前の[▶]をクリックして開き、中の[GN-KillGothic-U-KanaNB SDF Material]をクリックします。
Inspectorビューで下図の②の箇所を右クリックし、[Create Material Preset]をクリックします。
[GN-KillGothic-U-KanaNB SDF]という同じ名前のMaterialが作成されるので、[GN-KillGothic-U-KanaNB SDF White]と名前を変えておきます。
[TitleText]を右クリックし、[Duplicate]でコピーします。
まず、名前と[Rect Transform]を変更します。
さらに[Text Mesh Pro (Script)]セクションの設定をします。
下図のピンク枠部分を設定します。
先ほどと異なり、[Material Preset]のところを[GN-KillGothic-U-KanaNB SDF White]を選択してから、その下を設定するようにしてください。
これでタイトル画面は完成です。
続いて説明通りトリガーボタンでゲームを開始できるようにします。[OVRCameraRig]を選択し、[Player Controller (Script)]の右の歯車から[Edit Script]をクリックし、PlayerController.csをVisual Studioで編集し、以下のコードに差し替えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
// タイトル表示
[SerializeField]
private GameObject title;
// ゲームスタートしているかのフラグ
private bool isStart = false;
// GetComponentがコストが大きいのでStart時に取得しておく
private Rigidbody rb;
// 移動時の速度
private float speed = 2.0F;
// PCでの移動時の回転速度
private float rotateSpeed = 2.0F;
// Use this for initialization
void Start()
{
rb = GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update()
{
if (!isStart)
{
// トリガーでスタート
if (Input.GetKey(KeyCode.Space) || OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger))
{
GameStart();
}
}
else
{
// 戻るボタンが押されたらリセット
if (Input.GetKeyDown(KeyCode.Return) || OVRInput.Get(OVRInput.Button.Back))
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
}
void FixedUpdate()
{
// RigidbodyのAddForce以外での移動はFixedUpdateで呼ぶ
Move();
}
private void GameStart()
{
// タイトル画面を非表示
title.SetActive(false);
// レーザーポインターを有効化
GetComponent<LaserPointerController>().enabled = true;
isStart = true;
}
private void Move()
{
// スタートしてなかったら動けない
if (!isStart)
return;
// Oculus Goでの移動
if (OVRManager.isHmdPresent)
{
moveOculusGo();
}
// 開発用にPCでの移動
else
{
movePc();
}
}
/**
* カメラの向きをXY平面に射影した方向を取得
*/
private Vector3 getCameraFoward()
{
// カメラの向き
Vector3 cameraDir = Camera.main.transform.forward;
// 上下移動しないようXY平面に射影
return Vector3.ProjectOnPlane(cameraDir, Vector3.up);
}
/**
* Oculus Goでの移動
*/
private void moveOculusGo()
{
if (OVRInput.Get(OVRInput.Button.One))
{
// タッチパッドのタッチ位置
Vector2 touchPadPt = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad);
//Debug.Log("touchPadPt: " + touchPadPt.ToString());
// タッチパッドの横の方を押したときは移動しない
if (Mathf.Abs(touchPadPt.y) > 0.2)
{
// カメラの向きに進む
Vector3 direction = getCameraFoward();
if (touchPadPt.y > 0)
{
// 前進
rb.MovePosition(transform.position + direction * speed * Time.fixedDeltaTime);
}
else
{
// 後進
rb.MovePosition(transform.position - direction * speed * Time.fixedDeltaTime);
}
}
}
}
/**
* PCでの移動
*/
private void movePc()
{
// 左右矢印キーが押されたら向きを回転
if (Mathf.Abs(Input.GetAxis("Horizontal")) > 0)
{
transform.Rotate(0, Input.GetAxis("Horizontal") * rotateSpeed, 0);
}
// 上下矢印キーが押されたら前後進
if (Mathf.Abs(Input.GetAxis("Vertical")) > 0)
{
if (Input.GetAxis("Vertical") > 0)
rb.MovePosition(transform.position + transform.forward * speed * Time.fixedDeltaTime);
else
rb.MovePosition(transform.position - transform.forward * speed * Time.fixedDeltaTime);
}
}
}
元のコードにゲームスタート処理を追加しています。ゲームスタート時にタイトルを非表示にし、レーザーポインターを有効にしています。isStartフラグも立てて、立っていないときは動けないようにしています。
そして、以下の2つの設定をします。
・[OVRCameraRig]の[Player Controller (Script)]で[Title]パラメータに先ほど作った[TItle]オブジェクトをドロップ
・[Laser Pointer Controller (Script)]のチェックを外します。
これでトリガーボタン(PCではスペース)でゲームを開始することができるようになりました。
19. ゴール判定とクリア画面
最後の赤いドアを開けたらクリア表示と効果音を鳴らすようにしましょう。
赤いドアのすぐ外に透明のオブジェクトを置いて、それに当たったらクリアしたことにします。
Hierarchyビューで[OVRCameraRig] > [Title]を右クリックし、[Duplicate]でコピーします。作成された[Title (1)]を[Main Scene]にドラッグ&ドロップします。
[Title (1)]をInspectorビューで[Clear]に名称変更し、以下のように設定します。スクリプトで眼前に表示するようにするので、実のところ場所はどこでも構いません。
オブジェクト名の左のチェックを外すのを忘れないようにしてください。
[Clear] > [TriggerToStart]を削除し、[TitleText]を[ClearText]に変更します。[ClearText]はInspectorビューで[TEXT INPUT BOX]を以下のように変更します。
タイトルと見た目も位置も同じにしたのですぐ終わりましたね。
[OVRCameraRig]にクリア時効果音を追加します。[OVRCameraRig]を選択し、Inspectorビュー下部の[Add Component]で[Audio Source]を追加します。
[AudioClip]にCasual Game Soundsアセットに含まれている[DM-CGS-18]を設定します。開始時に鳴らないよう[Play On Awake]も外してください。
ゴール判定用の透明オブジェクトを配置します。
[Main Scene]を右クリックし、[Game Object] > [Create Empty]からからオブジェクトを追加し、[Goal]オブジェクトに名称変更します。
[Position]も赤いドアのすぐ外になるよう以下のように設定します。当たり判定が必要なので[Box Collider]も追加して、以下のように設定してください。
Projectビューで[Main] > [Scripts] > [PlayerContorller]をダブルクリックしVisual Studioで編集します。以下のコードに置き換えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
// タイトル表示
[SerializeField]
private GameObject title;
// クリア表示
[SerializeField]
private GameObject clear;
// ゲームスタートしているかのフラグ
private bool isStart = false;
// GetComponentがコストが大きいのでStart時に取得しておく
private AudioSource clearAudio;
private Rigidbody rb;
// 移動時の速度
private float speed = 2.0F;
// PCでの移動時の回転速度
private float rotateSpeed = 2.0F;
// Use this for initialization
void Start()
{
rb = GetComponent<Rigidbody>();
clearAudio = GetComponent<AudioSource>();
}
// Update is called once per frame
void Update()
{
if (!isStart)
{
// トリガーでスタート
if (Input.GetKey(KeyCode.Space) || OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger))
{
GameStart();
}
}
else
{
// 戻るボタンが押されたらリセット
if (Input.GetKeyDown(KeyCode.Return) || OVRInput.Get(OVRInput.Button.Back))
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
}
void FixedUpdate()
{
// RigidbodyのAddForce以外での移動はFixedUpdateで呼ぶ
Move();
}
/**
* 他のオブジェクトに衝突したときの処理
*/
private void OnTriggerEnter(Collider other)
{
// Goalにぶつかったら
if (other.gameObject.name == "Goal")
{
GameClear(other.gameObject);
}
}
private void GameStart()
{
// タイトル画面を非表示
title.SetActive(false);
// レーザーポインターを有効化
GetComponent<LaserPointerController>().enabled = true;
isStart = true;
}
private void GameClear(GameObject goal)
{
// 眼前にクリア表示
Vector3 forward = getCameraFoward();
clear.transform.position = transform.position + forward * 2.0f;
// クリア表示を見ている方向に回転させる
clear.transform.rotation = Quaternion.LookRotation(forward);
clear.SetActive(true);
// クリア時の効果音を再生
clearAudio.Play();
// ゴールを残しておくと通るたびに何度も上記が動いてしまうので削除する
Destroy(goal);
}
private void Move()
{
// スタートしてなかったら動けない
if (!isStart)
return;
// Oculus Goでの移動
if (OVRManager.isHmdPresent)
{
moveOculusGo();
}
// 開発用にPCでの移動
else
{
movePc();
}
}
/**
* カメラの向きをXY平面に射影した方向を取得
*/
private Vector3 getCameraFoward()
{
// カメラの向き
Vector3 cameraDir = Camera.main.transform.forward;
// 上下移動しないようXY平面に射影
return Vector3.ProjectOnPlane(cameraDir, Vector3.up);
}
/**
* Oculus Goでの移動
*/
private void moveOculusGo()
{
if (OVRInput.Get(OVRInput.Button.One))
{
// タッチパッドのタッチ位置
Vector2 touchPadPt = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad);
//Debug.Log("touchPadPt: " + touchPadPt.ToString());
// タッチパッドの横の方を押したときは移動しない
if (Mathf.Abs(touchPadPt.y) > 0.2)
{
// カメラの向きに進む
Vector3 direction = getCameraFoward();
if (touchPadPt.y > 0)
{
// 前進
rb.MovePosition(transform.position + direction * speed * Time.fixedDeltaTime);
}
else
{
// 後進
rb.MovePosition(transform.position - direction * speed * Time.fixedDeltaTime);
}
}
}
}
/**
* PCでの移動
*/
private void movePc()
{
// 左右矢印キーが押されたら向きを回転
if (Mathf.Abs(Input.GetAxis("Horizontal")) > 0)
{
transform.Rotate(0, Input.GetAxis("Horizontal") * rotateSpeed, 0);
}
// 上下矢印キーが押されたら前後進
if (Mathf.Abs(Input.GetAxis("Vertical")) > 0)
{
if (Input.GetAxis("Vertical") > 0)
rb.MovePosition(transform.position + transform.forward * speed * Time.fixedDeltaTime);
else
rb.MovePosition(transform.position - transform.forward * speed * Time.fixedDeltaTime);
}
}
}
Goalオブジェクトにぶつかったときのクリア処理を追加しているだけです。
20. BGMを追加
最後です!BGMを追加しましょう。今回は鳴らしっぱなしでいいので、BGMオブジェクトを作ってそこに開始時からループで鳴るようAudio Sourceを追加しましょう。
[Main Scene]の下に[Create Empty]で空オブジェクトを作成し、「BGM」に名称変更し、[Add Component]から[Audio Source]を追加し、以下のように設定します。
Complete Music Collection FREE Editionアセットに含まれる[At the Castle Gate (8bit, RPG)]をBGMに使用しています。
影を外すのを忘れいていました。壁を透過して影ができたりしていて変なので影を外しちゃいましょう。(ちゃんと影がつくようにしてもいいですよ。)[Directional Light]を選択し、[Light] > [Shadow Type]を「No Shadow」に設定します。
21. 完成
これで完成です!!長かった。お疲れ様でした。
[File] > [Build & Run]を実行し、Oculus Go実機で遊んでみましょう。
22. ソースコード
Unityプロジェクト丸ごとのソースコードも用意しました。以下の手順で動かすことができます。
1. 上記リンクからソースコードをダウンロードして、解凍したフォルダをUnityで開きます。
2. [Assets] > [Scenes] > [MainScene]を開く。
3. 前回の記事の「6. Unityの設定を変更する」をする。
4. 前回の記事の「7. この時点で動作確認」の手順でOculus Go実機にデプロイ。
途中でうまく動かなくなったときなどにこちらのコードを参考にしてください。
23. 終わりに
前回よりもめっちゃ長くなってしまいました。ここまでがんばった方、本当にお疲れ様でした。
今回も遊んでいるといろいろと不具合があるかと思います。鍵が壁や床を通過するとか(笑)ぜひそういうところを改善していってもらえたらと思います。
あとはこのゲームはおもしろさはあまり考えていないので、あんまりおもしろくはないですね(笑)投げるところを使って、ただ投げて飛距離を競うゲームとかも意外と単純で楽しいかもしれないです。ここで使ったテクニックを使ってぜひもっとおもしろいアプリを作ってみてください!
この記事が気に入ったらサポートをしてみませんか?