ML-Agents 自定义网格传感器(Custom Grid Sensors)完全指南:从 GridSensorBase 派生到自定义观测
【免费下载链接】ml-agentsThe Unity Machine Learning Agents Toolkit (ML-Agents) is an open-source project that enables games and simulations to serve as environments for training intelligent agents using deep reinforcement learning and imitation learning.项目地址: https://gitcode.com/gh_mirrors/ml/ml-agents
网格传感器(Grid Sensor)为 ML-Agents 强化学习环境提供了一种基于俯视 2D 网格的观测方式,可无死角地感知智能体周围区域内的物体。本文基于 Unity ML-Agents 官方文档《Custom Grid Sensors》展开,结合仓库中的 GridSensorBase.cs、GridSensorComponent.cs、OneHotGridSensor.cs 与 CountingGridSensor.cs 源码,系统讲解如何通过派生GridSensorBase与GridSensorComponent收集自定义属性作为观测,以及包内置的两种网格传感器实现原理。读完本文,你将掌握自定义网格传感器的完整开发流程、四个核心可重写方法的语义,以及数据归一化与压缩之间的约束关系。
Grid Sensor 与 RayCast 观测的取舍
在自定义网格传感器之前,先理解它解决什么问题。官方文档指出,Grid Sensor 提供一种自顶向下视角的 2D 观测,用于检测智能体周围的物体。与射线投射(RayCasts)相比,它有两个关键差异:
- 无盲区:射线只能探测有限数量的方向,网格传感器则在整片网格区域内获得完整观测,不存在间隙;
- 不被遮挡:物体检测不会被智能体周边的其他物体阻断,每个格子独立查询,检测结果互不干扰。
代价是更高的计算资源开销——网格越大、每格观测维度越高,物理查询与数据编码的开销越大。因此 Grid Sensor 适合需要细粒度空间感知的场景,例如需要区分"某方向存在多个目标""目标在网格中的精确位置"等射线难以表达的信息。
需要注意的适用前提:根据 Learning-Environment-Design-Agents.md 的说明,Grid Sensor 仅适用于 3D 环境,在 2D 环境中不会正常工作。
网格传感器的额外能力在于:你可以从GridSensorBase基类派生,在识别物体 Tag 之外,收集自定义属性作为观测,从而极大扩展 GridSensor 的用途。
给 Agent 挂载 GridSensorComponent 后,网格以俯视视角覆盖智能体周围区域(图片来源:Learning-Environment-Design-Agents.md)
创建自定义网格传感器的整体结构
创建自定义网格传感器需要从两个类派生:
GridSensorBase:传感器的实现本体,定义传感器如何处理检测到的 Collider、数据长什么样、如何从检测对象构造观测;GridSensorComponent:Unity 组件层,负责将传感器挂到 GameObject 上、管理参数并接入Agent的传感器系统。
这种"实现 + 组件"的双层结构意味着:GridSensorBase负责纯数据处理逻辑(可在单元测试中独立验证),GridSensorComponent负责与 Unity 场景、Inspector 交互,二者通过CreateSensors()桥接(见下文源码分析)。
派生自 GridSensorBase:四个核心可重写方法
GridSensorBase是网格传感器全部数据处理逻辑所在,它在构造时完成网格布局初始化,并在每帧感知时把检测结果写入观测缓冲区。根据用途,你可能需要重写以下方法:
GetCellObservationSize():定义每格观测维度
protected virtual int GetCellObservationSize() { return 1; }该方法返回每个格子的观测大小(即写入dataBuffer的 float 数量),默认值为1。从源码看,它决定了整个传感器的观测规格:在 GridSensorBase 构造函数 中,m_NumCells = m_GridSize.x * m_GridSize.z,随后以ObservationSpec.Visual(m_CellObservationSize, m_GridSize.x, m_GridSize.z)构造观测规格,因此总观测维度为每格维度 × 网格宽度 × 网格深度。重写GetObjectData时务必同步重写此方法,否则写入缓冲区的数据量与实际分配的缓冲区不一致。
GetObjectData():从检测对象构造观测
protected virtual void GetObjectData(GameObject detectedObject, int tagIndex, float[] dataBuffer) { dataBuffer[0] = tagIndex + 1; }这是自定义网格传感器的核心入口。输入参数为:
detectedObject:当前格子内检测到的 GameObject;tagIndex:该对象 Tag 在DetectableTags列表中的索引(从 0 开始);dataBuffer:待写入观测值的缓冲区,其大小由GetCellObservationSize()决定。
写入后的数据会从每个格子汇总,作为观测发送给训练器。基类默认实现写入tagIndex + 1(注意:基类默认编码不是 one-hot,而是索引偏移),而包内置的 OneHot 与 Counting 传感器则各自覆盖了此方法(见下文)。
GridSensorBase.cs 的 XML 文档给出了一个覆盖示例——读取检测对象的 Rigidbody 速度作为观测:
protected override void GetObjectData(GameObject detectedObject, int tagIndex, float[] dataBuffer) { if (tagIndex == Array.IndexOf(DetectableTags, "RigidBodyObject")) { Rigidbody rigidbody = detectedObject.GetComponent<Rigidbody>(); dataBuffer[0] = rigidbody.velocity.x; dataBuffer[1] = rigidbody.velocity.y; dataBuffer[2] = rigidbody.velocity.z; } }IsDataNormalized():声明数据是否归一化
protected virtual bool IsDataNormalized() { return false; }返回观测数据是否归一化到 0~1 区间(默认false)。这个返回值直接决定了是否可以使用压缩观测(PNG 压缩)——压缩只支持归一化数据,因为压缩本质上把观测值映射为图像像素通道。
从 GridSensorBase.cs 的CompressionType属性 setter 可见底层约束:
set { if (!IsDataNormalized() && value == SensorCompressionType.PNG) { Debug.LogWarning($"Compression type {value} is only supported with normalized data. " + "The sensor will not compress the data."); return; } m_CompressionType = value; }即:当IsDataNormalized()返回false而请求 PNG 压缩时,压缩设置会被静默忽略并给出警告。此外,ValidateValues 方法 在启用 PNG 压缩时会对每次写入的数据逐值校验,若出现<0或>1的数值会直接抛出UnityAgentsException,提示数据必须归一化到 0-1 之间。
当数据不在 0~1 区间但希望用压缩加速训练时,应先在GetObjectData中自行归一化。官方文档给出的实践建议:
- 数据天然有界时,归一化到可能范围即可。例如旋转角有界于
0 ~ 360,记录角度x时存x/360而非x; - 数据无界时(如位置、速度),设置合理的 min/max 范围并据此归一化。
GetProcessCollidersMethod():单元格内多对象的处理策略
protected internal virtual ProcessCollidersMethod GetProcessCollidersMethod() { return ProcessCollidersMethod.ProcessClosestColliders; }当某个格子内检测到多个带有可检测 Tag 的对象时,该方法决定传感器行为。ProcessCollidersMethod枚举定义于 GridSensorBase.cs,目前提供两种:
| 枚举值 | 语义 | 适用场景 |
|---|---|---|
ProcessClosestColliders(默认) | 只处理离智能体最近的 Collider,每个格子的数据只由一个对象表示 | 需要"每格一个物体"的空间表达 |
ProcessAllColliders | 处理格子内所有检测到的 Collider | 每格数据具有可加性,例如统计格子内的物体数量 |
使用ProcessAllColliders时,传入GetObjectData()的dataBuffer会包含同一格子内其他 Collider 已处理的数据,因此你更可能对缓冲区做加减而不是整体覆盖。这一行为在 ProcessDetectedObject 方法 中有明确实现:
if (GetProcessCollidersMethod() == ProcessCollidersMethod.ProcessAllColliders) { // 先把该格当前累计数据拷入 cellDataBuffer,再做增量修改 Array.Copy(m_PerceptionBuffer, cellIndex * m_CellObservationSize, m_CellDataBuffer, 0, m_CellObservationSize); } else { // 只取最近 Collider:先清空缓冲区再写入 Array.Clear(m_CellDataBuffer, 0, m_CellDataBuffer.Length); } GetObjectData(detectedObject, i, m_CellDataBuffer);这段源码清楚地展示了两种模式的本质区别:ProcessAllColliders是"累积式"写入,ProcessClosestColliders是"覆盖式"写入。
派生自 GridSensorComponent:把传感器接入 Unity
GridSensorComponent继承自SensorComponent,在 GridSensorComponent.cs 中定义。要创建你的传感器,需要重写GetGridSensors()并返回想要使用的网格传感器数组:
public class CustomGridSensorComponent : GridSensorComponent { protected override GridSensorBase[] GetGridSensors() { return new GridSensorBase[] { new CustomGridSensor(...)}; } }它既可以返回多个不同的自定义网格传感器,也可以混入包内自带的传感器。基类默认实现返回一个OneHotGridSensor(见 GetGridSensors 默认实现)。
CreateSensors() 的内部装配流程
理解组件如何工作,有助于正确使用它。从 CreateSensors 方法 可以看到完整的装配链路:
- 创建
BoxOverlapChecker(实现自IGridPerception),传入m_CellScale、m_GridSize、m_RotateWithAgent、m_ColliderMask、AgentGameObject、m_DetectableTags以及两个 Collider 缓冲区大小参数——这是物理查询的执行者; - 创建调试用
GridSensorBase("DebugGridSensor", ...),其压缩类型固定为SensorCompressionType.None(因为 gizmo 数据是正整数,压缩会触发校验异常); - 调用
GetGridSensors()获得自定义传感器数组,若为空数组或null会抛出UnityAgentsException,提示至少返回一个传感器; - 只把
m_GridPerception赋给第一个传感器,确保物理查询每帧只更新一次,其余传感器通过RegisterSensor注册; - 若
ObservationStacks != 1,每个传感器会用StackingSensor包裹以实现多帧观测堆叠。
组件的 Inspector 参数
基于 GridSensorComponentEditor.cs 的 Inspector 布局和组件的序列化字段,各参数说明如下:
- Sensor Name:生成的网格传感器名称,影响传感器排序(运行时修改不生效);
- Grid Settings(Cell Scale / Grid Size):每格大小与网格尺寸。
GridSize只支持 2D 网格,构造函数与属性 setter 都会强制y为 1(源码中若m_GridSize.y != 1会抛出UnityAgentsException:"GridSensor only supports 2D grids."),Editor 中也将 y 锁定为 1;二者创建传感器后不可运行时修改; - Agent GameObject:智能体根对象引用,用于消歧与智能体同 Tag 的对象,默认为当前 GameObject;
- Rotate With Agent:网格是否随智能体朝向旋转;
- Detectable Tags:可检测的 Tag 列表(其长度直接决定 OneHot 每格观测维度);
- Collider Mask:物理层遮罩,控制哪些层级的 Collider 参与检测;
- Sensor Settings(Observation Stacks):观测堆叠帧数,范围 1~50,1 表示不堆叠;
- Compression Type:压缩类型(默认 PNG),仅在数据归一化时生效;
- Collider and Buffer(Initial / Max Collider Buffer Size):预分配缓冲区大小(默认 4)与绝对上限(默认 500)。源码注释说明:使用
OverlapBoxNonAlloc物理 API 预分配数组,若发现 Collider 数达到InitialColliderBufferSize则数组翻倍扩容,硬上限为 500; - Debug Gizmo(Show Gizmos / Gizmo Y Offset / Debug Colors):是否显示网格 gizmo、gizmo 高度偏移、每个 Tag 对应的调试颜色。
包内置的两种网格传感器
仓库提供了两个开箱即用的网格传感器,它们的实现也是编写自定义传感器的最佳参考。
OneHotGridSensor:默认的 one-hot 传感器
这是GridSensorComponent使用的默认传感器,检测带可检测 Tag 的对象,观测为检测到的 Tag 索引的one-hot 表示。完整实现见 OneHotGridSensor.cs,其四个方法的行为为:
GetCellObservationSize():detectableTags.LengthIsDataNormalized():trueGetProcessCollidersMethod():ProcessCollidersMethod.ProcessClosestCollidersGetObjectData():
protected override void GetObjectData(GameObject detectedObject, int tagIndex, float[] dataBuffer) { dataBuffer[tagIndex] = 1; }即每个格子用一个长度等于可检测 Tag 数的向量表示,命中的 Tag 位置为 1,其余为 0。由于数据天然归一化,OneHot 传感器可直接使用 PNG 压缩。
CountingGridSensor:格子内物体计数
CountingGridSensor是使用"格子内所有 Collider"的示例,统计每种可检测 Tag 在格子中检测到的对象数量。完整实现见 CountingGridSensor.cs,注意该文件带有MovedFrom("Unity.MLAgents.Extensions.Sensors")特性,表明其命名空间历史上从 Extensions 迁移而来。其行为为:
GetCellObservationSize():detectableTags.LengthIsDataNormalized():falseGetProcessCollidersMethod():ProcessCollidersMethod.ProcessAllCollidersGetObjectData():
protected override void GetObjectData(GameObject detectedObject, int tagIndex, float[] dataBuffer) { dataBuffer[tagIndex] += 1; }注意这里用的是+=累加而非覆盖——与ProcessAllColliders累积语义严格对应。因为计数无上界、不满足 0~1 归一化约束,该传感器不能使用数据压缩:其构造函数中强制CompressionType = SensorCompressionType.None(见 CountingGridSensor.cs),即便在组件上设置了 PNG 压缩也会被覆盖为 None。
测试与验证
仓库在 com.unity.ml-agents.tests 下提供了网格传感器的运行时测试,其中 GridSensorTests.cs 覆盖了GridSensorBase、OneHotGridSensor、CountingGridSensor的观测规格、数据写入与压缩行为。编写自定义传感器时,可以参考这些测试的断言方式,验证GetCellObservationSize()与GetObjectData()写入的数据量是否匹配、归一化声明与压缩设置是否冲突。
完整自定义示例流程
综合以上内容,一个自定义网格传感器的标准流程为:
- 派生
GridSensorBase,重写GetCellObservationSize()定义每格维度; - 重写
GetObjectData()从detectedObject提取自定义属性(如速度、朝向、生命值)写入dataBuffer; - 重写
IsDataNormalized()如实声明数据是否归一化,若希望压缩训练则先归一化再返回true; - 需要统计型观测时重写
GetProcessCollidersMethod()返回ProcessAllColliders,并在GetObjectData中做累加式写入; - 派生
GridSensorComponent,重写GetGridSensors()返回上述传感器实例(可多个); - 在 Unity 中为 Agent 挂载自定义组件,配置
Detectable Tags、Collider Mask、网格尺寸与压缩类型; - 运行时可开启
Show Gizmos借助调试颜色检查各格子的检测结果是否符合预期。
通过这一套扩展机制,你可以把网格传感器从"Tag 的 one-hot 地图"升级为携带任意物理属性、游戏状态的自定义观测源,在保持网格观测无盲区、抗遮挡优势的同时,让强化学习智能体获得更丰富的空间特征输入。
【免费下载链接】ml-agentsThe Unity Machine Learning Agents Toolkit (ML-Agents) is an open-source project that enables games and simulations to serve as environments for training intelligent agents using deep reinforcement learning and imitation learning.项目地址: https://gitcode.com/gh_mirrors/ml/ml-agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考