UnityでWebVRのシューティングを作成する

# 概要

- `Unity 2020.3`でカンタンなVRのシューティングゲームを作成します
- 最初は普通にマウスやスワイプなどで遊べるシューティングゲームを作成します
- 最終的に`WebGL`でデプロイしてブラウザで楽しめるVRのコンテンツを作成します
- 一日で完成できたモノなので手順を参考にして作ってもらえれば幸いです

# 使用する素材(Asset)とWebVR用のパッケージ

## 素材(Asset)

https://assetstore.unity.com/packages/2d/textures-materials/milky-way-skybox-94001

https://assetstore.unity.com/packages/3d/environments/sci-fi/low-poly-space-rocks-58385

https://assetstore.unity.com/packages/vfx/particles/sherbb-s-particle-collection-170798

https://icooon-mono.com/12788-%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88%E3%81%AE%E3%82%A2%E3%82%A4%E3%82%B3%E3%83%B32/

## WebVR用のパッケージ

https://openupm.com/packages/com.de-panther.webxr/

https://openupm.com/packages/com.de-panther.webxr-interactions/

# 完成物

- パソコンで遊ぶ方はマウスでドラッグすることで視点を変えることができます
- 近寄ってくるオブジェクトに対して照準を合わせると破壊できます
- オブジェクトを破壊すると点数が入ります
- 制限時間が0になるゲーム終了です
- オブジェクトを破壊できずカメラにあたるとゲームオーバーです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/f68c1098-e618-ebb6-6824-4c320e3e73d2.png)

https://unity-webvr-stg.netlify.app/

# 作成手順

## 3Dシューティングゲームの作成

- `MainCamera`をプレイヤーとして扱います
- カメラの視点を動かしながら敵を倒すシューティングゲームです


### 3Dのプロジェクトを新規作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/7fef4100-a925-a85c-cb25-2740f9b545fa.png)


### カメラの視点をマウスで動かせるようにする

- `DragMove.cs`というスクリプトを作成して以下の内容を記述します
- 作成したスクリプトを`MainCamera`にアタッチします
- `MoveObj`にカメラのオブジェクトである`MainCamera`をアタッチします
- アタッチ後に実行するとマウスのドラッグでカメラの視点を動かすことができます


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/39410758-6ffd-66b8-d8a5-6bbd1673a7fa.png)


```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DragMove : MonoBehaviour
{
    public GameObject moveObj;
    private Vector3 newAngle = new Vector3(0, 0, 0);
    private Vector3 lastMousePosition;

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            // マウスクリック開始(マウスダウン)時にカメラの角度を保持(Z軸には回転させないため).
            newAngle = moveObj.transform.localEulerAngles;
            lastMousePosition = Input.mousePosition;
        }
        else if (Input.GetMouseButton(0))
        {
            // マウスの移動量分カメラを回転させる.
            newAngle.y -= (Input.mousePosition.x - lastMousePosition.x) * 0.1f;
            newAngle.x -= (Input.mousePosition.y - lastMousePosition.y) * 0.1f;
            moveObj.transform.localEulerAngles = newAngle;
            lastMousePosition = Input.mousePosition;
        }
    }

}
```

### 敵の設定

- `MainCamera`に目掛けて敵が近寄ってくるようにします

#### 敵のオブジェクトの準備

- `Asset`の`Low Poly Space Rocks`から敵として使うオブジェクトを選びます
  - 元の`Asset`とは別のフォルダにコピーしておきます
  - 今回は4つの`Emeny`のオブジェクトを作成しました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/569aa444-b206-5eb4-9cab-d6c16e4598de.png)

#### 敵のスクリプトの設定

- 敵のオブジェクトに対し以下の設定をします
  - `Enemy.cs`というスクリプトを作成して以下の内容を記述してアタッチします
  - プレイヤーとの接触のために`SphereCollider`を設定します
  - `Enemy`というタグを設定します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/569aa444-b206-5eb4-9cab-d6c16e4598de.png)


```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour
{
    GameObject target;
    float speed = 0.8f;
    // float speed = 0.8f;//PCだと0.8f
    float rotateSpeed = 0.5f;
    void Start()
    {
        target = GameObject.FindGameObjectWithTag("MainCamera");
    }

    void Update()
    {
        //自分の位置、ターゲット、速度
        transform.position = Vector3.MoveTowards(transform.position, target.transform.position, speed);
        transform.Rotate(new Vector3(rotateSpeed, rotateSpeed, rotateSpeed), Space.Self);
    }

}
```

### ゲーム全体を制御する

