UnityStandardAsset工程、源碼分析_5_賽車遊戲[AI控制]_AI機制

上一章地址: UnityStandardAsset工程、源碼分析_4_賽車遊戲[玩家控制]_攝像機控制
在這裏插入圖片描述
前幾章我們已經將賽車遊戲的絕大多數機制分析過了,而Unity還提供了不同的操控模式——AI控制。正如其名,是由AI來代替玩家進行控制,車輛自動繞場地行駛。AI控制的場景大體與玩家控制的場景相似,所以重複的部分不再贅述,我們着重分析AI相關的機制。

AI控制

在這裏插入圖片描述
AI控制場景的車附着的腳本,與玩家控制的車輛有所不同。第一章的截圖中,車輛上附着了CarUserControl腳本,用於讀入玩家輸入並傳給CarController,但這裏的車輛上沒有掛載CarUserControl,取而代之的是CarAIControlWaypointProgressTracker。同時,在場景中也存在着如下對象:
在這裏插入圖片描述
Waypoints上掛載了WaypointCircuit
在這裏插入圖片描述
此外,在第一章中,我們也觀察到有一個名爲WaypointTargetObject的物體,當時只是簡略的提了一下這是用於AI相關的物體。而在這章中,它卻扮演了一個非常重要的角色。


接下來我們根據上述提到的各個腳本、物體,來完整地分析AI駕駛的實現原理。我們先從CarAIControl入手:

namespace UnityStandardAssets.Vehicles.Car
{
    [RequireComponent(typeof (CarController))]
    public class CarAIControl : MonoBehaviour
    {
        // 三種行爲模式
        public enum BrakeCondition
        {
            // 一直加速,不減速
            NeverBrake,                 // the car simply accelerates at full throttle all the time.
            // 根據路徑點之間的角度減速
            TargetDirectionDifference,  // the car will brake according to the upcoming change in direction of the target. Useful for route-based AI, slowing for corners.
            // 在即將到達路近點的時候減速
            TargetDistance,             // the car will brake as it approaches its target, regardless of the target's direction. Useful if you want the car to
                                        // head for a stationary target and come to rest when it arrives there.
        }

        // 這個腳本爲車輛的控制提供了輸入,就像玩家的輸入一樣
        // 就這樣,這是真的在“駕駛”車輛,沒有使用什麼特殊的物理或者動畫效果
        // This script provides input to the car controller in the same way that the user control script does.
        // As such, it is really 'driving' the car, with no special physics or animation tricks to make the car behave properly.

        // “閒逛”是用來讓車輛變得更加像人類在操作,而不是機器在操作
        // 他能在駛向目標的時候輕微地改變速度和方向
        // "wandering" is used to give the cars a more human, less robotic feel. They can waver slightly
        // in speed and direction while driving towards their target.

        [SerializeField] [Range(0, 1)] private float m_CautiousSpeedFactor = 0.05f;               // percentage of max speed to use when being maximally cautious
        [SerializeField] [Range(0, 180)] private float m_CautiousMaxAngle = 50f;                  // angle of approaching corner to treat as warranting maximum caution
        [SerializeField] private float m_CautiousMaxDistance = 100f;                              // distance at which distance-based cautiousness begins
        [SerializeField] private float m_CautiousAngularVelocityFactor = 30f;                     // how cautious the AI should be when considering its own current angular velocity (i.e. easing off acceleration if spinning!)
        [SerializeField] private float m_SteerSensitivity = 0.05f;                                // how sensitively the AI uses steering input to turn to the desired direction
        [SerializeField] private float m_AccelSensitivity = 0.04f;                                // How sensitively the AI uses the accelerator to reach the current desired speed
        [SerializeField] private float m_BrakeSensitivity = 1f;                                   // How sensitively the AI uses the brake to reach the current desired speed
        [SerializeField] private float m_LateralWanderDistance = 3f;                              // how far the car will wander laterally towards its target
        [SerializeField] private float m_LateralWanderSpeed = 0.1f;                               // how fast the lateral wandering will fluctuate
        [SerializeField] [Range(0, 1)] private float m_AccelWanderAmount = 0.1f;                  // how much the cars acceleration will wander
        [SerializeField] private float m_AccelWanderSpeed = 0.1f;                                 // how fast the cars acceleration wandering will fluctuate
        [SerializeField] private BrakeCondition m_BrakeCondition = BrakeCondition.TargetDistance; // what should the AI consider when accelerating/braking?
        [SerializeField] private bool m_Driving;                                                  // whether the AI is currently actively driving or stopped.
        [SerializeField] private Transform m_Target;                                              // 'target' the target object to aim for.
        [SerializeField] private bool m_StopWhenTargetReached;                                    // should we stop driving when we reach the target?
        [SerializeField] private float m_ReachTargetThreshold = 2;                                // proximity to target to consider we 'reached' it, and stop driving.

