2013年3月6日水曜日

AI開発&クォータニオン入門~7.動作を加えてみる

関連記事

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


----------------

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

動作を加えてみる


さて面白みを加えるために敵キャラがプレイヤーを攻撃する動作を追加する。再びクォータニオンを使用して敵キャラがターゲットとする位置変化させ、プレイヤーの周りを違った形で回転させることで背後から回り込んで襲ってくるようなロジックに変えてみたい。いっぺんにそこまで出来ないので、まずは MovementStateMachine3 を見てみよう。面白みを足すために大幅にコード行数を足してみた:
using UnityEngine;
using System.Collections;
 
public class MovementStateMachine3 : MonoBehaviour {
 
    public bool sleeping = true;
    public Transform sleepingPrefab;
    public float attackDistance = 5;
    public float sleepDistance = 30;
    public float speed = 2;
    public float health = 20;
    public float maximumAttackEffectRange = 1f;
 
    Transform _transform;
    Transform _player;
    public Transform target;
 
    public EnemyMood _mood;
 
    CharacterController _controller;
    AnimationState _attack;
    AnimationState _die;
    AnimationState _hit;
    Animation _animation;
 
    float _attackDistanceSquared;
    float _sleepDistanceSquared;
    float _attackRotation;
    float _maximumAttackEffectRangeSquared;
    float _angleToTarget;
 
    bool _busy;
 
    // Use this for initialization
    IEnumerator Start () {
        _transform = transform;
        _player = Camera.main.transform;
 
        _mood = GetComponent();
 
        _attackDistanceSquared = attackDistance * attackDistance;
        _sleepDistanceSquared = sleepDistance * sleepDistance;
        _maximumAttackEffectRangeSquared = maximumAttackEffectRange * maximumAttackEffectRange;
 
        _controller = GetComponent();
 
        _animation = animation;
        _attack = _animation["attack"];
        _hit = _animation["gothit"];
        _die = _animation["die"];
 
        _attack.layer = 5;
        _hit.layer = 5;
        _die.layer = 5;
 
        _controller.Move(new Vector3(0,-20,0));
 
        while(true)
        {
            yield return new WaitForSeconds(Random.value * 6f + 3);
            if(sleeping)    
            {
                var newPrefab = Instantiate(sleepingPrefab, _transform.position + Vector3.up * 3f, Quaternion.identity) as Transform;
                newPrefab.forward = Camera.main.transform.forward;
            }
        }
 
    }
 
    void Update()
    {
        // 何か別にコントロールされてるかチェック
        if(_busy)
            return;
 
        if(sleeping)
        {
            if((_transform.position - _player.position).sqrMagnitude < _attackDistanceSquared)
            {
                sleeping = false;
                target = _player;
                // 敵キャラが攻撃する位置
                _attackRotation = Random.Range(60,310);
            }
        }
        else
        {
            // ターゲットが死亡した場合は、スリープに戻る
            if(!target)
            {
                sleeping = true;
                return;
            }
 
            var difference = (target.position - _transform.position);
            difference.y /= 6;
            var distanceSquared = difference.sqrMagnitude;
 
            // 十分に遠い位置にいるか?
            if( distanceSquared > _sleepDistanceSquared)
            {
                sleeping = true;
            }
            // 攻撃範囲内にいるか?
            else if( distanceSquared < _maximumAttackEffectRangeSquared && _angleToTarget < 40f)
            {
                StartCoroutine(Attack(target));
            }
            // それ以外の場合は移動
            else
            {
 
                // ターゲットの位置を決定
                var targetPosition = target.position + (Quaternion.AngleAxis(_attackRotation, Vector3.up) * target.forward * maximumAttackEffectRange * 0.8f);
                var basicMovement = (targetPosition - _transform.position).normalized * speed * Time.deltaTime;
                basicMovement.y = 0;
                //Only move when facing
                _angleToTarget = Vector3.Angle(basicMovement, _transform.forward);
                if( _angleToTarget < 70f)
                {
                    basicMovement.y = -20 * Time.deltaTime;
                    _controller.Move(basicMovement);
                }
            }
        }
    }
 
    void OnTriggerEnter(Collider hit) 
    {
        if(hit.transform == _transform)
            return;
 
        if(hit.transform == _player)
        {
            StartCoroutine(Attack(_player));
        }
        else
        {
            var rival = hit.transform.GetComponent();
            if(rival)
            {
                if(Random.value > _mood.mood/100)
                {
                    StartCoroutine("Attack",rival.transform);
                }
            }
        }
    }
 
