Unity玉転がし機能追加

# はじめに
Unityチュートリアルの玉転がしを改造する機会があったのでメモ。
https://unity3d.com/jp/learn/tutorials/s/roll-ball-tutorial
チュートリアルの完成品に手を加えていく。

バージョンは2019.2.1f1



## PickUpを自動配置する
手動でPickUpを置いていくのが面倒だったのでスクリプトで出そうと思った。
なんやかんやで綺麗になったので自慢する。

###円に配置する
![image_0015.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/c94be5e1-11f1-1c8f-a6e3-f0d7c5670d65.jpeg)


```c#:GameController.cs
public class GameController : MonoBehaviour
{
    public GameObject pickUp;   // PickUpプレハブ
    public int number = 12;     // 配置する個数
    public float radius = 5.0f; // 半径
    void Start()
    {
        for (int i = 0; i < number; i++)
        {
            float theta = Mathf.PI * i / number*2;
            Vector3 position;
            position = new Vector3(Mathf.Cos(theta) * radius, 0.5f, Mathf.Sin(theta) * radius);
            Instantiate(pickUp, position, Quaternion.identity);
        }
    }
}
```

空のゲームオブジェクトを用意し、このスクリプトをアタッチする。
元からあるPickUpも空のゲームオブジェクトにまとめて、非アクティブにするといい。

![スクリーンショット 2019-08-30 18.27.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/9245f4df-88ba-8505-2b1e-927ef3e7b0d0.png)

`Pick Up`にはPickUpプレハブを入れる。
`Number`で配置する個数を変える。(クリアまでに必要な個数は変わらないので注意)
`Radius`で半径が変わる。



### 順番に配置する

![gif_animation_005.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/4773fdd7-d996-39ad-e2e7-fe208a676400.gif)

```c#:GameController.cs
public class GameController : MonoBehaviour
{
    public GameObject pickUp;   // PickUpプレハブ
    public int number = 12;     // 配置する個数
    public float radius = 5.0f; // 半径
    void Start()
    {
        StartCoroutine(SetPickUp());    // コルーチンを呼び出す
    }

    IEnumerator SetPickUp()
    {
        for (int i = 0; i < number; i++)
        {
            float theta = Mathf.PI * i / number * 2;
            Vector3 position;
            position = new Vector3(Mathf.Cos(theta) * radius, 0.5f, Mathf.Sin(theta) * radius);
            Instantiate(pickUp, position, Quaternion.identity);

            yield return new WaitForSeconds(0.05f);     // 0.05秒間隔
        }
    }
}
```

