Unityで自前TPSカメラを作る

# はじめに

こんにちは、やまだたいしです。
当記事はUnity #2 Advent Calendar 2020 14日目の記事です。
https://qiita.com/advent-calendar/2020/unity2

今回Unityで特にアセットを使わずにTPSカメラを作っていこうと思います。
今回の実装方法は特に言い伝えられている実装方法ではなく私独自の実装方法です。
普通に実装するならcinemachineのFreeLook Cameraを使うことをおすすめします。

#追従モード

TPSカメラと一口に言っても、大まかに2つのモードがあると思っています。
ここでは追従モード・非追従モードと呼ばせて貰います。

最近Switchで録画したゼルダ無双なのですが、こちらは比較的分かりやすいです。
参考までに貼っておきます。

https://twitter.com/OrotiYamatano/status/1328234363276824576

この動画で言うところの走っている時のカメラの動きが追従モードで、軽く走っている所が非追従モードです。

## 非追従モード

ちょっと分かりづらいですが図を用意しました。
カメラを常にキャラクターから相対位置に配置します。
コントローラーの左右を入力すると、そのまま左右にキャラクターが移動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/ac9a3e26-c49e-ec83-3f62-68b3de4f9db8.png)


## 追従モード

カメラがキャラクターに追従するように相対距離のみ保ちます。
キャラクターから糸がついているかのような動作をします。
コントローラーの左右を入力するとカメラを中心に円を描くように移動します。
走っているとカメラがキャラクターの後ろに勝手に回り込みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/a9cd11dd-99b8-96d6-2390-6316f74888c8.png)

## 非追従モード・追従モードの利点欠点

ここで追従モード・非追従モードどちらが良いのかという利点欠点を書いて置こうと思います。
私が思う部分なので他にもあるかも知れません。


- 敵キャラクターを見ながら逃げれるか
    - 非追従モードではカメラが後ろに回り込まないので戦場判断がしやすいです
    - 追従モードでは勝手にカメラが後ろに回り込むので背後の敵に気を配ったりするのが大変です
        - ロックオンがあるゲームもありますが、今回は割愛
- 思った方向に攻撃が出せるか
    - 非追従モードでは真横に攻撃右下、左上傾けた方向に対してキャラクターがアクションするため直感的
    - 追従モードではコントローラーの操作を傾けた方向よりも違う方向に攻撃が出るため若干操作がし辛いです
- 勝手に回転してくれるか
    - 非追従モードでは移動の際に自動的に回転してくれないため一々ユーザー側でカメラの操作をしてやる必要があります
    - 追従モードでは勝手にカメラが後ろに追従してくれるため一々カメラの操作をしてやる必要がないです(アクションじゃないRPGとかでよく使われる)

と言った利点、欠点がそれぞれあります。
簡単に言うと、非追従モードは戦闘特化、追従モードは移動特化です。

ゼルダ無双は戦闘中は基本的に非追従モードのようですが、ある一定上距離の出る早い攻撃をする場合は
追従モードに切り切り替わるようになっている気がします。
製作者の心遣いが感じられます。

また、ゲームによって激しく戦闘するゲームでは非追従モードであってもプレイヤーの意図しないカメラワークが行われ3D酔いが発生します。
それを発生させないために追従距離に対して遊び、ピッタリとついてこない範囲を設けてカメラが激しく動かないようすることもあるようです。


# 作ってみよう非追従モードカメラ・追従モードカメラ

今回はInputSystemを使用して書いています。
Unity Versionは 2019.4.16f1です。
ロジック自体は以前の入力システムの考え方に置き換えれば使えると思うので以前の入力システムの方も読めば何となく分かると思います。

キャラクターの移動のスクリプトに関しては、追従モード・非追従モードのどちらでも使える書き方があるので
そちらを簡単に紹介しようと思います。

なお、スクリプトに関しては、今回の記事用に簡単に作成したもので、気持ちのいい操作感にするためには、
また別途修正する必要があると思うので個別に修正をオススメします。


## InputSystemの設定

InputSystemは今回は記事の中心ではないため、ある程度話は割愛しますが簡単に私の今回の設定の解説をします。
私もInputSystemについては理解していない部分もあるのですが、現在の設定はこんな感じです。
PlayerとCamera2つ用意しそれぞれのスクリプトで利用するのを想定しました。
とりあえず、移動用とカメラ回転用です。
ロジクールのコントローラーをPCに指して利用。
ActionTypeはValeでコントローラーの倒し具合を取得、値の受け取りはVector2Dに設定。(本当はAxisを利用したかったのですがUnityでは、まだ使えない?)
PlayerとCameraとでInteractionsとProcessorsの設定が異なりますが、ここはチョット私も分かっていなくて触っている途中ですので、
それぞれで触って見てください。