    IEnumerator Attack(Transform victim)
    {
        sleeping = false;
        _busy = true;
        target = victim;
        _attack.enabled = true;
        _attack.time = 0;
        _attack.weight = 1;
        // アニメショーンが半分進むまで待つ
        yield return StartCoroutine(WaitForAnimation(_attack, 0.5f));
        // まだ範囲内にいるか確認
        if(victim && (victim.position - _transform.position).sqrMagnitude < _maximumAttackEffectRangeSquared)
        {
            // ダメージを適用
            victim.SendMessage("TakeDamage", 1 + Random.value * 5, SendMessageOptions.DontRequireReceiver);
        }
        // アニメショーン終了を待機
        yield return StartCoroutine(WaitForAnimation(_attack, 1f));
        _attack.weight = 0;
        _busy = false;
    }
 
    void TakeDamage(float amount)
    {
        StopCoroutine("Attack");
        health -= amount;
        if(health < 0)
            StartCoroutine(Die());
        else
            StartCoroutine(Hit());
    }
 
    IEnumerator Die()
    {
        _busy = true;
        _animation.Stop();
        yield return StartCoroutine(PlayAnimation(_die));
        Destroy(gameObject);
    }
 
    IEnumerator Hit()
    {
        _busy = true;
        _animation.Stop();
        yield return StartCoroutine(PlayAnimation(_hit));
        _busy = false;
    }
 
    public static IEnumerator WaitForAnimation(AnimationState state, float ratio)
    {
        state.wrapMode = WrapMode.ClampForever;
        state.enabled = true;
        state.speed = state.speed == 0 ? 1 : state.speed;
        while(state.normalizedTime < ratio-float.Epsilon)
        {
            yield return null;
        }
    }
 
    public static IEnumerator PlayAnimation(AnimationState state)
    {
        state.time = 0;
        state.weight = 1;
        state.speed = 1;
        state.enabled = true;
        var wait = WaitForAnimation(state, 1f);
        while(wait.MoveNext())
            yield return null;
        state.weight = 0;
 
    }
 
}
さあ、面白みを増した箇所それぞれ見ていこう。まずは Start() の変更箇所だ。
_mood = GetComponent();

 _attackDistanceSquared = attackDistance * attackDistance;
 _sleepDistanceSquared = sleepDistance * sleepDistance;
 _maximumAttackEffectRangeSquared = maximumAttackEffectRange * maximumAttackEffectRange;

 _controller = GetComponent();

 _animation = animation;
 _attack = _animation["attack"];
 _hit = _animation["gothit"];
 _die = _animation["die"];

 _attack.layer = 5;
 _hit.layer = 5;
 _die.layer = 5;
自明なことだが、パフォーマンス上の理由であちらこちらでキャッシングを意識して行っている。また距離はすべて2乗の値を使用している。これは敵キャラからの距離を使用するときの平方根計算を避けるためである。

2点間の距離はピタゴラスの定理を使用して平方根計算を伴う。平方根計算は高価な再帰処理を使用する。この計算負荷を回避するにはチェックしたい処理の2乗を使用すれば良い。

いくつかのアニメショーンを高い値のレイヤーに使用して、通常のアイドル状態のアニメショーンが上書きされるようにした。

Update() 関数はかなり膨大だが、始まりの部分は次のようにコーディングする:
        //何か別にコントロールするものがあるかチェック
if(_busy)
    return;
攻撃するときや、ヒットされるときの動作については、キャラクターを別にコントロールするための仕組みを検討する。これが開始されると _busy が true となりUpdateを無視することができる。

次に敵がスリープしているときのロジックが必要だ。
        if(sleeping)
{
    if((_transform.position - _player.position).sqrMagnitude < _attackDistanceSquared)
    {
        sleeping = false;
        target = _player;
        // 攻撃する時の敵の位置
        _attackRotation = Random.Range(60,310);
    }
}
敵がスリープしているとき、attackDistance (攻撃距離) の範囲内にいるかどうかをチェックし、範囲内ならばスリープを解除してプレイヤーにターゲットを切り替える。さらにプレイヤーの周囲の特定の回転を割り当ててる。(敵キャラは巧妙に仕掛けてきて横や背後から迫ってくる。) 現時点ではただ変数として格納するだけで使用方法はこの後に見て行く。敵キャラがスリープ状態でないときの動作は次の通りとなる:
//ターゲットが死亡した場合はスリープに戻る
if(!target)
{
    sleeping = true;
    return;
}

