【Unity】一旦爆速でFPS視点を実現する

# 📖はじめに
なんとなく、Unityもバージョンが6になったし、自分が勤めている会社でもUnity6の機能についてふわっと耳に挟むこともあったので、なんとなく触ってみたときの記事です。
超久しぶりに触ったので、「特に作りたいものも決まってないし、一旦FPS視点だけ作るか～」とおもってやった作業まとめです。
~~内容としては、無限煎じ過ぎてもはや味すらしないレベルですが、~~ 本記事通りに作ると、一旦移動＋FPS視点が完成します。
正直、Unityについては全く詳しくないですが、「ゲームなんて作ったことないけど、一旦なにかしたい！！」みたいな人は是非真似してみてネ。（まずは楽しむために手を動かして"やってる感"を出すのも大事）

# 🔧一通り作ってみよう
## 足場を用意する
Unityのエディタを起動すると、まっさらな空間が出現する。
このままでは仮にキャラを用意しても、きっとY軸Infinityまで落ちていってしまう。
なので、一旦床を用意しよう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/8289222d-0e5f-444c-b9c8-19a54da7c53a.png)

## キャラクターオブジェクトを用意する
足場もできたので、早速キャラを作りはじめる。
とはいえ、今回はFPS視点で動ければ何でもいいので、キャラクターの外見は不要。
空のGameObjectを用意して、それをPlayerとする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/85dae48f-1d39-4ff9-9816-40336a2feb4f.png)

床やプレイヤーのオブジェクトにわかりやすく名前をつけてあげること、ついでに先ほど出した床とプレイヤーオブジェクトを今回はXYZ座標それぞれ0,0,0にそろえておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/e0001156-d4ff-420b-9663-b728fe7e8c7b.png)
Hierarchyでリネーム:
Plane → Floor (床)
GameObject → Player (プレイヤーオブジェクト)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/c37d0752-3a85-4e03-a1f4-f7348eedc737.png)
Floor・Playerの座標を(0,0,0)にセット。

## キャラクターにCharacterControllerを追加

もちろんだけど、このままではただ空のオブジェクトにPlayerという名前をつけただけなので、ここから機能実装をしていく必要がある。
キャラクターなどの移動機能の実装には実は様々なアプローチがあるが、今回はCharacterControllerというコンポーネントによる実装を行う。

他にもRigidbodyだとかTransform書き換えによる移動だとか、いろんな移動方法があるのだが、まあ大体FPS視点のゲームを作るのであれば初学者は脳死でCharacterControllerでいいんじゃないかなぁと思う。

:::note info
コンポーネントとか、初めて触る人には色々疑問もあると思うが、一旦細かい話は後にしてとりあえずやってみよう。
:::

HierarchyでPlayerをクリック選択後、InspectorのAdd ComponentをクリックしてCharacterControllerを追加する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/e0d7476e-1a81-4c06-9f36-3ed9b21e2c8e.png)

追加できたら以下の画像の様に項目ができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/4c50a4dd-89e8-4318-8af7-580eb2ca76ec.png)

**ここで注意。** この状態でUnityエディタを確認すると、緑色のカプセルが表示されるようになる。もし、表示されないのであればSceneタブの右上の球体アイコンを押すと表示されるはず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/3ad74059-37aa-4830-9cef-9e2ab0bbce1c.png)
この表示がいわばキャラクターの体であり、当たり判定なのだが、このままでは地面に埋まってしまっている状態。
なので、centerのXYZ座標を(0,1,0)にセットし、当たり判定の位置を調整しよう。
※初期値ではHeight(高さ)が2なので、centerのY座標を1上げてあげればちょうどよくなる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/6e833bba-8856-4aff-abc8-3d97160b5370.png)

次は、移動のためのプログラムを書いていく。
CharacterControllerをAdd Componentしたときみたいに、Playerを選択→InspectorのAdd Componentをクリック→New Scriptを選択して任意の名前で新しく移動スクリプトを作ろう。
名前は今回 `PlayerMove` とした。

:::note info
命名規則というやつを意識するのであれば、必ず `パスカルケース` というやつにしよう。
わからなければ調べてみてほしい。
:::

