☰
Unity 6中自研轻量EQS框架:从零实现动态环境查询与AI决策
2026/10/5 11:28:21 网站建设 项目流程

“如果 AI 只会走固定路径,那遇到动态掩体和多目标威胁时基本等于裸奔。”这句话是我在 Unity 社区里看到次数最多的一句吐槽,也是 Unreal EQS 被反复提起的原因。这次我们来看一套在 Unity 6 中复刻 Unreal Engine EQS(Environment Query System,环境查询系统)的完整思路与代码框架。

先说明白:这不是让你下载一个现成插件,而是从零搭建一个可以在运行时动态评估场景、对候选位置打分排序、最终让 AI 做出最优决策的模块。如果你熟悉 Unreal,一定知道 EQS 的工作方式:AI 发起一次查询,系统根据上下文生成一批候选点,再用距离、可见性、角度、路径可达性等多个维度给候选点打分,最后 AI 走向分数最高的位置。Unity 官方没有直接提供这套系统,传统做法是 NavMeshAgent 加上手动维护的路径点,但这套方案很难应对动态场景。

本文会从 Unreal EQS 的核心概念讲起,在 Unity 6 中用 C# 实现上下文、生成器、测试项和查询运行器,然后接入一个带 NavMeshAgent 的 AI 角色,演示动态选点和掩体选择。最后补充调试可视化、性能观察、常见问题排查和工程化扩展建议。适合对 Unity AI 有一定基础、想突破固定巡逻点限制的开发者,也适合从 Unreal 转向 Unity、想找回 EQS 开发体验的读者。

1. 核心能力速览

能力项说明
目标引擎Unity 6(也可用于 2022 LTS,需注意 AI Navigation 包版本差异)
实现语言C#,纯脚本实现
系统类型轻量环境查询系统,逻辑参考 Unreal EQS
核心能力查询上下文、候选点生成、多测试项评分、最优位置排序
可扩展性生成器与测试项均为抽象类,可自行扩展
依赖组件Unity AI Navigation 包、NavMeshAgent、Collider
运行方式MonoBehaviour + ScriptableObject,编辑器内可视化配置
外部插件不需要,复用 Unity 内置 API
调试能力Gizmos 绘制候选点与分数,支持运行时查看
性能策略查询冷却、候选点上限、后续可接 Job System 并行评分
适合场景AI 掩体选择、动态巡逻点、多目标威胁评估、技能释放位置选择等

这里再补充一句关于项目定位的判断:Unreal 的 EQS 是一套完整且复杂的系统,本文不会也没有必要在 Unity 里复刻全部细节。核心目标是复刻它的“查询-生成-测试-排序”工作流,把这套决策逻辑变成 Unity 工程里可复用、可调试、可扩展的模块。从实际项目角度看,这套精简版会比盲目照搬 Unreal 的完整架构更实用。

2. Unreal EQS 是什么,Unity 为什么需要它

Unreal 的 EQS 全称是 Environmental Query System,直译是环境查询系统。它不是某个具体功能,而是一套通用的“让 AI 在环境中找最优位置/最优目标”的决策框架。一个标准 EQS 查询流程包含五个部分:

  1. 查询请求方(Querier):通常是 AI Controller,发起一次查询请求。
  2. 上下文(Context):描述查询的参考对象,比如查询者自身、目标玩家、某个技能释放点。
  3. 生成器(Generator):根据上下文在空间中生成一批候选点,常见的有网格、圆环、圆锥、点阵。
  4. 测试项(Test):对每个候选点执行一个或多个测试,测试可以过滤候选点,也可以给候选点打分。
  5. 结果排序:把多个测试项的分数加权合并,选出最高分位置返回给 AI。

Unreal 的 EQS 之所以强,是因为它把“找位置”这件事从硬编码中解放出来。AI 不关心具体在哪几个点之间巡逻,而是通过查询动态获取候选位置,再用测试项模拟真实决策:太远不要、没掩体不要、敌人能看见不要、路径走不到不要。这些规则全部可配置,换一个场景或者换一种 AI,只需要调整查询资产,不需要改代码。