var difference = (target.position - _transform.position);
difference.y /= 6;
var distanceSquared = difference.sqrMagnitude;

// 十分に遠い位置にいるか?
if( distanceSquared > _sleepDistanceSquared)
{
    sleeping = true;
}
// 攻撃範囲内にいるか?
else if( distanceSquared < _maximumAttackEffectRangeSquared && _angleToTarget < 40f)
{
    StartCoroutine(Attack(target));
}
// それ以外の場合は移動する
else
{

    // ターゲットの位置を決定
    var targetPosition = target.position + (Quaternion.AngleAxis(_attackRotation, Vector3.up) * target.forward * maximumAttackEffectRange * 0.8f);
    var basicMovement = (targetPosition - _transform.position).normalized * speed * Time.deltaTime;
    basicMovement.y = 0;
    // 見える角度にいる場合のみ移動
    _angleToTarget = Vector3.Angle(basicMovement, _transform.forward);
    if( _angleToTarget < 70f)
    {
        basicMovement.y = -20 * Time.deltaTime;
        _controller.Move(basicMovement);
    }
}
ターゲットが死亡したか、十分に離れた場合はスリープとした。それ以外の場合は攻撃範囲内にいるならば攻撃するためのコルーチンを実行する。スリープ状態でなく攻撃範囲内にいない場合はターゲットの方向に移動することにする。

これを実現するためにまず必要なのはプレイヤー位置の算出だ。これはターゲットの位置、ターゲットと決めたときの角度、攻撃範囲の距離、の組み合わせだ (距離の80%ぐらいとしたい) 。つまり、この角度にもとづいてクォータニオンを使用してターゲットの向きおよびベクトルを攻撃範囲の 80% となるようにスケールして修正する。これら全てで targetPosition をコールする。例えばもし選択した角度が180度だったとして攻撃範囲の距離が2だった場合、ターゲットの位置はカメラの真後か 1.6 となる。

次にプレイヤーの向きと行きたい位置の間の角度を算出する。もし、差分ごが大きすぎる場合はまだ動かないことする (回転スクリプトにより角度の問題を解決する)。これが問題なければ次はターゲットの位置に移動して重力を加え、敵キャラが地面から浮かないようにする。
---------
次回がいよいよ今シリーズ最終回!


関連記事


AI開発&クォータニオン入門~6.カメラの方向を向くようにPlaneを回転

関連記事

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

----------------

カメラの方向を向くように Plane を回転させる


ここまで何とかしてテキストメッシュにカメラの方向を正しく向かせることには成功した。次は平面をどう扱うか考える番だ。プレイヤーに表示する 2D テクスチャを作成するのに Unity で作成した Plane は適している。画面の方向を常に向く 2D ビルボードを作成しよう。

ここで敵キャラに「ムード」があることにしよう。最初はある一定の機嫌の良さから始めて、時間の変化とともに機嫌が悪くなる。ゲームの中では色んな場面に応用して面白い要素に出来るはずだ。

プレイヤーは敵キャラを正面から見据えたときにこのムードが判別出来るようにしたい。敵キャラの頭上に機嫌を表すスマイルマークを表示して、色での工夫も重ねて機嫌がハッピーな時は緑色、怒りの時は赤色と、反映する。

当然、これにより必要なスクリプトは新たにいくつか必要となる。

最初に敵キャラを正面から見据える方法から考えて見よう。まずプレイヤーにスクリプトをアタッチして目の前のものに対して反応する処理を試す。
using UnityEngine;
using System.Collections;

public class LookTrigger : MonoBehaviour {

    Transform _transform;

    // Use this for initialization
    void Start () {
        _transform = transform;
    }

    // Update is called once per frame
    void Update () {
        RaycastHit hit;
        if(Physics.SphereCast(_transform.position + _transform.forward * 0.5f, 3f, _transform.forward, out hit, 400))
        {
            hit.collider.SendMessage("LookedAt", SendMessageOptions.DontRequireReceiver);
        }
    }
}
プレイヤーの正面の位置で SphereCast を実行してそれが何かと当たり判定があれば "LookedAt" されたとみなして、オブジェクトに対してその旨のメッセージ送信する。

ここで SphereCast を使用する理由は距離が離れたものについて多少のズレは許容する適度な正確さで扱ってくれるためである。

さて、ここまで簡単だったが次に敵キャラにムードを持たせる番だ。EnemyMood という新規のスクリプトの準備を進めていこう。
using UnityEngine;
using System.Collections;

