☰
Unity 6 从零复刻 Unreal EQS:环境查询系统设计与实现
2026/10/11 14:19:41 网站建设 项目流程

这次我们来看一个很经典但 Unity 侧没有内置的 AI 玩法:Unreal Engine 里的 EQS,也就是 Environment Query System,环境查询系统。很多射击、潜行、生存游戏里,AI 不会傻乎乎直线冲锋,而是会找掩体、绕后、选制高点,看起来很聪明,背后就是这套“生成候选位置 + 多维度打分 + 选择最优解”的逻辑在驱动。

比较麻烦的是,Unity 里没有完全等价的现成系统。行为树容易找,NavMesh 寻路也成熟,但“让 AI 在运行环境里动态评估多个候选点,再挑一个最好的点”这层能力,通常要自己搭。不过思路本身不复杂,完全可以在 Unity 6 里从零复刻出一套轻量 EQS,接上 NavMesh、视线检测、距离评分和可视化调试,让 AI 真正学会“看环境选位置”。

这篇文章会给出完整的系统设计方案、核心 C# 代码、候选点生成与测试逻辑、Gizmos 调试流程、性能优化方向,以及常见问题排查清单。开发环境以 Unity 6 + AI Navigation 包为例,不需要第三方 AI 插件,核心逻辑用纯 C# 就能跑通。如果你最近在做 AI 射击、潜行、恐怖游戏,或者正在研究行为树和寻路之外的“环境感知决策”,这篇可以直接收藏。

另外先说明一点:本篇文章是结合相关 Unity 6 复刻 EQS 教程思路整理的实现方案,不是原视频或付费内容的直接搬运。复现过程中的代码和场景设计都属于通用工程思路,但如果你后续要引用别人的脚本、模型或视频片段,仍然要注意来源授权。

1. 核心能力速览

能力项说明
项目类型Unity 6 技术方案 / EQS 系统复刻
对应引擎Unity 6,推荐 6000.0 及以上版本
核心依赖UnityEngine.AI、AI Navigation 包(ai.navigation)
主要功能候选点生成、距离测试、视线测试、NavMesh 可达性测试、掩蔽评分、AI 目标选择
交互方式组件配置 + C# 代码调用
调试方式Gizmos 可视化候选点与分数、Debug 面板
硬件要求纯 CPU 计算,不需要 GPU,建议 8GB 内存以上
是否支持批量支持多 Agent 查询,但需要分帧或 Job System 优化
是否支持 APIUnity 内部 C# 接口,可直接被行为树或状态机调用
适合场景射击 AI 找掩体、潜行 AI 规划路线、收集型 AI 选目标点

这套方案的核心价值不是做一个“千人同屏 AI”,而是把 Unreal EQS 的设计思路搬到 Unity:先生成大量候选位置,再用可配置的测试项逐个打分,最后选出得分最高的点。后续加新玩法只需要新增一个 Test 子类,不需要改整体框架。

2. EQS 原理拆解:查询、生成器、测试与评分

想在 Unity 里复刻 EQS,首先要理解 Unreal 那边 EQS 的五个核心组成部分。

2.1 Query(查询)

一个 Query 是完整的查询配置,包含上下文、生成器和测试列表。你可以把它理解成“一次完整决策”的蓝图。AI 需要一个目标点时,向 EQS 发起一次查询,系统依次完成候选点生成和评分。

2.2 Context(上下文)

Context 定义查询的“基准位置”。常见的有:

  • 执行查询的 Pawn 自身位置。
  • 当前追逐目标的 Actor 位置。
  • 某个自定义锚点,比如玩家上次被发现的位置。
  • 多个目标的平均位置或最近位置。

在 Unity 实现中,我会用一个轻量的EQQueryContext类来保存这些数据。

2.3 Generator(生成器)

生成器负责产生候选点或候选 Actor。Unreal 常见类型包括:

  • 同心圆生成器:围绕基准点生成一圈圈均匀分布的点。
  • 格栅生成器:在一定范围内生成网格状点。
  • 可点击点生成器:直接收集场景中的相关 Actor。

