【Unity】Cinemachineをスクリプトで機能拡張する

Cinemachineに独自の動きを追加させたいけど、何か良い方法はないの~?

CinemachineのExtensionを使えば良いわ。

Cinemachineはスクリプト無しでも様々なカメラワークが作れるのが特徴ですが、独自のスクリプトで機能拡張することで、更に凝ったカメラワークを作成する事も可能です。

これはExtension(拡張機能)としてコンポーネントを自作することで実現できます。

参考:Extension | Cinemachine | 2.6.0

コンポーネントを自作する方法はシンプルで、CinemachineExtensionクラスを継承した独自クラスを定義してスクリプトとしてUnityプロジェクト内に配置するだけです。

これで、次のようにCinemachine側に独自のExtensionが適用できるようになります。

本記事では、このようなExtensionを独自実装してCinemachineを拡張する方法について解説していきます。

この作品はユニティちゃんライセンス条項の元に提供されています

動作環境
  • Unity2021.1.14f1
  • Cinemachine2.7.4

前提条件

予めシーン上にCinemachineVirtualCameraが配置されているものとします。

このバーチャルカメラに対して独自のExtensionを適用していきます。

Cinemachineの設定が分からない方は、以下記事をご覧ください。

【Unity】滑らかに(遅れて)追従するカメラを簡単に実装する方法
Cinemachineを用いてキャラクターを追従するカメラワークを実装する方法の解説記事です。 キャラクターを追従するカメラワークはスクリプトでも実装できますが、Cinemachineを使うことで次のメリットが得られます ...

Extensionの仕組み

Cinemachineでは、カメラワーク処理の合間に独自処理を追加できる仕組みを備えています。これはExtension(拡張機能)という形で提供され、CinemachineExtensionクラスの派生クラスを実装することで独自の拡張を追加できます。

参考:Extension | Cinemachine | 2.6.0

ExtensionはCinemachine側でいくつか用意されており、これらをExtensionとして使う事もできます。バーチャルカメラコンポーネントのExtension > Add Extensionから該当のExtensionを追加します。

例えば、CinemachineCameraOffsetを使用すると、Cinemachineのカメラワーク処理によって計算された位置を補正できるようになります。

独自のExtensionを実装する

ここからは、Extensionをスクリプトで実装する手順を解説していきます。

Extensionクラスの作成

CinemachineExtensionクラスの派生クラスをスクリプトとして作成します。
Extensionの最小スクリプトは次のようになります。

TestCinemachineExtension.cs
using Cinemachine;
using UnityEngine;

/// <summary>
/// Extensionのテスト
/// </summary>
public class TestCinemachineExtension : CinemachineExtension
{
    // 各Stageのカメラワーク処理後に呼ばれるコールバック
    protected override void PostPipelineStageCallback(
        CinemachineVirtualCameraBase vcam,
        CinemachineCore.Stage stage,
        ref CameraState state,
        float deltaTime
    )
    {
        // ログにstageの値を出力
        Debug.Log($"stage = {stage}");
    }
}

引数stageの内容をコンソールログに出力するだけのサンプルです。

上記スクリプトをTestCinemachineExtension.csというファイル名でUnityプロジェクトに保存すると、CinemachineのバーチャルカメラにExtensionとして追加できるようになります。

上記を選択すると、コンポーネントとしてExtensionが追加されます。

この状態でゲームを実行すると、以下のような内容がコンソールログに出力されます。

Extensionの実行タイミング

上記コードのPostPipelineStageCallback()メソッドは、バーチャルカメラ側から呼び出されます。

Cinemachineでは、カメラワーク処理をBody(移動)Aim(回転)Noise(振動)の順に処理するパイプラインで管理しています。PostPipelineStageCallback()コールバックは、このパイプライン処理の合間に実行されます。

実行されるタイミングは、1フレームあたりのカメラワーク処理の中で次の4か所となります。

  • カメラの移動後(Body処理後)
  • カメラの回転後(Aim処理後)
  • カメラの振動後(Noise処理後)
  • カメラワークのパイプライン処理が完了した後

どのタイミングの呼び出しかは、PostPipelineStageCallback()メソッドの第2引数stageの値からチェックできます。特定のタイミングだけで処理したい場合、stageの値によって条件分岐させるコードで実現できます。

protected override void PostPipelineStageCallback(
    CinemachineVirtualCameraBase vcam,
    CinemachineCore.Stage stage,
    ref CameraState state,
    float deltaTime
)
{
    if (stage == CinemachineCore.Stage.Aim) {
        // Aim直後の処理
    }
}

PostPipelineStageCallbackの引数

CinemachineExtension.PostPipelineStageCallback()コールバックは次の形式で定義されています。

protected abstract void PostPipelineStageCallback(
    CinemachineVirtualCameraBase vcam,
    CinemachineCore.Stage stage,
    ref CameraState state,
    float deltaTime
);

