読者です 読者をやめる 読者になる 読者になる

夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【Unity5でレースゲーム】AIカーを昆虫なみの知能に改良

Unity オープンワールドレーシングゲーム

今回はAIを改良し、周囲の状況を検知してステアリング、ブレーキの操作に反映させる機能を実装しました。
とは言え、人間のように周囲360度の状況を把握して適切な操作をするというのは無理で、せいぜい昆虫が触角に触れるものを避けたりする程度の知能です。(いや、昆虫に失礼か。)

昆虫の触角に相当するのがUnityのRaycast機能で発射するRayになります。

f:id:takujidev:20150719192542j:plain

右下の横断歩道の上にいる昆虫のような物がAIカーで、前に3本、斜め方向に4本出ている緑の線がRayです。

前に出ている3本のうち中央のRayはコーナー手前でブレーキを掛けるために使用しています。
詳細はこちら。tf.hateblo.jp

斜め前方に出ている2本で周囲の状況を読み取ります。(斜め後方に出ている2本は今のところ未使用)

この斜め前方のレイが壁や他の車に当たるとAIカーは次の動作をします。

・ 衝突を避けるためハンドルを反対側に少し戻す
・ そちらの方向に進むとまた当たるので、自分が向っている目標地点を反対方向に少し動かす
・ 加速すると当たるかもしれないので本来目標とする速度より低い速度を目標速度とする
・ ブレーキングで追突しないようにいつもよりブレーキを早めに踏む

RaycastはFixedUpdateで毎回発射しているので、ヒットしなくなるまで上の操作が繰り返されます。

基本的に、後ろを走る車が前を走っている車に当たらないように気をつければ事故は発生しないはずなので、前の車は後ろを見ずにひたすら自分のラインを走り続けます。
ただ、実際には2本の触角では死角が多すぎるためか、事故を完全に防ぐ事はできていません。

前方に伸びる3本のRayのうち、左右の2本はスリップストリーム効果を出すために使用しています。前走車にこれらのRayがヒットすると、自車のRigidbodyのDragを少し下げる事で、加速、最高速度が上がるようにしています。
本来スリップストリームは低速では効かないのと、狭い路地で突然加速すると事故が多発するのは避けられないので、前走車が一定速度以上で走っているときだけスリップを効かせています。

動画です。Rayは実際のゲームでは画面に表示されませんが、今はデバッグ用Debug.DrawRayで線を表示しています。
ヒット中は色を変えているので、RayがヒットしたときのAIの動作が良くわかると思います。
また、スリップストリームが効き始めるタイミングがわかるようにスピードメーターも付けてみました。実際には前走車の速度を見ているので、少しずれがあると思いますが、150km/hを過ぎた当りから加速して行くのがわかります。

なお、UnityのスタンダードアセットのCarControllerスクリプトにはバグがあって単位をKPHにするとスピードが正しく取れません。(単位の設定に関わらず常にマイル/hの値を返すようです。)
Rigidbody.velocity.magnitudeの単位はm/sなので、km/hに直すときは3.6を掛けるように修正しました。

  //      public float CurrentSpeed{ get { return m_Rigidbody.velocity.magnitude*2.23693629f; }}
        public float CurrentSpeed{ 
            get {
                    float unitFactor = (m_SpeedType == SpeedType.KPH) ? 3.6f : 2.23693629f;
                    return m_Rigidbody.velocity.magnitude * unitFactor;
                }
        }

Unityではゲーム画面のStatsボタンを押せばフレームレートが表示されますが、Unityのエディター画面に実際に表示されているfpsより良い値が出るようです。
これは、シーンビューやインスペクターの表示などに必要な時間は含まれていないためで、実際にUpdateのタイミングでフレームレートを表示してみると低いところで20fpsほどでした。
フレームレートはこのスクリプトをUI Textに貼り付けて計測しています。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class FPSCounter : MonoBehaviour {

    private const float _updateInterval = 0.5f;
    private float _accumulatedTime;
    private int _frameCount;
    private Text _text;

	// Use this for initialization
	void Start () {
        _text = GetComponent<Text>();
        _text.text = "FPS: ";
        _accumulatedTime = 0f;
        _frameCount = 0;
	}
	
	// Update is called once per frame
	void Update () {
        _accumulatedTime += Time.deltaTime;
        _frameCount++;
        if (_accumulatedTime >= _updateInterval) {
            _text.text = "FPS: " + (float)_frameCount / _accumulatedTime;
            _accumulatedTime = 0f;
            _frameCount = 0;
        }
	}
}

