#概要
角度追尾は一定の需要があると思います。
例えば視線や監視カメラ、銃口など方向を追尾したいことはよくあります。
しかし単純にターゲットの方向を向き続けるのもリアリティがないですし、
エリアに入った時にぎゅんとこちらを向かれるとぎょっとします。
ところがこの絶妙な角度追尾、オイラー角やクォータニオンだとうまく実装できません。
ということでいい感じの角度追尾、やっていきましょう。
#仕様
今回はUArrowComponent
の向きをプレイヤーの方に向けていきます。
余談かもしれませんが、UArrowComponent
をゲームプレイ時に表示する場合は、
HiddenInGame
のチェックを外す必要があります。
ついでにエリアに入ったらプレイヤーの向きを追従する、ということにしましょう。
#オイラー角?クォータニオン?否!ベクトルを信じろ!
肝心の角度追尾の実装ですが、まず思いつくのは回転を線形補完すればいいのではないかということです。
しかし、オイラー角もクォータニオンも線形補完はうまくいきません。
確認するために、まずはクォータニオンで実装してみましょう。
実装は以下の通りです。
指定した角速度通りに動かしますが、ずっと指定角度でしか動かないとカクついてしまうので、
方向が近い場合はプレイヤーの方を向けちゃいます。
void AHomingArrow::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// ホーミング処理
if (player_.IsValid())
{
const FQuat currentQuat = arrow_->GetComponentRotation().Quaternion();
const FVector direction = (player_->GetActorLocation() - GetActorLocation()).GetSafeNormal();
const FQuat targetQuat = direction.Rotation().Quaternion();
const float degree = FMath::Abs(FMath::RadiansToDegrees(FMath::Acos( arrow_->GetForwardVector() | direction)));
const float deltaDegree = angularSpeed_ * DeltaSeconds;
if (degree <= deltaDegree)
{
arrow_->SetWorldRotation(targetQuat);
}
else
{
const FQuat nextQuat = FQuat::FastLerp(currentQuat, targetQuat, deltaDegree / degree);
arrow_->SetWorldRotation(nextQuat);
}
}
}
これを実装すると以下のようになります。
一見問題ないように見えますが、右上の矢印が遠回りしています。
これはクォータニオンの性質上仕方のないものになります。
他の補完関数のFQuatSlerp
を使用しても同様の結果となります。
それでは実装をベクトルに移してみましょう。
ベクトルから向きを取り出すにはFVector::Rotation
という関数を使います。
FVector::RotateAngleAxis
という関数を用います。
軸のベクトルを指定して指定角度回転させる関数です。
回転の軸は現在の方向とプレイヤーへの方向の外積によって求めることができます。
以下の画像がわかりやすいですね。
外積とは?ベクトルの積の意味/計算法/公式をわかりやすく解説
注意点としてはUE4は左手系ですので、左手による左ねじの法則になります。
実装は以下のような感じです。
void AHomingArrow::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// ホーミング処理
if (player_.IsValid())
{
const FVector targetDirection = (player_->GetActorLocation() - GetActorLocation()).GetSafeNormal();
const FVector currentDirection = arrow_->GetForwardVector();
const float degree = FMath::Abs(FMath::RadiansToDegrees(FMath::Acos(targetDirection | currentDirection)));
const float deltaDegree = angularSpeed_ * DeltaSeconds;
if (degree <= deltaDegree)
{
arrow_->SetWorldRotation(targetDirection.Rotation());
}
else
{
const FVector axis = currentDirection ^ targetDirection;
arrow_->SetWorldRotation(currentDirection.RotateAngleAxis(deltaDegree, axis).Rotation());
}
}
}
外積によって回転の向きが決定されるので、角度の正負は問わないところがポイントです。
上から真ん中に向く場合と下から真ん中に向く場合でどう違うか、左手を回せば理解できると思います。
~~大して時間がかからないので何も考えずに両方試して…~~外積をちゃんと理解して正しい方向に実装しましょう。
すると以下のようになります。
右上の矢印も最短距離でこちらを向いてくれていますね。
#まとめ
向きの実装に悩んだらオイラー角やクォータニオンではなくベクトルで考えてみると楽になることがあります。