ゼルダの伝説BotWのカメラ研究 with Unity 其の一「基本の動き」

# はじめに

- ゼルダの伝説BotWはここでは「ゼルダの伝説 ブレスオブザワイルド」を指します。
- 場合によっては誤りがある場合があります。十分に気を付けてはおりますが、筆者は特に数学が苦手ですのでその部分で誤りがあるやしれません。
- 解説を分かりやすくするために一部初級者向けの内容が含まれています。
- Unityで動きを演出する部分があります。
- 何回かに分けて考察を書いていきます。今回は基本的な動きに目を付けたいと思います。
- ~~**現在、この項に関して考察は完了していますがそれを実現するための処理は完成していません。予めご了承ください。**~~
- →**（2020年2月25日追記）処理が完成しました。**

## Unityの動作のテスト環境
Windows10 Home
Unity 2019.3.0f6
VisualStudio 2019 Professional

# 基本の動き
基本の映し方としては一般的な三人称視点のカメラと変わりはありません。

![20200224Zelda-image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449607/59ad2e5f-2635-6d6b-a66f-c2a9bcaca341.jpeg)


注目していただきたいのは**カメラの回転速度（左右の軸周り）**です。

***[動画はこちらを確認してください](https://youtu.be/VqmSRHHrbMI)***

動画を確認された方は「何を言ってるんだ、普通の動きじゃないか」などと思われるかもしれませんが、例えばUnityのRotateAround関数の処理をC#で書き表すとこんな感じになります。

## ソースコード
```csharp:RotateAround.cs
using UnityEngine;

public class RotateAround : MonoBehaviour
{
    [SerializeField]
    GameObject parent;

    float speed;
    

    // Start is called before the first frame update
    void Awake()
    {
        speed = 50.0f;
    }

    // Update is called once per frame
    void Update()
    {
        float x = Input.GetAxis("RightHorizontal");
        float y = Input.GetAxis("RightVirtical");

        //初期値
        float rad = 2 * Mathf.PI;

        //RotateAroundと同等の動きをする
        Vector3 radSpeed = x * transform.right * rad;

        transform.position += radSpeed * Time.deltaTime;

        //振動を防ぐためにMathf.Clampは利用していない
        //振動を防ぐ方法として一定以上もしくは以下になったらそれ以上動きを足さない処理を利用する
        float eulerAnglesX = transform.eulerAngles.x;

        if (!((eulerAnglesX < 180 && eulerAnglesX > 89.0f && y > 0) || (eulerAnglesX > 180 && eulerAnglesX < 271.0f && y < 0)))
            transform.RotateAround(parent.transform.position, transform.right, y * speed * Time.deltaTime);

        transform.LookAt(parent.transform);
    }
}
```
### 簡単なコード解説

- 使っているのはXboxController for Windows
- 上下の軸周りはそのままUnityのRotateAround関数を利用しています。
- 動きを分かりやすくするためにLookAt関数を利用しています。
- カメラの角度制限を設けた際の振動を抑える処理を追加しています。

## 動かすとこうなる

***[動画はこちらを確認してください](https://youtu.be/X6Zi0cqlydc)***

**y軸と、基準点とカメラ位置のなす直線がなす角が狭まれば狭まるほど高速に回転を始める**ことがわかると思います。
（下記の画像と上記の動画で言いたいことがわかっていただけると幸いです。）
![nasukaku.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449607/9f840006-63dd-8a7b-84e3-638589438a00.png)

なお、下から覗くような形でも同様の現象が確認できます。

# 考察
まず必要な条件として、カメラが基準となる位置を周回するときに**回転数が半径に関わらず常に一定である**ということが挙げられるかと思います。

## 周回する際の半径に関わらず回転数が一定である条件
まず、Unityで動かした方は**半径に関わらず速度が一定**という条件での動作でした。
これでは半径が短くなることで円周の長さも短くなった場合に一周する速さが変わり、半径によって回転数が変化してしまします。

そこで必要になるのが**角速度を利用する**という考え方です。
### そもそも角速度とは
角速度は**回転運動の速度を単位時間当たりの角度で表したもの**になります。
**回転運動（円運動）をしている物体の角速度が一定**である場合、その物体は**等速円運動**を行っていると定義できます。

## 結論:ゼルダBotWのカメラを再現するための条件
**角速度が一定である運動 ＝ 等速円運動を行わせることでゼルダのカメラの基礎的な部分は再現できる！！**
と、条件を定めました。

# 結論が出たうえでの再現処理
```csharp:RotateAround.cs

    [SerializeField]
    GameObject parent;

    float speed;
    float distance; //追加 基準オブジェクトとの距離。半径の役割を持つ
    Vector3 distancePosition; //追加 半径設定用のベクトル

    // Start is called before the first frame update
    void Awake()
    {
        speed = 50.0f;

        //半径設定 高さをカメラと同一にすることで距離をそのまま半径として利用できる
        distancePosition = new Vector3(parent.transform.position.x, transform.position.y, parent.transform.position.z); 
        distance = Vector3.Distance(distancePosition, transform.position);
    }

    // Update is called once per frame
    void Update()
    {
        float x = Input.GetAxis("RightHorizontal");
        float y = Input.GetAxis("RightVirtical");

        //等速円運動をする物体の角速度(rad/s)の公式は 角速度(ベクトル) = 速度 / 半径 もしくは 角速度(ベクトル) = 角度 / 時間 (2π * 半径 / 時間)
        //今回は角速度(ベクトル) = 2π * 半径 / 時間 を利用する
        //今回の場合、秒間72°移動する処理である
        float radSpeedValue = 2 * Mathf.PI * distance / 5.0f; //変更

        //RotateAroundと同等の動きをする
        Vector3 radSpeed = x * transform.right * radSpeedValue;
 
        transform.position += radSpeed * Time.deltaTime;
        
        //振動を防ぐためにMathf.Clampは利用していない
        //振動を防ぐ方法として一定以上もしくは以下になったらそれ以上動きを足さない処理を利用する
        float eulerAnglesX = transform.eulerAngles.x;

        if (!((eulerAnglesX < 180 && eulerAnglesX > 89.0f && y > 0) || (eulerAnglesX > 180 && eulerAnglesX < 271.0f && y < 0)))
            transform.RotateAround(parent.transform.position, transform.right, y * speed * Time.deltaTime);

        transform.LookAt(parent.transform);

        //半径設定 高さをカメラと同一にすることで距離をそのまま半径として利用できる
        distancePosition = new Vector3(parent.transform.position.x, transform.position.y, parent.transform.position.z);
        //y軸となす角度によって距離が変動するため、毎フレーム更新
        distance = Vector3.Distance(distancePosition, transform.position);
    }
```
### 簡単なコード解説

- Update関数の一部を変更し、半径を設定することで角速度の計算を行えるように変更しました。
- 距離を取得するための変数を追加しました。

# 関連する記事
まだ書いていません。