スクリプトが追加できたら以下のように記述する。
```PlayerMove.cs
using UnityEngine;

public class PlayerMove : MonoBehaviour
{
    // パラメータ
    public float moveSpeed  = 5f;           // 移動速度
    public float gravity    = -9.8f;        // 重力加速度
    public CharacterController controller;  // 移動に使うCharacterController

    // 演算用変数
    private Vector3 velocity;       // 加速度を保持する変数
    private bool isGrounded;        // 地面に着地しているかどうかのフラグ変数

    // ゲーム中実行されるUpdate関数
    void Update()
    {
        // 着地状態のチェック
        isGrounded = controller.isGrounded;  
        
        // 着地している場合は落下速度をリセット
        if (isGrounded && velocity.y < 0)
        {
            velocity.y = -2f; // 地面に着いた場合、速度をリセット
        }

        // 入力の取得
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");

        // ローカル座標をワールド座標に変換して移動方向を計算
        Vector3 moveDirection = transform.TransformDirection(new Vector3(h, 0, v)) * moveSpeed;

        // 重力を加算
        velocity.y += gravity * Time.deltaTime;

        // 移動と重力を一度のcontroller.Moveで処理
        controller.Move((moveDirection + velocity) * Time.deltaTime);
    }
}
```

Update関数の挙動については以下のようになっている。

```mermaid
graph TD;
    Start["開始"] --> CheckGrounded["着地しているか?"]
    CheckGrounded -- "Yes" --> ResetVelocity["落下速度をリセット"]
    CheckGrounded -- "No" --> Continue["そのまま継続"]
    ResetVelocity --> GetInput["入力取得: 水平・垂直"]
    Continue --> GetInput

    GetInput --> CalcMoveDirection["移動方向を計算"]
    CalcMoveDirection --> ApplyGravity["重力を適用"]
    ApplyGravity --> CombineMovement["移動と重力を統合"]
    CombineMovement --> MovePlayer["プレイヤーを移動"]
    MovePlayer --> End["終了"]
```

PlayerMoveを保存してUnityエディタに戻ると、InspectorのPlayerMoveコンポーネントに新しく項目が追加される。
そのままだと、CharacterControllerがNoneになっていて、操作することができないのでPlayerをD&Dして設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/fdc8c65c-b97a-4bae-8bac-04c1fc49e45e.png)

実行してみて以下のように動けばうまく実装できている。
![5dbef231-f233-4844-a2c3-215d0f32e5d5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/7223e8db-f6a3-4135-85a9-46916477821c.gif)

## カメラの制御のプログラム
キャラクターは動くようになったはずだが、おそらくゲーム画面上はカメラが固定されている状態になっているはず。
FPSゲームにしたいのでマウスでの視点操作はマストだろう。
なので次は以下の画像のように空のオブジェクト `Neck` とカメラオブジェクト `Camera` を追加しよう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/49968e90-e195-437b-9701-610fcbe475c5.png)

Cameraは新しく作ってもいいし、すでにMainCameraが存在するようなら、それを入れ子にしても良い。
ひとまず、ゲームシーン上に2つ以上のカメラが無いようにだけ気をつけよう。

親子関係を正しく設定したら、NeckとCameraのTransformは以下のようにしよう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/5324bc95-6938-489c-8d81-860bcb75a108.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/22983dcf-453a-432f-ac39-e5316ff5cd86.png)

そしたら、PlayerにまたAdd ComponentでNew Script、新しくスクリプトを作成する。今度はPlayerPOVという名前で作成した。
```PlayerPOV.cs
using UnityEngine;

public class PlayerPOV : MonoBehaviour
{
    // パラメータ
    public Transform neck;                  // プレイヤーの首のTransformを指定
    public float sensitivity    = 2.0f;     // マウス感度（視点の移動の速さを調整）
    public float minVertical    = -90.0f;   // 視点の最小角度（縦の回転制限）
    public float maxVertical    = 90.0f;    // 視点の最大角度（縦の回転制限）

    // 演算用変数
    private float rotationX     = 0f;       // 縦方向の回転角度（首の回転）

    // ゲーム開始時に呼ばれる
    void Start()
    {
        // カーソルを非表示＆ロック
        Cursor.lockState = CursorLockMode.Locked;   // カーソルを画面中央に固定
        Cursor.visible = false;                     // カーソルを非表示にする
    }

    // 毎フレーム実行される
    void Update()
    {
        // マウス入力の取得
        float mouseX = Input.GetAxis("Mouse X") * sensitivity;  // 横のマウス移動量を取得し、感度で調整
        float mouseY = Input.GetAxis("Mouse Y") * sensitivity;  // 縦のマウス移動量を取得し、感度で調整

        // Player（体）の回転（左右）
        transform.Rotate(0, mouseX, 0);   // プレイヤー（体）の左右の回転をマウスX方向の入力に合わせて行う

        // Neck（首）の回転（上下）
        rotationX -= mouseY; // マウスY方向の入力によって縦方向の回転を更新
        rotationX = Mathf.Clamp(rotationX, minVertical, maxVertical);   // 回転角度を指定された範囲に制限
        neck.localRotation = Quaternion.Euler(rotationX, 0, 0);         // 首の回転を設定。縦方向のみ回転させる
    }
}
```
フローチャートは以下。
開始で分岐してしまっているが、条件分岐ではなく単にX軸とY軸でそれぞれ違う反映処理をしている、と考えてもらえれば。
```mermaid
flowchart TD
    A["開始"] --> C["マウスのX軸（左右）入力を取得"]
    C --> D["感度を考慮して横方向の移動量を計算"]
    A --> E["マウスのY軸（上下）入力を取得"]
    E --> F["感度を考慮して縦方向の移動量を計算"]
    D --> G["プレイヤー（体）を回転"]
    G --> M
    F --> H["首の回転（上下）を更新"]
    H --> K["回転角度を制限する（縦方向の角度制限）"]
    K --> L["首の回転を設定する（rotationXを反映）"]
    L --> M["終了"]
```

