上一章地址: UnityStandardAsset工程、源碼分析_4_賽車遊戲[玩家控制]_攝像機控制
前幾章我們已經將賽車遊戲的絕大多數機制分析過了,而Unity還提供了不同的操控模式——AI控制。正如其名,是由AI來代替玩家進行控制,車輛自動繞場地行駛。AI控制的場景大體與玩家控制的場景相似,所以重複的部分不再贅述,我們着重分析AI相關的機制。
AI控制
AI控制場景的車附着的腳本,與玩家控制的車輛有所不同。第一章的截圖中,車輛上附着了CarUserControl
腳本,用於讀入玩家輸入並傳給CarController
,但這裏的車輛上沒有掛載CarUserControl
,取而代之的是CarAIControl
和WaypointProgressTracker
。同時,在場景中也存在着如下對象:
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
上的。那麼事情就很明瞭了:
- 在場景中定義一系列空物體,他們有着規律的座標,能夠組成一圈閉合的路徑,將其賦值給
WaypointCircuit
WaypointCircuit
根據這些數據進行緩存,將他們的座標和路程轉換成數組,方便計算WaypointProgressTracker
調用WaypointCircuit
的GetRoutePoint
方法以及它的公開屬性計算並更新WaypointTargetObject
的座標CarAIControl
使用WaypointTargetObject
的座標和車輛自身數據調用CarController
的Move
方法更新車輛數據
一條完整的AI邏輯鏈,從數據到決策再到數據,就分析完成了。算法的實現我同Unity的註釋一起清楚地標識在代碼中。此外還有一些Editor和Gizmos的代碼,由於這些代碼我運行的時候還在報數組越界的Error,我也不想分析了,有興趣的話可以自己去AssetStore下載,或是克隆我包含了註釋的Git倉庫:https://github.com/t61789/StandardAssetWithAnnotation
賽車遊戲場景到這裏就告一段落了,下一章分析第三人稱場景或者第一人稱場景吧,看心情。