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 となる。

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


関連記事


0 件のコメント:

コメントを投稿

ブックマークに追加

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

自己紹介

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

ページビューの合計

過去7日間の人気投稿