【Unity】Cinemachineでマウスホイールのズームを拡張機能で実装する

こじゃらこじゃら

Cinemachineでマウスホイールによるズームイン/ズームアウトを実現する方法を教えてほしいの…

このはこのは

スクリプトを書くことになるけど実現可能だわ。拡張機能(Extension)として作れば色々なカメラに適用できるようになっておススメだわ。

Cinemachineカメラをマウスホイールでズームイン/ズームアウトする方法の紹介です。

本記事では拡張機能(Extension)を用いて、既存のカメラワークにFOV制御を加える方法を紹介します。

拡張機能を用いると、任意のバーチャルカメラに対して挙動を拡張できるようになるため、柔軟性や汎用性が高まるというメリットがあります。

本記事の内容を実践すると、次のようなカメラワークが実現できます。

FOVの滑らかな変化を実現する方法も紹介します。また、Input Systemに対応させる方法も紹介します。

本記事で解説する方法は、マウスホイール操作を基本としていますが、キーボード入力やゲームパッドのジョイスティック入力などにも対応しています。

UCL UCL

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

動作環境
  • Unity2021.2.3f1
  • Cinemachine2.8.2
  • Input System1.1.1

スポンサーリンク

前提条件

予めCinemachineでキャラクターを追従するカメラワークがセットアップされているものとします。

セットアップ方法が分からない方は、以下記事をご覧ください。

拡張機能(Extension)とは

Cinemachineのバーチャルカメラの動作を拡張するためのコンポーネントです。バーチャルカメラは、複数の拡張機能を追加できる仕組みを備えています。

参考:Extension | Cinemachine | 2.6.0

今回は、マウスホイールでカメラのFOVを変える拡張機能を実装する形でズームイン/ズームアウト操作を実現していきます。

拡張機能について詳しく知りたい方は、以下記事も参考にしてください。

マウスホイールでズーム操作する拡張機能

初めに、マウスホイールでFOV調整する最小限の拡張機能の例です。マウスホイールの移動量取得にはInput Managerを経由しています。

CinemachineUserInputZoom.cs
using Cinemachine;
using UnityEngine;

public class CinemachineUserInputZoom : CinemachineExtension
{
    // Input Managerの入力名
    [SerializeField] private string _inputName = "Mouse ScrollWheel";

    // 入力値に掛ける値
    [SerializeField] private float _inputScale = 100;

    // FOVの最小・最大
    [SerializeField, Range(1, 179)] private float _minFOV = 10;
    [SerializeField, Range(1, 179)] private float _maxFOV = 90;

    // ユーザー入力を必要とする
    public override bool RequiresUserInput => true;

    private float _scrollDelta;
    private float _adjustFOV;

    private void Update()
    {
        // スクロール量を加算
        _scrollDelta += Input.GetAxis(_inputName);
    }

    // 各ステージ毎に実行されるコールバック
    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 (!Mathf.Approximately(_scrollDelta, 0))
        {
            // FOVの補正量を計算
            _adjustFOV = Mathf.Clamp(
                _adjustFOV - _scrollDelta * _inputScale,
                _minFOV - lens.FieldOfView,
                _maxFOV - lens.FieldOfView
            );

            _scrollDelta = 0;
        }

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

        state.Lens = lens;
    }
}

スクリプトの適用方法

上記スクリプトをCinemachineUserInputZoom.csとしてUnityプロジェクト内に保存します。

保存したら、該当するバーチャルカメラのExtensions > Add Extensionから該当の拡張機能CinemachineUserInputZoomを選択します。

必要に応じてパラメータを調整します。

Input Nameにはマウスホイール入力の名前、Input Scaleには入力値に掛ける係数、Min FOVとMax FOVにそれぞれFOVの最小と最大を指定します。

実行結果

マウスホイールで段階的にズームイン/ズームアウトできるようになりました。

スクリプトの解説

スクリプトの内容について知りたい方向けに、要点を解説します。

独自の拡張機能クラスの定義

CinemachineExtensionの継承クラスを実装すると、拡張機能を自作することができます。

public class CinemachineUserInputZoom : CinemachineExtension

ユーザー入力の要求

次のコードで、Cinemachine側にユーザー入力を必要とするフラグを返すようにしています。

// ユーザー入力を必要とする
public override bool RequiresUserInput => true;

参考:Class CinemachineExtension| Cinemachine | 2.8.9

マウスホイールのスクロール量の計算

スクロール量の計算は、Updateイベントで行うようにしています。

private void Update()
{
    // スクロール量を加算
    _scrollDelta += Input.GetAxis(_inputName);
}

Updateイベント内で行う理由は、後述するカメラ制御コールバックがFixedUpdateイベントから発行される可能性があり、この場合は正しくスクロールを検知できないタイミングが発生してしまうためです。

カメラ制御コールバックの定義