候选点的数量直接决定查询开销,所以生成器通常要能配置半径、点数和分布密度。

2.4 Test(测试)

每个候选点都要经过一个或多个测试。测试有两种模式:

  • Filter 模式:不满足条件直接淘汰,比如“候选点必须可达”。
  • Scorer 模式:按结果打分,比如“离目标越近分数越高”。

Unreal 里面常见的测试有距离测试、视线测试、NavMesh 可达性测试、掩蔽测试、自热测试等。复刻到 Unity 时,我会把测试设计成可继承的抽象类,每一个测试只做一件事。

2.5 Score(评分)

多个测试的分数乘上各自权重后累加,得到候选点的最终得分。分数越高越优。为了调试方便,每个测试最好把输出归一化到 0 到 1,这样权重才有对比意义。

整体执行流程可以概括为:

  1. 获取查询上下文,包括查询者位置、目标位置等。
  2. 生成器根据上下文生成候选点列表。
  3. 对每个候选点,依次运行测试列表。
  4. Filter 测试不通过则直接排除该点。
  5. Scorer 测试得到 0 到 1 分,乘权重后累加。
  6. 对所有候选点按分数降序排序,返回最高分点或前 N 个点。

3. Unity 6 本地部署环境准备

Unity 6 中接入这套 EQS 系统不需要额外安装复杂插件,但环境准备要做扎实。

3.1 安装 Unity 6

从 Unity Hub 安装 Unity 6。一般来说,使用 6000.0 及以上版本稳定性足够。安装时建议勾选 Windows Build Support 或目标平台支持,方便后续打包测试。

3.2 安装 AI Navigation 包

后续章节里的 NavMesh 可达性测试依赖 AI Navigation 包。打开 Package Manager:

Window -> Package Manager -> Unity Registry -> AI Navigation -> Install

安装完成后,场景中会多出NavMeshSurface相关组件,用来烘焙 NavMesh。

3.3 搭建测试场景

建议创建一个单独场景来验证 EQS:

  • 地面:一个 Plane 或 Cube 拉伸成地面。
  • 墙体:几个 Cube 作为障碍物,给它们单独设置 Layer,比如Obstacle。
  • AI 角色:一个 Capsule,挂NavMeshAgent组件,作为查询发起者。
  • 目标对象:一个 Sphere,代表玩家或敌人,作为 AI 要评估的目标位置。

3.4 烘焙 NavMesh

使用 AI Navigation 包时,可以通过NavMeshSurface组件烘焙:

  1. 在地面对象上添加NavMeshSurface组件。
  2. 将墙体和障碍物设为静态导航阻挡物。
  3. 点击Bake按钮生成 NavMesh。

烘焙完成后,进入 Play Mode,NavMeshAgent才能正确计算路径。

3.5 推荐目录结构

建议把代码按模块分离,方便后续扩展。

Assets/Scripts/EQS/ EQEnvironmentQuery.cs EQQueryContext.cs EQItemGeneratorBase.cs EQGenerator_RadialPoints.cs EQTestBase.cs EQTest_Distance.cs EQTest_LineOfSight.cs EQTest_NavMeshReachable.cs EQTest_Cover.cs Assets/Scripts/Demo/ EQAgent.cs

4. 复刻 EQS 的 Unity 系统设计

在写代码之前,先把类设计说清楚。整体设计目标是“一个可配置的查询入口 + 一组可插拔的测试项”,这样不需要后续改动查询主逻辑。

4.1 核心类职责

类/接口职责
EQQueryContext保存查询者位置、目标位置、查询者 Transform
EQItemGeneratorBase抽象生成器,子类决定候选点如何分布
EQGenerator_RadialPoints在圆形范围内生成均匀候选点
EQTestBase抽象测试,子类决定如何计算分数或过滤
EQTest_Distance距离测试,可设置最近/最远距离和偏好方向
EQTest_LineOfSight视线测试,从查询者到候选点是否可见
EQTest_NavMeshReachable可达性测试,候选点是否在 NavMesh 上有可达路径
EQTest_Cover简化掩体测试,候选点对目标是否形成遮挡
EQEnvironmentQuery查询入口,负责生成候选点并汇总评分
EQAgent演示组件,控制 AI 定期执行查询并移动

