这次我们来看一个很经典但 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 优化 |
| 是否支持 API | Unity 内部 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,这样权重才有对比意义。
整体执行流程可以概括为:
- 获取查询上下文,包括查询者位置、目标位置等。
- 生成器根据上下文生成候选点列表。
- 对每个候选点,依次运行测试列表。
- Filter 测试不通过则直接排除该点。
- Scorer 测试得到 0 到 1 分,乘权重后累加。
- 对所有候选点按分数降序排序,返回最高分点或前 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组件烘焙:
- 在地面对象上添加
NavMeshSurface组件。 - 将墙体和障碍物设为静态导航阻挡物。
- 点击
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.cs4. 复刻 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 中,一次完整查询可以拆成五个阶段:
- 调用
RunQuery(querier, targetPos),创建上下文。 - 调用生成器的
Generate方法,得到候选点列表。 - 遍历候选点,依次执行所有测试。
- 测试结果为 Fail 且模式为 Filter 时,直接丢弃该点。
- 测试结果为 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 场景搭建步骤
建议按下面的顺序验证:
- 创建地面、墙体、AI Capsule、Target Sphere。
- 给 AI 添加
NavMeshAgent和EQAgent组件。 - 给场景中的动态对象添加
EQEnvironmentQuery组件。 - 在 Inspector 中指定生成器,设置
Count = 16、Radius = 8。 - 添加一个
EQTest_Distance,设置MaxDistance = 20,PreferCloser = true。 - 添加一个
EQTest_NavMeshReachable,让它以 Filter 模式运行。 - 点击 Play,观察 AI 是否移动到离目标最近的可达点。
然后再逐步添加视线测试和掩体测试,观察候选点颜色和 AI 行为变化。
6.3 功能验证用例
| 测试场景 | 输入配置 | 预期结果 |
|---|---|---|
| 空场地找最近点 | 16 点圆形生成器,距离测试 | AI 走向离目标最近且可达的点 |
| 一堵墙挡路 | 加视线测试,RequireVisible=false | AI 可能选择墙后掩体点 |
| 墙后点不可达 | 加 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 决策系统的时候直接照着搭。