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

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

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

【Unity5でレースゲーム】市街地レースに適したAIを作る

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

これまでUnityのスタンダードアセットに入っていたAIを使って他車を動かしてきましたが、市街地のドライビングにはあまり適していないようなので自分でAIを作ってみました。

秋葉原のような市街地はカーブがほとんどないのでレース用のコースを作ろうとするとどうしても直線と90度のコーナーの組み合わせになってしまいます。

スタンダードアセットのCarAIControlスクリプトによるAIには次の3つのブレーキモードがあるのですが、どれも市街地レースでは使いにくいと感じました。

1. NeverBrake (ノーブレーキ走法)
危な過ぎるので論外。

2. TargetDirectionDifference (ターゲットが示すの方向との差によりブレーキ)
これは、ターゲットとなるWaypointに向きを設定しそのz方向と自車のz方向の差によってブレーキを掛けます。専用サーキットのように滑らかにカーブする場合はこれが使えるかも知れませんが、市街地コースのように交差点毎にWaypointを設置する場合は、その向きの設定によりブレーキが遅れるか、常に速度を調整して直線を走るかになってしまうと思います。

3. TargetDistance (ターゲットとの距離によりブレーキ)
わずかに曲がっている部分にWaypointを設置するとブレーキの必要がないのに必ずブレーキを掛けてしまいます。追突しそうなので危険ですね。

ではどのようなAIが良いでしょうか?

自分が車を運転して交差点を曲がる事を想像してみます。

1. アクセル全開で交差点に向う
2. 交差点の大きさと車の速度を考慮し、「ここぞ!」というところでブレーキを開始
3. 曲がれそうな速度まで減速
4. 交差点に進入したら出口に向けてハンドルを切りアクセルを踏んで交差点を脱出

といった感じでしょうか。
2.を実現するためには交差点への距離と自車の速度を検出し、それによってブレーキポイントを計算する必要があります。
3.を実現するには交差点毎に曲がりきれる速度を定義する必要があります。
4.を実現するには交差点に侵入した事を知る必要があります。

2, 3, 4全てに共通する要素は交差点の大きさです。これはWaypoint毎にSphere Colliderコンポーネントを付け、その大きさを調整する事で実現できそうです。コライダーに車が衝突しないようにIs Triggerをチェックします。

2.の自車の速度はCarControllerスクリプトのCurrentSpeedから取得できます。距離を測るにはレイキャストを使用します。車の前方に向けてレイキャストし、その長さは速度が速いほど長くします。
レイキャストがWaypointのコライダーにヒットしたらそこをブレーキ開始ポイントとします。

3.の交差点を曲がりきれる速度はWaypointのコライダーの大きさから適当に算出します。交差点が大きければ高速、小さければ低速になります。AIは自車の速度と算出した曲がりきれる速度を比較し、曲がりきれる速度に向けてブレーキを掛けて減速します。
元々曲がりきれる速度で侵入した場合はブレーキは掛けません。

4.については自車がWaypointのコライダーの半径まで近づいたら次のWaypointに切り替えることでステアリングを切り始めます。また、レイとコライダーのヒットが終了し、再びつぎの交差点に向け全開加速して行きます。

このAIを搭載した4台車を走らせてみました。まだランダム要素を入れていないので電車のように綺麗に連なって走ります。また、前方へのレイがスタート時に速度の増加に応じてニューンと伸びて行くのがわかると思います。
緑色のレイが交差点の半円に突入すると赤く変化します。最初のいくつかのコーナーは設定速度が速いのでブレーキをかけていませんが、中盤の路地に入るとレイが赤く変化するのと同時にブレーキランプが光るのがわかります。

斜め方向のレイは今後壁や他車との衝突判定に使う予定です。今は機能していません。

レイキャスト用のスクリプトです。

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

public class Raycaster : MonoBehaviour
{
    public RaycastHit hitF; //前方ヒット情報
    public RaycastHit hitFR; // 右前方ヒット情報
    public RaycastHit hitFL; // 左前方ヒット情報
    public RaycastHit hitRR; // 右後方ヒット情報
    public RaycastHit hitRL; // 左後方ヒット情報
    public bool hittingFront { get; private set; } // 前方のヒットがあればtrue;
    public bool hittingAround { get; private set; } // 周囲のヒットがあればtrue;

    // レイを出す位置のオフセット
    private Vector3 _offFront = new Vector3(0f, 0.3f, 2f);
    private Vector3 _offAround = new Vector3(0f, 0.3f, 0f);