4.2 执行流程设计

在 Unity 中,一次完整查询可以拆成五个阶段:

  1. 调用RunQuery(querier, targetPos),创建上下文。
  2. 调用生成器的Generate方法,得到候选点列表。
  3. 遍历候选点,依次执行所有测试。
  4. 测试结果为 Fail 且模式为 Filter 时,直接丢弃该点。
  5. 测试结果为 Scorer 时,累加加权分数,最后选出最高分。

这个流程的核心特点是“每个测试只负责一个维度”,后续加新的评分逻辑不会污染主流程。

5. 核心代码实现

下面进入代码部分。我按时间线实现一个最小可运行的 EQS:圆形生成器、距离测试、视线测试、NavMesh 可达性测试、汇总查询入口,最后接一个移动 Demo Agent。

5.1 查询上下文

using UnityEngine; namespace EQS.Core { public class EQQueryContext { public Transform Querier; public Vector3 QuerierPosition; public Vector3 TargetPosition; } }

5.2 生成器抽象类与圆形生成器

using System.Collections.Generic; using UnityEngine; namespace EQS.Core { public abstract class EQItemGeneratorBase { public abstract List<Vector3> Generate(Vector3 center, Quaternion orientation); } public class EQGenerator_RadialPoints : EQItemGeneratorBase { public int Count = 16; public float Radius = 10f; public override List<Vector3> Generate(Vector3 center, Quaternion orientation) { List<Vector3> points = new List<Vector3>(); for (int i = 0; i < Count; i++) { float angle = (360f / Count) * i * Mathf.Deg2Rad; Vector3 dir = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)); points.Add(center + dir * Radius); } return points; } } }

这个生成器适合快速验证。实际项目中可以继续扩展格栅生成器、沿路径生成器、Actor 收集生成器。

5.3 测试基类

namespace EQS.Core { public enum EQTestMode { Filter, Scorer } public abstract class EQTestBase { public EQTestMode Mode = EQTestMode.Scorer; public float Weight = 1f; public abstract bool Run(Vector3 candidate, EQQueryContext ctx, out float score); } }

5.4 距离测试

using UnityEngine; namespace EQS.Tests { public class EQTest_Distance : EQTestBase { public float MinDistance = 0f; public float MaxDistance = 20f; public bool PreferCloser = true; public override bool Run(Vector3 candidate, EQQueryContext ctx, out float score) { float dist = Vector3.Distance(candidate, ctx.TargetPosition); if (dist < MinDistance || dist > MaxDistance) { score = 0f; return false; } float t = Mathf.InverseLerp(0f, MaxDistance, dist); score = Mathf.Clamp01(PreferCloser ? 1f - t : t); return true; } } }

这个测试的MinDistance和MaxDistance可以直接理解为 Filter 边界,但测试仍返回 false,让上层根据 Mode 决定是否丢弃候选点。

5.5 视线测试

using UnityEngine; namespace EQS.Tests { public class EQTest_LineOfSight : EQTestBase { public LayerMask ObstacleMask; public bool RequireVisible = false; public override bool Run(Vector3 candidate, EQQueryContext ctx, out float score) { Vector3 start = ctx.QuerierPosition + Vector3.up * 1.2f; Vector3 end = candidate + Vector3.up * 1.2f; bool visible = !Physics.Linecast(start, end, ObstacleMask); if (RequireVisible && !visible) { score = 0f; return false; } score = visible ? 1f : 0f; return true; } } }

RequireVisible设置为 true 时,AI 只会选择能直接看见的目标点;设置为 false 时,AI 会选择被遮挡的掩体点。射击游戏找掩体时,通常更想要“被遮挡”的点。

5.6 NavMesh 可达性测试

