[Unity]Cinemachineを使わないお手軽なカメラワーク設計

Unityでカメラワークを作るといえば **Cinemachine** です。
Cinemachineはいくつか設定をするだけで、ある程度いい感じに被写体を画面に収めてくれるので便利ですが、**どちらかというと名前の通りシネマティックなカメラワークを作るのに向いていて**、アクションゲームやアドベンチャーゲームでプレイヤーを追いかけて程よく演出を入れるような使い方をするにはややクセがあるように感じます。

そこで、以前

* わかりやすいパラメータで画面を制御できる
* 仕組みが単純で動きをイメージしやすい
* 汎用性が高い
* かんたん

を満たす独自のカメラ設計を作って、それ以降よくこの仕組みを使っているのでまとめてみました。

# コントロールできるカメラの動き

https://twitter.com/flankids/status/1528460859743162368?s=20&t=LNPik-OpH0omMzT5Mrmw9Q

こんな感じで被写体（Transformや座標）を指定して、追いかけたり回り込んだり画面中央からズラしたりできます。
この仕組みの良いところは、被写体に対して同一のパラメータを与えると**必ず同じ見え方**を再現できることです。

Githubにデモプロジェクトを用意しました。よろしければご利用下さい！

https://github.com/fluncle/SimpleCameraControl/wiki

**※↑は、一部ご自身でインポートして貰う必要のあるAssetStoreの無料アセットがあります(DOTweenなど)**

# オブジェクト構成
まず、下記のように`Camera`コンポーネントを持つオブジェクトを入れ子構造の孫にしてください。