[円に配置する](#円に配置する)からの変更部分は
`IEnumerator SetPickUp()`を用意する
`Start()`の中身を`SetPickUp()`に移す
`yield return new WaitForSeconds(0.05f)`を追加する
`StartCoroutine(SetPickUp())`を追加する
だけ。

見ててちょっと楽しい配置になる。

`position = new Vector3(Mathf.Cos(theta) * radius, 0.5f, Mathf.Sin(theta) * radius)`
の`Mathf.Sin(theta)`を`Mathf.Sin(-theta)`にすると逆回転になる。

### うずまきに配置する

![gif_animation_004.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/3c1383fd-20e1-2b77-9161-f0bf08450c77.gif)

```c#:Gamecontroller.cs
public class GameController : MonoBehaviour
{
    public GameObject pickUp;   // PickUpプレハブ
    public int number = 12;     // 配置する個数
    public float radius = 5.0f; // 半径
    public int windings = 1;    // 巻き数
    void Start()
    {
        StartCoroutine(SetPickUp());    // コルーチンを呼び出す
    }

    IEnumerator SetPickUp()
    {
        int windingsNum = number / windings;    // 一巻きあたりの個数
        for (int i = 0; i < number; i++)
        {
            float theta = Mathf.PI * i / windingsNum * 2;   // 変更
            float radius_ex = radius + (float)i / windingsNum;  // 少しずつ広がる半径
            Vector3 position;
            position = new Vector3(Mathf.Cos(theta) * radius_ex, 0.5f, Mathf.Sin(theta) * radius_ex);   // 変更
            Instantiate(pickUp, position, Quaternion.identity);

            yield return new WaitForSeconds(0.05f);     // 0.05秒間隔
        }
    }
}
```

[順番に配置する](#順番に配置する)からの変更部分は
`public int windings = 1;    // 巻き数`を追加する
`int windingsNum = number / windings;    // 一巻きあたりの個数`を追加する
`float theta = Mathf.PI * i / windingsNum * 2;   // 変更`を変更する
`float radius_ex = radius + (float)i / windingsNum;  // 少しずつ広がる半径`を追加する。
`position = new Vector3(Mathf.Cos(theta) * radius_ex, 0.5f, Mathf.Sin(theta) * radius_ex);   // 変更`を変更する

![スクリーンショット 2019-09-03 11.53.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/9be60099-4d95-83ae-4f23-d66875ffc5b8.png)

`Windings`に巻き数を入れる。
`Number`を割り切れるを入れないとPickUpの個数が`Number`より多くなってしまうので注意。

もっと楽しい。

### 上むきうずまきにもできる

![gif_animation_006.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/f0325d05-d54d-9a0c-befa-7ca69febc448.gif)

`position = new Vector3(Mathf.Cos(theta) * radius_ex, 0.5f, Mathf.Sin(theta) * radius_ex);`
↓
`position = new Vector3(Mathf.Cos(theta) * radius, 0.5f + (float)i / windingsNum, Mathf.Sin(theta) * radius);`
に書き換えると上むきうずまき。
ボールにジャンプ機能があるなら上むきも楽しい。

## ジャンプ機能
シンプルにジャンプ。

```c#:JumpController
public class JumpController : MonoBehaviour
{
    public float jumpPower = 100.0f;     // ジャンプ力
    private Rigidbody rb;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        var ray = new Ray(this.transform.position, Vector3.down);
        if (Input.GetButton("Jump") && Physics.Raycast(ray, 0.8f))
        {
            rb.AddForce(new Vector3(0.0f, jumpPower, 0.0f));
        }
    }
}
```

地上にいるときスペースキーでジャンプ。
`AddForce`で上方向に力を加えて跳んでいる。
ボールの中心から`0.8f`の長さのレイを下方向に飛ばし、
レイが地面に衝突しているかつスペースキーを押している間は上方向の力を加えられる。
ボールの直径が`1.0f`、つまり半径が`0,5f`なのでレイはボールの表面から`0.3f`だけ伸びていることになる。
![jump.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/a831c907-28a0-a624-b1f8-a18cf0860d96.png)
この`0.3f`が助走距離のようなものになるのでジャンプ力を上げたいときは`JumpPower`を上げてもいいがレイの長さを伸ばしても良い。

この仕組みを使うことで長く押すと高く跳び、短く押すと低く跳ぶようになる。

## 危険なオブジェクト

当たるとリセット。

![gif_animation_007.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/25d2d88e-40eb-2577-0b6c-dc0938b55652.gif)

```c#:DangerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class DangerController : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Player")
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
        }
    }
}
```

新しく触れてはいけない`GameObject`を作成しこのスクリプトをアタッチする。
`using UnityEngine.SceneManagement`が必要。
ボールに`Player`タグをつける。
作成したGameObjectについてる`~ Collider`の`Is Trigger`にチェックを入れる。
色はお好みでつける。

危険なオブジェクトに触れるとボールが初期位置にリセット。
それまで集めたPickUpもリセットされる。

### 動かす
単純な往復運動。

```c#:Mover.cs
public class Mover : MonoBehaviour
{
    public float height = 3;    // 高さ
    public float speed = 3;     // 速さ

    private Vector3 pos;
    private float theta;

    void Start()
    {
        pos = GetComponent<Transform>().position;
        theta = 0;
    }

    void Update()
    {
        this.transform.position = new Vector3(pos.x, pos.y + Mathf.Sin(theta) * height, pos.z);
        theta += 0.01f * speed;
    }
}
```

このスクリプトをアタッチしたオブジェクトが上下運動をする。
インスペクターで高さと速さを変えられる。
`this.transform.position = new Vector3(pos.x, pos.y + Mathf.Sin(theta) * height, pos.z)`
の`pos.y + Mathf.Sin(theta) * height`
の`+ Mathf.Sin(theta) * height`を`pos.x`や`pos.z`に移すことでx軸やz軸にも動かせる。
CosとSinにわけて二箇所につければ円運動にもなる。

アニメーションで動かす方法もあるのでもっと複雑な動きをさせたい場合は調べてみて。

##　追いかける
Nav Mesh Agentを使って簡単に。

![gif_animation_009.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/dacf8db6-eb29-6c4a-a5bd-599113974859.gif)

#### 追跡者が地面であると認識してほしいオブジェクトをStaticにする
![スクリーンショット 2019-09-03 15.38.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/dbaa0c10-e73d-a5e5-d522-c7b22ece11fc.png)
正確には「Navigation Static」にチェックが入っていれば良い。

#### ベイクして通れる場所を明らかにする
Window>AI>Navigationを選択し、Navigationウィンドウを開く。
BakeタブのBakeをクリック。
ちょっと待つと通れる範囲がシーンビューにて水色で表示されるはず。
![スクリーンショット 2019-09-03 15.59.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/303b0437-b83c-f638-3800-403cfa3afa00.png)
壁際が水色になっていないのはNavigationウィンドウで追跡者の半径(Agent Radius)が定められており、
追跡者が壁にぶつからないようにするため。
その他登れる段差の高さや坂道の角度などもここで決められる。
今回はぶつかってもいいし気にしない。

#### オブジェクトにNav Mesh Agentコンポーネントをアタッチする
追跡者であるオブジェクトを作成、選択し、Component>Navigation>Nav Mesh Agentを選択する。
![スクリーンショット 2019-09-03 16.12.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446130/9dc0c82e-c73c-fd1e-0f87-4035ba8e0b79.png)
速さや旋回速度、加速度などはここで設定できる。

#### 誰を追跡するか、スクリプトで決める

```c#:Stoker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class Stoker : MonoBehaviour
{
    public GameObject target;   // 追跡するターゲット
    private NavMeshAgent agent;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
    }

    void Update()
    {
        agent.destination = target.transform.position;
    }
}
```

先ほど作った追跡者オブジェクトにこのスクリプトをアタッチする。
Targetにボールを入れる。
`using UnityEngine.AI`が必要。

これで追いかけるオブジェクトの完成。

触ったらアウトにするなら[危険なオブジェクト](#危険なオブジェクト)と組み合わせる。


# おわり
思ったより長くなった。
初めて画像やgifを使ってみたけど長くなったから重くならないか心配。