読者です 読者をやめる 読者になる 読者になる

夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【Unity5でレースゲーム】理想のカメラの動きを追い求める

Unity オープンワールドレーシングゲーム

前回までは車オブジェクトの子オブジェクトしてメインカメラを設置していました。

これは非常に簡単で良いのですが、常に寸分の狂いもなく車の真後ろに位置しているので車に動きが感じられません。また、車の細かい左右の動きまで忠実に拾ってしまうので景色がせわしなく左右に揺れてしまいます。

今回は、ゲーセンのゲームのようにコーナーでは少し車体のイン側が見えるような感じにカメラを動かしてみます。
いろいろな方法を試した結果、カメラの動きを車の動きから遅らせつつも距離は強制的に一定に保つ事で距離は変わらず回転方向だけ遅れる処理に行き着きました。

早速スクリプトです。

targetには車体に取り付けたEmpty Objectを入れています。前後方向の位置は前輪の中心付近、高さは2mに設定しました。

Empty Objectの後方5.2mの位置がカメラが本来あるべき位置desiredPositionになります。

カメラの動きを遅らせるためLerpでdesiredPositionに向って少し(1/10の距離)だけ動かし、その位置からターゲットに向うベクトルtoTargetの長さを5.2mに調整した位置にカメラを動かし、LookAtでターゲットにカメラを向け、さらにRotateで10度カメラを下に向けるという処理になっています。

ちなみに、カメラを自車に対してスクリプトで動かすと高速なるほど自車のエンジン音がどんどん音が濁って気になるので自車のオーディオのSpatialBlendは0にして3D効果を切りました。こうすることで音の濁りは解消しました。

using UnityEngine;
using System.Collections;

public class CameraFollowDelay : MonoBehaviour {

    public Transform target;

    private const float _distance = 5.2f;
    private Vector3 _offset = new Vector3(0f, 0f, -_distance);
    private Vector3 _lookDown = new Vector3(10f, 0f, 0f);
    private const float _followRate = 0.1f;

	void Start () {
        transform.position = target.TransformPoint(_offset);
        transform.LookAt(target, Vector3.up);
	}
	
    void FixedUpdate () {
        Vector3 desiredPosition = target.TransformPoint(_offset);
        Vector3 lerp = Vector3.Lerp(transform.position, desiredPosition, _followRate);
        Vector3 toTarget = target.position - lerp;
        toTarget.Normalize();
        toTarget *= _distance;
        transform.position = target.position - toTarget;
        transform.LookAt(target, Vector3.up);
        transform.Rotate(_lookDown);
	}
}

動画です。

意図せず車とカメラの距離が少し変化して迫力が増しているのがわかりますか?
高速になると車との距離が離れ、ブレーキをかけると縮みます。
これはこれで良いのですが、自分の意図した動きと違うのが引っかかります。

スクリプトの実行順が影響しているのかと思いましたが、実はそうではなく、「FixedUpdateで読む事ができる他のオブジェクトの位置は前回の物理演算の結果」というのが真相のようです。
つまり、全てのオブジェクトのFixedUpdate完了後Unityが物理シミュレーションを行い、その結果各オブジェクトの位置が定まるのですが、カメラがその結果を知る事ができるのは次のFixedUpdateの実行時という事です。

それでは、自分の意図した動きにするにはどうすれば良いでしょう?
Updateでカメラを動かせば物理シミュレーションの結果の位置を使用できますが、FixedUpdateとUpdateは必ずしも同期していないので、カクツキが発生してしまいます。(Time.deltaTimeを掛けても)

LateFixedUpdateのようなメソッドがあれば良いのですが、残念ながらUnityのマニュアルを探しても見つかりません。
いろいろ探していると良い情報が見つかりました。
Event Execution Order - Unify Community Wiki
コルーチンをつかってyield return new WaitForFixedUpdate()で待てばまさにLateFixedUpdateと言うべきタイミングで処理が再開するようです。
実はこれはUnityのマニュアルのスクリプトライフサイクルフローチャートの内容とは違っています。
Unity - マニュアル: イベント関数の実行順
実際に試した結果、正しいのはマニュアルではなくてwikiの方でした。

本来意図した動きのスクリプトです。

using UnityEngine;
using System.Collections;

public class CameraFollow : MonoBehaviour {

    public Transform target;

    private const float _distance = 6.2f;
    private Vector3 _offset = new Vector3(0f, 0f, -_distance);
    private Vector3 _lookDown = new Vector3(10f, 0f, 0f);
    private const float _followRate = 0.1f;

	void Start () {
        transform.position = target.TransformPoint(_offset);
        transform.LookAt(target, Vector3.up);
        StartCoroutine(UpdatePosition());
	}

    IEnumerator UpdatePosition () {
        while (true) {
            yield return new WaitForFixedUpdate();
            Vector3 desiredPosition = target.TransformPoint(_offset);
            Vector3 lerp = Vector3.Lerp(transform.position, desiredPosition, _followRate);
            Vector3 toTarget = target.position - lerp;
            toTarget.Normalize();
            toTarget *= _distance;
            transform.position = target.position - toTarget;
            transform.LookAt(target, Vector3.up);
            transform.Rotate(_lookDown);
        }
    }
}

こちらのバージョンの動画です。

カメラと車の距離が一定に保たれているのがわかると思います。
確かに意図した動きではあるのですが、どっちがより良いかと言えば最初の方だと思うので、頑張って見つけたこちらの方法はお蔵入りになりそうです。