Unity 的问题在于没有原生 EQS。常用 AI 方案是 NavMeshAgent + 固定路径点,或者运行时随机选一个 NavMesh 上的位置。固定路径点显然不够灵活,随机位置又缺少合理性判断。更关键的是,当场景里存在多个 AI 角色,每个 AI 需要根据视野、距离、覆盖物、路径长度做不同决策时,没有一套统一的“评估环境”框架,代码很快就会变成一团乱麻。

所以,在 Unity 中复刻 EQS 的核心意义不是炫技,而是补上 Unity AI 开发中缺失的这一层抽象:把环境感知变成可配置、可评分、可查询的模块。这里的“可配置”非常关键,它意味着策划或者搞 AI 的同事可以在 Inspector 里调整测试项权重,而不是每次改逻辑都打开 IDE 改代码。

3. 环境准备与 Unity 6 项目配置

开始写代码之前,先准备一个干净的 Unity 6 工程。后面所有实现都基于这个工程,建议按下面的清单逐项确认:

3.1 Unity 6 版本与模块

  • 通过 Unity Hub 安装 Unity 6 正式版,选择包含 Windows/Mac Build Support 的模块。
  • 创建 3D(Built-In Render Pipeline)项目即可,不需要 URP/HDRP。
  • 代码编辑器推荐 VS Code 或 Rider,也可以直接用 Visual Studio。

3.2 AI Navigation 包

Unity 6 中 NavMesh 相关功能被整理进了 AI Navigation 包。打开 Package Manager,搜索AI Navigation,点击 Install。这个包会提供 NavMeshSurface 组件,用于在场景中烘焙 NavMesh。

# 如果命令行安装,可以在项目根目录执行: # 但更推荐直接在 Package Manager 中搜索安装

3.3 场景基础结构

搭建一个简易测试场景,至少包含以下几类物件:

  • 一个地面(Plane 或者任意 Cube 拉伸,注意加 Collider)。
  • 一个烘焙好的 NavMesh(挂 NavMeshSurface,点击 Bake)。
  • 一个 AI 角色:一个 Capsule + NavMeshAgent 组件。
  • 一个玩家目标:一个 Cube 或 Capsule,作为 Target 上下文。

场景结构参考如下:

场景 ├── Ground │ └── NavMeshSurface ├── AI_Agent │ ├── Capsule │ └── NavMeshAgent └── Player_Target └── Capsule

3.4 目录与脚本规划

建议在 Assets 下建立如下目录结构,保持代码和配置分离:

Assets/ ├── Scripts/ │ ├── EQS/ │ │ ├── Core/ │ │ ├── Generators/ │ │ └── Tests/ │ └── AI/ ├── EQSAssets/ └── Scenes/

Core 放上下文、候选点、查询运行器等基础类;Generators 放生成器;Tests 放测试项;AI 放最终调用查询的角色脚本。EQSAssets 用来存放运行时创建的 ScriptableObject 查询配置资产。

4. 自研 EQS 框架的架构设计

在写代码前,先把架构过一遍。这套 Unity EQS 框架包含 5 个核心类型:

4.1 核心概念映射

Unreal EQS 概念Unity 自研实现
上下文(Context)EQContext类,保存 Querier、Target、QueryCenter
候选点(Item)EQItem类,包含位置、总分、各测试得分
生成器(Generator)EQGenerator抽象类,提供候选点列表
测试项(Test)EQTest抽象类,对候选点返回分数
查询配置(Query)EQSQueryConfigScriptableObject 资产
查询运行器EQSRunner静态类,执行完整查询流程

4.2 执行流程

一次完整查询的执行顺序如下:

  1. 外部调用方(AI 角色脚本)构造EQContext并传入查询配置EQSQueryConfig。
  2. 运行器调用生成器的Generate(context)获取候选点列表。
  3. 对每个候选点创建EQItem。
  4. 遍历config.tests中的每个测试项,调用test.Score(item, context),按权重累加分数。
  5. 对全部候选点按总分排序。
  6. 截取前 N 个候选点,返回给调用方。

这个流程和 Unreal EQS 的“生成优先、测试评分、排序选择”一致,只是处理方式更轻量。设计上,生成器和测试项都用抽象类实现,这样后续扩展新的生成策略或测试维度,不需要改动运行器逻辑,符合开放封闭原则。

5. 从零实现 EQS 核心:生成器与测试

下面开始写核心代码。建议按小节顺序逐个创建脚本。

