はじめに
本記事ではUnityで「機動戦士ガンダムVS.」シリーズにあるような、操作キャラクターを視界に入れつつ、ターゲットに向き続けるTPSカメラを作ってみます。
近い動きで「ディシディア ファイナルファンタジー」や「Fate/Grand Order Arcade」のようなカメラにも応用できるんじゃないかと思います。
完成イメージ
デモプロジェクト
GitHubにアップロードしています。
unity-tps-lock-on-camera
位置と向きのロジック
カメラの動きを作るときは先に位置を考え、その後に向きを考えるのが進めやすいと思います。
位置
まずカメラの位置ですが、下図のようにターゲットと操作キャラを結んだ線上、操作キャラ後方の少し上辺りにいる感じがしますね。
なので、操作キャラ→ターゲットのベクトル$\vec{V_{tgt}}$を基準に、後ろ距離と高さを示すベクトル$\vec{V_{ofsLocal}}$分ずらせば欲しい位置が得られそうです。ただそれは$\vec{V_{tgt}}$を前方とするローカル空間での話なので、ワールド座標に直す必要があります。
あるベクトルを座標系(の一部)とみなしてそれを基準に位置を得る方法ですが、クォータニオンを掛けることでベクトルを回転することができるという性質を利用します。
$\vec{V_{tgt}}$を前方とするクォータニオン$q_{tgt}$を得ることができれば、ワールド座標でずらすべき位置ベクトル$\vec{V_{ofs}}$は、
\vec{V_{ofs}} = q_{tgt} \times \vec{V_{ofsLocal}}
となります。
幸いUnityにはQuaternion.LookRotationという便利なAPIが用意されていますので、$q_{tgt}$を得るのは難しくありません。
カメラの位置を算出するまでのコードイメージはこんな感じになります。
// ターゲットへのベクトル
Vector3 vTgt = target.position - player.position;
// ターゲットへのベクトルを前方とするクォータニオン
// 第二引数はワールド空間的な上(Vector3.up)でいいので省略
Quaternion qTgt = Quaternion.LookRotation(vTgt);
// ずらすべき位置ベクトル
// ずらしたい量をここでは後方5、高さ2とした場合
Vector3 vOfs = qTgt * new Vector3(0f, 2f, -5f);
// 最終的なカメラ位置(ワールド座標)
Vector3 cameraPosition = player.position + vOfs;
向き
次に向きですが、こちらはシンプルです。
常にターゲットに向くだけなので、カメラの位置が決まればQuaternion.LookRotationにカメラ→ターゲットのベクトルを前方ベクトルとして渡してあげれば得られます。
// ターゲットへの向き
Quaternion cameraRotation = Quaternion.LookRotation(target.position - cameraPosition);
プロトタイプ完成
変数名を分かりやすくして、MonoBehaviourにのっけたコードに直すとこんな感じです。
ここまでで操作キャラの後方を維持しつつ、ターゲットを見続けるカメラができました。
using UnityEngine;
public class TpsLockOnCameraPrototype : MonoBehaviour
{
/// <summary>
/// 取りつくキャラクター
/// </summary>
[SerializeField]
private Transform _attachTarget = null;
/// <summary>
/// 取りつくキャラクターからのカメラオフセット位置
/// </summary>
[SerializeField]
private Vector3 _attachOffset = new Vector3(0f, 2f, -5f);
/// <summary>
/// 注視ターゲット
/// </summary>
[SerializeField]
private Transform _lookTarget = null;
/// <summary>
/// 現在の注視点
/// </summary>
private Vector3 _lookTargetPosition = Vector3.zero;
private void LateUpdate()
{
_lookTargetPosition = _lookTarget.position;
// ターゲットへのベクトル
Vector3 targetVector = _lookTargetPosition - _attachTarget.position;
// ターゲットへのベクトルを前方とするクォータニオン
Quaternion targetRotation = targetVector != Vector3.zero ? Quaternion.LookRotation(targetVector) : transform.rotation;
// 位置と向き
Vector3 position = _attachTarget.position + targetRotation * _attachOffset;
Quaternion rotation = Quaternion.LookRotation(_lookTargetPosition - position);
transform.SetPositionAndRotation(position, rotation);
}
}
ターゲット切り替えのロジック
続いてターゲット切り替えの動きを考えます。
最終的にはターゲットとして指定しているtransformを変えればいいわけですが、単純に変えるとカメラが位置と向きをがっつりワープすることになるので、プレイヤーは切り替わり前後で混乱してしまいますよね。滑らかに繋ぎたいところです。
ターゲット切り替え
処理の流れを考えます。
まず、元々見ていたターゲットは、切り替わり動作中も含めて無視としたいので、切り替え開始の瞬間の位置$P_{old}$だけ覚えておけばよさそうです。新しく見るターゲットは切り替わり動作中も含めてその位置$P_{new}$を追従し続けたいです。
なので$P_{old}$は固定位置、$P_{new}$はTransformのpositionとして、$P_{old}$から$P_{new}$へ補間しつつ一定時間かけて移動すればよさそうです。
(見る位置を滑らかに変えることで、カメラの位置と向きもスムーズに変化するよねという考え方です。)
位置の補間にはVector3.Lerpのメソッドが使えます。(好みでイージングしてもいいかもしれません。)
よって、先程のコードにターゲット変更の処理を組み込んだ最終版はこのようになります。
using UnityEngine;
public class TpsLockOnCamera : MonoBehaviour
{
/// <summary>
/// 取りつくキャラクター
/// </summary>
[SerializeField]
private Transform _attachTarget = null;
/// <summary>
/// 取りつくキャラクターからのカメラオフセット位置
/// </summary>
[SerializeField]
private Vector3 _attachOffset = new Vector3(0f, 2f, -5f);
/// <summary>
/// 注視ターゲット
/// </summary>
[SerializeField]
private Transform _lookTarget = null;
/// <summary>
/// ターゲットがいないときの注視点
/// </summary>
[SerializeField]
private Vector3 _defaultLookPosition = Vector3.zero;
/// <summary>
/// ロック切り替え時間
/// </summary>
[SerializeField]
private float _changeDuration = 0.1f;
/// <summary>
/// ロック切り替えタイマー
/// </summary>
private float _timer = 0f;
/// <summary>
/// 現在の注視点
/// </summary>
private Vector3 _lookTargetPosition = Vector3.zero;
/// <summary>
/// ロックを移すときの最後の注視点
/// </summary>
private Vector3 _latestTargetPosition = Vector3.zero;
/// <summary>
/// ターゲット切り替え
/// </summary>
/// <param name="target"></param>
public void ChangeTarget(Transform target)
{
_latestTargetPosition = _lookTargetPosition;
_lookTarget = target;
_timer = 0f;
}
private void LateUpdate()
{
var targetPosition = _lookTarget != null ? _lookTarget.position : _defaultLookPosition;
// 現在の注視点を更新
if (_timer < _changeDuration)
{
_timer += Time.deltaTime;
_lookTargetPosition = Vector3.Lerp(_latestTargetPosition, targetPosition, _timer / _changeDuration);
}
else
{
_lookTargetPosition = targetPosition;
}
// ターゲットへのベクトル
Vector3 targetVector = _lookTargetPosition - _attachTarget.position;
// ターゲットへのベクトルを前方とするクォータニオン
Quaternion targetRotation = targetVector != Vector3.zero ? Quaternion.LookRotation(targetVector) : transform.rotation;
// 位置と向き
Vector3 position = _attachTarget.position + targetRotation * _attachOffset;
Quaternion rotation = Quaternion.LookRotation(_lookTargetPosition - position);
transform.SetPositionAndRotation(position, rotation);
}
}
おわりに
このタイプのカメラ挙動はカメラ操作がシンプルになるというメリットがありますね。
拙い説明であることに加え、細かくは再現しきれておらず申し訳ないですが、少しでも何かの参考になれば幸いです。
また、説明の間違いやソースコードの不具合などあるかもしれません。その場合は是非ご指摘いただけますと幸いです。