Unity物理ベースキャラコン制作でハマった3つの罠と学び

# はじめに

Unity/C#のスキルアップに取り組んでいます。今回はその第一歩として、「物理ベースのプレイヤーコントローラー」の作成に挑戦しました。

この記事は、その過程で直面した問題と、それをどう乗り越えたかの学習記録です。同じように学習を始めた方の助けになれば幸いです。

# 目標とした動き

今回目指したのは、以下のような「操作していて気持ちいい」キャラクターコントローラーです。

* キーボード入力に応じて、キャラクターが前後左右に滑らかに移動する。
* 移動処理は物理演算 (Rigidbody) に基づいており、カクついたりしない。
* キャラクターは、移動している方向へ\*\*滑らかに（ぬるっと）\*\*体を向ける。
* 移動速度や回転速度は、後から簡単に調整できる。

# 直面した3つの問題（罠）とその解決策

早速実装に取り掛かりましたが、案の定いくつかの壁にぶつかりました。

## 罠1：変数が存在しない！ - 「スコープ」の壁

最初に書いたコードでは、`Update()`でキー入力を検知し、`FixedUpdate()`でその情報を使って移動させようとしました。

### 問題点

```csharp
void FixedUpdate()
{
    // ここでHorizontalやVerticalを使おうとしてもエラーになる
    Vector3 movement = (transform.forward * Horizontal + ...);
}

void Update()
{
    if (Input.GetKey(KeyCode.W))
    {
        // このHorizontal変数は、このifブロックの中でしか生存できない
        float Horizontal = 1.0f;
    }
    // ...
}
```

### 原因と技術解説：変数のスコープ

C#では、変数が使える範囲（スコープ）が決まっています。if文の`{}`ブロックの中で宣言された変数は、そのブロックを抜けると消滅してしまいます。
そのため、`Update()`内のif文で宣言した変数を、`FixedUpdate()`から参照することはできませんでした。

### 解決策：メンバー変数として宣言

`Update()`と`FixedUpdate()`の両方からアクセスできるよう、クラスの直下でメンバー変数として宣言しました。

```csharp
public class PlayerController : MonoBehaviour
{
    // クラスのメンバー変数として宣言
    private float horizontalInput;
    private float verticalInput;

    void Update()
    {
        // メンバー変数に入力値を代入
        horizontalInput = Input.GetAxis("Horizontal");
        verticalInput = Input.GetAxis("Vertical");
    }

    void FixedUpdate()
    {
        // これでFixedUpdate内でも使える！
        Vector3 moveDirection = new Vector3(horizontalInput, 0f, verticalInput);
        // ...
    }
}
```

## 罠2：まっすぐ進めない！ - 「循環参照」の罠

キャラクターが前進しようとしても、円を描くようにふらふらと進んでしまいました。

### 問題のコード

```csharp
void FixedUpdate()
{
    // 1. キャラクターの現在の向き(transform.forward)を基準に移動方向を計算
    Vector3 moveDirection = (transform.forward * verticalInput + ...);

    // 2. その移動方向(moveDirection)に体を向けようとする
    Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
    rb.rotation = Quaternion.Slerp(rb.rotation, targetRotation, ...);
}
```

### 原因と技術解説：循環参照

* 移動方向は、キャラクターの現在の向きに依存している。
* 回転は、その移動方向に追いつこうとする。

結果として、キャラクターは「自分が向こうとしていて、かつ自分自身のせいで絶えず変化する目標」を追いかけ続けてしまっていたのです。

### 解決策：ワールド座標基準で方向を計算

キャラクターの向きに依存しない、ワールド座標基準で方向を計算するようにしました。

```csharp
void FixedUpdate()
{
    Vector3 moveDirection = new Vector3(horizontalInput, 0f, verticalInput).normalized;

    if (moveDirection != Vector3.zero)
    {
        Quaternion targetRotation = Quaternion.LookRotation(moveDirection, Vector3.up);
        rb.rotation = Quaternion.Slerp(rb.rotation, targetRotation, ...);
    }
    // ...
}
```

## 罠3：キャラクターが爆速で飛んでいく！ - 「時間」の壁

移動処理を入れたところ、キャラクターがとんでもないスピードで吹っ飛んでしまいました。

### 問題のコード

```csharp
void FixedUpdate()
{
    Vector3 moveDirection = new Vector3(horizontalInput, 0f, verticalInput).normalized;
    Vector3 movement = moveDirection * speed;
    rb.MovePosition(rb.position + movement);
}
```

### 原因と技術解説：`Time.fixedDeltaTime`の欠如

`FixedUpdate()`は1秒間に50回程度実行されます。
`movement`が「1秒あたりに移動すべき量」なのに、それを毎フレーム実行していたため、実際の速度は50倍になってしまっていました。

### 解決策：「1フレームあたりの移動量」を計算

```csharp
void FixedUpdate()
{
    Vector3 moveDirection = new Vector3(horizontalInput, 0f, verticalInput).normalized;
    Vector3 movementThisFrame = moveDirection * speed * Time.fixedDeltaTime;
    rb.MovePosition(rb.position + movementThisFrame);
}
```

# 最終的なコード

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float speed = 5f;
    public float rotationSpeed = 15f;
    private Rigidbody rb;
    private float horizontalInput;
    private float verticalInput;
    private Vector3 moveDirection;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;
    }

    void Update()
    {
        horizontalInput = Input.GetAxis("Horizontal");
        verticalInput = Input.GetAxis("Vertical");
    }

    void FixedUpdate()
    {
        moveDirection = new Vector3(horizontalInput, 0f, verticalInput).normalized;
        Vector3 movementThisFrame = moveDirection * speed * Time.fixedDeltaTime;
        rb.MovePosition(rb.position + movementThisFrame);

        if (moveDirection != Vector3.zero)
        {
            Quaternion targetRotation = Quaternion.LookRotation(moveDirection, Vector3.up);
            rb.rotation = Quaternion.Slerp(rb.rotation, targetRotation, rotationSpeed * Time.fixedDeltaTime);
        }
    }
}
```

# このステップでの学び

* **UpdateとFixedUpdateの役割分担**: 入力は`Update`、物理演算は`FixedUpdate`で行うのが鉄則。
* **時間の概念**: `Time.deltaTime`や`Time.fixedDeltaTime`を掛けることで、安定した処理が書ける。
* **変数のスコープ**: どこで使えるかを意識して変数を宣言する。
* **座標系の意識**: ローカル座標とワールド座標の使い分けが重要。
* **Quaternionによる滑らかな回転**: `Quaternion.LookRotation`と`Slerp`の組み合わせで、リッチな操作感を実現。

# さいごに

たった一つのコントローラー作成でしたが、チーム開発でも重要になるであろう多くの基礎を実践的に学ぶことができました。次のステップも頑張ります！