5.1 上下文与候选点

上下文是查询的输入,候选点是查询的输出基础。新建Assets/Scripts/EQS/Core/EQContext.cs:

using UnityEngine; namespace EQSFramework { /// <summary> /// 查询上下文:所有生成器和测试项都从这里获取参考信息。 /// </summary> public class EQContext { public Transform Querier; // 查询者,通常是 AI 角色 public Transform Target; // 目标,通常是玩家 public Vector3 QueryCenter; // 查询中心点 } }

再新建EQItem.cs:

using UnityEngine; namespace EQSFramework { /// <summary> /// 候选点:一个待评估的位置。 /// Score 是多个测试项加权后的最终分数。 /// </summary> public class EQItem { public Vector3 Position; public float Score; public float DistanceToTarget; public bool IsVisible; } }

这里把距离和可见性作为常用字段直接放进候选点,是为了方便在调试 Gizmos 中直接读取,也方便后续测试项复用。实际项目中可以按需扩展,但不要塞太多一次性字段,否则候选点对象会变得臃肿。

5.2 生成器:网格与圆环

生成器负责产生候选位置。先定义抽象基类EQGenerator.cs:

using System.Collections.Generic; using UnityEngine; namespace EQSFramework { public abstract class EQGenerator { public abstract List<Vector3> Generate(EQContext context); } }

网格生成器是最常用的生成器,适合在某个区域内均匀撒点。新建EQGridGenerator.cs:

using System.Collections.Generic; using UnityEngine; namespace EQSFramework { [System.Serializable] public class EQGridGenerator : EQGenerator { public Vector2 size = new Vector2(10f, 10f); public float cellSize = 1f; public float heightOffset = 0f; public override List<Vector3> Generate(EQContext context) { List<Vector3> points = new List<Vector3>(); Vector3 center = context.QueryCenter; int xCount = Mathf.Max(1, Mathf.RoundToInt(size.x / cellSize)); int zCount = Mathf.Max(1, Mathf.RoundToInt(size.y / cellSize)); Vector3 start = center - new Vector3(size.x * 0.5f, 0f, size.y * 0.5f); for (int x = 0; x < xCount; x++) { for (int z = 0; z < zCount; z++) { Vector3 p = start + new Vector3( x * cellSize + cellSize * 0.5f, heightOffset, z * cellSize + cellSize * 0.5f ); points.Add(p); } } return points; } } }

圆环生成器适合在角色周围呈圆形分布候选点,常用于掩体选择或技能位置选择。新建EQRingGenerator.cs:

using System.Collections.Generic; using UnityEngine; namespace EQSFramework { [System.Serializable] public class EQRingGenerator : EQGenerator { public int count = 8; public float radius = 5f; public float heightOffset = 0f; public override List<Vector3> Generate(EQContext context) { List<Vector3> points = new List<Vector3>(); Vector3 center = context.QueryCenter; for (int i = 0; i < count; i++) { float angle = (360f / count) * i * Mathf.Deg2Rad; Vector3 offset = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * radius; Vector3 p = center + offset; p.y += heightOffset; points.Add(p); } return points; } } }

生成器本身不关心点是否可到达,这是测试项的事。这样职责分离,生成策略可以随意组合测试策略。

5.3 测试项:距离、可见性、角度、导航

测试项是 EQS 的核心。先定义抽象基类EQTest.cs:

using UnityEngine; namespace EQSFramework { public abstract class EQTest { public bool negate; // 取反,用于做“反向过滤” [Range(0f, 1f)] public float weight = 1f; // 权重 public abstract float Score(EQItem item, EQContext context); } }

距离测试:候选点离目标越近分越高。

using UnityEngine; namespace EQSFramework { [System.Serializable] public class EQDistanceTest : EQTest { public float maxDistance = 10f; public override float Score(EQItem item, EQContext context) { Vector3 targetPos = context.Target ? context.Target.position : context.QueryCenter; float dist = Vector3.Distance(item.Position, targetPos); item.DistanceToTarget = dist; float score = 1f - Mathf.Clamp01(dist / maxDistance); float result = negate ? 1f - score : score; return result * weight; } } }

可见性测试:AI 所在候选位置能否看到目标。用 Physics.Linecast 实现,注意需要排除 AI 自身碰撞体,最好通过 LayerMask 处理。

using UnityEngine; namespace EQSFramework { [System.Serializable] public class EQVisibilityTest : EQTest { public float eyeHeight = 1.5f; public LayerMask obstacleMask = ~0; public override float Score(EQItem item, EQContext context) { Vector3 from = item.Position + Vector3.up * eyeHeight; Vector3 to = context.Target ? context.Target.position + Vector3.up * eyeHeight : context.QueryCenter + Vector3.up * eyeHeight; bool visible = !Physics.Linecast(from, to, obstacleMask); item.IsVisible = visible; float score = visible ? 1f : 0f; float result = negate ? 1f - score : score; return result * weight; } } }

角度测试:AI 朝向和候选点方向的夹角越小,分越高。这个测试适合“优先选择正前方位置”的场景。

using UnityEngine; namespace EQSFramework { [System.Serializable] public class EQDotTest : EQTest { public float maxAngle = 60f; public override float Score(EQItem item, EQContext context) { Vector3 from = context.Querier ? context.Querier.position : context.QueryCenter; Vector3 forward = context.Querier ? context.Querier.forward : Vector3.forward; Vector3 dir = item.Position - from; dir.y = 0f; dir.Normalize(); float angle = Vector3.Angle(forward, dir); float score = 1f - Mathf.Clamp01(angle / maxAngle); float result = negate ? 1f - score : score; return result * weight; } } }

导航可达性测试:候选点是否能通过 NavMesh 到达,路径越短分越高。这个测试可以在一定程度上避免 AI 选中不可达位置。

using UnityEngine; using UnityEngine.AI; namespace EQSFramework { [System.Serializable] public class EQPathfindingTest : EQTest { public float maxPathDistance = 20f; public override float Score(EQItem item, EQContext context) { Vector3 from = context.Querier ? context.Querier.position : context.QueryCenter; NavMeshPath path = new NavMeshPath(); bool hasPath = NavMesh.CalculatePath(from, item.Position, NavMesh.AllAreas, path); if (!hasPath || path.status != NavMeshPathStatus.PathComplete) { float failScore = negate ? 1f : 0f; return failScore * weight; } float pathLen = 0f; if (path.corners.Length > 1) { for (int i = 1; i < path.corners.Length; i++) { pathLen += Vector3.Distance(path.corners[i - 1], path.corners[i]); } } float successScore = 1f - Mathf.Clamp01(pathLen / maxPathDistance); float result = negate ? 1f - successScore : successScore; return result * weight; } } }

这里有个工程细节要说明:NavMesh.CalculatePath不保证终点一定在 NavMesh 上,如果候选点偏到 NavMesh 外,路径状态会变成PathPartial或PathInvalid。因此更稳妥的做法是在EQItem生成后首先用NavMesh.SamplePosition把候选点修正到最近 NavMesh 表面上,然后再执行其他测试。后面第 5.4 节会给出这个修正步骤。

5.4 查询配置与运行器

查询配置使用 ScriptableObject,这样可以把一组生成器和测试项保存成资产,多个 AI 角色复用同一份配置。新建EQSQueryConfig.cs:

using System.Collections.Generic; using UnityEngine; namespace EQSFramework { [CreateAssetMenu(fileName = "EQSQuery", menuName = "AI/EQS Query")] public class EQSQueryConfig : ScriptableObject { [SerializeReference] public EQGenerator generator = new EQGridGenerator(); [SerializeReference] public List<EQTest> tests = new List<EQTest>(); public int maxItemCount = 32; public bool sampleNavMeshPosition = true; public float sampleNavMeshDistance = 2f; } }

这里使用了[SerializeReference],它的作用是让 ScriptableObject 在 Inspector 中能够序列化保存抽象类派生实例。生成器和测试列表都会在 Inspector 中显示为可折叠对象,可以手动添加不同的生成器类型和测试类型。这是 Unity 对多态序列化的标准做法,需要 Unity 2019.3 以上版本,Unity 6 完全支持。

然后编写查询运行器EQSRunner.cs:

using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; namespace EQSFramework { /// <summary> /// 查询运行器:负责执行一次完整的 EQS 查询。 /// </summary> public static class EQSRunner { public static List<EQItem> Run(EQSQueryConfig config, EQContext context) { if (config == null || context == null) { Debug.LogWarning("EQS 查询失败:配置或上下文为空。"); return new List<EQItem>(); } List<Vector3> points = config.generator.Generate(context); List<EQItem> items = new List<EQItem>(points.Count); foreach (Vector3 p in points) { Vector3 candidatePos = p; if (config.sampleNavMeshPosition && NavMesh.SamplePosition(p, out NavMeshHit hit, config.sampleNavMeshDistance, NavMesh.AllAreas)) { candidatePos = hit.position; } EQItem item = new EQItem { Position = candidatePos, Score = 0f }; foreach (EQTest test in config.tests) { if (test == null) continue; item.Score += test.Score(item, context); } items.Add(item); } items.Sort((a, b) => b.Score.CompareTo(a.Score)); if (items.Count > config.maxItemCount) { items.RemoveRange(config.maxItemCount, items.Count - config.maxItemCount); } return items; } } }

到这里,一套基础的 EQS 查询框架已经跑通了。它的工作流程是:传入配置和上下文,运行器生成候选点,修正 NavMesh 位置,逐项测试打分,最后返回排序后的列表。接下来要验证的是,这套框架能不能真正驱动 Unity 场景里的 AI 角色。

6. 在 Unity 6 场景中接入 AI 角色

写完框架只是第一步,接入 AI 角色才能验证功能。下面给出一个可以实际运行的测试脚本,并演示三种典型用法。

6.1 创建 AI 角色脚本

新建Assets/Scripts/AI/AISoldier.cs:

using UnityEngine; using UnityEngine.AI; using EQSFramework; namespace AITest { public class AISoldier : MonoBehaviour { public EQSQueryConfig moveQuery; // 移动到最优位置 public Transform playerTarget; // 目标玩家 private NavMeshAgent agent; private void Start() { agent = GetComponent<NavMeshAgent>(); } [ContextMenu("执行一次查询")] public void ExecuteQuery() { if (moveQuery == null) { Debug.LogWarning("未配置 moveQuery。"); return; } EQContext context = new EQContext { Querier = transform, Target = playerTarget, QueryCenter = transform.position }; List<EQItem> results = EQSRunner.Run(moveQuery, context); if (results.Count > 0) { agent.SetDestination(results[0].Position); Debug.Log($"最优候选点:{results[0].Position},分数:{results[0].Score:F2},共 {results.Count} 个候选点"); } else { Debug.LogWarning("查询结果为空。"); } } } }

在 Inspector 中把AISoldier挂到 AI 角色上,创建一个 EQSQuery 资产,配置好生成器和测试项,然后通过 ContextMenu 右键调用ExecuteQuery,就能看到 AI 角色走出来。这个验证方式对排查问题非常高效。

6.2 场景一:掩体选择

掩体选择是 EQS 的经典用法:AI 在受到威胁时,需要快速找到一个“离目标足够近、目标看不见我、我能通过路径到达”的位置。

测试配置建议:

配置项建议值
生成器类型EQRingGenerator,半径 6 米,数量 16
测试项 1EQDistanceTest,maxDistance 10,weight 0.4
测试项 2EQVisibilityTest,negate 勾选,weight 1.0
测试项 3EQPathfindingTest,maxPathDistance 20,weight 0.6

这里的核心是EQVisibilityTest的negate勾选。正常情况下,候选点可见目标会得到 1 分;勾选 negate 后,可见得到 0 分,不可见得到 1 分。这样 AI 会优先选择“目标看不到我”的位置。

6.3 场景二:动态巡逻点

固定巡逻点的问题在于,玩家蹲在一个固定点就能猜到 AI 路线。动态巡逻点可以让 AI 每次巡逻时通过 EQS 重新选一个当前位置周边最优的点,距离和朝向的权重可以自行调整。

测试配置建议:

配置项建议值
生成器类型EQGridGenerator,size 12x12,cellSize 2
测试项 1EQDistanceTest,maxDistance 15,weight 0.5
测试项 2EQDotTest,maxAngle 90,weight 0.5
测试项 3EQPathfindingTest,maxPathDistance 30,weight 0.3

这样 AI 会优先选择“离目标距离适中、在自身朝向前方、路径可达”的位置进行巡逻,而不是每次都走向同一个固定点。

6.4 场景三:多目标威胁评估

EQS 不仅可以评估位置,也可以评估目标。思路很简单:把每个目标的位置当作一个候选点,用距离、可见性、目标当前状态等测试项给目标打分,然后选择分数最高的目标作为攻击对象。

具体做法是写一个EQTargetTest,让它从context.Target之外的多目标列表中逐个取出目标,构造EQItem。这里不再展开完整代码,只说明思路:运行时把目标列表作为候选点传入框架,测试项按目标属性评分。这是 EQS 从“位置查询”向“目标查询”扩展的自然方式。

7. 调试可视化与性能观察

EQS 这类系统调试起来比普通 AI 逻辑更困难,因为候选点可能有一百多个,如果不做可视化,你根本不知道分数为什么高、为什么低。所以调试可视化要优先做。

7.1 用 Gizmos 绘制候选点

在 AI 角色脚本中加上 Gizmos 绘制代码,把最近一次查询结果显示在 Scene 视图里。新建Assets/Scripts/AI/AISoldierDebug.cs,或者直接把下面的代码合并到 AISoldier 中:

#if UNITY_EDITOR using UnityEngine; using EQSFramework; namespace AITest { public partial class AISoldier { private void OnDrawGizmosSelected() { if (lastResults == null || lastResults.Count == 0) return; foreach (EQItem item in lastResults) { float t = Mathf.Clamp01(item.Score / 2f); Gizmos.color = Color.Lerp(Color.red, Color.green, t); Gizmos.DrawWireSphere(item.Position, 0.3f); } } } } #endif

注意lastResults是需要在AISoldier类中新增的字段,把ExecuteQuery中得到的results赋值给它。这样在 Scene 视图中选中 AI 角色,就能看到所有候选点的分数分布。

7.2 性能数据怎么看

EQS 的性能瓶颈主要在于候选点数量和测试项数量相乘的评分次数。10x10 网格 100 个候选点、4 个测试项,就是 400 次评分;20x20 网格 400 个候选点、8 个测试项,就是 3200 次评分。如果 AI 角色数量超过 10 个,同步执行就可能出现瞬时卡顿。

建议在查询运行器中加入耗时统计,最简单的方式是使用System.Diagnostics.Stopwatch:

System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); List<EQItem> items = EQSRunner.Run(moveQuery, context); sw.Stop(); Debug.Log($"EQS 查询耗时:{sw.ElapsedMilliseconds} ms");

判断标准很简单:单次查询耗时稳定低于 2ms,可以接受;超过 5ms,就要降候选点数量或者测试项数量。如果 AI 数量多,优先做查询冷却,比如每个 AI 至少间隔 0.3 秒到 0.5 秒再发起下一次查询;更进一步可以用 Unity Job System 把多个候选点的评分并行化。

7.3 降低性能压力的策略

  • 降低网格密度:cellSize 从 1 改成 1.5 或 2,候选点数量会显著下降。
  • 缩短查询冷却时间:每次查询后强制等待最短间隔。
  • 分帧执行:把候选点分批,一帧只测试其中一部分。
  • 结果缓存:如果场景环境变化不剧烈,可以缓存最近一次查询结果,短时间复用。
  • 按需查询:比如角色只有在进入某种状态时才触发 EQS,而不是每帧都执行。

8. 常见问题与排查方法

下面的表格列出了使用这套 Unity EQS 框架时最可能遇到的问题和排查方向:

问题现象可能原因排查方式解决方案
查询结果为空生成器没配置或上下文为空检查EQSQueryConfig资产和EQContext赋值确认 Querier、Target、QueryCenter 都已赋值
所有候选点分数都是 0测试项权重为 0,或所有测试项返回 0在 Inspector 中展开测试项,检查 weight 和 negate调整 weight;打开可视化逐项看分数
候选点不在 NavMesh 上生成器产生的位置高度不对检查sampleNavMeshPosition是否开启勾选sampleNavMeshPosition,调大sampleNavMeshDistance
AI 走向不可达位置缺少路径可达性测试查看最终选择位置是否在 NavMesh 内添加EQPathfindingTest并提高权重
可见性检测结果不符合预期Linecast 检测到了 AI 自身或地面的 Collider检查obstacleMask设置把 AI 本体放到指定 Layer,并在obstacleMask中排除
每帧执行查询导致卡顿查询过于频繁,候选点数量过大用 Stopwatch 统计耗时,查看候选点数量加查询冷却,降低网格密度,改用 Job System
Inspector 中无法添加生成器或测试项[SerializeReference]未正确显示检查 ScriptableObject 资产是否保存点击资产面板中的 Add 按钮,选择具体类型
NavMesh.CalculatePath 总是返回 PathInvalid起点或终点不在 NavMesh 上分别检查起点和终点位置对起点和终点都执行NavMesh.SamplePosition修正

排查时有个技巧:先只保留一个测试项,把其他的 weight 临时设为 0,逐项验证每个测试的单独表现。如果单个测试都能跑通,再叠加所有测试权重,问题通常出在权重配比上。

9. 最佳实践与扩展方向

框架跑通只是基础,工程化才是关键。下面这些实践建议来自常见的 Unity 项目经验,按重要性排序:

  1. 查询配置资产化:不要把生成器和测试项直接写死在场景里,尽量用 ScriptableObject 资产。这样同一种 AI 可以复用配置,也方便在版本控制中统一管理调整记录。

  2. 测试项分数统一归一化:框架中的每个测试项最好都返回 0 到 1 之间的分数,权重再乘以这个分数。如果某个测试项返回超大值或负值,整体排序会被带偏。尽量不要在测试项内部直接做复杂的分数转换。

  3. AI 数量多时引入查询冷却:每个 AI 设置一个lastQueryTime字段,只有间隔超过阈值才允许发起新的 EQS 查询。否则多个 AI 同时查询,场景会有明显的帧率抖动。

  4. 与行为树或状态机配合:EQS 的价值在于“决策位置”,而什么时候触发查询、查询结果如何使用,应该交给行为树或状态机。不要让 AI 角色每帧都调用 EQS,也不要在任何状态里无脑执行查询。

  5. 运行时调试信息分层:编辑器下可以用 Gizmos 画出上千个候选点,但打包后这些调试代码要全部去掉。建议在#if UNITY_EDITOR中包裹所有调试绘制逻辑。

  6. 注意数据合规与资源使用:如果项目包含玩家位置、角色生命值、朝向等数据,在接入网络同步、云端统计或对局回放功能时,要注意隐私边界和数据权限。AI 查询过程中产生的候选点坐标、路径计算耗时等数据,如果不做脱敏处理,不建议直接上报到第三方分析平台。

  7. 扩展方向:走向 ECS 与 Job System。当前框架的评分循环是纯同步的,候选点数量增加后,可以把测试循环改成IJobEntity或IJobParallelFor,用 Burst 编译加速。Unity 6 中对 ECS 的支持更成熟,这个方向可以作为后续优化重点。

  8. 扩展方向:增加过滤阶段。Unreal EQS 支持先过滤再评分,本框架目前只实现了评分阶段。如果需要高性能,可以在EQTest中增加一个bool PassFilter(EQItem item, EQContext context)方法,先过滤掉完全不合要求的候选点,再执行得分累加,能有效降低无效计算。

10. 总结

这套 Unity 6 自研 EQS 框架最值得尝试的点,是它用不到 300 行核心代码复刻了 Unreal EQS 的完整工作流。第一次上手建议先做两件事:第一,用 6.1 小节的AISoldier场景跑通一次查询,确认候选点能生成、分数能排序、AI 能移动;第二,打开 Gizmos 可视化,观察不同测试项权重对最终选点的影响。最容易踩的坑有两个:一是[SerializeReference]字段在 Inspector 中不显示,二是 NavMesh 候选点没有修正导致路径计算始终失败。这两个问题在代码中都已经给出了解决方案,直接参照即可。

从项目后续扩展看,这套框架的价值不在代码量,而在它把环境评估从“硬编码路径点”提升到了“动态查询排序”的层面。用它可以继续做掩体、巡逻、目标威胁评估,也可以和行为树、状态机、ECS 结合,演变成一套更完整的 Unity AI 支撑模块。如果你正在做射击类、潜行类或者其他需要 AI 独立做空间决策的项目,这套 EQS 思路值得直接拿过去用。先跑通一次查询,剩下的按项目需求慢慢扩展。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询