第1引数vcamには、対象のバーチャルカメラが指定されます。

第2引数stageには、実行タイミングが指定されます。

第3引数stateには、カメラ状態の構造体データが参照渡しされます。Extensionで計算した結果は、このstateの中身を編集する形で格納します。

第4引数deltaTimeには、現在のデルタタイムが指定されます。

参考:Class CinemachineExtension | Package Manager UI website

Extensionによるカメラワークの実装例

Extensionを使ったCinemachineの拡張例として、マウスホイールのスクロールでズームイン/ズームアウトする機能を追加する例を紹介します。

サンプルスクリプト

MouseWheelZoom.cs
using Cinemachine;
using UnityEngine;

/// <summary>
/// マウスホイールによるズームを行えるようにする拡張
/// </summary>
public class MouseWheelZoom : CinemachineExtension
{
    // 画角の最小値
    [SerializeField] private float _minFOV = 10;

    // 画角の最大値
    [SerializeField] private float _maxFOV = 80;

    // 画角の変更単位
    [SerializeField] private float _angleStep = 10;

    private int _scrollDelta;
    private float _adjustAngle;

    // カメラワーク処理
    protected override void PostPipelineStageCallback(
        CinemachineVirtualCameraBase vcam,
        CinemachineCore.Stage stage,
        ref CameraState state,
        float deltaTime
    )
    {
        // Aimの直後だけ処理を実施
        if (stage != CinemachineCore.Stage.Aim)
            return;

        var lens = state.Lens;

        // マウスホイール操作があったら
        if (_scrollDelta != 0)
        {
            // 画角をスクロール方向に変更
            _adjustAngle = Mathf.Clamp(
                _adjustAngle - _scrollDelta * _angleStep,
                _minFOV - lens.FieldOfView,
                _maxFOV - lens.FieldOfView
            );

            _scrollDelta = 0;
        }

        // stateの内容は毎回リセットされるので、
        // 毎回補正する必要がある
        lens.FieldOfView += _adjustAngle;

        state.Lens = lens;
    }

    // マウスホイール操作チェック
    private void Update()
    {
        // マウスホイールの移動量取得
        var delta = Input.mouseScrollDelta.y;
        if (Mathf.Approximately(delta, 0))
            return;

        // 移動量を保持
        _scrollDelta = (int) delta;
    }
}

このスクリプトをMouseWheelZoom.csとしてUnityプロジェクトに保存すると、バーチャルカメラのExtensionからMouseWheelZoomが選択できるようになります。

実行結果

マウスホイールをスクロールさせると、カメラがズームイン/ズームアウトするようになりました。

サンプルスクリプトの説明

サンプルスクリプトでどのような処理を行っているかについても軽く触れておきます。

タイミングの判定

次の処理で、Aim直後だけExtensionのカメラワーク処理を実行するようにしています。

// Aimの直後だけ処理を実施
if (stage != CinemachineCore.Stage.Aim)
    return;

マウスホイールによる画角の調整

バーチャルカメラの画角は、state.Lens.FieldOfViewフィールドに格納されています。
state.Lensは構造体なので、一時的に変数に格納してから値を書き換えるようにしています。

var lens = state.Lens;

// マウスホイール操作があったら
if (_scrollDelta != 0)
{
    // 画角をスクロール方向に変更
    _adjustAngle = Mathf.Clamp(
        _adjustAngle - _scrollDelta * _angleStep,
        _minFOV - lens.FieldOfView,
        _maxFOV - lens.FieldOfView
    );

    _scrollDelta = 0;
}

// stateの内容は毎回リセットされるので、
// 毎回補正する必要がある
lens.FieldOfView += _adjustAngle;

state.Lens = lens;

stateの内部状態は、Cinemachineのカメラワーク処理が実行されるたびに元に戻ります。前回書き換えた値が引き継がれないことに注意です。

そのため、コールバックが呼び出されるたびにFieldOfViewの内容を書き換えています。

マウスホイールのスクロール判定

マウスホイール操作の状態取得は、PostPipelineStageCallbackではなくUpdateイベントで行うようにします。

// マウスホイール操作チェック
private void Update()
{
    // マウスホイールの移動量取得
    var delta = Input.mouseScrollDelta.y;
    if (Mathf.Approximately(delta, 0))
        return;

    // 移動量を保持
    _scrollDelta = (int) delta;
}

これは、PostPipelineStageCallbackの呼び出しはFixedUpdateイベントで行われる可能性があり、マウスホイールの操作量を正しく取得できなくなる可能性があるためです。

さいごに

本記事では、Cinemachineをスクリプトで拡張する方法について解説しました。

Extensionは、Cinemachineに独自のカメラワークを追加していける便利な機能です。Cinemachineの既存のカメラワークだけではどうしても物足りない場合に検討すると良いでしょう。

参考サイト