schedule2021-01-27

【Unity】TPSっぽいカメラワークをCinemahineでつくる

ユニティちゃんを動かせるようになりました。 キャラクターとのオフセットでカメラを追従させてましたが、ちょっと物足りなくなったのでTPSっぽくマウスでカメラワークを変更できるようにしました。

この記事では、TPS視点のカメラワークとCinemachineをスクリプトで拡張する方法です。CinemachineVirtualCameraCinemachineOrbitalTransposerをスクリプトで制御します。

操作はSkyrimをイメージしている。The Elder Scrollsの次回作が楽しみだけどまだ数年はかかるかな。

Unity 2019.4

Chinemachine

UnityのカメラツールのCinemachineというパッケージを使います。

CinemachineはUnityのカメラワークのためのパッケージです。 ターゲットをカメラで追う動きでも、定点で追う、ターゲットの真後ろから追う、ターゲットの周囲を回るなどたくさんの動きをノーコードで実現できます。 また、複数のカメラを滑らかに切り替える、手振れを付ける、画角に遊びを持たせるなど高度なカメラワークも出来ます。

▼ノーコードでもこのGIFのようにTPS視点+マウスで左右に操作することができるようになりました。 unitychan

他のカメラワークの動きについては▼のリンク先の動画がわかりやすいです。

▼Cinemachineの機能についてはこちらの記事を見た方がいい。わかりやすく手順が書いてあります。

Cinemachine のインストール

Unityの「Window」→「Package Manager」を開いて「Unity Resistry」で検索するとCinemachineが出てくるのでインポートします。

import

Cinemachineは外部依存関係を持ってないため、以上でOKです。

TPS視点で追従するカメラを作ってみる

Cinemahineを使ってノーコードでTPS視点+マウス操作のカメラを作っていきます。 ちなみにTPSはThird Person perspectiveの略で三人称視点という意味で、 一人称視点はFPS(First Person Perspective)です。 今までFPSはシューティングゲームの視点って意味かと思ってました。

CinemahineBrain

まずは、Main CameraCinemahineBrainのコンポーネントを追加します。 これによって後で追加するバーチャルカメラのカットやブレンドを制御します。

main

CinemahineVirtualCamera

続いて空のGameObjectを作ってCinemahineVirtualCameraコンポーネントを追加します。 バーチャルカメラのAim、Body、Noise プロパティーを使用して、バーチャルカメラ が位置、回転、その他のプロパティーをどのようにアニメーション化するかを設定します。 空のGameObjectはVirtualCameraと名付けます。

Followに紐づけたオブジェクトに追従し、Look Atのオブジェクトにカメラの照準を合わせます。 FollowLook Atにプレイヤーのunitychanをアタッチしました。

virtualcamera

CinemahineVirtualCameraBodyAimをそれぞれOrbital TranspoterComposerと選択しました。

Body

Bodyプロパティーは、バーチャルカメラを動かすアルゴリズムを指定します。

そのアルゴリズムの内のひとつOrbital Transposerは、バーチャルカメラの Followターゲットから可変の距離でUnityのカメラを移動させます。 Orbital TransposerのBinding Modeにもいくつか種類があって、これは好みで選んで下さい。動きが少しずつ異なります。 私はLock To Target With World Upがなんとなく好みで使ってます。

binfingmode

Aim

Aim プロパティーはバーチャルカメラの回転をどのように行うかを設定するものです。 Look At ターゲットをカメラフレーム内に維持するComposerを利用します。

aim

Noise

せっかくなのでNoiseにも言及します。 これはカメラの振動をシミュレートすることができます。 手振れや混乱、ドスンとする表現にも使えます。

動かしてみる

以上の設定で冒頭のTPS視点のカメラワークになりました。 unitychan

マウスを左右に動かすと水平方向にカメラの位置が回転します。

Cinemachineをスクリプトで拡張する

TPS視点のマウス操作で、水平方向の操作に加えて(個人的に)欲しいのは次の2点です。

  • 垂直方向のカメラの操作
  • ホイールで前後に動ける

この2つのスクリプトを書いて拡張します。

VirtualCameraオブジェクトにスクリプトを追加する。

VirtualCameraController.cs
using UnityEngine;
using Cinemachine;

[RequireComponent(typeof(CinemachineVirtualCamera))]
public class VirtualCameraController : MonoBehaviour
{
  private CinemachineVirtualCamera virtualCamera;
  private CinemachineOrbitalTransposer orbitalTransposer;
  private Vector2 lastMousePosition;
  // カメラの角度を格納する変数(初期値に0,0を代入)
  private Vector2 cameraAngle = new Vector2(0, 0);

