Unity 2020.3で作るWebVRゲーム【フードキャッチ】

# 概要

- `Unity 2020.3`でカンタンなVRのゲームを作成します
- 食べ物をカゴでキャッチするゲームを作成します
- 最終的に`WebGL`でデプロイしてブラウザで楽しめるVRのコンテンツを作成します
- 一日で完成できたモノなので手順を参考にして作ってもらえれば幸いです


## 素材(Asset)

https://assetstore.unity.com/packages/3d/props/food/free-casual-food-pack-mobile-vr-85884

https://assetstore.unity.com/packages/3d/white-porcelain-dish-set-demo-82858

https://assetstore.unity.com/packages/3d/environments/landscapes/rpg-poly-pack-lite-148410

https://assetstore.unity.com/packages/vfx/particles/effect-textures-and-prefabs-109031


## WebVR用のパッケージ

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

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

# 完成物

- `Bowl`を操作して落ちてくる食べ物をキャッチするゲームです

https://food-catch-vr.netlify.app/

![Videotogif (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/6ff2da3d-3766-4731-9cb2-dace946c2462.gif)






# 作成手順

## ゲームの作成

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/f19c9ec7-4f7d-9c16-1918-a4d35e570950.png)

### 素材(Asset)を全てインポートする

- 上記の素材(Asset)を全てインポートします

### サンプルのシーンをコピーする

- `RPGPP_LT`のサンプルシーンを使用します
- シーン自体をコピーして複製し任意の名前をつけます
- 複製したシーンを加工していきます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/89b693a7-147d-9651-4a1d-2d0ebf10d532.png)


### サンプルのシーンのオブジェクトを整理する

- `Environment`という空のオブジェクトを作成します
- 作成したオブジェクトを親にして全てのオブジェクトを子にします
- `Environment`の位置を調整します
  - `Position` X:0 Y:-35 Z:-85
  - `Rotation` X:0 Y:110 Z:0

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/5aadaaee-debc-ae94-4c23-e9f993572b83.png)


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

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


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/adf6dd60-67e4-828f-9345-fab264837805.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;
        }
    }
}
```

### ゲーム全体を管理するスクリプトを作成する

- `TextMechPro`の`Text`で`ScoreText`と`TimeText`というオブジェクトを作成します
- 作成すると`Canvas`が自動的に生成されるのでアンカーなどで位置を調整します
- 空のオブジェクトで`GameControl`を作成します
- `GameControl`に以下のスクリプトを作成してアタッチします
- スクリプトに`ScoreText`と`TimeText`をアタッチします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/bb1742e9-a742-04a5-fe1b-c5dd625d8160.png)

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

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

    public static float time = 60;
    public static int score = 0;

    // 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";
        }
    }
}

```

### 各食べ物のオブジェクトの設定

- 各食べ物のオブジェクトに対して回転させたり点数を設定できるようにします
- 食べ物のオブジェクトを`Ctrl + A`で全て選択している状態で以下の`Scale`を設定します
  - `Scale` X:15 Y:15 Z:15
- `Rigidbody`と`SphereCollider`を設定します
- `Fruits`というタグを設定します
- `Fruit`のスクリプトを`Add Component`で追加します
- スクリプトの`score`でそれぞれに個別の点数を設定することができます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/2211b3fe-b84b-d48b-3209-a7ba5868df9d.png)

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

public class Fruit : MonoBehaviour
{
    public int score = 1;
    float rotateSpeed = 1.0f;