public class EnemyMood : MonoBehaviour {

    public MoodIndicator moodIndicatorPrefab;
    public float _mood;

    Transform _transform;
    MoodIndicator _currentIndicator;

    void Start () {
        _transform = transform;
        mood = Random.Range(30,99);
    }

    void Update () {
        mood -= Time.deltaTime/2;
    }

    void LookedAt()
    {
        if(_currentIndicator)
            return;

        _currentIndicator = Instantiate(moodIndicatorPrefab, _transform.position + Vector3.up * 3.5f,
            Quaternion.identity) as MoodIndicator;

        _currentIndicator.enemy = this;
    }

}
初めに mood 変数を 30 と 99 の間の値で初期化する。次にこれを Update コールの中で 徐々にデクリメントさせる。

プレハブ変数を Plane に追加してムードを表示したうえ、プレハブに MoodIndicator スクリプトを与えるので、後ほど詳細に中身を検証していきたい。

EnemyMood は LookedAt メッセージを受け取り、すでに 既存の MoodIndicator があるかチェックし、あれば return で関数を終了する。

Unity は MoodIndicator が破棄されたときは自動的にこの変数の値として null/false を戻す。

もし既存の MoodIndicator がなかった場合、新たに作成して敵キャラの上に配置する。次に indicator に対して敵キャラのムードが現在どの状態であるかを通知する。

MoodIndicator は Plane の表示をハンドリングして適切な画像の表示を行う。
using UnityEngine;
using System.Collections;

public class MoodIndicator : MonoBehaviour {

    public Texture2D[] moodIndicators;
    public Color happyColor = Color.green;
    public Color angryColor = Color.red;
    public float fadeTime = 4f;
    public EnemyMood enemy;

    Transform _transform;
    Transform _cameraTransform;
    Material _material;
    Color _color;
    Quaternion _pointUpAtForward;

    void Start () {

        _pointUpAtForward = Quaternion.FromToRotation(Vector3.up, Vector3.forward);

        _material = renderer.material;

        // 画像のセット
        _material.mainTexture = moodIndicators[
            Mathf.Clamp(
                Mathf.RoundToInt( enemy.mood/(100/moodIndicators.Length)),
                0,
                moodIndicators.Length-1)
            ];

        // 色の計算
        var moodRatio = enemy.mood/100;
        _material.color = _color = new Color(
            angryColor.r * (1 - moodRatio) + happyColor.r * moodRatio,
            angryColor.g * (1 - moodRatio) + happyColor.g * moodRatio,
            angryColor.b * (1 - moodRatio) + happyColor.b * moodRatio
        );

        Update();
    }

    void Awake()
    {
        _transform = transform;
        _cameraTransform = Camera.main.transform;
    }

    public void Update () {

        // カメラに対して Plane を表示
        _transform.rotation = Quaternion.LookRotation(-_cameraTransform.forward, Vector3.up)
            * _pointUpAtForward;

        // 画像のフェードアウト処理
        _color.a -= Time.deltaTime/fadeTime;
        _material.color = _color;
    }
}
重要なスクリプトの初登場なのでいくつかの部分に分けて見ていこう。
public Texture2D[] moodIndicators;
public Color happyColor = Color.green;
public Color angryColor = Color.red;
public float fadeTime = 4f;
public EnemyMood enemy;
パブリック変数は画像イメージの配列であり、ムードのインジケータとして使用して、悲しみ、喜び、等々を表す。サンプルプロジェクトには4種類含まれる。ハッピーな色と怒りの色があり、後は時間の経過とともにフェードアウトさせて最終的に全てが透明になる。最後に、どの敵キャラのものかの紐付け情報がある。これら全て Start 実行開始前までにセットされる。
void Start () {

    _pointUpAtForward = Quaternion.FromToRotation(Vector3.up, Vector3.forward);

    _material = renderer.material;

    //Set the graphic
    _material.mainTexture = moodIndicators[
        Mathf.Clamp(
            Mathf.RoundToInt( enemy.mood/(100/moodIndicators.Length)),
            0,
            moodIndicators.Length-1)
        ];

    // 色の計算
    var moodRatio = enemy.mood/100;
    _material.color = _color = new Color(
        angryColor.r * (1 - moodRatio) + happyColor.r * moodRatio,
        angryColor.g * (1 - moodRatio) + happyColor.g * moodRatio,
        angryColor.b * (1 - moodRatio) + happyColor.b * moodRatio
    );

    Update();
}
最初に行うべきことはキャッシュされたクォータニオンを作成して、その up ベクトルが forward ベクトルを向くようにする。これは Plane にカメラに向けるのと全く同じ方法の復習だ。