マウスロックについては、Startにああやって書けば一旦OK、と思ってもらってよい。

プログラムを保存したら、Unityエディタに戻り、NeckをD&Dでセットする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/50118803-fdcc-4610-a4d5-89517c3d1ae4.png)

ここまででちゃんと実行できれば以下のようになる。
![b22599d2-316e-4720-afb4-fb43215eb866.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621007/4efc5034-037b-4f48-8bb7-f4e5b18faa2e.gif)

# 細かい話
## コンポーネントって何？
Unityでは、例えば物理演算だとか、キャラの移動だとか、あるいは当たり判定だとか、そういった機能を"コンポーネント"としてまとめていて、オブジェクトにそれらコンポーネントを追加してあげることで、そのオブジェクトにサクッと機能が追加される。
もちろん、機能が追加されただけでは何もかも全てうまくいくわけもなく、ちゃんと自分が実装したいものに合わせてパラメータを設定したり、プログラムから制御したりする必要はある。
キャラクターの移動で使ったCharacterControllerについては、オブジェクトの移動コンポーネントなので、これをつけるだけで例えば「小さい段差があったら自動で乗り越える」とかをいちいち実装しないでもやってくれている。一方で「どれくらいの段差は乗り越えられて、どこまでは乗り越えられない」みたいなパラメータはちゃんと設定する必要がある…と言った具合だ。

## 大体の3Dゲームのキャラ移動はCharacterControllerでいい
Rigidbodyはオブジェクトに物理演算をもたらすコンポーネント。なので、Rigidbodyを使った移動の考え方は「キャラクターオブジェクトが見えざる力によって"動かされている"」とか「神(プレイヤー)の意思によって無理やり加速度が"加えられている"」みたいな挙動になる。
これは玉転がしアクション的なゲームであればいいのだけど、FPSゲームみたいな「Wキーを押しただけ進む、爆発があっても基本的にプレイヤーが吹っ飛んだりしない」ゲームでは向かない。物理演算の機能が強すぎる。
Transform(位置情報の書き換え)による移動は、CharacterControllerというコンポーネントの存在を知らないと真っ先にやりがちかな、と思う(というか、僕が幼き頃真っ先にやった)のだけど、CharacterControllerやRigidbodyみたいに当たり判定をよしなにして、壁に埋まらないようにとか、ジャンプのときの挙動を他の実装ほどサクッと作れないので、もし、まず一作品目を作ろうとしているUnity初心者はCharacterControllerを覚えておけばいいよな、と思う。
とはいえ、作るゲームにもよる話で、CharacterControllerによる実装が正解だとか、RigidbodyやTransformの移動が間違いだとかそういうことではない。

## Neckを挟んだ理由
視点制御のところで、もしかしたら「Neckオブジェクトっている？」って思った人もいるかも知れない。
正直、なくても良い。
なぜつけたかといえば、なんとなくそちらのほうが人間的な視界になるかなと思ったからだ。
人間は下を見るとき、基本的に眼球だけで下を見るようなことはせず、首を起点として下に顔を向ける。そう考えると、カメラを直接回転させて下を向く挙動を作るのではなく、Neckオブジェクトを回転させてカメラを下に向けたほうがどちらかといえば自然な視界になるだろうなぁと。

# 📕さいごに
うん、これ、一体何番煎じなんだろう？🤔