using UnityEngine; using UnityEngine.AI; namespace EQS.Tests { public class EQTest_NavMeshReachable : EQTestBase { public float SampleDistance = 2f; public override bool Run(Vector3 candidate, EQQueryContext ctx, out float score) { if (!NavMesh.SamplePosition(candidate, out NavMeshHit hit, SampleDistance, NavMesh.AllAreas)) { score = 0f; return false; } NavMeshPath path = new NavMeshPath(); if (NavMesh.CalculatePath(ctx.QuerierPosition, hit.position, NavMesh.AllAreas, path)) { if (path.status == NavMeshPathStatus.PathComplete) { score = 1f; return true; } } score = 0f; return false; } } }

这个测试在候选点数量多时会比较贵,后面性能优化章节会专门说明。

5.7 简化掩体测试

掩体测试的思路是:从候选点看向目标,如果中间有障碍物,说明这个候选点对目标形成了遮挡,加分。

using UnityEngine; namespace EQS.Tests { public class EQTest_Cover : EQTestBase { public LayerMask CoverMask; public override bool Run(Vector3 candidate, EQQueryContext ctx, out float score) { Vector3 fromCandidate = candidate + Vector3.up * 1.2f; Vector3 toTarget = ctx.TargetPosition + Vector3.up * 1.2f; bool occluded = Physics.Linecast(fromCandidate, toTarget, CoverMask); score = occluded ? 1f : 0f; return true; } } }

和EQTest_LineOfSight的区别是:这里是“从候选点看向目标”,用于判断掩体效果;视线测试是“从 AI 当前位置看向候选点”,用于判断路径上是否可见。

5.8 EQS 查询入口

using System.Collections.Generic; using UnityEngine; namespace EQS.Core { public class EQEnvironmentQuery : MonoBehaviour { public EQItemGeneratorBase Generator; public List<EQTestBase> Tests = new List<EQTestBase>(); [Header("Debug")] public bool DrawPoints = true; public bool DrawBestPoint = true; private EQQueryContext _ctx = new EQQueryContext(); public EQResult RunQuery(Transform querier, Vector3 targetPos) { _ctx.Querier = querier; _ctx.QuerierPosition = querier.position; _ctx.TargetPosition = targetPos; List<Vector3> candidates = Generator.Generate(querier.position, querier.rotation); EQResult best = null; foreach (Vector3 point in candidates) { EQResult result = EvaluatePoint(point, _ctx); if (result.Passed && (best == null || result.Score > best.Score)) { best = result; } } return best; } private EQResult EvaluatePoint(Vector3 point, EQQueryContext ctx) { EQResult result = new EQResult(); result.Point = point; result.Passed = true; result.Score = 0f; foreach (EQTestBase test in Tests) { if (test == null) continue; bool ok = test.Run(point, ctx, out float score); if (!ok) { if (test.Mode == EQTestMode.Filter) { result.Passed = false; result.RejectReasons.Add(test.GetType().Name); result.Score = 0f; return result; } score = 0f; } result.Score += Mathf.Clamp01(score) * test.Weight; } return result; } } }

对应的结果类:

using System.Collections.Generic; using UnityEngine; namespace EQS.Core { public class EQResult { public Vector3 Point; public float Score; public bool Passed = true; public List<string> RejectReasons = new List<string>(); } }

5.9 Demo AI Agent

最后写一个演示组件,让 AI 每隔一段时间执行一次查询,然后走到最高分的点。

using UnityEngine; using UnityEngine.AI; using EQS.Core; namespace EQS.Demo { public class EQAgent : MonoBehaviour { public EQEnvironmentQuery EQS; public NavMeshAgent Agent; public Transform Target; public float QueryInterval = 1f; private float _lastTime = -999f; void Update() { if (EQS == null || Agent == null || Target == null) return; if (Time.time - _lastTime < QueryInterval) return; _lastTime = Time.time; EQResult best = EQS.RunQuery(transform, Target.position); if (best != null && best.Passed) { Agent.SetDestination(best.Point); } } } }

这个QueryInterval很重要,后续优化时会反复提到。

6. 可视化调试与功能测试

EQS 这种系统最大的问题是“黑盒”,分数不知道怎么来的。所以可视化调试是这套系统能不能用起来的关键。

6.1 Gizmos 绘制候选点

在EQEnvironmentQuery中添加 Gizmos 绘制逻辑:

private void OnDrawGizmos() { if (!Application.isPlaying) return; if (Generator == null) return; List<Vector3> candidates = Generator.Generate( transform.position, transform.rotation ); foreach (Vector3 point in candidates) { Vector3 center = point + Vector3.up * 0.5f; float d = Vector3.Distance(center, _ctx.TargetPosition); Color color = Color.Lerp(Color.green, Color.red, Mathf.Clamp01(d / 10f)); Gizmos.color = color; Gizmos.DrawWireSphere(center, 0.25f); } }

这个方案比较粗糙,正式项目里最好把候选点、最终选择点、各测试分数存到调试列表里,然后逐项绘制。

更实用的做法是在EQAgent中把best结果缓存起来,持续绘制选中点:

private EQResult _lastBest; void Update() { // ... 省略查询逻辑 _lastBest = EQS.RunQuery(transform, Target.position); } private void OnDrawGizmos() { if (_lastBest == null || !_lastBest.Passed) return; Gizmos.color = Color.cyan; Gizmos.DrawSphere(_lastBest.Point + Vector3.up * 0.5f, 0.4f); }

6.2 场景搭建步骤

建议按下面的顺序验证:

  1. 创建地面、墙体、AI Capsule、Target Sphere。
  2. 给 AI 添加NavMeshAgent和EQAgent组件。
  3. 给场景中的动态对象添加EQEnvironmentQuery组件。
  4. 在 Inspector 中指定生成器,设置Count = 16、Radius = 8。
  5. 添加一个EQTest_Distance,设置MaxDistance = 20,PreferCloser = true。
  6. 添加一个EQTest_NavMeshReachable,让它以 Filter 模式运行。
  7. 点击 Play,观察 AI 是否移动到离目标最近的可达点。

然后再逐步添加视线测试和掩体测试,观察候选点颜色和 AI 行为变化。

6.3 功能验证用例

测试场景输入配置预期结果
空场地找最近点16 点圆形生成器,距离测试AI 走向离目标最近且可达的点
一堵墙挡路加视线测试,RequireVisible=falseAI 可能选择墙后掩体点
墙后点不可达加 NavMesh 可达性 Filter不可达点被过滤,AI 选最近可达点
多个掩体对比距离 + 视线 + 掩体测试AI 选择遮挡更充分且距离合理的点
无有效点所有点都被 Filter 淘汰best为 null,AI 停在原地

通过这组用例,可以判断系统是否真正具备“环境感知决策”能力。

7. 功能扩展:API 与批量任务思路

EQS 本质上是 C# 内部的逻辑模块,不是 Web 服务,所以不需要 HTTP API。但如果你做的是服务器驱动的 AI 决策,或者想让策划通过配置驱动 AI,可以把查询封装成可序列化配置。

7.1 配置驱动思路

可以用ScriptableObject保存查询配置:

[CreateAssetMenu(fileName = "EQSQueryConfig", menuName = "AI/EQS Query Config")] public class EQSQueryConfig : ScriptableObject { public EQItemGeneratorBase Generator; public List<EQTestBase> Tests = new List<EQTestBase>(); }

这样策划可以创建多套查询配置,比如“找掩体”“找巡逻点”“找攻击位置”,运行时按状态切换。

7.2 多 Agent 批量查询

多 Agent 同时调用RunQuery会出现明显的 CPU 峰值,因为每个 Agent 都要生成候选点、计算路径。优化思路有两个方向:

  • 时间分片:每个 Agent 错开查询时间,避免同帧计算。
  • 空间分块:同一个区域内多个 Agent 共用一份查询结果,而不是各算各的。

下面是一个简单的错开查询示例:

public class StaggeredQueryAgent : MonoBehaviour { public EQEnvironmentQuery EQS; public float Offset = 0f; public float Interval = 1f; void Update() { float t = Time.time + Offset; if (t % Interval < Time.deltaTime) { // 执行查询 } } }

通过给不同 Agent 设置不同Offset,可以显著降低同帧计算压力。

7.3 RaycastCommand 批量视线测试

视线测试在候选点多的时候会产生大量 Raycast。Unity 提供了RaycastCommand做批量射线检测,配合 Job System 可以并行执行。下面是一个简化示例:

using Unity.Collections; using UnityEngine; public static class LineOfSightBatcher { public static bool[] BatchCheck(int count, Vector3 start, Vector3[] ends, LayerMask mask) { var commands = new NativeArray<RaycastCommand>(count, Allocator.TempJob); var results = new NativeArray<RaycastHit>(count, Allocator.TempJob); for (int i = 0; i < count; i++) { commands[i] = new RaycastCommand( start, (ends[i] - start).normalized, Vector3.Distance(start, ends[i]), mask ); } var handle = RaycastCommand.ScheduleBatch(commands, results, 32, default); handle.Complete(); bool[] visible = new bool[count]; for (int i = 0; i < count; i++) { visible[i] = results[i].collider == null; } commands.Dispose(); results.Dispose(); return visible; } }

这个示例展示了批量思路,实际接入时要注意RaycastCommand的初始位置不能位于碰撞体内部,否则会产生误判。

7.4 NavMeshPath 缓存与复用

NavMesh.CalculatePath是 EQS 里最贵的操作之一。候选点越多,路径计算越频繁。工程化建议:

  • 只对通过距离和视线测试的点做可达性测试。
  • 把NavMeshPath对象缓存复用,避免每次 new 一个。
  • 同一帧内相同起始点和终点不重复计算路径。

8. 资源占用与性能观察

EQS 属于 CPU 密集逻辑,它的瓶颈不是 GPU,而是候选点数、测试复杂度和查询频率。

8.1 性能瓶颈分析

假设一次查询有 32 个候选点、3 个测试,那么单次查询最多执行 96 次测试。如果其中包含可达性测试,最坏情况是 32 次 NavMesh 路径搜索,这已经会带来几毫秒的开销。

所以不要让 AI 每帧查询。一般射击游戏 AI 的决策频率是 1 到 4 Hz,也就是每 0.25 到 1 秒查询一次,完全够用。

8.2 如何观察 CPU 峰值

Unity Profiler 是主要工具:

Window -> Analysis -> Profiler

重点观察PlayerLoop下的逻辑开销,如果单帧耗时突然变高,优先查看是否有多个 Agent 在同一帧执行路径计算。

8.3 降低开销的建议

手段说明
降低候选点数先用 16 到 32 个点验证效果,不够再加
降低查询频率0.5 到 1 秒一次通常足够
Filter 前置先做便宜的 Filter 测试,再做贵的路径测试
调整 NavMesh SampleDistance过大的采样距离会放宽候选点范围
使用 Job System将视线测试批量并行化
多 Agent 错帧给每个 Agent 设置随机起始偏移

8.4 内存与显存

这套方案是纯 CPU 计算,基本不占用显存。内存方面主要开销来自测试过程中产生的NavMeshPath和候选点列表。工程上建议复用这些对象,不要在高频查询时反复 new。

9. 常见问题与排查方法

问题现象可能原因排查方式解决方案
候选点没有生成生成器未配置或 Count 为 0检查 Inspector 中 Generator 引用和 Count 值设置有效生成器并让 Count > 0
AI 永远停在原地所有候选点被 Filter 淘汰开启 Gizmos 看候选点颜色调整测试阈值或关闭 Filter 模式
可达性测试全失败NavMesh 未烘焙查看 Navigation 窗口是否已有数据重新 Bake NavMesh
视线测试结果不符合预期LayerMask 未包含墙体检查 ObstacleMask 配置让墙体使用单独 Layer
游戏运行时卡顿明显每帧查询且计算路径打开 Profiler 定位耗时增加 QueryInterval,减少候选点数
Gizmos 不显示场景视图 Gizmos 被关闭检查 Scene 右上角 Gizmos 开关打开 Gizmos 并确认绘制代码存在
多个 Agent 同帧卡顿缺乏错帧逻辑查看同时发起查询的 Agent 数量使用偏移量错开查询时间
分数结果不稳定测试分数未归一化输出各测试得分在汇总前对 score 做 Clamp01
Linecast 起点在物体内部起点高度太低或偏移不够调试时绘制射线把起点抬高并添加偏移
候选点集中在 NavMesh 外生成器只按圆分布,未贴地采样观察候选点 Y 轴高度使用 NavMesh.SamplePosition 修正高度

其中最容易踩的坑是“视线测试结果全反”。原因很常见:把RequireVisible配置成 true,却期待 AI 找掩体。实际上找掩体时应该用掩体测试,而不是视线测试。建议把每个测试的作用写在 Inspector 的 Tooltip 里,避免策划配错。

10. 边界与合规使用提醒

这篇文章是基于通用工程思路的复现整理,不是直接照搬原视频或付费内容。如果你学习某个教程后做项目,请遵守下面几条边界:

  • 不要直接复制教程中的付费脚本、美术资源和关卡内容。
  • 如果引用了开源实现,保留原作者署名和许可证声明。
  • 使用 AI 生成素材时,确认素材版权和商业使用权限。
  • 如果是团队项目,涉及玩家数据、网络同步等功能,需要遵守隐私和数据合规要求。
  • 正式发布前,对 EQS 生成的 AI 行为做充分测试,避免出现卡墙、穿墙或无限循环等异常。

复刻 EQS 这类系统,价值在于把“决策逻辑”从具体玩法中抽离出来。它本身不涉及敏感内容,但如果 AI 行为应用到商业游戏,仍然需要做完整的功能与性能验证。

11. 最佳实践与后续扩展

如果你决定在项目里正式落地这套 EQS,建议按下面的次序推进。

11.1 先跑通最小闭环

不急着做掩体、视线、Job System。先让 AI 能通过“圆形生成器 + 距离测试 + 可达性测试”选出一个点并走过去。这个闭环跑通后,后面所有测试项都是增量工作。

11.2 每个测试单独开关

在EQEnvironmentQuery里为每个测试增加enable字段,或者使用[SerializeReference]让 Inspector 支持动态增删测试项。调试时只开一个测试,更容易定位问题。

11.3 把得分结构输出到 UI

更进阶的做法是在场景里用OnGUI或 TextMeshPro 输出每个候选点的得分明细:

void OnGUI() { if (_lastResult == null) return; GUILayout.Label($"Best Point: {_lastResult.Point}"); GUILayout.Label($"Score: {_lastResult.Score}"); }

这样策划不用看代码也能理解 AI 为什么选这个位置。

11.4 扩展方向

这套框架可以继续扩展成更完整的 AI 决策层:

  • 接入行为树:把查询结果写进黑板变量,行为树中的MoveTo任务直接读取目标点。
  • 增加 Actor 生成器:直接收集场景中的敌人或掉落物,做收集型 AI。
  • 增加“方向偏好”测试:让 AI 偏向玩家背后方向,实现绕后效果。
  • 增加“热度”测试:通过体素热度场让 AI 避开危险区域。
  • 生成器支持组合:圆形生成器生成后经过 NavMesh 采样修正高度,再做一次附近掩体投影。
  • 服务端 AI 决策:如果做 MOBA 或俯视角射击,可以将查询逻辑放在服务器,使用纯 C# 计算,不依赖 Unity 物理。

12. 总结与下一步

这次整理的 Unity 6 EQS 复刻方案,核心思路是“生成候选点 -> 多维度测试 -> 加权打分 -> 输出最优解”。它不是一个具体插件,而是一套可插拔、可调试、可扩展的 AI 环境决策框架。复制代码跑通 Demo 之后,你会发现 Unity 里做“AI 找掩体”“AI 选巡逻点”“AI 绕后”这类需求,都比直接写死逻辑要稳得多。

接下来建议先做两件事:

  • 把最小闭环跑通,验证候选点生成和得分选择。
  • 配置 Gizmos 调试,把每个候选点的分数可视化。

如果每一步都能看到“为什么选这个点”,这套 EQS 就算真正落地了。这个方向后面还能扩展很多实用玩法,建议收藏备用,准备做 AI 决策系统的时候直接照着搭。

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

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

立即咨询