ちなみにiPhone5で実行すると13fps程度です。これでは快適なゲームは難しいので、次回は高速化にチャレンジしたいと思います。

今回のAIのスクリプトです。かなりごちゃごちゃしてきました。

using UnityEngine;
using System.Collections;
using UnityStandardAssets.Vehicles.Car;
using Random = UnityEngine.Random;

public class CarAIDriver : MonoBehaviour
{
    [Range(-1f, 1f)] public float inOutLiking = 0f; // ライン取りの好み。プラスはイン側マイナスはアウト側
    [Range(0f, 1f)] public float maxDispersion = 0f; // ライン取りのばらつきの最大値
    public float earlySteer = 0f; // ステアリングを切り始めるタイミングを早める

    private const float _accelFactor = 0.04f;
    private const float _brakeFactor = 1f;
    private const float _speedFactor = 8f;
    private const float _steerFactor = 0.02f;
    private const float _avoidShiftDistance = 0.1f;
    private const float _avoidRightSteer = 0.04f;
    private const float _avoidLeftSteer = -_avoidRightSteer;
    private const float _avoidRadiusFactor = 0.3f;
    private const float _slipstreamSpeed = 150f;
    private const float _speedIncrease = 20f;
    private const float _dragReduceFactor = 0.75f;
    private const float _rayExtension = 10f;
    private const float _hitSpeedReduction = 50f;
    private float _normalDrag;
    private CarController _carController;
    private Raycaster _raycaster;
    private Rigidbody _rigidbody;
    private Transform[] _raceTrack;
    private Transform _target;
    private SphereCollider _targetCollider;
    private int _currentIndex = 0;
    private int _raceTrackLength;

    private const float _radiusFactor = 0.5f;
    private Vector3 _shiftedTarget;

    void Start()
    {
        _carController = GetComponent<CarController>();
        _raycaster = GetComponent<Raycaster>();
        _rigidbody = GetComponent<Rigidbody>();
        _raceTrack = GameObject.Find("Waypoints").GetComponent<RaceTrack>().raceTrack;
        _raceTrackLength = GameObject.Find("Waypoints").GetComponent<RaceTrack>().raceTrackLength;
        _target = _raceTrack[_currentIndex];
        _targetCollider = _target.GetComponent<SphereCollider>();
        _shiftedTarget = ShiftTargetRandom(_target.position, _target.forward, _targetCollider.radius * _radiusFactor);
        _normalDrag = _rigidbody.drag;
    }
	
    void FixedUpdate()
    {
        float currentSpeed = _carController.CurrentSpeed;
        float desiredSpeed = _carController.MaxSpeed;

        desiredSpeed = CheckSlipstream(desiredSpeed);

        desiredSpeed = CheckSpeedLimit(desiredSpeed);

        float steerOffset;
        desiredSpeed = AvoidObstacles(desiredSpeed, out steerOffset);

        float accelBrakeFactor = (desiredSpeed < currentSpeed) ? _brakeFactor : _accelFactor;
        float accel = Mathf.Clamp((desiredSpeed - currentSpeed) * accelBrakeFactor, -1, 1);
        // _shiftedTargetを目指す
        Vector3 localTarget = transform.InverseTransformPoint(_shiftedTarget);
        Debug.DrawLine(transform.position, _shiftedTarget, Color.blue, 0f, false);

        float targetAngle = Mathf.Atan2(localTarget.x, localTarget.z) * Mathf.Rad2Deg;
        float steer = Mathf.Clamp((targetAngle * _steerFactor) + steerOffset, -1, 1) * Mathf.Sign(currentSpeed);

        _carController.Move(steer, accel, accel, 0f);

        UpdateTarget(_shiftedTarget, earlySteer);

    }

    // 受けとったspeedにスリップスストリームによる効果を加算した値を返す。
    // _slipsteamSpeed以上だとスリップストリームが効く。
    // 効いているときは最高速度を上げ、ドラッグを下げる。
    float CheckSlipstream(float speed)
    {
        float desiredSpeed = speed;
        Transform parent;
        CarController hitController;
        if (_raycaster.hittingCarFrontL) {
            parent = _raycaster.hitFCL.collider.transform.parent.parent;
            hitController = parent.gameObject.GetComponent<CarController>();

            // 前走車の速度が_slipstramSpeed以上ならスリップストリームを効かせる
            desiredSpeed = GoFaster(hitController.CurrentSpeed, speed);
        }
        if (_raycaster.hittingCarFrontR) {
            parent = _raycaster.hitFCR.collider.transform.parent.parent;
            hitController = parent.gameObject.GetComponent<CarController>();

            // 前走車の速度が_slipstramSpeed以上ならスリップストリームを効かせる
            desiredSpeed = GoFaster(hitController.CurrentSpeed, speed);
        }
        return desiredSpeed;
    }