CinemachineExtension継承クラスでは、次のメソッド定義を必須としています。

// 各ステージ毎に実行されるコールバック
protected override void PostPipelineStageCallback(
    CinemachineVirtualCameraBase vcam,
    CinemachineCore.Stage stage,
    ref CameraState state,
    float deltaTime
)

引数の説明は割愛します。詳しく知りたい方は、以下公式リファレンスか記事をご覧ください。

参考:Class CinemachineExtension| Cinemachine | 2.8.9

ズーム処理は、Aimの処理が終わった後に行うようにしています。

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

FOVの補正量の計算

ズーム制御の肝となる部分は次のコードです。

// FOVの補正量を計算
_adjustFOV = Mathf.Clamp(
    _adjustFOV - _scrollDelta * _inputScale,
    _minFOV - lens.FieldOfView,
    _maxFOV - lens.FieldOfView
);

_scrollDelta = 0;

マウスホイールがスクロールされていれば、_scrollDelta変数スクロール量が積算されるので、カメラFOVの補正値_adjustFOV加算しています。

FOVの下限と上限を設けているため、この範囲を超えないようにClamp関数で値を丸めています。

_scrollDeltaの値はUpdateイベント内で積算されるようになっているので、計算に使ったら0にリセットする必要があります。

FOVの反映

計算したFOVの補正値は、次のコードで加算して反映します。

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

state.Lens = lens;

補足コメントの通り、コールバック毎に毎回FOVを補正する必要があることに注意します。

滑らかなズーム操作に対応する

先述のサンプルに対し、滑らかにFOVを変化させるように改変した拡張機能スクリプトです。

CinemachineUserInputZoom.cs
using Cinemachine;
using UnityEngine;

public class CinemachineUserInputZoom : CinemachineExtension
{
    // Input Managerの入力名
    [SerializeField] private string _inputName = "Mouse ScrollWheel";

    // 入力値に掛ける値
    [SerializeField] private float _inputScale = 100;

    // FOVの最小・最大
    [SerializeField, Range(1, 179)] private float _minFOV = 10;
    [SerializeField, Range(1, 179)] private float _maxFOV = 90;

    // 変化するおおよその時間[s]
    [SerializeField] private float _smoothTime = 0.1f;
    // 変化の最大速度
    [SerializeField] private float _maxSpeed = Mathf.Infinity;

    // ユーザー入力を必要とする
    public override bool RequiresUserInput => true;

    private float _scrollDelta;
    
    // 滑らかに変化するFOVの計算用変数
    private float _targetAdjustFOV;
    private float _currentAdjustFOV;
    private float _currentAdjustFOVVelocity;

    private void Update()
    {
        // スクロール量を加算
        _scrollDelta += Input.GetAxis(_inputName);
    }

    // 各ステージ毎に実行されるコールバック
    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 (!Mathf.Approximately(_scrollDelta, 0))
        {
            // FOVの補正量を計算
            _targetAdjustFOV = Mathf.Clamp(
                _targetAdjustFOV - _scrollDelta * _inputScale,
                _minFOV - lens.FieldOfView,
                _maxFOV - lens.FieldOfView
            );

            _scrollDelta = 0;
        }
        
        // 滑らかに変化するFOV値を計算
        _currentAdjustFOV = Mathf.SmoothDamp(
            _currentAdjustFOV,
            _targetAdjustFOV,
            ref _currentAdjustFOVVelocity,
            _smoothTime,
            _maxSpeed,
            deltaTime
        );

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

        state.Lens = lens;
    }
}

以下の赤枠部分のパラメータが追加されています。

Smooth TImeにはFOVの変化にかかるおおよその時間、Max SpeedにはFOV変化の最大速度を指定します。

実行結果

FOVが滑らかに変化するようになりました。

動画では、おおよそ0.1秒かけてFOVを変化させるようにしています。

ソースコードの説明

主な変更箇所は以下部分です。

// 滑らかに変化するFOV値を計算
_currentAdjustFOV = Mathf.SmoothDamp(
    _currentAdjustFOV,
    _targetAdjustFOV,
    ref _currentAdjustFOVVelocity,
    _smoothTime,
    _maxSpeed,
    deltaTime
);

Mathf.SmoothDamp()メソッドを使ってFOVを目標値_targetAdjustFOVに滑らかに変化させるようにしています。

メソッドの戻り値を代入している_currentAdjustFOVに滑らかに変化するFOVが格納されます。

あとは、この値を補正値として加算するのみです。

lens.FieldOfView += _currentAdjustFOV;

SmoothDamp関数の使い方については、以下記事をご覧ください。

Input Systemへの対応

ここまで紹介した例では、すべてInput Manager経由でFOVを制御していました。

CinemachineがInput System経由で入力値を受け取れるようにするためには、CinemachineInputProviderコンポーネントをバーチャルカメラにアタッチしておく必要があります。