        private float m_RandomPerlin;             // A random value for the car to base its wander on (so that AI cars don't all wander in the same pattern)
        private CarController m_CarController;    // Reference to actual car controller we are controlling
        private float m_AvoidOtherCarTime;        // time until which to avoid the car we recently collided with
        private float m_AvoidOtherCarSlowdown;    // how much to slow down due to colliding with another car, whilst avoiding
        private float m_AvoidPathOffset;          // direction (-1 or 1) in which to offset path to avoid other car, whilst avoiding
        private Rigidbody m_Rigidbody;


        private void Awake()
        {
            // 獲得車輛的核心邏輯控件
            // get the car controller reference
            m_CarController = GetComponent<CarController>();

            // 車輛閒逛的隨機種子
            // give the random perlin a random value
            m_RandomPerlin = Random.value*100;

            m_Rigidbody = GetComponent<Rigidbody>();
        }


        private void FixedUpdate()
        {
            if (m_Target == null || !m_Driving)
            {
                // 沒有在駕駛或者沒有目標時不應該移動,使用手剎來停下車輛
                // Car should not be moving,
                // use handbrake to stop
                m_CarController.Move(0, 0, -1f, 1f);
            }
            else
            {
                // 正朝向,如果速度大於最大速度的10%,則爲速度方向,否則爲模型方向
                Vector3 fwd = transform.forward;
                if (m_Rigidbody.velocity.magnitude > m_CarController.MaxSpeed*0.1f)
                {
                    fwd = m_Rigidbody.velocity;
                }

                float desiredSpeed = m_CarController.MaxSpeed;

                // 現在是時候決定我們是否該減速了
                // now it's time to decide if we should be slowing down...
                switch (m_BrakeCondition)
                {
                    // 根據路徑點角度限制速度
                    case BrakeCondition.TargetDirectionDifference:
                        {
                            // the car will brake according to the upcoming change in direction of the target. Useful for route-based AI, slowing for corners.

                            // 先計算我們當前朝向與路徑點的朝向之間的角度
                            // check out the angle of our target compared to the current direction of the car
                            float approachingCornerAngle = Vector3.Angle(m_Target.forward, fwd);

                            // 也考慮一下我們當前正在轉向的角速度
                            // also consider the current amount we're turning, multiplied up and then compared in the same way as an upcoming corner angle
                            float spinningAngle = m_Rigidbody.angularVelocity.magnitude*m_CautiousAngularVelocityFactor;

                            // 角度越大越需要謹慎
                            // if it's different to our current angle, we need to be cautious (i.e. slow down) a certain amount
                            float cautiousnessRequired = Mathf.InverseLerp(0, m_CautiousMaxAngle,
                                                                           Mathf.Max(spinningAngle,
                                                                                     approachingCornerAngle));
                            // 獲得需要的速度,cautiousnessRequired越大desiredSpeed越小
                            desiredSpeed = Mathf.Lerp(m_CarController.MaxSpeed, m_CarController.MaxSpeed*m_CautiousSpeedFactor,
                                                      cautiousnessRequired);
                            break;
                        }
                    // 根據路近點的距離限制速度
                    case BrakeCondition.TargetDistance:
                        {
                            // the car will brake as it approaches its target, regardless of the target's direction. Useful if you want the car to
                            // head for a stationary target and come to rest when it arrives there.

                            // 計算到達目標與自身之間的距離向量
                            // check out the distance to target
                            Vector3 delta = m_Target.position - transform.position;
                            // 根據最大謹慎距離和當前距離計算距離謹慎因子
                            float distanceCautiousFactor = Mathf.InverseLerp(m_CautiousMaxDistance, 0, delta.magnitude);

                            // 也考慮一下我們當前正在轉向的角速度
                            // also consider the current amount we're turning, multiplied up and then compared in the same way as an upcoming corner angle
                            float spinningAngle = m_Rigidbody.angularVelocity.magnitude*m_CautiousAngularVelocityFactor;

                            // 角度越大越需要謹慎
                            // if it's different to our current angle, we need to be cautious (i.e. slow down) a certain amount
                            float cautiousnessRequired = Mathf.Max(
                                Mathf.InverseLerp(0, m_CautiousMaxAngle, spinningAngle), distanceCautiousFactor);

                            // 獲得需要的速度,謹慎程度越大desiredSpeed越小
                            desiredSpeed = Mathf.Lerp(m_CarController.MaxSpeed, m_CarController.MaxSpeed*m_CautiousSpeedFactor,
                                                      cautiousnessRequired);
                            break;
                        }

                    // 無限加速模式不需要謹慎,desiredSpeed取m_CarController.MaxSpeed,也就是不作減小
                    case BrakeCondition.NeverBrake:
                        break;
                }

                // 撞到其他車輛時的逃避行動
                // Evasive action due to collision with other cars:

                // 目標偏移座標始於真正的目標座標
                // our target position starts off as the 'real' target position
                Vector3 offsetTargetPos = m_Target.position;

                // 如果我們正在爲了避免和其他車卡在一起而採取迴避行動
                // if are we currently taking evasive action to prevent being stuck against another car:
                if (Time.time < m_AvoidOtherCarTime)
                {
                    // 如果有必要的話就減速(發生碰撞的時候我們在其他車後面)
                    // slow down if necessary (if we were behind the other car when collision occured)
                    desiredSpeed *= m_AvoidOtherCarSlowdown;

                    // 轉向其他方向
                    // and veer towards the side of our path-to-target that is away from the other car
                    offsetTargetPos += m_Target.right*m_AvoidPathOffset;
                }
                else
                {
                    // 無需採取迴避行動,我們就可以沿着路徑隨機閒逛,避免AI駕駛車輛的時候看起來太死板
                    // no need for evasive action, we can just wander across the path-to-target in a random way,
                    // which can help prevent AI from seeming too uniform and robotic in their driving
                    offsetTargetPos += m_Target.right*
                                       (Mathf.PerlinNoise(Time.time*m_LateralWanderSpeed, m_RandomPerlin)*2 - 1)*
                                       m_LateralWanderDistance;
                }

                // 使用不同的靈敏度,取決於是在加速還是減速
                // use different sensitivity depending on whether accelerating or braking:
                float accelBrakeSensitivity = (desiredSpeed < m_CarController.CurrentSpeed)
                                                  ? m_BrakeSensitivity
                                                  : m_AccelSensitivity;

                // 根據靈敏度決定真正的 加速/減速 輸入,clamp到[-1,1]
                // decide the actual amount of accel/brake input to achieve desired speed.
                float accel = Mathf.Clamp((desiredSpeed - m_CarController.CurrentSpeed)*accelBrakeSensitivity, -1, 1);

                // 利用柏林噪聲來使加速度變得隨機,以此來讓AI的操作更像人類,不過我沒太看懂爲什麼要這麼算
                // add acceleration 'wander', which also prevents AI from seeming too uniform and robotic in their driving
                // i.e. increasing the accel wander amount can introduce jostling and bumps between AI cars in a race
                accel *= (1 - m_AccelWanderAmount) +
                         (Mathf.PerlinNoise(Time.time*m_AccelWanderSpeed, m_RandomPerlin)*m_AccelWanderAmount);

                // 將之前計算過的偏移的目標座標轉換爲本地座標
                // calculate the local-relative position of the target, to steer towards
                Vector3 localTarget = transform.InverseTransformPoint(offsetTargetPos);

                // 計算繞y軸的本地目標角度
                // work out the local angle towards the target
                float targetAngle = Mathf.Atan2(localTarget.x, localTarget.z)*Mathf.Rad2Deg;

                // 獲得爲了轉向目標所需要的角度
                // get the amount of steering needed to aim the car towards the target
                float steer = Mathf.Clamp(targetAngle*m_SteerSensitivity, -1, 1)*Mathf.Sign(m_CarController.CurrentSpeed);

                // 使用這些數據調用Move方法
                // feed input to the car controller.
                m_CarController.Move(steer, accel, accel, 0f);

                // 如果過於接近目標,停止駕駛
                // if appropriate, stop driving when we're close enough to the target.
                if (m_StopWhenTargetReached && localTarget.magnitude < m_ReachTargetThreshold)
                {
                    m_Driving = false;
                }
            }
        }


