# 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/