- スコアや制限時間の更新
- ゲームオーバーの演出などを設定します

#### Canvasに表示するオブジェクトの準備

- `TextMeshPro`で`ScoreText`と`TimeText`というオブジェクトを作成します
  - `ScoreText`のアンカーを右上に設定します
  - `TimeText`はのアンカーを左上に設定します
- `GameOverImage`という`Image`のオブジェクトを作成します
  - `GameOverImage`はアンカーを中央にしてサイズを`x:4000 y:4000`に設定します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/c9cc9309-0978-9009-07bf-7b77a5314c24.png)

#### 真ん中に照準の画像を配置する

- 以下のアイコンの画像をダウンロードしてプロジェクトに追加します
- `Texture Type`を`Sprite (2D and UI)`に設定します
  - SpriteにすることでCanvasのImageから選択できるようになります
- CanvasにTargetImageというImageのオブジェクトを作成します
  - オブジェクトのアンカーは中央にします
  - `Source Image`を先ほど追加したspriteを設定します

https://icooon-mono.com/12788-%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88%E3%81%AE%E3%82%A2%E3%82%A4%E3%82%B3%E3%83%B32/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/89270171-8078-9edf-e499-112062922e23.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/7807ef6e-352e-0180-6722-e2fffaa13914.png)



#### スクリプトの設定

- `GameController.cs`というスクリプトを作成して以下の内容を記述します
- Canvasにある`ScoreText`オブジェクトを`scoreText`にアタッチします
- Canvasにある`timeText`オブジェクトと`timeText`にアタッチします
- 実際に作成したときは一番最後で作成したスクリプトです
  - 今回の手順では`static`の変数で他のスクリプトから読み取る都合から先に作成しています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/1a20f470-0981-ee73-55ae-2aa672a1e200.png)

```csharp
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class GameController : MonoBehaviour
{
    public TextMeshProUGUI scoreText;
    public TextMeshProUGUI timeText;

    public static float time = 60;
    public static int score = 0;
    const string DAMAGE_FX_OBJ_NAME = "GameOverImage";

    // Update is called once per frame
    void Update()
    {
        GameEndCheck();
        scoreText.text = $"SCORE: {score}";
    }

    void GameEndCheck()
    {
        if (time >= 0)
        {
            time -= Time.deltaTime;
            timeText.text = $"TIME: {Math.Floor(time)}";
        }
        else
        {
            timeText.text = "TIME: 0";
            EnemyAllDestroy();
        }
    }

    public static void EnemyAllDestroy()
    {
        GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
        foreach (GameObject enemy in enemies)
        {
            Destroy(enemy);
        }
    }

    public static void GameOver()
    {
        GameObject damageFx = GameObject.Find(DAMAGE_FX_OBJ_NAME);
        Image damageFxImg = damageFx.GetComponent<Image>();
        damageFxImg.color = new Color(0.5f, 0f, 0f, 0.5f);
        time = 0;
    }
}
```

### 敵を定期的に生成

#### 空のオブジェクトの作成

- `EnemyGenerate`という空のオブジェクトを作成します
- `EnemyGenerate`オブジェクトの位置は`X:0, Y:0, Z:80`にします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/4305c0e1-9645-49cd-a1e8-c84ebd243349.png)


#### スクリプトの設定

- `EnemyGenerate.cs`というスクリプトを作成して以下の内容を記述します
- `EnemyGenerate`オブジェクトに`Prefab`で作成した敵を複数アタッチします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/9ef538eb-0220-8a53-8838-8fbc70faf74f.png)


```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyGenerate : MonoBehaviour
{
    public GameObject[] enemies;
    float posRangeX = 40;
    float posRangeY = 40;
    float geneTimeSec = 4;
    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine("EnemyGanerate");
    }

    // Update is called once per frame
    void Update()
    {
        if (GameController.time < 0)
            StopCoroutine("EnemyGanerate");
    }

    IEnumerator EnemyGanerate()
    {
        while (true)
        {
            GameLevelChenge();
            int enemyRnd = Random.Range(0, enemies.Length);
            float posXrnd = Random.Range(posRangeX * -1, posRangeX);
            float posYrnd = Random.Range(posRangeY * -1, posRangeY);
            Instantiate(
                enemies[enemyRnd],
                new Vector3(
                    transform.position.x + posXrnd,
                    transform.position.y + posYrnd,
                    transform.position.z
                ),
                Quaternion.identity
            );
            yield return new WaitForSeconds(geneTimeSec);
        }
    }

    void GameLevelChenge()
    {
        if (geneTimeSec > 1.0f) geneTimeSec -= 0.15f;
    }
}
```