        private void OnCollisionStay(Collision col)
        {
            // 檢測與其他車輛的碰撞,併爲此採取迴避行動
            // detect collision against other cars, so that we can take evasive action
            if (col.rigidbody != null)
            {
                var otherAI = col.rigidbody.GetComponent<CarAIControl>();
                // 與之發生碰撞的物體上需要同樣掛載有CarAIControl,否則不採取行動
                if (otherAI != null)
                {
                    // 我們會在1秒內採取迴避行動
                    // we'll take evasive action for 1 second
                    m_AvoidOtherCarTime = Time.time + 1;

                    // 那麼誰在前面?
                    // but who's in front?...
                    if (Vector3.Angle(transform.forward, otherAI.transform.position - transform.position) < 90)
                    {
                        // 對方在前面,我們就需要減速
                        // the other ai is in front, so it is only good manners that we ought to brake...
                        m_AvoidOtherCarSlowdown = 0.5f;
                    }
                    else
                    {
                        // 我們在前面,無需減速
                        // we're in front! ain't slowing down for anybody...
                        m_AvoidOtherCarSlowdown = 1;
                    }

                    // 兩輛車都需要採取迴避行動,駛向偏離目標的方向,遠離對方
                    // both cars should take evasive action by driving along an offset from the path centre,
                    // away from the other car
                    var otherCarLocalDelta = transform.InverseTransformPoint(otherAI.transform.position);
                    float otherCarAngle = Mathf.Atan2(otherCarLocalDelta.x, otherCarLocalDelta.z);
                    m_AvoidPathOffset = m_LateralWanderDistance*-Mathf.Sign(otherCarAngle);
                }
            }
        }


