2013年3月6日水曜日

AI開発&クォータニオン入門~3.敵キャラをプレイヤーに向かせる

関連記事

では例のごとく、Unity Gemsからの翻訳をどうぞ!

--------

http://unitygems.com/quaternions-rotations-part-1-c/
October 14, 2012


敵キャラをプレイヤーに向かせる方法


最も良くあるケースは、何か別のものを見させるということだ。3D宇宙空間など全ての角度から見られるケースは特に問題がない。しかし、敵キャラを地面の上に立つ時は問題が生じるケースがある。


チュートリアルのシーン1


敵キャラに何かを見させる時、明らかにやらないといけないことは、Transform.LookAt(指定位置)  を使用することだ。最初のサンプルではプレイヤーのカプセルがあり、カメラをアタッチしてある。標準的なFPS入力で制御して移動し、MouseLook コントローラによりビューが回転できるようにしてある。

シーンには4人の敵がいるとする。最初のシーンでは敵の動作は単純で、プレイヤーの方を見るだけだ。 BasicLookAtPlayer.cs というスクリプトを使用することにする:
using UnityEngine;
using System.Collections;
 
public class BasicLookAtPlayer : MonoBehaviour {
    // Update is called once per frame
    void Update () {
        transform.LookAt(Camera.main.transform.position);
    }
}
見て分かるとおり、これは標準的なLookAt コマンドを使用して敵キャラはメインカメラの方向、すなわち今回の場合はプレイヤーを見る。残念ながらこの手法はプレイヤーと敵で高さが異なる位置にいる場合に欠点がある。結果的に敵キャラが不自然に仰向けとなる可能性がある。

Y軸周りのみを回転


やらないといけないことは、プレイヤーの方を向くように敵をY軸周りに回転させることであり、X軸やZ軸も回転できるようにすると不自然な方向に傾くことになる。

チュートリアルのシーン2

安心してほしいが、これを実現するのは難しくない。このシーンでは LookAtPlayerOnOneAxis スクリプトを使用する。
using UnityEngine;
using System.Collections;

public class LookAtPlayerOnOneAxis : MonoBehaviour {

    // Update is called once per frame
    void Update () {
        var newRotation = Quaternion.LookRotation(Camera.main.transform.position - transform.position).eulerAngles;
        newRotation.x = 0;
        newRotation.z = 0;
        transform.rotation = Quaternion.Euler(newRotation);
    }
}
はじめに、敵の現在位置からメインカメラ(プレイヤー) に向かう方向の新規のクォータニオンを作成する。Quaternion.LookRotation は引数として direction (向き) 、すなわちカメラの位置から敵の位置を引算して得られる向き、を使用する。クォータニオンを作成した後、Vector3 角度の表現に戻すためには eulerAngles を使用してnewRotation に格納する。 作成した回転は最初のシーンで LookAt メソッドを使用して得られたものと同一の結果となる。 これをy 軸だけの回転にするためには単にx およびz 軸を0 にセットする。次にQuaternion.Eulerを用いて更新された値をクォータニオンに戻す。

回転をスムーズにする


現時点では敵はプレイヤーの方向に一瞬でピタリと向くだけだ。これは現実的ではなく敵がシーンを動き回り始めると奇妙に映る。

チュートリアルのシーン3

Quaternion.Slerpを使用して敵現在のキャラクターの回転とプレイヤーの方向に向いたターゲットの回転の間の動きを補間できる。
using UnityEngine;
using System.Collections;

public class SmoothLookAtPlayerOnOneAxis : MonoBehaviour {

    // Update is called once per frame
    void Update () {
        var newRotation = Quaternion.LookRotation(Camera.main.transform.position - transform.position).eulerAngles;
        newRotation.x = 0;
        newRotation.z = 0;
        transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler(newRotation), Time.deltaTime);
    }
}

ここでもまた、LookRotation を使用して回転を行い、x およびz の角度をゼロにする。次にSlerpを使用して現在の回転とターゲットの間を補間する。

Slerp は3 つのパラメータをとる。最初が開始時の回転、二つ目は終了時の回転、三つ目は 0 と 1 の間の値であり、開始あるいは終了にどれだけ近いかを示す。つまり関数の結果は、値が 0 のときは開始時点の回転と等しく、値が 1 のときは終了時点の回転と等しく、値が 0.5 のときは二つの回転の中間値と等しくなる、という要領だ。

もし開始と終了を結んだ直線上の線形補間でない位置をセットしたい場合、値を緩和 (ease)する関数を適用することが出来る。ease 関数は 0 と 1 の間の値をとる引数をとり、 戻す値も 0 と 1 の間だが、戻り値は曲線にもとづく。これを使用するメリットは、最初の動作は遅く、途中から速くして、最後にまた遅くする動きが実現できることだ。さらには関数を組んで好きなように調整が出来る。