### Playerの制御

#### 敵を倒したときのパーティクルを準備する

- `Asset`の`Sherbb's Particle Collection`から敵を倒したときのパーティクルを選びます
  - 選んだパーティクルは別のフォルダにコピーしておきます
  - コピーしたパーティクルは`Stop Action`で`Destroy`を設定します
  - `Destroy`を設定しないとパーティクルが残り続けるので気を付けます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/37482d17-dea9-b5fe-1ac2-3b25227be51d.png)


#### スクリプトの設定

- `Player.cs`というスクリプトを作成して以下の内容を記述します
- 作成したスクリプトを`MainCamera`にアタッチします
- 上記で準備したパーティクルのオブジェクトをスクリプトの`shootVFX`にアタッチします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/7a7ebf0c-58ce-7583-79cf-6455e5f893b3.png)


```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class Player : MonoBehaviour
{
    float distance = 100f;

    public GameObject shootVFX;

    // Update is called once per frame
    void Update()
    {
        RayShoot();
    }

    void OnCollisionEnter(Collision col)
    {
        if (col.gameObject.tag == "Enemy") GameController.GameOver();
    }

    void RayShoot()
    {
        Ray ray = new Ray(transform.position, transform.forward);
        Debug.DrawRay(ray.origin, ray.direction * distance, Color.red, 0.1f, false);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, distance))
        {
            if (hit.collider.gameObject.tag == "Enemy")
            {
                GameController.score++;
                Debug.Log(hit.collider.gameObject.name);
                Instantiate(
                    shootVFX,
                    new Vector3(
                            hit.collider.gameObject.transform.position.x,
                            hit.collider.gameObject.transform.position.y,
                            hit.collider.gameObject.transform.position.z
                    ),
                    hit.collider.gameObject.transform.rotation
                );
                Destroy(hit.collider.gameObject);
            }
        }
    }
}
```

### ここまでの手順で3Dシューティングゲームとして完成

- 上記の手順を終えればカメラの視点を元にした3Dシューティングゲームとして遊べます
- `PC, Mac & Linux Standalone`でそれぞれのパソコンで遊ぶモノをビルドできます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/e7ec9bf2-cc0f-a87d-6081-aa702bd4565e.png)


## WebVR化する

### 作成したシーンの複製（シーンのバックアップ）

- すでに3Dシューティングとしては完成しているので別のシーンファイルでWebVR化していきます
- シーンファイルを選択して`Ctrl + D`で複製できます
- 複製したシーンファイルはWebVR用とわかるよう名前を変更しておきます
- 複製しておけばWebVR化したときに上手くいかなかってもある程度は復元することができます
  - シーン共通で使用しているPrefabなどのオブジェクトを直接編集したりするとダメですが・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/71e34dcf-f5c9-f774-b8c7-7de248e95cbc.png)

### WebVRのパッケージをインストールしサンプルのシーンの追加

- 以下の記事に手順が記載されています
  - パッケージの追加から各種設定
  - サンプルシーンの追加

https://qiita.com/tamoco/items/22d4f6640dc361277f2f

### サンプルシーンからWebXRCameraSetのコピー

- `WebXRCameraSet`を`Ctrl + C`でコピーします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/1b4024d4-ca9b-8be4-1761-841236bc5dfa.png)

### WebVR化するシーンにWebXRCameraSetの貼り付け

- 先ほど複製したWebVR化のシーンを開きます
- 開いて`Hierarchy`で`Ctrl + V`でコピーした`WebXRCameraSet`を貼り付けます
- 貼り付けた後は元からある`MainCamera`は削除します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/8485af44-c550-d12a-a0c5-f926e8198d2f.png)

### WebXRCameraSetの設定

- 各コンポネントを追加します
  - RigidBody
  - BoxCollider：Sizeを「X:10, Y:10, Z:10」
- Player.csをアタッチする
- DragMove.csをアタッチする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/af8e7a93-2bf1-0dc5-02a2-06975196b9cd.png)

### WebVRとしてビルドする

- `Build Settings`で複製したシーンを追加してビルドします
  - 元からあるシーンは削除します
- ビルドでできたファイルをウェブサーバーにアップロードするとWebVRとして遊べます

# ふりかえり

- 3DゲームをつくってそれをWebVRすることができました
- Canvas上のオブジェクトはVR化するにあたって工夫は必要そうです
- 一日で完成できるので難易度的にもそこまで難しくないかもしれません