    // レイを出す方向
    private Vector3 _dirF = new Vector3(0f, 0f, 1f);
    private Vector3 _dirFR = new Vector3(1f, 0f, 2f);
    private Vector3 _dirFL = new Vector3(-1f, 0f, 2f);
    private Vector3 _dirRR = new Vector3(1f, 0f, -2f);
    private Vector3 _dirRL = new Vector3(-1f, 0f, -2f);

    private float _speedFactorFront = 0.6f;
    private float _distAround = 5f; // 周囲をレイキャストでチェックする距離
    private int layerMaskWaypoints = 1 << 8;
    private int layerMaskVehicleWall = 1 << 9 | 1 << 10;

    private  CarController carController;

    void Start ()
    {
        carController = GetComponent<CarController>();
    }

    void FixedUpdate()
    {
        // 前方方向の成分のベクトルをワールド座標に変換する
        Vector3 position = transform.TransformPoint(_offFront);
        Vector3 direction = transform.TransformDirection(_dirF);
        float distance = carController.CurrentSpeed * _speedFactorFront;
        hittingFront = Physics.Raycast(position, direction, out hitF, distance, layerMaskWaypoints);
        DebugDraw(position, direction, distance, hitF.collider);


        // 前後左右にX成分1, Z成分2のベクトルを作成しワールド座標に変換する
        hittingAround = false; // falseでクリア。どれかのレイがヒットすればtrueになる

        position = transform.TransformPoint(_offAround);
        direction = transform.TransformDirection(_dirFR);
        hittingAround |= Physics.Raycast(position, direction, out hitFR, _distAround, layerMaskVehicleWall);
        DebugDraw(position, direction, _distAround, hitFR.collider);

        direction = transform.TransformDirection(_dirFL);
        hittingAround |= Physics.Raycast(position, direction, out hitFL, _distAround, layerMaskVehicleWall);
        DebugDraw(position, direction, _distAround, hitFL.collider);

        direction = transform.TransformDirection(_dirRR);
        hittingAround |= Physics.Raycast(position, direction, out hitRR, _distAround, layerMaskVehicleWall);
        DebugDraw(position, direction, _distAround, hitRR.collider);

        direction = transform.TransformDirection(_dirRL);
        hittingAround |= Physics.Raycast(position, direction, out hitRL, _distAround, layerMaskVehicleWall);
        DebugDraw(position, direction, _distAround, hitRL.collider);
                  
    }

    void DebugDraw(Vector3 position, Vector3 direction, float distance, Collider collider) 
    {
        Color color = collider ? Color.red : Color.green;
        Debug.DrawRay(position, direction.normalized * distance, color, 0, false);
    }
}

AIのスクリプトです。UnityのスタンダードアセットではWaypointの切り替えはAIとは別のスクリプトが担当していましたが、その機能も入っています。

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

public class CarAIDriver : MonoBehaviour
{
    private float _accelFactor = 0.04f;
    private float _brakeFactor = 1f;
    private float _speedFactor = 5f;
    private float _steerFactor = 0.05f;
    private CarController _carController;
    private Raycaster _raycaster;
    private Rigidbody _rigidbody;
    private Transform[] _raceTrack;
    private Transform _target;
    private Collider _targetCollider;
    private int _currentIndex = 0;
    private int _raceTrackLength;

    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<Collider>();
    }
	
    void FixedUpdate()
    {
        float currentSpeed = _carController.CurrentSpeed;
        float desiredSpeed = _carController.MaxSpeed;
        // ターゲットにレイがヒット中ならブレーキを掛ける
        if (_raycaster.hittingFront) {
            if (_raycaster.hitF.collider == _targetCollider) {
                desiredSpeed = _target.GetComponent<SphereCollider>().radius * _speedFactor;
            }
        }
        float accelBrakeFactor = (desiredSpeed < currentSpeed) ? _brakeFactor : _accelFactor;
        float accel = Mathf.Clamp((desiredSpeed - currentSpeed) * accelBrakeFactor, -1, 1);

        Vector3 localTarget = transform.InverseTransformPoint(_target.position); //自分からターゲットへのベクトル
        float targetAngle = Mathf.Atan2(localTarget.x, localTarget.z) * Mathf.Rad2Deg;
        float steer = Mathf.Clamp(targetAngle * _steerFactor, -1, 1) * Mathf.Sign(currentSpeed);


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


        // ターゲットのコライダーの半径の距離まで近づいたら次のターゲットに切り替える
        if (localTarget.magnitude < _target.GetComponent<SphereCollider>().radius) {
            _currentIndex = (_currentIndex + 1) % _raceTrackLength;
            _target = _raceTrack[_currentIndex];
            _targetCollider = _target.GetComponent<Collider>();
        }
    }
}