そして、拡張機能のスクリプト側では、このCinemachineInputProviderコンポーネントからInput Systemの入力値を参照するようにします。

参考:代替の入力システム | Cinemachine | 2.6.0

以上を踏まえたInput System対応版のサンプルスクリプトは次のようになります。

CinemachineUserInputZoom.cs
using Cinemachine;
using UnityEngine;

public class CinemachineUserInputZoom : CinemachineExtension
{
    // Input Managerの入力名
    [SerializeField] private string _inputName = "Mouse ScrollWheel";

    // 入力値に掛ける値
    [SerializeField] private float _inputScale = 100;

    // FOVの最小・最大
    [SerializeField, Range(1, 179)] private float _minFOV = 10;
    [SerializeField, Range(1, 179)] private float _maxFOV = 90;

    // 変化するおおよその時間[s]
    [SerializeField] private float _smoothTime = 0.1f;

    // 変化の最大速度
    [SerializeField] private float _maxSpeed = Mathf.Infinity;

    // ユーザー入力を必要とする
    public override bool RequiresUserInput => true;

    private float _scrollDelta;

    // 滑らかに変化するFOVの計算用変数
    private float _targetAdjustFOV;
    private float _currentAdjustFOV;
    private float _currentAdjustFOVVelocity;

    // Input Systemの入力を受け取るためのコンポーネント
    private CinemachineInputProvider _inputProvider;

    protected override void Awake()
    {
        base.Awake();

        // Input Systemの入力受け取り元
        _inputProvider = GetComponent<CinemachineInputProvider>();

        if (_inputProvider != null)
        {
            // スクロール量を加算(Input System)
            _inputProvider.ZAxis.action.performed +=
                context => _scrollDelta = context.ReadValue<float>();
        }
    }

    private void Update()
    {
        // Input System使用時はInput ManagerのAPIを参照しない
        if (_inputProvider != null)
            return;

        // スクロール量を加算(Input Manager)
        _scrollDelta += Input.GetAxis(_inputName);
    }

    // 各ステージ毎に実行されるコールバック
    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 (!Mathf.Approximately(_scrollDelta, 0))
        {
            // FOVの補正量を計算
            _targetAdjustFOV = Mathf.Clamp(
                _targetAdjustFOV - _scrollDelta * _inputScale,
                _minFOV - lens.FieldOfView,
                _maxFOV - lens.FieldOfView
            );

            _scrollDelta = 0;
        }

        // 滑らかに変化するFOV値を計算
        _currentAdjustFOV = Mathf.SmoothDamp(
            _currentAdjustFOV,
            _targetAdjustFOV,
            ref _currentAdjustFOVVelocity,
            _smoothTime,
            _maxSpeed,
            deltaTime
        );

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

        state.Lens = lens;
    }
}

Input Systemシステムを使うためには、以下動画のようにCinemachineInputProviderコンポーネントを追加して、Z Axis項目にアクションを指定します。

実行結果は2つ目の例と一緒のため割愛します。

ソースコードの説明

Input System経由でスクロール量を取得する処理は次の部分です。

protected override void Awake()
{
    base.Awake();

    // Input Systemの入力受け取り元
    _inputProvider = GetComponent<CinemachineInputProvider>();

    if (_inputProvider != null)
    {
        // スクロール量を加算(Input System)
        _inputProvider.ZAxis.action.performed +=
            context => _scrollDelta = context.ReadValue<float>();
    }
}

CinemachineExtension継承クラスでAwake()メソッドを定義する場合は、オーバーライドして必ずスーパークラスのAwake()メソッドを呼び出す必要があります。

参考:Class CinemachineExtension| Cinemachine | 2.8.9

そして、GetComponent()メソッドでCinemachineInputProviderコンポーネントを取得し、取得出来たらInput Actionのperformedイベントにスクロール量を取得する処理を登録します。

取得する軸はZAxisとしました。

また、このままではInput Managerを必ず参照してしまうため、Updateイベント内で次のようにCinemachineInputProviderを取得出来ていたら参照しないように対策しています。

private void Update()
{
    // Input System使用時はInput ManagerのAPIを参照しない
    if (_inputProvider != null)
        return;

    // スクロール量を加算(Input Manager)
    _scrollDelta += Input.GetAxis(_inputName);
}

さいごに

Cinemachineの拡張機能(Extension)を使うと、様々なバーチャルカメラに対して動きを加えることができるようになります。

また、Input Systemを使いたい場合はCinemachineInputProviderコンポーネントを通じて行うようにすると汎用性が高まるでしょう。

Input ManagerやInput System経由で入力値を取得する方式をとっているため、ゲームパッドなどあらゆる操作に対応しています。

関連記事

参考サイト

スポンサーリンク