        public void SetTarget(Transform target)
        {
            m_Target = target;
            m_Driving = true;
        }
    }
}

Unity自身寫的註釋也很詳細,比之其他腳本而言完全不同,可能不是由同一個開發者製作的。
可見,這個腳本的主要功能,就是根據自身的狀態(速度,角速度,駕駛模式),以及很重要的m_Target物體等數據調用Move方法,來實現車輛狀態的更新。關於算法的實現,Unity和我的註釋已經寫的很清楚了,那麼現在最主要的問題是,那個m_Target是什麼?從上面的算法可以看出,我們的車輛是一直在“追趕”這個m_Target的,它不可能一直處於靜止,否則車輛也不會啓動了,那麼它是怎樣移動的?這個問題的答案潛藏在WaypointProgressTracker中:

namespace UnityStandardAssets.Utility
{
    public class WaypointProgressTracker : MonoBehaviour
    {
        // 這個腳本適用於任何的想要跟隨一系列路徑點的物體
        // This script can be used with any object that is supposed to follow a
        // route marked out by waypoints.

        // 這個腳本管理向前看的數量?(就是管理路徑點吧)
        // This script manages the amount to look ahead along the route,
        // and keeps track of progress and laps.

        [SerializeField] private WaypointCircuit circuit; // A reference to the waypoint-based route we should follow

        [SerializeField] private float lookAheadForTargetOffset = 5;
        // The offset ahead along the route that the we will aim for

        [SerializeField] private float lookAheadForTargetFactor = .1f;
        // A multiplier adding distance ahead along the route to aim for, based on current speed

        [SerializeField] private float lookAheadForSpeedOffset = 10;
        // The offset ahead only the route for speed adjustments (applied as the rotation of the waypoint target transform)

        [SerializeField] private float lookAheadForSpeedFactor = .2f;
        // A multiplier adding distance ahead along the route for speed adjustments

        [SerializeField] private ProgressStyle progressStyle = ProgressStyle.SmoothAlongRoute;
        // whether to update the position smoothly along the route (good for curved paths) or just when we reach each waypoint.

        [SerializeField] private float pointToPointThreshold = 4;
        // proximity to waypoint which must be reached to switch target to next waypoint : only used in PointToPoint mode.

        public enum ProgressStyle
        {
            SmoothAlongRoute,
            PointToPoint,
        }