次にムードを表すテクスチャを選択するために、敵である怪物のムード(0 から 98 の間の数字)を一定の係数で割り算して、提供されているテクスチャの範囲に収まるようにして、必ずテクスチャが有効となるように clamp を行う。

次にスマイルマークの色を判断させるためにムードにもとづいて怒りおよびハッピーマークの一部を使用する。

最後に Update を呼び出して回転のコードが直ちに呼び出しされるようにする。

Update関数は次のようになる:
public void Update () {

        // Plane をカメラに向ける
        _transform.rotation = Quaternion.LookRotation(-_cameraTransform.forward, Vector3.up)
            * _pointUpAtForward;

        // 画像のフェードアウト
        _color.a -= Time.deltaTime/fadeTime;
        _material.color = _color;
    }

最初にカメラに向く通常のオブジェクトの回転を作成するために Quaternion.LookRotation 関数を使用してカメラの -transform.forward を渡して、これを、オブジェクトの up を forward を向けてキャッシュした回転と組み合わせる。これで Plane の設定は大丈夫だ。

回転の二つ目の部分は画像のアルファ値を事前に定めた時間でフェードアウトさせるだけの処理だ。

これで準備完了だ。実行して試すと敵キャラを画面の中央に持ってきた時にインジケータがポップアップする。敵キャラの機嫌もあっという間に怒りモードに変化する。
------
次に続くぜ!


関連記事


AI開発&クォータニオン入門~5.回転を組み合わせる


関連記事

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

----------------

回転を組み合わせる


ここまでの例で Zzzzzz の表示は退屈だったかもしれないが、ここでは一歩進んでクォータニオンの組み合わせをどうするか見て行くことにする。Zzzzzz 記号が上方向に流れながら徐々に回転する様子を見て行く。ここで意味していることは、カメラの方向は向いているけど、画面上をゆっくりとスピンする動作とする。

チュートリアル シーン7

この回転を得るために新しく SpinAroundForward スクリプトを作成する。
using UnityEngine;
using System.Collections;

public class SpinAroundForward : MonoBehaviour {

    Transform _transform;
    float _rotatingFactor;

    void Start()
    {
        _transform = transform;
        _rotatingFactor = 0.2f + (Random.value * 0.8f);
    }

    // Update is called once per frame
    void Update () {
        _transform.rotation = _transform.rotation * Quaternion.AngleAxis(Time.deltaTime * 20 * _rotatingFactor, Vector3.forward);
    }
}
このスクリプトは、Startの際に新規の回転の係数をランダム作成して、Updateの際にそれを適用する。

オブジェクトの現状の回転を Vector3.forward の周りの回転とどう組み合わせていくかの様子を見ることが出来る。* 演算子を用いて Zzzzzz の現在の回転と組み合わせることが出来る。

もしチュートリアル シーンでこのスクリプトを実行する場合、テキストは回転するけどカメラの方向を向いてないことが分かる。これには理由がある: ReverseCameraDirection スクリプトがオフになるからだ。もし Sleeping Text 3をクリックして ReverseCameraDirection を有効化すると今度は Zzzzzz ごカメラを向くが回転しないことが分かる。これは Transform ベクトルを直接に値変更するときにハマりやすいことだ。回転の組み合わせがグチャグチャになる。目的どおりにやりたい場合は別のアプローチが必要だ。

チュートリアル シーン8

シーン8 では ReverseCameraDirection にてクォータニオンを使用するように書き換えた AdjustForwardToPointToCamera と置き換える。
using UnityEngine;
using System.Collections;

public class AdjustForwardToPointToCamera : MonoBehaviour {

    Transform _transform;
    Transform _cameraTransform;

    // Use this for initialization
    void Start () {
        _transform = transform;
        _cameraTransform = Camera.main.transform;
    }

    // Update is called once per frame
    void Update () {
        _transform.rotation = Quaternion.FromToRotation(_transform.forward, _cameraTransform.forward) * _transform.rotation;
    }
}
Quaternion.FromToRotationは調整が必要なベクトルがある場合に強力だ。これを使ってオブジェクトが配置されている物体の表面に合わせて回転させて、例えばそれが表面の法線と並ばせることが出来る。このケースでは Zzzzzz の transform.forward の回転を調整させてカメラのtransform.forwardと一致させることが出来る。