安定のテラシュールブログさんがわかりやすかったです。
http://tsubakit1.hateblo.jp/entry/2019/01/09/001510

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/642164c7-e7b4-e83b-6266-93dc0bc8f084.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/6c372c26-0e41-c5fc-eca4-036828f83716.png)

## キャラクターの操作

まずは非追従モード・追従モードどちらでも使えるキャラクターの操作を紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/ec28242a-5b16-0dcb-ab41-6dc22bdf4658.png)
今回の記事用にモデリングしたキャラ「くわぽん」です。
実際に私が書いたコードはこんな感じです。

以下スクリプトは「くわぽん」にアタッチ。

```player.cs

using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour, Controls.IPlayerActions{
    private Controls inputPlayer;        //インプット情報
    private Vector2 inputLeftStick;    //入力値を保持
    private Vector3 prevPos;  //前回のPosition
    public Rigidbody rb;    //自身のRigidBody

    private void Awake(){
        inputPlayer = new Controls();
        inputPlayer.Player.SetCallbacks(this);    //InputSystemのPlayer定義をここで使うという宣言
        inputLeftStick = Vector2.zero;
        rb = GetComponent<Rigidbody>();
    }

    private void OnEnable() {
        inputPlayer.Enable();
    }

    private void OnDisable() {
        inputPlayer.Disable();
    }

    private void FixedUpdate() {
        float horizontal = 0;
        float vertical = 0;
        
        if (Mathf.Abs(inputLeftStick.x) > 0.25f){    //入力排除範囲いらないかも？円状に入力禁止(デッドゾーンの定義)をしたいなら別途斜辺を求める計算でXとY合わせて計算が必要
            horizontal = inputLeftStick.x * Time.deltaTime * 40;
        }

        if (Mathf.Abs(inputLeftStick.y) > 0.25f){   //いらないかも？
            vertical = inputLeftStick.y * Time.deltaTime * 40;
        }

        //空中でも移動できてしまうので、気になる人は地面に着地してる判定をRayを飛ばして判定すると良いかも。今回は割愛
        Vector3 moveVec = transform.position;
        //カメラの回転に合わせて入力方向を変換
        if (!(Camera.main is null)) {
            var forward = Camera.main.transform.forward;
            var x = Quaternion.FromToRotation(new Vector3(1.0f, 0f, 0), new Vector3(forward.x, 0f, forward.z));
            var z = Quaternion.FromToRotation(new Vector3(0.0f, 0f, 1.0f), new Vector3(forward.x, 0f, forward.z));
            moveVec += (z * Vector3.forward) * vertical;    
            moveVec += (x * Vector3.forward) * -horizontal;
        }
        rb.MovePosition(moveVec);    //移動(CharacterComponentでの移動でも良いかも？)
        
        
        //キャラクターの向きの補正-------------------------------------------------
        var position = transform.position;
        position.y = 0;    //y方向には補正しない
        Vector3 diff = position - prevPos;
        prevPos = position;
        if (diff.magnitude > 0.01f){
            transform.rotation = Quaternion.LookRotation(diff); //向きを変更する
        }
        //---------------------------------------------------------------------
    }

    public void OnAxisLeft(InputAction.CallbackContext context) {    //InputSystemから値を取得
        inputLeftStick.x = context.ReadValue<Vector2>().x;    //Lスティックの値を格納しておく
        inputLeftStick.y = context.ReadValue<Vector2>().y;    //Lスティックの値を格納しておく
    }

}

```

InputSystemの設定 or 入力拒否範囲の設定だと思うのですが、ちょっと入力の値が怪しいです。
まぁ、今回の解説の主旨ではないので気にせず続けます。(気が向いたら記事を直します)

![camera.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/345e4474-56e0-ca4d-0a56-cc167734e526.gif)

重要なポイントはカメラの回転角度を参照しキャラクターの移動を行っているところです。
この設定がないとカメラを回転したときにキャラクターが上手く動いてくれません。

## 非追従モードのカメラ

こちらのカメラについては結構Unity TPSと検索すると出てくる一般的なカメラです。
非追従モードのカメラを作るのは比較的簡単です。
カメラをキャラクターの位置に追従させるロジックを作成すればいいです。