    float GoFaster(float rivalSpeed, float currentSpeed)
    {
        if (rivalSpeed >= _slipstreamSpeed) {
            currentSpeed += _speedIncrease;
            _rigidbody.drag = _normalDrag * _dragReduceFactor;
        } else {
            _rigidbody.drag = _normalDrag;
        }
        return currentSpeed;
    }


    // スピード制限値を返す。制限値がなければ受けとったspeedをそのまま返す
    float CheckSpeedLimit(float speed)
    {
        float ret = speed;
        // ターゲットにレイがヒット中ならブレーキを掛ける
        if (_raycaster.hittingWaypoint) {
            if (_raycaster.hitFW.collider == _targetCollider) {
                ret = _targetCollider.radius * _speedFactor;
            }
        }
        return ret;
    }

    // 壁や他車を避けるために目標の位置をずらし、ステアリングを切る量をoutで返す。
    // リターン値は変更したdesiredSpeed
    float AvoidObstacles(float desiredSpeed, out float steerOffset)
    {
        steerOffset = 0f;
        float shiftMax = _targetCollider.radius * _avoidRadiusFactor;
        // 周囲のレイキャストにヒットしていなければ操作は不要
        if (!_raycaster.hittingAround) {
            _raycaster.waypointRayExtension = 0f;
            return desiredSpeed;
        }

        // 左前にヒット
        if (_raycaster.frontL) {
            // コーナーの基準からの現在のずれを求める
            float shiftedAmount = (_shiftedTarget - _target.position).magnitude;
            // まだずれが最大量に達していなければさらにずらし、その方向にハンドルを少し切る
            if (shiftedAmount < shiftMax) {
                _shiftedTarget = ShiftTarget(_shiftedTarget, transform.right, _avoidShiftDistance);
                // ハンドルを右に切る量
                steerOffset = _avoidRightSteer;
            }
            _raycaster.waypointRayExtension = _rayExtension;
            desiredSpeed -= _hitSpeedReduction;
        //右前にヒット
        } else if (_raycaster.frontR) {
            float shiftedAmount = (_shiftedTarget - _target.position).magnitude;
            if (shiftedAmount < shiftMax) {
                _shiftedTarget = ShiftTarget(_shiftedTarget, -transform.right, _avoidShiftDistance);
                // ハンドルを左に切る量
                steerOffset = _avoidLeftSteer;
            }
            _raycaster.waypointRayExtension = _rayExtension;
            desiredSpeed -= _hitSpeedReduction;
        } else {
            _raycaster.waypointRayExtension = 0f;
        }
        return desiredSpeed;
    }

    // コーナーに進入したら目標を次のコーナーに切り替える
    void UpdateTarget(Vector3 position, float earlySteer) 
    {
        // ターゲットのコライダーの半径の距離+earlySteerまで近づいたら次のターゲットに切り替える
        float distance = (position - transform.position).magnitude;
        if (distance < _targetCollider.radius + earlySteer) {
            _currentIndex = (_currentIndex + 1) % _raceTrackLength;
            _target = _raceTrack[_currentIndex];
            _targetCollider = _target.GetComponent<SphereCollider>();
            _shiftedTarget = ShiftTargetRandom(_target.position, _target.forward, _targetCollider.radius * _radiusFactor);
        }
    }

    // originからdirection(ノーマライズされている事)方向に+-rangeの範囲でシフトした位置を返す
    Vector3 ShiftTargetRandom(Vector3 origin, Vector3 direction, float range)
    {
        float offset = Mathf.Clamp(inOutLiking + Random.Range(-maxDispersion / 2f, maxDispersion / 2f), -1f, 1f);
        float magnitude = offset * range;
        return origin + direction * magnitude;
    }

    // originからdirection方向にoffset分シフトした位置を返す
    Vector3 ShiftTarget(Vector3 origin, Vector3 direction, float offset)
    {
        return origin + direction * offset;
    }
}