繰り返しになるが、メッシュは逆向きに反転していることを学習すべきだ。通常のオブジェクトの場合、オブジェクトの transform.forward を - transform.forward としないとカメラ方向を向かない。

作成したクォータニオンを前方向に回転させていたものに加えて、正しい角度と現在の回転 (SpinAroundForward スクリプトによりさらに変更されているもの) を組み合わせて完成だ。カメラが正しい方向を向いて、回転して Zzzzzz がついている (ようやくここまで来た!)。
-----
次に続くぜ!


関連記事

AI開発&クォータニオン入門~4.テキストをカメラの方向に向かせる

関連記事

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

----------------

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


テキストをカメラの方向に向かせる


さあ、サンプルゲーム作成をもう一歩進めてみることにする。敵の動作はまだ単純すぎるので AI の要素を加えてみることにする。最初にすることはプレイヤーが近づくまでスリープ状態にすることだ。ここでは、スリープ状態を視覚的に分かるように頭上に Zzzzz とイビキをかいてる様子を描画してみることとする。

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

ここでは、まず初歩的なFSM (ステートマシン) を各々の敵に追加する。これは  MovementStateMachine に含まれる。
using UnityEngine;
using System.Collections;
 
public class MovementStateMachine : MonoBehaviour {
 
    public bool sleeping = true;
    public Transform sleepingPrefab;
 
    // Use this for initialization
    IEnumerator Start () {
        var t = transform;
        while(true)
        {
            yield return new WaitForSeconds(Random.value * 3f + 2);
            if(sleeping)    
            {
                Instantiate(sleepingPrefab, t.position + Vector3.up * 3f, t.rotation);
            }
        }
    }
}

これで、スリープ状態か判別する単純なステートマシンが得られた。ただし、これだけだとウェイクアップしたときに何も処理が行われない。さらにはウェイクアップさせる条件なども何も決めてない。ただ、それでもアイテムを回転させてカメラの方向に向ける動作は十分に実現出来る。

スクリプトの起動時にコルーチン処理を開始して、もしスリープ状態とスクリプトで判定すれば数秒おきに敵の頭上にプレハブをインスタンス化する。

コルーチンを使用するとき、ローカル変数にコンポーネントをキャッシュする方法があることに注目してほしい。

プレハブそのものは単にテキストメッシュとメッシュレンダラ、時間の経過とともに上昇するようにな」っているスクリプト、シーン1のBasicLookAtPlayer スクリプトから構成される。

テキストを上昇させるスクリプトもまた自明で簡単なものだ。
using UnityEngine;
using System.Collections;

public class GentleRiseAndDestroy : MonoBehaviour {

    public float timeToLive = 2f;
    Transform _transform;

    // Use this for initialization
    void Start () {
        _transform = transform;
        Destroy(gameObject, timeToLive);
    }

    // Update is called once per frame
    void Update () {
        _transform.position += Vector3.up * Time.deltaTime;
    }
}
スクリプトは数秒後にオブジェクトを破棄して、毎フレーム毎にゆっくりとテキストを上昇させる。

期待している動作は Zzzzzz の記号が、カメラの位置に関わらず画面上に表示されることだ。プレイヤーが動くことでテキストの裏側に回り込めるのは論外だ。しかし、ここで解決しないといけない課題がいくつかある。

まず Zzzzzz は逆向きに表示されるが、その理由はUnityによりテキストメッシュの forward がカメラから離れる方向と判断してテキストを左から右へと読めるようにしようとするためだ。

Unity では、オブジェクトの向きに関して変な仕様がいくつかある。Unity で生成した Plane は forward ではなく up に向く。また、テキストメッシュは通常と逆に感じる - forward 方向に向く。

敵に近づいたとき、もう一つの問題に気付くことになるが、Zzzzzz は画面から外の方向に向かない。多くの人がこれで混乱する (私もその一人だった) 。LookAt 関数でカメラに向けたつもりが「何で上手く行かないの?」、と。

Transform ベクトルの修正


LookAt の問題は、実際にはカメラの位置をみていることだ。しかしカメラはワールド座標の固定位置にありながら、それと同時に Focal Area = 焦平面(シャッターの範囲)にある映像が投影される仕組みだ。オブジェクトは自らの位置にもとづいて固定の位置をみているので、カメラによりレンダリングされる位置とズレが生じることを学習する必要がある。

チュートリアル シーン6

