還原基本步重新寫一寫 NavMeshAgent.
在1000 agent, 0gc 還免強達到 60fps 我覺得已經足夠的了.
- Pathfinding AI 在 Player 某個特定半徑上停下.
- 在 Player 不移動或改變很少的時候不更新
- 在 Agent 不超過某特定半徑範圍下不更新
- 訂立最少更新時間間隔

- 黃線 – Agent 希望到達的目的地
- 黃圈 / 紅圈 – Agent 具體停留的距離, 不超過不進行更新
- 灰圈 – Target 上次紀錄的地點半徑, 不超過不更新.
運行結果如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace Kit.Avatar
{
[RequireComponent(typeof(NavMeshAgent))]
public class NPCOrbitMovement : MonoBehaviour
{
#region Variables
[SerializeField]
private NavMeshAgent m_NavMeshAgent = null;
[SerializeField] bool m_DebugGizmos = false;
[Help("The min period for AI's to remake decision."), SerializeField]
private float m_ThinkInterval = 0.02f;
private float m_LastThinkTime = 0f;
[Tooltip("The target we tracking."), SerializeField]
private Transform m_Target = null;
[SerializeField]
private Constraints m_Constraints = new Constraints();
[System.Serializable]
private class Constraints
{
[Tooltip("The agent will attempt to keep the distance between target, not too far, not too close.")]
public float maintainDistance = 2f;
[Tooltip("when target position changed," +
"\nif position remain inside this radius," +
"\nwill concider as no change at all.")]
public float agentPosDeviation = 0.25f;
public Color minRangeColor = Color.red;
public Color maxRangeColor = Color.yellow;
public float targetPosDeviation = 0.25f;
public Color deviationColor = Color.gray;
public void Validate()
{
maintainDistance = Mathf.Max(float.Epsilon, maintainDistance);
agentPosDeviation = Mathf.Clamp(agentPosDeviation, float.Epsilon, maintainDistance * 2f);
}
}
#endregion Variables
#region System
private void Reset()
{
m_NavMeshAgent = GetComponent<NavMeshAgent>();
}
private void OnValidate()
{
m_Constraints.Validate();
if (!Application.isPlaying)
Think(m_Target, true);
}
private void Awake()
{
if (m_NavMeshAgent == null)
m_NavMeshAgent = GetComponent<NavMeshAgent>();
m_Decision = new Decision(OnDestinationChanged);
}
private void OnDrawGizmos()
{
if (!m_DebugGizmos)
return;
if (!Application.isPlaying)
Think(m_Target);
m_Decision.DrawGizmos(m_Constraints);
}
private void FixedUpdate()
{
if (Time.timeSinceLevelLoad - m_LastThinkTime > m_ThinkInterval)
{
Think(m_Target);
}
}
#endregion System
#region Decision making
private void Think(Transform _target, bool forceUpdate = false)
{
m_LastThinkTime = Time.timeSinceLevelLoad;
m_Decision.MakeDecision(transform, _target, m_Constraints, forceUpdate);
}
private void OnDestinationChanged(Vector3 destination)
{
m_NavMeshAgent.SetDestination(destination);
}
private Decision m_Decision = new Decision(null);
private class Decision
{
public Transform target;
public Vector3 targetPos, myPos, myFacing, groundNormal, xzVector, xzDir, destination;
public float minDistance, maxDistance, deviationDistance, targetPosDeviation;
public delegate void DestinationChanged(Vector3 pos);
public DestinationChanged Event_DestinationChanged;
public Decision(DestinationChanged destinationChanged)
{
Event_DestinationChanged = destinationChanged;
}
public void MakeDecision(Transform _my, Transform _target, Constraints _constraints, bool forceUpdate)
{
if (!forceUpdate)
{
if (_target == target &&
IsTargetNotMoving(_target.position, _constraints.targetPosDeviation))
{
return;
}
}
if (_target == null)
{
Clear();
return;
}
// define new destination
target = _target;
groundNormal = Vector3.up;
myPos = _my.position;
myFacing = Vector3.ProjectOnPlane(_my.forward, groundNormal);
targetPos = target.position;
targetPosDeviation = _constraints.targetPosDeviation;
// flatten local Y-axis
xzVector = Vector3.ProjectOnPlane(myPos - targetPos, groundNormal);
xzDir = xzVector == Vector3.zero ?
-myFacing : // bias when it's not difference, we step back.
xzVector.normalized;
float halfDeviation = _constraints.agentPosDeviation * 0.5f;
minDistance = _constraints.maintainDistance - halfDeviation;
maxDistance = _constraints.maintainDistance + halfDeviation;
deviationDistance = _constraints.maintainDistance - xzVector.magnitude;
destination = myPos + xzDir * deviationDistance;
Event_DestinationChanged?.Invoke(destination);
}
private bool IsTargetNotMoving(Vector3 newPos, float posDeviation)
{
float diffSqr = (targetPos - newPos).sqrMagnitude;
float acceptDevSqr = posDeviation * posDeviation;
return diffSqr < acceptDevSqr; // within acceptable deviation
}
public void Clear()
{
target = null;
targetPos = myPos = myFacing = groundNormal = xzVector = xzDir = destination = default;
minDistance = maxDistance = deviationDistance = targetPosDeviation = 0f;
}
public void DrawGizmos(Constraints c)
{
if (target == null)
return;
/**** This code will not work without gizmos lib.
GizmosExtend.DrawCircle(targetPos, groundNormal, c.deviationColor, targetPosDeviation);
GizmosExtend.DrawCircle(targetPos, groundNormal, c.minRangeColor, minDistance);
GizmosExtend.DrawCircle(targetPos, groundNormal, c.maxRangeColor, maxDistance);
Color deviationColor = deviationDistance > 0 ? c.minRangeColor : c.maxRangeColor;
GizmosExtend.DrawLine(myPos, destination, deviationColor);
GizmosExtend.DrawPoint(destination, deviationColor, 0.3f);
//****/
}
}
#endregion Decision making
}
}