  public float forwardSpeed;
  public float riseSpeed;
  void Start()
  {
    this.virtualCamera = this.GetComponent<CinemachineVirtualCamera>();
    this.orbitalTransposer = this.virtualCamera.GetComponentInChildren<CinemachineOrbitalTransposer>();
  }

  // Update is called once per frame
  void Update()
  {
    forwardViewPoint();
    heightViewPoint();
  }

  // 前後のカメラ操作
  private void forwardViewPoint()
  {
    // マウスホイールの回転値を変数 scroll に渡す
    float scroll = Input.GetAxis("Mouse ScrollWheel");
    Vector3 offset = this.virtualCamera.transform.forward * scroll * forwardSpeed;
    orbitalTransposer.m_FollowOffset -= offset;
    Debug.Log(offset.ToString());
  }


  // 垂直方向のカメラ操作
  private void heightViewPoint()
  {
    // 左クリックした時
    if (Input.GetMouseButtonDown(0))
    {
      // マウス座標を変数"lastMousePosition"に格納
      lastMousePosition = Input.mousePosition;
    }
    // 左ドラッグしている間
    else if (Input.GetMouseButton(0))
    {
      float y = (lastMousePosition.y - Input.mousePosition.y);
      orbitalTransposer.m_FollowOffset.y += y * riseSpeed;
      // マウス座標を変数"lastMousePosition"に格納
      lastMousePosition = Input.mousePosition;
    }
  }
}

Cinemachineのクラスを利用するため名前空間を追加。

[RequireComponent(typeof(CinemachineVirtualCamera))]はスクリプトをアタッチするオブジェクトにCinemachineVirtualCameraのアタッチも要求します。

forwardViewPoint()では前後のカメラ操作を制御します。 マウスホイールの前後に合わせてOrbital Transposerの位置を更新します。 this.virtualCamera.transform.forwardがカメラの方向ベクトルです。

heightViewPoint()はカメラの高さを制御します。 マウスをドラッグして視点の高さを動かせる。 lastMousePositionとマウスの上下の差分が、Orbital Transposerのzの値を下げるか上げるか決めます。

▼イメージ通りの動きになってきました! script

ホイールがスマホの拡大みたいにカクつくのと、水平方向の操作がマウスの絶対位置になってしまってるのでドラッグしたときに条件付けしたい。

おまけ、Cinemachineを使わずスクリプトで実現しようとすると

始めはCinemahineを使わずにTPS視点をスクリプトで実現しようとしてました。 結構いい感じにはなりましたが、水平方向が傾いたり動きがぎこちなかったりと細部を納得できるまで作りこむのはかなり大変だと感じました。

▼作ってみたスクリプトのカメラワーク。 マウスをドラッグして視点を動かせる。 垂直な軸の回転がうまくできておらず、横で動かすと水平軸が傾いてしまう。 makescript

一応、視点操作のリバースとFPS視点への切り替えも作りこんでいる。