このあたり巧いテクニックがある。但しいつでも使えるものではなく次のセクションでは別のアプローチも見ていきたい。transform.forward によりワールド座標でのVector3.forward と同様にオブジェクトの向きを表現できることはご存知のはずだ。意外にも知られていないのは transform.forward をセットしてオブジェクトを回転出来ることだ。

次にやるべきことはカメラの位置を見ることをやめて、テキストメッシュにカメラのFocal Area(焦平面)への向きを定義することだ。

テキストメッシュが反転されてることを覚えてほしい。通常のモデルであれば -transform.forward としないとカメラと逆方向を向いてしまう。
using UnityEngine;
using System.Collections;

public class ReversedCameraDirection : MonoBehaviour {

    Transform _transform;
    Transform _cameraTransform;

    // Use this for initialization
    void Start () {
        _transform = transform;
        _cameraTransform = Camera.main.transform;
    }

    // Update is called once per frame
    void Update () {
        _transform.forward = _cameraTransform.forward;
    }
}
Zzzzz のスリープ状態テキストのプレハブに ReverseCameraDirection を BasicLookAtPlayer の代わりにアタッチすることでようやく準備は万端だ。

transform のベクトルを forward, up, right の方向をセットする機能は強力であり、回転でなくベクトルをベースに考えることが出来て便利だが、注意すべきことがある。これらの値の一つをセットした場合、他のベクトルは制御出来ておらず、ときどきおかしな向きに反転したり回転していることがある。
-------
次に続くぜ!


関連記事

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 で置き換えて敵キャラをプレイヤーの方向に向くように回転させる。非常に便利で使い勝手の良い動作が得られる。
-------
次に続くぜ!


関連記事


AI開発&クォータニオン入門~2.入門編

関連記事

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

--------

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

クォータニオン入門

クォータニオンはUnityで回転を表現するために使用される内部形式だ。(つまり変換するための行列とは別次元のもの)

そしてクォータニオンは多くの開発者がいきなりつまづくところだ。分からなくなる大きな要因はインスペクタ上の表示でもあり、初めて見るとき x、y、z軸のVector3 で表示されていてこれがミスリードだ。この表示は本当はクォータニオンではない。むしろ回転のオイラー角に当たる。クォータニオン自身は確かに、x、y、zというプロパティを本当に持っているのだが、ひどいことにそれらの値はインスペクタで表示されているものと一切関係がないことだ。(反論もあるかもしれないが、あえてここでは言い切ってみよう)

クォータニオンを作成する方法はいくつかある。角度のVector3 から作成するには 三つの回転軸をパラメータとしてQuaternion.Euler を使用するか、回転を表すVector3を使用する。一定方向に向けた回転を作成するにはQuaternion.LookRotation(directionVector) を使用する。このチュートリアルでは、進めて行きながらクォータニオンを作成する様々な方法や組み合わせをみていきたい。

さて、オイラー角およびクォータニオンの違いについて整理すると:

オイラー角:


長所:
  • 3Dベクトルを使用するので、理解しやすく視覚化も容易
  • クォータニオンで扱うのは4変数、こちらは3変数なので演算が速い
短所:
  • 回転により得られる結果が必ずひととおりでない。エンジンによって  (x,y,z) または (y,x,z) またはそれ以外の順番を使用することがある。結果は毎回異なる
  • ジンバル ロックが発生する。一つの回転が他と並んだとき、互いにロックされ、一つの軸が失われる。

クォータニオン:


長所:
  • ジンバル ロックの発生を回避できる
  • 回転を、複数の軸回転の組み合わせでなく、ターゲット ベクトル にもとづいて処理できる
