解释Layer Collision Matrix的位运算实现
解读
在国内 Unity 面试中,“Layer Collision Matrix 的位运算实现” 并不是让你背引擎菜单,而是考察两点:
- 你是否理解 Physics.GetIgnoreLayerCollision 底层只是一张 32×32 的位图;
- 你能否用 C# 位运算 把这张位图压缩到 一个 32 位整数数组(uint[32]) 里,并给出 O(1) 的读写接口。
这道题经常作为 “性能优化” 环节的追问出现:当项目有 200+ 个动态生成的 Layer、且需要在逻辑层每秒查询上千次碰撞忽略规则时,用反射去调 Unity API 会成为热点,位运算压缩 就是标准解法。
知识点
- Layer 索引 范围 0~31,与 uint 32 位 一一对应。
- 对称矩阵:A 与 B 的忽略标志等同于 B 与 A,因此只需 上三角 或 整行 存储。
- 位运算:
- 置位:
row |= 1U << col - 清位:
row &= ~(1U << col) - 查询:
(row & (1U << col)) != 0
- 置位:
- 与 Unity 同步:在 RuntimeInitializeOnLoadMethod 阶段用 Physics.GetIgnoreLayerCollision 把矩阵一次性缓存到 uint[32],后续逻辑层 零 GC 查询。
- 线程安全:uint 读写是 原子 的,但批量修改需 lock 或 Version++ 快照。
答案
“Layer Collision Matrix 在 Unity 底层就是 32×32 的对称位图。我们可以用 一个长度为 32 的 uint 数组 来压缩存储,每一行是一个 uint,其中第 col 位为 1 表示 忽略 LayerRow 与 LayerCol 的碰撞。
public static class LayerMatrix
{
private static readonly uint[] _ignore = new uint[32]; // 32×32 位图
// 设置忽略标志
public static void SetIgnore(int layerA, int layerB, bool ignore)
{
if (layerA > layerB) (layerA, layerB) = (layerB, layerA); // 保证 A<=B,统一用上三角
uint mask = 1U << layerB;
if (ignore)
_ignore[layerA] |= mask;
else
_ignore[layerA] &= ~mask;
}
// O(1) 查询
public static bool GetIgnore(int layerA, int layerB)
{
if (layerA > layerB) (layerA, layerB) = (layerB, layerA);
return (_ignore[layerA] & (1U << layerB)) != 0;
}
// 与 Unity 物理引擎同步,只在初始化时调用一次
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void SyncWithUnity()
{
for (int i = 0; i < 32; ++i)
{
uint row = 0;
for (int j = 0; j < 32; ++j)
if (Physics.GetIgnoreLayerCollision(i, j))
row |= 1U << j;
_ignore[i] = row;
}
}
}
这样就把 32×32=1024 个 bool 压缩到 32 个 uint(128 字节),查询和修改都是 位运算 O(1),零 GC,适合在战斗帧逻辑里高频使用。”
拓展思考
- 增量同步:如果策划在运行时通过 Editor 脚本 改了 TagManager,可以在 Undo.postprocessModifications 里 标记 dirty,下一帧 增量更新 变化的行,而不是全表重建。
- Burst 友好:把 uint[32] 转成 Unity.Mathematics.uint32x4x8 可以在 JobSystem 里向量化查询,实现 批量射线检测 的 忽略掩码预过滤,比逐条调 Unity API 快一个数量级。
- 网络同步:在 帧同步 模型中,把 uint[32] 直接 BitConverter.GetBytes 后 压缩到 128 字节 的状态快照里,保证 客户端碰撞判定 完全一致,避免 浮点差异 导致的 不同步。
- Layer 扩容:Unity 目前硬编码 32 层,若项目 自定义层超过 32,需要 二级位图(uint[32][N])或 NativeBitArray,但 物理引擎仍只认前 32 层,此时必须把 业务层 与 物理层 的索引 分离映射,防止 位运算越界。
掌握这套位运算压缩后,再遇到 “如何优化 Layer 碰撞查询” 或 “如何自定义物理忽略规则” 的追问,就能直接把 时间复杂度、内存占用、线程安全、Burst 向量化 全抛出来,稳稳拿到性能优化加分。