GameObjectの構成を↓のようにして、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/e232fed1-ca01-d18f-26be-d5d714ba7eb1.png)
メインカメラのポジションを↓のように10Xぶん離します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/d065bfb7-ab53-94d2-ab07-74f6421e7756.png)
そうするとCameraArmObjectから常に10離れた位置にカメラが出来ます。
後はCamraArmをキャラクターにくっつくように移動させ、カメラの向きを常にキャラクターに向けるようにしてあげれば良いです。

以下スクリプトはCameraArmにアタッチ。

```SpringArmScript.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class CameraArmScript : MonoBehaviour, Controls.ICameraActions {
    private Controls inputCamera;
    [SerializeField] private Transform character;
    [SerializeField] private GameObject cameraObj;

    private Vector2 inputRightStick;
    private Vector3 lookPos;        //カメラが見る方向を格納
    private Vector3 followPos;      //CameraArmが追従する位置を格納
    private float horizontal;       //現在の水平角度
    private float vertical;        //現在の垂直角度


    private void Awake() {
        inputCamera = new Controls();
        inputRightStick = Vector2.zero;
        inputCamera.Camera.SetCallbacks(this);
    }

    private void OnEnable() {
        inputCamera.Enable();
    }

    private void OnDisable() {
        inputCamera.Disable();
    }

    private void Update() {
        var position = character.position;
        lookPos = Vector3.Lerp(lookPos, position, 0.1f);    //カメラの見る位置を減速移動で計算
        followPos = Vector3.Lerp(followPos, position, 0.1f);    //追従する位置を減速移動で計算
    }

    private void FixedUpdate() {
        if (Mathf.Abs(inputRightStick.x) > 0.25f) {    //入力制限。いらないかも
            horizontal += inputRightStick.x * Time.deltaTime * 40;
        }

        if (Mathf.Abs(inputRightStick.y) > 0.25f) {    //入力制限。いらないかも
            vertical += inputRightStick.y * Time.deltaTime * 40;
            //回りすぎるとUnityでは上下反転してしまうので上限を設ける-98～98までなら設定可能だと思う
            vertical = Mathf.Clamp(vertical, -80, 80);
        }

        Transform myTransform;
        (myTransform = transform).rotation = Quaternion.Euler(0, horizontal, -vertical);    
        myTransform.position = followPos;
        cameraObj.transform.LookAt(lookPos);
    }

    public void OnAxisRight(InputAction.CallbackContext context) {    //InputSystemから値を取得
        inputRightStick.x = context.ReadValue<Vector2>().x;
        inputRightStick.y = context.ReadValue<Vector2>().y;
    }

    public void OnR_Stick_Button(InputAction.CallbackContext context) {
        Debug.Log("ButtonLeftStick");        //カメラリセットの処理を書く予定でしたが、遅刻しているので割愛時間がアレば修正します
    }
}
```

![hituijyuu.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/3ab9f56d-b7ba-1611-68fd-a80757c58c96.gif)

減速移動でカメラの位置と角度を決めている理由は、キャラクターが猛スピードで動いたときに遅れて追従することでスピード感を出すことが出来るからです。
キャラクターと同じ速度で追従した場合、3D酔いをする人も居る、演出的にこちらの方がカッコいいためです。
今回はカメラの遊びは設けていません。

#追従モードのカメラ

ちょっと、Unityのクォータニオンに手こずって時間がかかりましたが、作成できました。
まず、ゲームオブジェクトの構成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/0ffb051c-94b7-a0b9-b3de-b161dd25b47b.png)
先ほどと同じです。

しかし、MainCameraの値を少し変えました。(クォータニオンの変換がかかって面倒だったため変えました)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/efe25f3a-aa8c-79a6-bbf6-fb7156ab4a1d.png)
Z軸を-10してあります。

スクリプトは以下のとおりです。
先程同様、CameraArmオブジェクトにアタッチ。
ちょっと、FixedUpdateからズレている処理がありますが概ね同じ処理です。