欠点:
  • オイラー角以外にもう一つ値があるため僅かに遅い
  • スティーブン・ホーキングでもないかぎり完全には理解出来ない(笑
複数の回転は * 演算子により組み合わせることが出来る(このチュートリアルの後の方でかなり、役に立つ)。さらに Vector3 を修正するには、クォータニオンを乗算するが、ベクトルを回転した戻り値が返される。このクォータニオン演算の順番には意味がある。
var newVector = Quaternion.AngleAxis(90, Vector3.up) * Quaternion.LookRotation(someDirection) * someVector;
+ (プラス記号) ではクォータニオンを追加、組み合わせることは出来ない。 *  (アスタリスク記号) で複数の回転を組み合わせる。

回転を表すのにVector3 の代わりにクォータニオンを使用する理由の一つはオブジェクトを回転させて、あるターゲットの値をセットするのに、3つの軸回転の値を動かすだけでは実現出来ないケースがあるためだ。これはオイラー角に影響するジンバル ロックと呼ばれる現象に起因するものであり、興味あるならばウィキペディアで調べると良いが、原因は値をひとつづつ個別に動かす必要があることに起因していて、特定の組み合わせにおいては、一つの次元による変更のために別の次元がターゲットの値に到達出来ないケースがある。クォータニオンはこの問題を解決するために、回転全体を基本的に四次元にてワンステップで実現する。

オイラー角で物事を考えるのは簡単だが、クォータニオンほど強力ではない。Unityにより二つの表現方法を互いに変換させることが容易なので使用する都度に最適な方法を選択することが可能だ。例えば、ローテーションをClamp (ある値の範囲に限定 ) したい場合、クォータニオンの .eulerAngles を取得すれば良く、それからMathf.Clamp の演算で Clamp 処理を適用してオイラー角を、.eulerAngles の値を更新するか、または新しいクォータニオンをQuaternion.Euler(指定した角度)にて作成することで、クォータニオンに再び変換すれば良い。

回転を組み合わせたいとき、例えば頭の傾きと体の回転を組み合わせたいとき、どちらのアプローチも使うことが出来るが、これらの回転はワールド座標軸を中心とした回転ではない場合、例えばキャラクターがさらに前にかがんでいるようなケースはクォータニオンを使用すべきだ。クォータニオンを使用して各々の軸の周りの回転を作成してそれらを組み合わせるのは実に簡単だ。

Unityはさらに便利な軸の定義として、transform.forward, transform.right および transform.up を使用することが出来て Quaternion.AngleAxis とともに使用することで既存のオブジェクト回転に重ねて適用することご出来る。

//回転する体のうえで頭を傾けるような動作
transform.rotation = Quaternion.AngleAxis(degrees, transform.right) * transform.rotation;

AI開発&クォータニオン入門~1.目次

3Dゲーム開発で良く必要になるのが、プレイヤーに向かってくる敵キャラの存在だ。

そしてプレイヤーが移動したとき、敵キャラはターゲットとなる角度の修正を行う必要がある。そこでプログラミングするうえで必要な知識はクォータニオンと呼ばれるものだ。

いわゆるオイラー角での計算は分かるけど、クォータニオンは苦手・・・。そんな場合にUnity Gemsのこのチュートリアルをみてみると便利だろう。

クォータニオンの勉強だけでなく、Unityでダウンロードできるサンプルつきでコード解説が丁寧につている。

まずはどのようなものが出来るか見てもらいたい:
http://youtu.be/wU5RT2sXghQ

まあ、外国の方が作成している敵キャラや、上に表示されるテキストが日本人のテイストからするとやや雑だったりするとおもうが、そういった演出レベルは自ら改善することは簡単。ここで覚えてほしいのはキャラの動作を制御するプログラムの中身だ。

このUnity Gemsというウェブサイトは、初心者のためにより有用な情報を集積しようという目的で運営されている。Unity Answersの常連が集まって、過去の投稿の中から選別されているだけあって、本当に優良なサイトだと感じていて、今回の投稿も評判は悪くない。

それではAIのスキルアップも兼ねて、Unity Gemsからの翻訳をどうぞ!
--------

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

このチュートリアルで扱う内容


  • クォータニオンが便利な理由
  • クォータニオンを扱う上で良くある間違い
  • 敵キャラがプレイヤーの方を向く動作にリアリティを持たせる方法
  • テキストをカメラに向ける方法
  • 複数のクォータニオンを組み合わせる方法
  • プレーンで作成したビルボードをカメラに向ける方法
  • クォータニオンを使って敵AIのポジションをセットする方法
  • コルーチンを使用してアニメーションの完了を待つ方法
  • コルーチンで動かす基本的なステートマシンを作成する方法

チュートリアル プロジェクト


このチュートリアルで扱うプロジェクトファイルはダウンロードが出来る:

リンクからダウンロード


ダウンロードしたUnityのプロジェクトは、各々のシーンがこのチュートリアルの内容に対応するように構成されている。

各々のシーン、同梱スクリプト、オブジェクトはTutorial/Part1以下に配置されているのでそこからアクセスすると良い。

そして、このチュートリアルの中で、次の記号で表示するときは、別のシーンに移動して説明が始まるということだ:
チュートリアルのシーン1

チュートリアルはアセットストアにある、いくつかの無料アセットを使用しているので、その素晴らしい機能を提供してくださった方にも感謝したい。
--------

ブックマークに追加

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

自己紹介

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

ページビューの合計

過去7日間の人気投稿