        // these are public, readable by other objects - i.e. for an AI to know where to head!
        public WaypointCircuit.RoutePoint targetPoint { get; private set; }
        public WaypointCircuit.RoutePoint speedPoint { get; private set; }
        public WaypointCircuit.RoutePoint progressPoint { get; private set; }

        public Transform target;

        private float progressDistance; // The progress round the route, used in smooth mode.
        private int progressNum; // the current waypoint number, used in point-to-point mode.
        private Vector3 lastPosition; // Used to calculate current speed (since we may not have a rigidbody component)
        private float speed; // current speed of this object (calculated from delta since last frame)

        // setup script properties
        private void Start()
        {
            // 我們使用一個物體來表示應當瞄準的點,並且這個點考慮了即將到來的速度變化
            // 這允許這個組件跟AI交流,不要求進一步的依賴
            // we use a transform to represent the point to aim for, and the point which
            // is considered for upcoming changes-of-speed. This allows this component
            // to communicate this information to the AI without requiring further dependencies.

            // 你可以手動創造一個物體並把它付給這個組件和AI,如此這個組件就可以更新它,AI也可以從他身上讀取數據
            // You can manually create a transform and assign it to this component *and* the AI,
            // then this component will update it, and the AI can read it.
            if (target == null)
            {
                target = new GameObject(name + " Waypoint Target").transform;
            }

            Reset();
        }

        // 把對象重置爲合適的值
        // reset the object to sensible values
        public void Reset()
        {
            progressDistance = 0;
            progressNum = 0;
            if (progressStyle == ProgressStyle.PointToPoint)
            {
                target.position = circuit.Waypoints[progressNum].position;
                target.rotation = circuit.Waypoints[progressNum].rotation;
            }
        }


        private void Update()
        {
            if (progressStyle == ProgressStyle.SmoothAlongRoute)
            {
                // 平滑路徑點模式

                // 確定我們應當瞄準的位置
                // 這與當前的進度位置不同,這是兩個路近的中間量
                // 我們使用插值來簡單地平滑速度
                // determine the position we should currently be aiming for
                // (this is different to the current progress position, it is a a certain amount ahead along the route)
                // we use lerp as a simple way of smoothing out the speed over time.
                if (Time.deltaTime > 0)
                {
                    speed = Mathf.Lerp(speed, (lastPosition - transform.position).magnitude/Time.deltaTime,
                                       Time.deltaTime);
                }
                // 根據路程向前偏移一定距離,獲取路徑點
                target.position =
                    circuit.GetRoutePoint(progressDistance + lookAheadForTargetOffset + lookAheadForTargetFactor*speed)
                           .position;
                // 路徑點方向調整。這裏重複計算了,爲什麼不緩存?
                target.rotation =
                    Quaternion.LookRotation(
                        circuit.GetRoutePoint(progressDistance + lookAheadForSpeedOffset + lookAheadForSpeedFactor*speed)
                               .direction);

                // 獲取未偏移的路徑點
                // get our current progress along the route
                progressPoint = circuit.GetRoutePoint(progressDistance);
                // 車輛的移動超過路徑點的話,將路徑點前移
                Vector3 progressDelta = progressPoint.position - transform.position;
                if (Vector3.Dot(progressDelta, progressPoint.direction) < 0)
                {
                    progressDistance += progressDelta.magnitude*0.5f;
                }

                // 記錄位置
                lastPosition = transform.position;
            }
            else
            {
                // 點對點模式,如果足夠近的話就增加路程
                // point to point mode. Just increase the waypoint if we're close enough:

                // 距離小於閾值,就將路徑點移動到下一個
                Vector3 targetDelta = target.position - transform.position;
                if (targetDelta.magnitude < pointToPointThreshold)
                {
                    progressNum = (progressNum + 1)%circuit.Waypoints.Length;
                }

                // 設置路徑對象的位置和旋轉方向
                target.position = circuit.Waypoints[progressNum].position;
                target.rotation = circuit.Waypoints[progressNum].rotation;

                // 同平滑路徑點模式一樣進行路程計算
                // get our current progress along the route
                progressPoint = circuit.GetRoutePoint(progressDistance);
                Vector3 progressDelta = progressPoint.position - transform.position;
                if (Vector3.Dot(progressDelta, progressPoint.direction) < 0)
                {
                    progressDistance += progressDelta.magnitude;
                }
                lastPosition = transform.position;
            }
        }