```AutoFollowCameraArm.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class AutoFollowCameraArm : MonoBehaviour, Controls.ICameraActions
{
    private Controls inputCamera;
    [SerializeField] private Transform character;
    [SerializeField] private GameObject cameraObj;

    private Vector2 inputRightStick;
    private Vector3 followPos;      //CameraArmが追従する位置を格納
    private Vector3 lookPos;        //カメラが見る方向を格納
    private float horizontal;       //現在の水平角度
    private float vertical;        //現在の垂直角度
    private Quaternion cameraArmLerpRot;


    private void Awake() {
        inputCamera = new Controls();
        inputRightStick = Vector2.zero;
        inputCamera.Camera.SetCallbacks(this);
    }

    private void OnEnable() {
        inputCamera.Enable();
    }

    private void OnDisable() {
        inputCamera.Disable();
    }

    private void Update() {
        var position = character.position;
        
        var difference = position-cameraObj.transform.position;
        followPos = Vector3.Lerp(followPos, position, 0.1f);    //追従する位置を減速移動で計算
        Quaternion rot = Quaternion.LookRotation(difference, Vector3.up);
        cameraArmLerpRot = Quaternion.Lerp(transform.rotation , rot, 0.1f);
        lookPos = Vector3.Lerp(lookPos, position, 0.1f);    //カメラの見る位置を減速移動で計算
        
        if (Mathf.Abs(inputRightStick.x) > 0.25f) {    //入力制限。いらないかも
            horizontal = inputRightStick.x * Time.deltaTime * 40;
        }
        else {
            horizontal = 0;    
        }

        if (Mathf.Abs(inputRightStick.y) > 0.25f) {    //入力制限。いらないかも
            vertical += inputRightStick.y * Time.deltaTime * 40;
            //回りすぎるとUnityでは上下反転してしまうので上限を設ける-98～98までなら設定可能だと思う
            vertical = Mathf.Clamp(vertical, -80, 80);
        }
        Vector3 vec = cameraArmLerpRot.eulerAngles;
        transform.rotation = Quaternion.Euler(new Vector3(-vertical,vec.y,vec.z));     //移動中に見下ろし角度を変えたくないのでVerticalの値はそのまま使う
        transform.Rotate(0, horizontal, 0, Space.Self);    //横は毎回リセットを行う
        transform.position = followPos;
        cameraObj.transform.LookAt(lookPos);
    }

    void Controls.ICameraActions.OnR_Stick_Button(InputAction.CallbackContext context) {
        Debug.Log("ButtonLeftStick");        //カメラリセットの処理を書く予定でしたが、遅刻しているので割愛時間がアレば修正します
    }

    public void OnAxisRight(InputAction.CallbackContext context) {
        inputRightStick.x = context.ReadValue<Vector2>().x;
        inputRightStick.y = context.ReadValue<Vector2>().y;
    }
}

```

![autoCam.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/02ab7473-6ea6-a34c-ac85-fa977a62e2e0.gif)
↑横に移動するとカメラを中心に回転するようになりました。

若干キャラクターの動きがカクつくところがありますがPlayerの移動スクリプトを直せばなおると思います。
また、カメラの方向にRayを飛ばしてZ軸の長さを調整することで、壁などに埋もれずにカメラを操作することも可能です。
今回は割愛しますが参考になればと思います。

# その他小ネタ

今回追従カメラを作るにあたって苦戦したのがクォータニオンです。

追従モードのスクリプトの以下の部分なんですが……

```追従モード.cs
        Vector3 vec = cameraArmLerpRot.eulerAngles;
        transform.rotation = Quaternion.Euler(new Vector3(-vertical,vec.y,vac,z); 
        transform.Rotate(0, horizontal, 0, Space.Self);
```

最初↓のように書いていたのですが、コントローラーである一定角度以上回転させるとカメラが荒ぶってしまいました。
クォータニオン同士の足し算が、掛け算であるからなのかなぁと何となく邪推しますが、取り扱いは難しいようなので、
今回のコードを変更される場合はしっかり理解された上で触ることをおすすめします。

```追従モード.cs
        Vector3 vec = cameraArmLerpRot.eulerAngles;
        vec.x = 0;//X軸は補正したくない
        transform.rotation = Quaternion.Euler(vec); 
        //verticalはそのままの値を使いたい
        transform.Rotate(-vertical, horizontal, 0, Space.Self);
```

↓荒ぶったカメラ
![muri.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173560/f31e3bc5-ca38-3629-e263-009dcdee853c.gif)


# 参考コード

https://www.hanachiru-blog.com/entry/2019/02/20/183552
https://zenn.dev/supple/articles/a18e9282765b85c255db

# まとめ

一日遅れになってしまいましたが、UnityでTPSカメラを作るでした。
参考になれば幸いです。
以上、やまだたいしでした。
https://twitter.com/OrotiYamatano