今回のケースでいえば、毎フレームごとに潜在的な回転を更新したいため、開始と終了時点およびどれぐらいの速さで移動させるべきかが明確ではない。回転をスムーズにみせるために現在の回転を使って終了時の回転に毎フレーム更新する。これを行うために三つ目の引数にTime.deltaTime をセットする。

このメリットとして、回転の初期は素早く回転することだ(キャラクターと敵の角度の差が大きいため)。Time.deltaTime を使用する意味はターゲットの回転に向かって毎フレーム n% だけ近づくことである(Time.deltaTime が0.1秒なら、n% は10%といった具合)。次のフレームでターゲットの回転により近づき(キャラクターは動いてないと仮定)、差の n% だけ移動する。ターゲットの回転に近づくにつれて、回転速度は遅くなり、徐々に速度が緩やかになる見映えの良い動作が得られる。

回転がターゲットに近づくにつれて差は小さくなるが、それでも n% だけ近づくため、全く同じ値となるまではかなり長い時間がかかる。見た目は良くなるが、回転が全く同じとなるのをチェックするのでは、常に動き続けるため失敗することになる。

最大速度でのスムーズな回転

先ほどの例での問題は、回転が大きく異なる場合はキャラクターが急に速い回転をするため不自然な動作になることがある。そこで、最大の回転速度をある角度毎秒に限定する方法が必要となる。
チュートリアルのシーン4

四つ目のシーンでは RotateToPlayerWithAMaximumSpeed 関数を使用する。
using UnityEngine;
using System.Collections;
 
public class RotateToPlayerWithAMaximumSpeed : MonoBehaviour {
 
    public float maximumRotateSpeed = 40;
    public float minimumTimeToReachTarget = 0.5f;
    Transform _transform;
    Transform _cameraTransform;
    float _velocity;
 
    void Start()
    {
        _transform = transform;
        _cameraTransform = Camera.main.transform;
    }
 
    // Update is called once per frame
    void Update () {
        var newRotation = Quaternion.LookRotation(_cameraTransform.position - _transform.position).eulerAngles;
        var angles = _transform.rotation.eulerAngles;
        _transform.rotation = Quaternion.Euler(angles.x, Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed),
            angles.z);
    }
} 
この先は、パフォーマンスの最適化についても検討する。カメラおよび敵のTransform を取得するところを最適化する。青色ハイライト表示された行はこれらのプライベート変数をキャッシュするところを表す。

Transform を使用することを変数のアクセスと同様に捉えている人がいるかもしれないが、それは誤りだ。transform.position と記述したときは GetComponent<Transform>().position を短く記述していることと同様だ。繰り返しこの処理を行う場合はパフォーマンス上明らかなオーバーヘッドとなり、Transformおよびその他の同様の変数をキャッシュするのは良い習慣だ。

このため最大の速度で回転を行うためにはMathf.SmoothDampAngle をコールする。今回は回転の eulerAngles を組み合わせてY軸回転には新しい変数を用いる。SmoothDampXXXX 関数(Vector3 および Mathfに存在する)の働きは、現在の速度、速度の最大値およびターゲット時間を取得し て動作を完成させる。現在の速度はルーチンとして使用され、float 値を与えて格納する必要がある。ただし、自前でセットするわけではない。
Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed) 
maximumRotateSpeedは解説が必要と思うが、minimumTimeToReachTarget により回転の開始および終了はスムーズとなる。回転の値に 0 をセットすると maximumSpeed を超えない範囲で出来るかぎり早く終了し、値が大きいほど回転がより遅くなり、理想的なタイミングで継続的な回転が終わることが期待どおりの動きである。こうすることの長所はターゲット値に近づくにつれて回転がスムーズに遅くなることだ。
void Update () {
    var newRotation = Quaternion.LookRotation(_cameraTransform.position - _transform.position).eulerAngles;
    var angles = _transform.rotation.eulerAngles;
    _transform.rotation = Quaternion.Euler(angles.x, Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed),
        angles.z);
}
update のルーチン全体でターゲットの回転の計算処理をを行い、敵の現在の回転を取得して Y 軸の回転を SmoothDampAngle で置き換えて敵キャラをプレイヤーの方向に向くように回転させる。非常に便利で使い勝手の良い動作が得られる。
-------
次に続くぜ!


関連記事


0 件のコメント:

コメントを投稿

ブックマークに追加

このエントリーをはてなブックマークに追加

自己紹介

自分の写真
Unity3D公式マニュアル翻訳やってる人がスマホ(iPhone, Android)のゲーム開発しています。気軽に面白く初心者が遊べる内容がモットー。Blogでは開発情報をひたすら、Twitterではゲーム作成の過程で参考にしている情報を中心につぶやきます

ページビューの合計

過去7日間の人気投稿