        private void OnDrawGizmos()
        {
            // 畫Gizmos
            if (Application.isPlaying)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawLine(transform.position, target.position);   // 車輛與路徑對象的連線
                Gizmos.DrawWireSphere(circuit.GetRoutePosition(progressDistance), 1);   // 在平滑路徑點上畫球體
                Gizmos.color = Color.yellow;
                Gizmos.DrawLine(target.position, target.position + target.forward); // 畫出路徑對象的朝向
            }
        }
    }
}

可見這個腳本的主要目的就是更新target的狀態,這個target就是之前提到的m_Target,也是場景中一直都沒有用上的WaypointTargetObject。問題又來了,這個腳本是根據什麼來更新target的狀態的?是circuit.GetRoutePoint()。那這個東西又是什麼?我們來看看circuit的類型,就能得到答案:

namespace UnityStandardAssets.Utility
{
    public class WaypointCircuit : MonoBehaviour
    {
        // 管理路徑點的類,主要功能是根據路徑值獲取在閉合路徑上的路徑點

        public WaypointList waypointList = new WaypointList();
        [SerializeField] private bool smoothRoute = true;
        private int numPoints;
        private Vector3[] points;
        private float[] distances;

        public float editorVisualisationSubsteps = 100;
        public float Length { get; private set; }

        public Transform[] Waypoints
        {
            get { return waypointList.items; }
        }

        //this being here will save GC allocs
        private int p0n;
        private int p1n;
        private int p2n;
        private int p3n;

        private float i;
        private Vector3 P0;
        private Vector3 P1;
        private Vector3 P2;
        private Vector3 P3;

        // Use this for initialization
        private void Awake()
        {
            if (Waypoints.Length > 1)
            {
                // 緩存路徑點和路程
                CachePositionsAndDistances();
            }
            numPoints = Waypoints.Length;
        }


        public RoutePoint GetRoutePoint(float dist)
        {
            // 計算插值後的路徑點和他的方向
            // position and direction
            Vector3 p1 = GetRoutePosition(dist);
            Vector3 p2 = GetRoutePosition(dist + 0.1f);
            Vector3 delta = p2 - p1;
            return new RoutePoint(p1, delta.normalized);
        }


        public Vector3 GetRoutePosition(float dist)
        {
            int point = 0;

            // 獲取一週的長度
            if (Length == 0)
            {
                Length = distances[distances.Length - 1];
            }

            // 把dist規定在[0,Length]內
            dist = Mathf.Repeat(dist, Length);

            // 從起點數起,尋找dist所在的路段
            while (distances[point] < dist)
            {
                ++point;
            }

            // 獲得距離dist最近的兩個路徑點
            // get nearest two points, ensuring points wrap-around start & end of circuit
            p1n = ((point - 1) + numPoints)%numPoints;
            p2n = point;

            // 獲得兩點距離間的百分值
            // found point numbers, now find interpolation value between the two middle points
            i = Mathf.InverseLerp(distances[p1n], distances[p2n], dist);

            if (smoothRoute)
            {
                // 使用平滑catmull-rom曲線
                // smooth catmull-rom calculation between the two relevant points

                // 再獲得最近的兩個點,一共四個,用於計算catmull-rom曲線
                // get indices for the surrounding 2 points, because
                // four points are required by the catmull-rom function
                p0n = ((point - 2) + numPoints)%numPoints;
                p3n = (point + 1)%numPoints;

                // 這裏沒太懂,似乎是隻有三個路徑點時,計算出的兩個新路徑點會重合
                // 2nd point may have been the 'last' point - a dupe of the first,
                // (to give a value of max track distance instead of zero)
                // but now it must be wrapped back to zero if that was the case.
                p2n = p2n%numPoints;

                P0 = points[p0n];
                P1 = points[p1n];
                P2 = points[p2n];
                P3 = points[p3n];

                // 計算catmull-rom曲線
                // 爲什麼這裏的i值時1、2號點的百分值?爲什麼不是0、3號點的?
                return CatmullRom(P0, P1, P2, P3, i);
            }
            else
            {
                // simple linear lerp between the two points:

                p1n = ((point - 1) + numPoints)%numPoints;
                p2n = point;

                return Vector3.Lerp(points[p1n], points[p2n], i);
            }
        }