CameraControll.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraControll : MonoBehaviour
{

  public Camera mainCamera;
  public GameObject playerObject;
  private GameObject cameraControll;
  public GameObject tpsViewPoint;
  public GameObject fpsViewPoint;

  // カメラの回転速度を格納する変数
  public Vector2 fpsRotationSpeed;
  public Vector2 tpsRotationSpeed;
  // カメラの前後の移動の速度
  public float tpsForwardSpeed;
  // マウスホイールの回転値を格納する変数
  private float scroll;

  // マウス移動方向とカメラ回転方向を反転する判定フラグ
  public bool isReversed;

  // true: 三人称視点(TPS)。false: 一人称視点(FPS)。
  public bool isTPS;
  // マウス座標を格納する変数
  private Vector2 lastMousePosition;
  // カメラの角度を格納する変数(初期値に0,0を代入)
  private Vector2 cameraAngle = new Vector2(0, 0);


  //呼び出し時に実行される関数
  void Start()
  {
    //メインカメラとユニティちゃんをそれぞれ取得
    cameraControll = GameObject.Find("CameraController");
    setViewPoint();
  }


  //単位時間ごとに実行される関数
  void Update()
  {
    //rotateCameraの呼び出し
    rotateCamera();
    trakingCamera();
  }

  //カメラを回転させる関数
  private void rotateCamera()
  {
    toggleViewPoint();

    if (isTPS)
    {
      thirdPersonShooter();
    }
    else
    {
      firstPersonShooter();
    }
  }

  // F5キーでTPS, FPSの切り替え
  private void toggleViewPoint()
  {
    if (Input.GetKeyDown(KeyCode.F5))
    {
      setViewPoint();
    }
  }

  // 視点をセットする
  private void setViewPoint()
  {
    isTPS = !isTPS;
    // 視点切り替え
    if (isTPS)
    {
      // TPS視点に初期化
      cameraAngle = Vector2.zero;
      mainCamera.transform.localEulerAngles = cameraAngle;
      mainCamera.transform.position = tpsViewPoint.transform.position;
    }
    else
    {
      // FPS視点に初期化
      cameraAngle = Vector2.zero;
      mainCamera.transform.localEulerAngles = cameraAngle;
      mainCamera.transform.position = fpsViewPoint.transform.position;
    }
    Debug.Log(mainCamera.transform.position.ToString());

  }

  // 一人称視点のカメラ
  private void firstPersonShooter()
  {

    // 左クリックした時
    if (Input.GetMouseButtonDown(0))
    {
      // カメラの角度を変数"cameraAngle"に格納
      cameraAngle = mainCamera.transform.localEulerAngles;
      // マウス座標を変数"lastMousePosition"に格納
      lastMousePosition = Input.mousePosition;
    }
    // 左ドラッグしている間
    else if (Input.GetMouseButton(0))
    {
      float reverse = isReversed ? -1f : 1f;
      // Y軸の回転:マウスドラッグ方向に視点回転
      // マウスの水平移動値に変数"rotationSpeed"を掛ける
      //(クリック時の座標とマウス座標の現在値の差分値)
      cameraAngle.y -= (Input.mousePosition.x - lastMousePosition.x) * reverse * fpsRotationSpeed.y;
      cameraAngle.x -= (lastMousePosition.y - Input.mousePosition.y) * reverse * fpsRotationSpeed.x;

      // カメラアングルの制限
      cameraAngle.y = cameraAngle.y > 80 ? 80 : cameraAngle.y;
      cameraAngle.y = cameraAngle.y < -80 ? -80 : cameraAngle.y;
      cameraAngle.x = cameraAngle.x > 80 ? 80 : cameraAngle.x;
      cameraAngle.x = cameraAngle.x < -80 ? -80 : cameraAngle.x;
      // "cameraAngle"の角度をカメラ角度に格納
      mainCamera.transform.localEulerAngles = cameraAngle;
      // マウス座標を変数"lastMousePosition"に格納
      lastMousePosition = Input.mousePosition;
    }
  }

  private void thirdPersonShooter()
  {
    // マウスホイールの回転値を変数 scroll に渡す
    scroll = Input.GetAxis("Mouse ScrollWheel");
    mainCamera.transform.position += mainCamera.transform.forward * scroll * tpsForwardSpeed;

    // 左クリックした時
    if (Input.GetMouseButtonDown(0))
    {
      // カメラの角度を変数"cameraAngle"に格納
      cameraAngle = mainCamera.transform.localEulerAngles;
      // マウス座標を変数"lastMousePosition"に格納
      lastMousePosition = Input.mousePosition;
    }
    // 左ドラッグしている間
    else if (Input.GetMouseButton(0))
    {
      float reverse = isReversed ? -1f : 1f;
      float x = (Input.mousePosition.x - lastMousePosition.x) * reverse;
      float y = (lastMousePosition.y - Input.mousePosition.y) * reverse;
      if (Mathf.Abs(x) < Mathf.Abs(y))
        x = 0;
      else
        y = 0;
      Vector3 angle = Vector3.zero;
      angle.x -= x * tpsRotationSpeed.x;
      angle.y -= y * tpsRotationSpeed.y;
      // カメラアングルの制限
      angle.y = angle.y > 80 ? 80 : angle.y;
      angle.y = angle.y < -80 ? -80 : angle.y;
      // "cameraAngle"の角度をカメラ角度に格納
      mainCamera.transform.RotateAround(playerObject.transform.position, Vector3.up, angle.x);
      mainCamera.transform.RotateAround(playerObject.transform.position, transform.right, angle.y);
      // マウス座標を変数"lastMousePosition"に格納
      lastMousePosition = Input.mousePosition;
    }
  }

  // Playerを追跡するカメラ
  private void trakingCamera()
  {
    cameraControll.transform.position = playerObject.transform.position;
    cameraControll.transform.rotation = playerObject.transform.rotation;
    // playerObject.transform.rotation = mainCamera.transform.rotation;
  }
}

前フレームのマウスの位置lastMousePositionと現在のマウスの位置の差分だけX,Y軸方向のアングルを変えることでマウスと連動したカメラワークを実現してます。

TPS視点はアングルをキャラクター方向に向けつつ、transform.RotateAround()でプレイヤーを中心に回転している。 FPS視点はカメラを動かさなくて良いため、アングルだけマウスに合わせて向きを変える。

▼Inspecterと設定。 CameraControllerをキャラクターに追従させて、子オブジェクトのViewPointにメインカメラの位置を切り替えている。

makescriptinspecter