![kouzou.JPG](https://qiita-image-store.s3.amazonaws.com/0/184294/cec4f821-a84c-12b9-8e5e-5777e82b2aad.jpeg)

完成です。楽ちんですね。

それぞれ、下記の要素が記載してあるパラメータに対応しています。

|パラメータ|要素|
|---|---|
|被写体の座標|CameraParentの座標|
|被写体からの距離|CameraChildのローカルZ座標（負数）|
|画角|Main CameraのField Of View|
|被写体への回り込み角度|CameraParentの角度|
|視界オフセット座標|Main Cameraのローカル座標|
|視界オフセット角度|Main Cameraのローカル角度|

例えば、CameraChildを動かすことで被写体に寄ったり離れたり、CameraParentを回すことで被写体を好きな位置から見ることができます。

https://twitter.com/flankids/status/1528461554676510721?s=20&t=LNPik-OpH0omMzT5Mrmw9Q

# スクリプトでパラメータを管理

カメラのビューを作る各種オブジェクトにいちいちアクセスするのは大変なので、パラメータを一元管理できるようスクリプトを用意します。

```CameraManager.cs
using System;
using UnityEngine;

// シーンを実行しなくてもカメラワークが反映されるよう、ExecuteInEditModeを付与
[ExecuteInEditMode]
public class CameraManager : MonoBehaviour
{
    /// <summary> カメラのパラメータ </summary>
    [Serializable]
    public class Parameter
    {
        public Vector3 position;
        public Vector3 angles = new Vector3(10f, 0f, 0f);
        public float distance = 7f;
        public float fieldOfView = 45f;
        public Vector3 offsetPosition = new Vector3(0f, 1f, 0f);
        public Vector3 offsetAngles;
    }

    [SerializeField]
    private Transform _parent;

    [SerializeField]
    private Transform _child;

    [SerializeField]
    private Camera _camera;

    [SerializeField]
    private Parameter _parameter;

    // 被写体などの移動更新が済んだ後にカメラを更新したいので、LateUpdateを使う
    private void LateUpdate()
    {
        if(_parent == null || _child == null || _camera == null)
        {
            return;
        }

        // パラメータを各種オブジェクトに反映
        _parent.position = _parameter.position;
        _parent.eulerAngles = _parameter.angles;

        var childPos = _child.localPosition;
        childPos.z = -_parameter.distance;
        _child.localPosition = childPos;

        _camera.fieldOfView = _parameter.fieldOfView;
        _camera.transform.localPosition = _parameter.offsetPosition;
        _camera.transform.localEulerAngles = _parameter.offsetAngles;
    }
}
```

１つのインスタンスを参照するだけでカメラワークをイジれるようになりました。
インスペクターからの調整はもちろん、スクリプトからもいろいろできるようになります。

https://twitter.com/flankids/status/1528462076141658112?s=20&t=2iHvio4lTO5OlsWZaSd3Dw

# 被写体の追いかけ

上記スクリプトはあくまでパラメータをカメラビューに反映するためだけの管理クラスなので、被写体を追いかけたりはしません。

https://twitter.com/flankids/status/1528462293347889152?s=20&t=2iHvio4lTO5OlsWZaSd3Dw

被写体を追いかけるには、`Parameter.position`に被写体の座標を代入し続ける必要があります。
多くの場合、被写体はTransformを持つと思うので`Camera.Parameter`クラスに追従対象(**trackTarget**)をTransformで指定できるように拡張してみます。

```CameraManager.cs
/// <summary> カメラのパラメータ </summary>
[Serializable]
public class Parameter
{
    public Transform trackTarget; // ← 追加
    public Vector3 position;
    public Vector3 angles = new Vector3(10f, 0f, 0f);
    public float distance = 7f;
    public float fieldOfView = 45f;
    public Vector3 offsetPosition = new Vector3(0f, 1f, 0f);
    public Vector3 offsetAngles;
}
```

```CameraManager.cs
private void LateUpdate()
{
    /* ↓↓↓ここから追加↓↓↓ */
    if(_parameter.trackTarget != null)
    {
        // 被写体がTransformで指定されている場合、positionパラメータに座標を上書き
        _parameter.position = _parameter.trackTarget.position;
    }
    /* ↑↑↑追加ここまで↑↑↑ */

    // パラメータを各種オブジェクトに反映
    _parent.position = _parameter.position;
```

`targetTransform`が設定されている場合、そのTransformの座標を常に被写体座標として使うようになりました。
これで、被写体を追いかけるようなカメラワークが完成しました。

https://twitter.com/flankids/status/1528462582675230720?s=20&t=LNPik-OpH0omMzT5Mrmw9Q

## 自然に追いかけさせるには

上記スクリプトでは被写体にピッタリついてくるカメラワークであるため、ちょっと機械的というか、**プレイヤーにカメラを接着させてるような不自然さがあります。**
自然なカメラワークを作るためには急な加減速のないアナログな動きであるべきなので、ちょっと工夫して被写体の動きに対してちょっと遅れてついてくるような実装に変えてみましょう。

```CameraManager.cs
private void LateUpdate()
{
    if(_parameter.trackTarget != null)
    {
        // がTransformで指定されている場合、positionパラメータに座標を上書き
        _parameter.position = Vector3.Lerp(
            a: _parameter.position,
            b: _parameter.trackTarget.position,
            t: Time.deltaTime * 4f
        );
    }
```

よくある`Lerp`を使った簡易的なイージングを入れて、座標更新の変化をなめらかなものにします。
ピッタリカメラがついてくるよりは自然な印象のカメラワークになりました。

https://twitter.com/flankids/status/1528462865589366784?s=20&t=2iHvio4lTO5OlsWZaSd3Dw

# 発展的な使い方

ここまでは本カメラ設計の基本的な使い方を解説しました。
構造が簡単なので、ゲーム仕様に合わせて自由な使い方ができます。いくつか実例を載せます。

## ①カメラの向きを操作しながらキャラを操作する

`Camera.Parameter.angles`の値をコントローラの右スティックや画面スワイプなどに割り当てることで、3Dアクションゲームによくあるカメラの回転操作に使うことができます。

例として、マウスでカメラを回転させるFPS/TPSっぽい動きを実装してみました

https://twitter.com/flankids/status/1528463408609169408?s=20&t=LNPik-OpH0omMzT5Mrmw9Q

```CameraManager.cs
public class CameraManager : MonoBehaviour
{
    private void Update()
    {
        // マウスの動きの差分をカメラの回り込み角度に反映
        Vector3 diffAngles = new Vector3(
            x: -Input.GetAxis("Mouse Y"),
            y: Input.GetAxis("Mouse X")
        ) * 10f;
        _parameter.angles += diffAngles;
    }
```

※実際には`CameraManager.cs`に実装するよりは、操作用のクラスを作って外部からカメラの`Parameter`を変更するほうが設計としては良いと思います。

## ②`Camera.Parameter`のLerpメソッドを作って２つのカメラワークをブレンド遷移する

下記のように、２つのパラメータ間を繋ぐようなカメラワークパラメータを作るメソッドを用意することで、見え方の大きく違うビューをなめらかに繋ぐことができます。

```CameraManager.cs
public class CameraManager : MonoBehaviour
{
     /// <summary> カメラのパラメータ </summary>
    [Serializable]
    public class Parameter
    {
        public static Parameter Lerp(Parameter a, Parameter b, float t, Parameter ret)
        {
            ret.position = Vector3.Lerp(a.position, b.position, t);
            ret.angles = LerpAngles(a.angles, b.angles, t);
            ret.distance = Mathf.Lerp(a.distance, b.distance, t);
            ret.fieldOfView = Mathf.Lerp(a.fieldOfView, b.fieldOfView, t);
            ret.offsetPosition = Vector3.Lerp(a.offsetPosition, b.offsetPosition, t);
            ret.offsetAngles = LerpAngles(a.offsetAngles, b.offsetAngles, t);

            return ret;
        }

        private static Vector3 LerpAngles(Vector3 a, Vector3 b, float t)
        {
            Vector3 ret = Vector3.zero;
            ret.x = Mathf.LerpAngle(a.x, b.x, t);
            ret.y = Mathf.LerpAngle(a.y, b.y, t);
            ret.z = Mathf.LerpAngle(a.z, b.z, t);
            return ret;
        }
    }
}
```

アドベンチャーゲームなどで、次に行くべき場所を示したり、仕掛けを操作したことで動いたオブジェクトを一度見せたりするときに便利です。

https://twitter.com/flankids/status/1528463664767852546?s=20&t=2iHvio4lTO5OlsWZaSd3Dw

TPSゲームで通常のビューから銃を構えたビューに切り替えるなど、ちょっとしたモード変化にも使える仕組みです。

下記のズーム時のビューは`distance`(被写体からの距離)を小さくするだけでなく、`fieldOfView`(画角)も小さくすることで、位置的な近さ＋遠近感を薄めて遠くのものが見えやすいようにしています。また、`offsetPosition`（Main Cameraのローカル座標）の **x** の値をズラして、被写体であるプレイヤーが画面中央の照準に被らないようにしていることもポイントです。

https://twitter.com/flankids/status/1528464350704304128?s=20&t=LNPik-OpH0omMzT5Mrmw9Q

うまく拡張すれば、Timelineにも活用できるかもしれません。

# まとめ

複雑な計算なしに画面上のオブジェクトの見せ方を制御するカメラ設計ができた・・・と思ってますが、どうでしょう。
ある程度触るとビューを作るためにどんなカメラパラメータを用意すればよいか、数字でイメージが湧くようになってくるため、スクリプトでちょっとしたカメラ演出を作ることもできるはずです。

設計が単純でAnimatorやDOTweenによる制御もやりやすいので、簡単なカメラワークを作るときにこの設計がお役に立てると幸いです！