        private Vector3 CatmullRom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float i)
        {
            // 魔幻代碼,計算catmull-rom曲線
            // (其實google一下就有公式了)
            // comments are no use here... it's the catmull-rom equation.
            // Un-magic this, lord vector!
            return 0.5f *
                   ((2*p1) + (-p0 + p2)*i + (2*p0 - 5*p1 + 4*p2 - p3)*i*i +
                    (-p0 + 3*p1 - 3*p2 + p3)*i*i*i);
        }


        private void CachePositionsAndDistances()
        {
            // 把每個點的座標和到達某點的總距離轉換成數組
            // 距離數組中的值表示從起點到達第n個路徑點走過的路程,最後一個爲起點,路程爲一週的路程而不是0
            // transfer the position of each point and distances between points to arrays for
            // speed of lookup at runtime
            points = new Vector3[Waypoints.Length + 1];
            distances = new float[Waypoints.Length + 1];

            float accumulateDistance = 0;
            for (int i = 0; i < points.Length; ++i)
            {
                var t1 = Waypoints[(i)%Waypoints.Length];
                var t2 = Waypoints[(i + 1)%Waypoints.Length];
                if (t1 != null && t2 != null)
                {
                    Vector3 p1 = t1.position;
                    Vector3 p2 = t2.position;
                    points[i] = Waypoints[i%Waypoints.Length].position;
                    distances[i] = accumulateDistance;
                    accumulateDistance += (p1 - p2).magnitude;
                }
            }
        }


        private void OnDrawGizmos()
        {
            DrawGizmos(false);
        }


        private void OnDrawGizmosSelected()
        {
            DrawGizmos(true);
        }


        private void DrawGizmos(bool selected)
        {
            waypointList.circuit = this;
            if (Waypoints.Length > 1)
            {
                numPoints = Waypoints.Length;

                CachePositionsAndDistances();
                Length = distances[distances.Length - 1];

                Gizmos.color = selected ? Color.yellow : new Color(1, 1, 0, 0.5f);
                Vector3 prev = Waypoints[0].position;
                if (smoothRoute)
                {
                    for (float dist = 0; dist < Length; dist += Length/editorVisualisationSubsteps)
                    {
                        Vector3 next = GetRoutePosition(dist + 1);
                        Gizmos.DrawLine(prev, next);
                        prev = next;
                    }
                    Gizmos.DrawLine(prev, Waypoints[0].position);
                }
                else
                {
                    for (int n = 0; n < Waypoints.Length; ++n)
                    {
                        Vector3 next = Waypoints[(n + 1)%Waypoints.Length].position;
                        Gizmos.DrawLine(prev, next);
                        prev = next;
                    }
                }
            }
        }


        [Serializable]
        public class WaypointList
        {
            public WaypointCircuit circuit;
            public Transform[] items = new Transform[0];
        }

        // 路徑點結構體
        public struct RoutePoint
        {
            public Vector3 position;
            public Vector3 direction;


            public RoutePoint(Vector3 position, Vector3 direction)
            {
                this.position = position;
                this.direction = direction;
            }
        }
    }
}

這個類所使用的數據來自場景中的Waypoints對象及其子對象,並且這個類就是掛載在Waypoints上的。那麼事情就很明瞭了:

  1. 在場景中定義一系列空物體,他們有着規律的座標,能夠組成一圈閉合的路徑,將其賦值給WaypointCircuit
  2. WaypointCircuit根據這些數據進行緩存,將他們的座標和路程轉換成數組,方便計算
  3. WaypointProgressTracker調用WaypointCircuitGetRoutePoint方法以及它的公開屬性計算並更新WaypointTargetObject的座標
  4. CarAIControl使用WaypointTargetObject的座標和車輛自身數據調用CarControllerMove方法更新車輛數據

一條完整的AI邏輯鏈,從數據到決策再到數據,就分析完成了。算法的實現我同Unity的註釋一起清楚地標識在代碼中。此外還有一些Editor和Gizmos的代碼,由於這些代碼我運行的時候還在報數組越界的Error,我也不想分析了,有興趣的話可以自己去AssetStore下載,或是克隆我包含了註釋的Git倉庫:https://github.com/t61789/StandardAssetWithAnnotation

賽車遊戲場景到這裏就告一段落了,下一章分析第三人稱場景或者第一人稱場景吧,看心情。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章