【Unity】目に優しいTPSカメラ

6 min読了の目安(約5400字TECH技術記事

はじめに

私は3Dゲームに酔いやすい体質です。
相性の悪いゲームだと大体1時間ほどのプレイで激しい頭痛に悩まされます。
今回は第三者視点のカメラであるTPSカメラで酔いを軽減する工夫を紹介したいと思います。

酔いやすいカメラ

酔いやすいカメラとは何でしょうか。
俗に3D酔い/ゲーム酔いと呼ばれる現象は目で受け取る回転や移動の情報に対し、体が実際には動いていないことによる錯覚からくると言われています。
特に反復運動や振動、急加速・急減速のような動きは酔いの原因となりやすいでしょう。

どうするか

急なカメラの動きを緩和するようにします。
具体的には以下の2点を実装します。

視点、カメラ位置に「遊び」を設ける

カメラがキャラクターにピッタリ追従する場合、キャラクターが左右に細かく動くとカメラも小刻みに左右してしまい画面揺れが発生してしまいます。
キャラクターが移動してもカメラを追従させない範囲を作ることでカメラ揺れを軽減させることができます。

カメラの移動はスムースする

キャラクターが移動したとき同じ速度で追うのではなく、徐々に加速してカメラを移動させることで画面の急な変化を防ぎます。

カメラ視点の制御

まずは視点となるターゲットのオブジェクトが必要になります。
そしてカメラの視点に遊びを設けるため、実際にカメラを向ける座標を別途用意します。

public GameObject followObject = null;     // 視点となるオブジェクト
private Vector3 lookPos = Vector3.zero;     // 実際にカメラを向ける座標

遊びの広さと、遊びを超えて離れたときに追従するスムースの強さのパラメーターが必要です。

public float lookPlayDistance = 0.3f;   // 視点の遊び
public float followSmooth = 4.0f;       // 追いかけるときの速度

カメラの視点の更新メソッドです。
現在の視点とオブジェクトの座標が遊びを超えて離れたとき、遊びを引いたぶんの距離をスムースして移動します。

void UpdateLookPosition()
{
    // 目標の視点と現在の視点の距離を求める
    Vector3 vec = followObject.transform.position - lookPos;
    float distance = vec.magnitude;

    if (distance > lookPlayDistance)
    {   // 遊びの距離を超えていたら目標の視点に近づける
        float move_distance = (distance - lookPlayDistance) * (Time.deltaTime * followSmooth);
        lookPos += vec.normalized * move_distance;
    }
}

カメラ座標の制御

視点だけでなくカメラ座標にも遊びとスムースを設けます。
こちらは視点に比べて少し複雑です。
まずは視点からどれだけ離れた位置にカメラを置くのかを距離と高さのパラメーターで表します。

public float cameraDistance = 2.5f;        // 視点からカメラまでの距離
public float cameraHeight = 1.0f;          // デフォルトのカメラの高さ
float currentCameraHeight = 1.0f;          // 現在のカメラの高さ

そして遊びの広さとスムースの強さですが、カメラ座標の場合離れる時と近付く時の2種類が存在します。
離れるときは followSmooth を共用してもさほど問題ないですが、近付く場合は強めの度合いで移動したほうがよいでしょう。
カメラとキャラクターは近くにいる事が多いので、スムースの強さが足りなければキャラクターがカメラに埋まってしまうことになります。

public float cameraPlayDiatance = 0.3f;    // 視点からカメラまでの距離の遊び
public float leaveSmooth = 20.0f;          // 離れるときの速度

カメラの座標の更新メソッドです。
XZ軸に対して遊びとスムースを処理します。
離れるときは弱めに、近づくときには強めにスムースをかけます。

    void UpdateCameraPosition()
    {
        // XZ平面におけるカメラと視点の距離を取得する
        Vector3 xz_vec = followObject.transform.position - transform.position;
        xz_vec.y = 0;
        float distance = xz_vec.magnitude;

        // カメラの移動距離を求める
        float move_distance = 0;
        if (distance > cameraDistance + cameraPlayDiatance)
        {   // カメラが遊びを超えて離れたら追いかける
            move_distance = distance - (cameraDistance + cameraPlayDiatance);
            move_distance *= Time.deltaTime * followSmooth;
        }
        else if (distance < cameraDistance - cameraPlayDiatance)
        {   // カメラが遊びを超えて近づいたら離れる
            move_distance = distance - (cameraDistance - cameraPlayDiatance);
            move_distance *= Time.deltaTime * leaveSmooth;
        }

    // 新しいカメラの位置を求める
        Vector3 camera_pos = transform.position + (xz_vec.normalized * move_distance);
	
	// 高さは常に現在の視点からの一定の高さを維持する
        camera_pos.y = lookPos.y + currentCameraHeight;

        transform.position = camera_pos;
    }

FixedUpdate

視点とカメラ座標はFixedUpdateで毎フレーム更新しましょう。
更新が終わったらLookAtでカメラに反映します。

    void FixedUpdate()
    {
        if(followObject == null) return;

        UpdateLookPosition();
        UpdateCameraPosition();

        transform.LookAt(lookPos);
    }

カメラの回転

アナログパッドでカメラをグリグリ動かせるのはほとんどのゲームで必須要件です。
遊びを設けたカメラで素直にカメラの回転をすると、遊びの分だけズレた位置で回転してしまいます。
回転しつつこっそり近付いて遊びをなくしてしまいましょう。
回転方法についてですが、三角関数を使用した球面移動はよく紹介されているので別の簡単な方法を紹介します。
といってもカメラのrightとup方向に平行移動するだけです。
それだけだと徐々に離れて行ってしまうので、視線を向けた後に距離が変わった分だけ前後に移動して補正します。

    public float cameraHeightMin = 0.1f;    // カメラの最低の高さ
    public float cameraHeightMax = 3.0f;    // カメラの最大の高さ
    
    public void Roll(float x, float y)
    {
        // 移動前の距離を保存する
        float prev_distance = Vector3.Distance(followObject.transform.position, transform.position);
        Vector3 pos = transform.position;

        // 横に移動する
        pos += transform.right * x;

        // 縦に移動する
        currentCameraHeight = Mathf.Clamp(currentCameraHeight + y, cameraHeightMin, cameraHeightMax);
        pos.y = lookPos.y + currentCameraHeight;

        // 移動後の距離を取得する
        float after_distance = Vector3.Distance(followObject.transform.position, pos);

        // 視点を対象に向けて近づける(遊びをなくす)
        lookPos = Vector3.Lerp(lookPos, followObject.transform.position, 0.1f);

        // カメラの更新
        transform.position = pos;
        transform.LookAt(lookPos);

        // 平行移動により若干距離が変わるので補正する
        transform.position += transform.forward * (after_distance - prev_distance);
    }

カメラリセット

カメラリセットもゲームには欠かせない機能です。
視点、高さ、カメラ位置を基本位置に近付けてあげます。
rateに1を設定した場合一瞬で元の位置に戻り、それ以下の値を指定した場合は毎フレーム呼び続けることで押している間、元の位置に戻り続けようとします。

    public void Reset(float rate = 1)
    {
        // 視点対象に近づける
        lookPos = Vector3.Lerp(lookPos, followObject.transform.position, rate);

        // 高さをデフォルトに近づける
        currentCameraHeight = Mathf.Lerp(currentCameraHeight, cameraHeight, rate);

        // カメラを基本位置に近づける
        Vector3 pos_goal = followObject.transform.position;
        pos_goal -= followObject.transform.forward * cameraDistance;
        pos_goal.y = followObject.transform.position.y + currentCameraHeight;
        transform.position = Vector3.Lerp(transform.position, pos_goal, rate);

        // 視線を更新する
        transform.LookAt(lookPos);
    }

最後に

このカメラがどう見えるか動画を添付しました。
キャラクターが激しい動きをしても動きの少ない落ち着いたカメラになります。
まだ改良するべき部分はあると思いますが、ゲームは特に目を酷使するので目に優しいカメラワークを心がけていきたいですね。