    void Update()
    {
        transform.Rotate(new Vector3(0, rotateSpeed, 0), Space.Self);
    }
}
```

### 食べ物を繰り返し生成する

- 先ほど作成した`GameControl`に以下のスクリプトを作成してアタッチします
- スクリプトの`Fruits`は配列なので任意の数を設定してください
- それぞれの`Element`に`FREE Food Pack`の`Prefab`をアタッチします
- カメラの周囲（水平の円状）に食べ物のオブジェクトが生成されます
- `Unity`で動作させているときのみキーボードのスペースキーで食べ物を生成できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/6c968358-e2d2-37c8-c2de-581effce3b4f.png)


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

public class FruitsGenerater : MonoBehaviour
{
    public GameObject[] fruits;

    void Start()
    {
        StartCoroutine("CoroutineFruitsGenarate");
    }

#if UNITY_EDITOR
    void Update()
    {
        if (Input.GetKey(KeyCode.Space))
        {
            GenerateFruit();
        }
    }
#endif

    private Vector3 RandHorizonCircleVec3(
        float aMin = 0,
        float aMax = 180,
        float rMin = 5,
        float rMax = 5,
        float yHeight = 1
    )
    {
        var angle = Random.Range(aMin, aMax);
        var radius = Random.Range(rMin, rMax);
        var rad = angle * Mathf.Deg2Rad;
        var px = Mathf.Cos(rad) * radius;
        var pz = Mathf.Sin(rad) * radius;
        return new Vector3(px, yHeight, pz);
    }

    private void GenerateFruit()
    {
        int enemyRnd = Random.Range(0, fruits.Length);
        GameObject fruit = Instantiate(
            fruits[enemyRnd],
            // RandHorizonCircleVec3(60, 100, 10, 10, 5),
            RandHorizonCircleVec3(50, 110, 10, 10, 5),
            Quaternion.identity
        );
        Destroy(fruit, 4f);
    }

    IEnumerator CoroutineFruitsGenarate()
    {
        while (true && GameManager.time > 0)
        {
            GenerateFruit();
            yield return new WaitForSeconds(1);
        }
    }
}

```

### カメラを親にしてボウルを操作できるようにする

- 以下の`Bowl.000`のオブジェクトを使用します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/4970603d-7fd9-bd32-e761-988940b7e4fc.png)

- MainCameraの子にして`Bowl`にリネームします
- `Bowl`の位置とスケールを調整します
  - `Position` X:0 Y:-5 Z:-10
  - `Scale` X:50 Y:60 Z:50

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/9fb0446c-3b79-14a3-6e80-79fb66d28121.png)


### ボウルの中で食べ物をキャッチして点数を加算する

- ボウルの`Sphere`という球形のオブジェクトを作成します
- `Sphere`の位置を調整します
  - `Position` X:0 Y:0.01 Z:0
  - `Scale` X:0.005 Y:0.005 Z:0.005
- `Sphere`に落ちてくる食べ物が接触することで点数が加算されます
- `Sphere`を作ることでボウルの中に入らないと点数が入らないようになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/bcdf9002-03da-3fe1-f98e-852fa2d94c5b.png)


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

public class Player : MonoBehaviour
{
    public GameObject catchVFX;

    void OnCollisionEnter(Collision col)
    {
        if (col.gameObject.tag == "Fruits") {
            GameManager.score += col.gameObject.GetComponent<Fruit>().score;
            GameObject tempVfx = Instantiate(
                    catchVFX, 
                    new Vector3(
                        col.gameObject.transform.position.x,
                        col.gameObject.transform.position.y,
                        col.gameObject.transform.position.z
                    ),
                    catchVFX.transform.rotation
                );
            Destroy(tempVfx, 0.3f);
            Destroy(col.gameObject);
        }
    }
}
```

### キャッチしたときのパーティクルの設定をする

- `EffectTexturesAndPrefabs`のパーティクルの大きさを調整します
  - `Scale` X:5 Y:5 Z:5
  - 親ではなく子のパーティクル２つのスケールを変更します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/30f5bb67-d2ef-c0f5-3d59-2236932b829b.png)

- `Sphere`にパーティクルの`Prefab`をアタッチします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/3225a241-5e36-c7c7-d0da-22944fd8c2c0.png)



## WebVR化する

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

- 別のシーンファイルでWebVR化していきます
- シーンファイルを選択して`Ctrl + D`で複製できます
- 複製したシーンファイルはWebVR用とわかるよう名前を変更しておきます


### 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/ad5588e7-ac7d-1815-82b9-da511eb07394.png)

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

- 先ほど複製したWebVR化のシーンを開きます
- 開いて`Hierarchy`で`Ctrl + V`でコピーした`WebXRCameraSet`を貼り付けます
- 貼り付けた後は元からある`MainCamera`は無効にしておきます
- `MainCamera`から`Bowl`をコピーして同じ構造で`WebXRCameraSet`の`CameraFollower`の子にします
  - `WebXRCameraSet/Cameras/CameraFollower/Bowl/Sphere`


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/a3c9e17c-1951-123d-a479-950dbe0a2dfd.png)


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

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

# ふりかえり

- シューティング以外にも一視点型のゲームができました
- `WebVR`となるとコントローラーの採用が難しいのでカメラで操作させるゲーム性がよさそうです
- 制限がありそうですが制限があるうえでいろいろなゲーム性を考えられると思います
- 実際に作ったモノで`CameraFollower`の子にしないと動作しないのでアセらず設定しましょう