# SteamVR でゲームを作る ## はじめに この記事は[CPS Lab Advent Calendar2020](https://adventar.org/calendars/5298)の14日目の記事です。 - [12日目](https://note.com/hasu_min/n/n8a9dab12882c) - [13日目] - [15日目] - [17日目](https://narusei.hatenablog.com/entry/2020/12/26/170448?_ga=2.106032451.1967202264.1608957199-125158461.1586418052) ここ 1 年、SteamVR と戦ってきたので、備忘録的に書こうと思っています。 去年は技術記事を用意できなかったので、今年こそは・・・と。 なにかのお役に立てると嬉しいです。 中身としては個人の備忘録としてみてください。 ## SteamVR でゲームを作る 簡単にですがこんなゲームを作っていこうと思います。(長くなってしまって後悔してます) <iframe id="ytplayer" type="text/html" width="640" height="360" src="https://www.youtube.com/embed/i3rBK1aDSLU?autoplay=1&origin=http://example.com" frameborder="0"></iframe> ### SteamVRとは Steam の開発元の Valve が提供する VR の実行環境です。Steam からインストールできます。 現在はこれが主流となっていて、多くの VR コンテンツが対応しています。 ### 環境 [SteamVR](https://store.steampowered.com/app/250820/SteamVR/?l=japanese) がインストールされている前提で進めます。 必要なもの - Unity 2019.4系 - 2020 だと VR の環境が異なるため、今回は 2019.4.12f を使用します。 - (自分がまだ導入できていないので・・・) - SteamVR 対応の HMD - 今回は Oculus Quest で Oculus Link を用いた環境で行います。 #### Oculus Link についての補足 Oculus Quest に 対応するケーブルをつなぐことで、PCVR と同じ環境を再現するシステムです。これによって、Oculus Rift のコンテンツや、SteamVR で遊ぶこともできます。 対応するケーブルは PCにつなぐ側(USB-A)- Quest につなぐ側(USB-C)で、長さは 3m 以上が推奨されています。 ## プロジェクトの用意 - VR 機器をつないで、SteamVR を起動しておく。 - UnityHub に[ここ](https://github.com/nemlyc/StruckOutGame)からデータをダウンロードし、リストに追加を行う。 - プロジェクトを開く。 - ctrl + 9 を押して、AssetStore から [SteamVRPlugin](https://assetstore.unity.com/packages/tools/integration/steamvr-plugin-32647) をダウンロード。 ![](https://i.imgur.com/EtBEiis.png) - インポートを選択し、そのままインポートします。 ![](https://i.imgur.com/xt8xzae.png) - 表示される設定を適用します。 ![](https://i.imgur.com/lFthXOg.png) (僕の環境で出なくなってしまったので補足) - 1・3・4 番目を変更します(2 番目は有償ライセンス持ちの方のみ変更できます)。 **Build Target** - File - BuildSettings を開く。 - Platform を 「PC, Mac & Linuc Standalone」に変更し、Archtexture を「x86_64」に変更する。 ![](https://i.imgur.com/n6G8tRf.png) **ResizableWindow、Color Space** - そのまま左下の PlayerSettings を開く - 「Resolution and Presentation」から、Resizable Window を探しチェックを入れる。 - また、「Other Settings」から、Color Space を Linear に変更する。 ![](https://i.imgur.com/tW1SpnJ.png) ## コントローラーのバインドを設定する **Window - SteamVR Input を開く** - デフォルト設定をコピーしますか?と聞かれますが、今回はごちゃごちゃになるのを避けるため No を選択し、コピーしません。 **VRThrow という名前の ActionSet を作成** ![](https://i.imgur.com/W4L3O3k.png) **Actions の欄で、+ を押して Action を 3 つ作成し、「Save and generate」** - grab - Type : boolean - reset - Type : boolean - pose - Type : pose **「Open binding UI」を開く。** バインドの新規作成 ![](https://i.imgur.com/xQcLloq.png) - トリガーで+で新規作成 - ボタンとして使用 - クリックを grab に設定 - ジョイスティックで+で新規作成 - ボタンとして使用 - クリックを reset に設定 - 真ん中のアクションポーズの編集 - 左手 未加工 を pose に設定 設定が終わったら、デフォルトバインドの置換 を押して保存 ## シーンの土台を作る 新しくシーンを作成し、プロジェクトウィンドウの`SteamVR - Prefab`から、`CameraRig`をシーンに出します。 `CameraRig`にはカメラオブジェクトが含まれているため、すでにある`MainCamera`を削除します。 Player という名前の`EmptyObject`を(0 ,0 ,0)に作成しその下に`CameraRig`を配置します。 Environment プレハブ、Canvas プレハブをプロジェクトウィンドウの Prefabs からシーンに配置します。 Lighting は今回は AutoGenerate でお茶を濁します・・・。 (ここまでの手順はテンプレートに含まれています) ![](https://i.imgur.com/ILcuZZR.png) ## プレイヤーとコントローラーの設定を行う `CameraRig`を展開し、Controller の`SteamVR_Behaviour_Pose`の PoseAction に`\Actions\{ゲーム名}\in\pose`を設定します。デフォルト設定をコピーしていないため、手動で設定が必要です。 そして、それぞれに`Hand`スクリプトをアタッチします。 - OtherHand に他方の手を設定 - HandType に該当する手を設定 - TrackedObject にすでにアタッチされている`SteamVR_Behaviour_Pose`を設定 - GrabGripAction に、grab アクションを設定 - RenderModelPrefab に、Asset から`controller`を設定 ![](https://i.imgur.com/5YXs4hB.png) Player に `Player`スクリプトをアタッチします。 - Hands を展開し、size に 2 と入力し、Controller を2つとも設定 - RigSteamVR に CameraRig を設定 - Rig2DFallback にも CameraRig を設定 - (本来、VR が使えないときなどに起動するものらしいですが、今回はごまかします) - AudioListener に `CameraRig`の Camera を設定します。 ![](https://i.imgur.com/9Y10lLn.png) ## ボールを作る Sphere を作成し、`ModalThrowable`スクリプトをアタッチします。関連するスクリプトが自動的にたくさんアタッチされます。 - `Release Velocity Style`を`AdvancedEstimation`に変更 - `Scale Release Velocity` を 3 くらいに設定(任意ですが、低いと飛びません) ### ボールのスクリプトを作る ボールの機能を実装します。 - ボールはターゲットにあたった場合に、スコアのカウントを増やします。 - また、リトライボタンにあたった場合に、進行状況をリセットします。 - ボールは、オブジェクトにあたった場合や、地面に接したときに消滅します。 ```csharp= using UnityEngine; public class Ball : MonoBehaviour { readonly string targetTag = "Target"; readonly string retry = "RetryButton"; readonly string ground = "Ground"; private void OnTriggerEnter(Collider other) { if (other.gameObject.CompareTag(targetTag)) { Destroy(other.gameObject); StruckOutManager.CountPlus(); Destroy(gameObject); } if (other.gameObject.name.Equals(retry)) { StruckOutManager.Retry(); Destroy(gameObject); } } private void OnCollisionEnter(Collision collision) { if (collision.gameObject.name.Equals(ground)) { Destroy(gameObject); } } } ``` スクリプトをボールにアタッチしたら、Ball としてプレハブ化します。 ## オブジェクト生成管理をするスクリプトを作る ターゲットやボールの生成管理を実装します。 (簡単のために一部ハードコード) - ボールは、全て消滅していることを確認してから生成 - ターゲットは、残っていた場合は削除してから生成 ```csharp= using UnityEngine; public class ObjectSpawner : MonoBehaviour { [SerializeField] GameObject ballPrefab, targetPrefab; const string BallTag = "Ball"; const string Target = "Target"; static readonly Vector3 SpawnPoint1 = new Vector3(0, 0.6f, 0.8f); static readonly Vector3 SpawnPoint2 = new Vector3(0.4f, 0.6f, 0.8f); static readonly Vector3 SpawnPoint3 = new Vector3(-0.4f, 0.6f, 0.8f); static readonly Vector3 targetPoint = new Vector3(0, 3, 10); public void RespawnBalls() { if (!CheckEmpty(BallTag)) return; Instantiate(ballPrefab, SpawnPoint1, Quaternion.identity); Instantiate(ballPrefab, SpawnPoint2, Quaternion.identity); Instantiate(ballPrefab, SpawnPoint3, Quaternion.identity); } public void RespawnTarget() { Destroy(GameObject.FindGameObjectWithTag(Target)); Instantiate(targetPrefab, targetPoint, Quaternion.identity); } bool CheckEmpty(string name) { if (GameObject.FindGameObjectsWithTag(name).Length == 0) { return true; } else { return false; } } } ``` ## ゲームの進行管理をするスクリプトを作る ゲームの全体の流れを管理するスクリプトを作成します。 - ゲーム状態とリザルト状態を切り替える - テキスト表示 - スコアの管理 - コントローラー入力の管理 ```csharp= using UnityEngine; using Valve.VR; using TMPro; public class StruckOutManager : MonoBehaviour { /* * ゲームの状態管理を行う。 */ private enum View { Game, Result } [SerializeField] TMP_Text scoreText; [SerializeField] GameObject GameView, ResultView; SteamVR_Input_Sources resetButton = SteamVR_Input_Sources.Any; SteamVR_Action_Boolean reset = SteamVR_Actions.VRThrow.reset; private static int score; private const int Point = 10; private static int viewIndex; private static ObjectSpawner spawner; private void Awake() { // スポナーの初期化 spawner = GetComponent<ObjectSpawner>(); score = 0; viewIndex = 0; if (scoreText == null) { scoreText = GameObject.Find("ScoreText").GetComponent<TMP_Text>(); } } private void Start() { spawner.RespawnBalls(); spawner.RespawnTarget(); scoreText.text = score.ToString(); GameView.SetActive(true); ResultView.SetActive(false); } private void Update() { switch (viewIndex) { case (int)View.Game: ResultView.SetActive(false); GameView.SetActive(true); break; case (int)View.Result: GameView.SetActive(false); ResultView.SetActive(true); break; } if (reset.GetStateDown(resetButton)) { spawner.RespawnBalls(); } scoreText.text = score.ToString(); if(score > 80) { viewIndex = (int)View.Result; } } /// <summary> /// スコアのカウントをプラスする。 /// </summary> public static void CountPlus() { score += Point; } public static void Retry() { score = 0; viewIndex = 0; spawner.RespawnBalls(); spawner.RespawnTarget(); } } ``` ## ゲームの進行管理オブジェクトを作成する CreateEmpty で空オブジェクトを作成します。 先程作った、`ObjectSpawner.cs`と、`StruckOutManager.cs`をアタッチします。 ObjectSpawner の設定をしていきます。 - BallPrefab に先程作った Ball を設定 - TargetPrefab に Target プレハブを設定 StruckOutManager の設定を行います。 - ScoreText にCanvasプレハブの`Canvas - GameCanvas - Panel - ScoreText`を設定 - GameView にCanvasプレハブの`Canvas - GameCanvas`を設定 - ResultCanvas にCanvasプレハブの`Canvas - ResultCanvas`を設定 > Canvas は、RenderMode を WorldSpace にすることで画面内に表示します。 ## ゲームを実行する 遊びます。動くことを確認できたら、ビルドします。シーンの追加を忘れずに。 - [完成データ](https://github.com/nemlyc/StruckOutGame) - [リリースページ](https://github.com/nemlyc/StruckOutGame/releases/tag/1.0) ## おわりに だいぶごまかしていたり、半ば自分用のような書き方でしたが、参考になればうれしいです。 動かないよ!などあったらお気軽にご連絡ください・・・。 もしこうしたらより良いよみたいなのあったら教えて下さい。 --- ## 参考 - [SteamVR 公式ドキュメント](https://valvesoftware.github.io/steamvr_unity_plugin/tutorials/intro.html) - https://framesynthesis.jp/tech/unity/htcvive/