Unity 2D游戏开发:从零实现高性能AABB碰撞检测系统

发布时间:2026/8/3 21:27:44
Unity 2D游戏开发:从零实现高性能AABB碰撞检测系统 1. 项目概述为什么我们要自己动手实现碰撞检测在Unity里做2D游戏物理系统Physics 2D几乎是所有新手的首选。拖个Rigidbody 2D加个Collider 2D运行一下两个物体就“Duang”地一声弹开了简单又直观。但当你做的项目稍微复杂一点比如是一个需要精确控制性能的移动端游戏或者是一个对物理反馈有特殊要求的像素风游戏你可能会开始觉得这套“黑盒”系统有点力不从心。性能开销、不可预测的微小抖动、复杂的参数调试甚至是某些特定碰撞逻辑难以实现都成了拦路虎。这时候回归基础自己用C#实现一套碰撞检测系统就从“没必要”变成了“很值得”。AABBAxis-Aligned Bounding Box轴对齐包围盒就是踏入这个领域最经典、也最实用的第一块敲门砖。它的核心思想简单到极致用一个矩形在2D里把物体包起来判断两个矩形是否重叠。你别看它简单市面上绝大多数2D游戏的碰撞检测尤其是前期粗检测都离不开它。它计算量极小只需要几次简单的数值比较性能极高是优化游戏逻辑、实现特定物理效果的基石。这篇文章我就以一个老开发者的身份带你彻底告别对Unity物理系统的“舒适区依赖”从零开始用最纯粹的C#代码构建一套属于你自己的、高性能、可完全定制的2D AABB碰撞检测系统。我会把每一步的原理、每一行代码的意图以及我在实际项目中踩过的坑、总结的技巧毫无保留地分享给你。无论你是想深入理解游戏物理的底层逻辑还是迫切需要优化项目性能这篇文章都能给你一套可以直接“抄作业”的完整方案。2. 核心原理与数学基础AABB到底是什么在动手写代码之前我们必须把原理吃透。AABB全称Axis-Aligned Bounding Box中文叫“轴对齐包围盒”。这个名字听起来有点唬人但其实拆开看就很简单。“包围盒”Bounding Box顾名思义就是一个把物体包起来的盒子。在2D世界里这个盒子就是一个矩形。我们并不关心物体内部复杂的形状比如一个不规则的角色Sprite我们只用一个尽可能贴合的矩形来代表它用于快速的碰撞初筛。“轴对齐”Axis-Aligned这是关键限制条件。意味着这个矩形的四条边必须分别平行于世界坐标系的X轴和Y轴。它不能旋转。也就是说这个矩形完全由四个值定义最小X坐标left或minX、最大X坐标right或maxX、最小Y坐标bottom或minY、最大Y坐标top或maxY。为什么非要“轴对齐”因为这样判断两个矩形是否相交碰撞的算法会变得极其简单高效。我们不需要进行复杂的旋转矩阵计算或几何相交测试只需要比较坐标。分离轴定理Separating Axis Theorem, SAT在AABB上的简化应用SAT是碰撞检测的通用理论对于AABB来说它被简化到了一个极致如果两个AABB在所有轴这里只有X轴和Y轴上的投影都不重叠那么它们一定没有碰撞反之如果在所有轴上的投影都重叠那么它们就碰撞了。对于轴对齐的矩形在X轴上的投影就是[minX, maxX]这个区间在Y轴上的投影就是[minY, maxY]这个区间。所以判断两个AABB是否碰撞就等价于判断两个区间在X轴上是否重叠并且在Y轴上是否重叠。用逻辑表达就是碰撞 (RectA.minX RectB.maxX) (RectA.maxX RectB.minX) (RectA.minY RectB.maxY) (RectA.maxY RectB.minY)这个公式请你务必理解并记住它是我们所有代码的灵魂。它比我们直觉上想的“计算矩形中心距离”或“比较四个角”要高效和准确得多。注意这里使用的比较是“小于”和“大于”而不是“小于等于”和“大于等于”。这定义了一种“严格相交”的逻辑即两个矩形刚好边贴边时不算碰撞。这在很多游戏逻辑里是符合预期的比如玩家刚好站在地面上。如果你需要包含边接触的情况将和改为和即可。这个细节决定了角色移动的“手感”非常重要。3. 系统设计与数据结构如何优雅地组织我们的碰撞体理解了原理我们就要开始设计代码结构了。一个好的设计能让后续的扩展和维护事半功倍。我们不会去模仿Unity那套复杂的Component系统而是设计一个轻量、清晰的数据结构。首先我们定义一个AABBCollider类它代表一个AABB碰撞体。这个类需要存储什么信息位置和大小这是核心。我们可以存储中心点Vector2 center和半宽高Vector2 extents也可以存储最小点和最大点。存储中心点和半宽高在计算和更新时通常更方便。关联的实体这个碰撞体是属于哪个游戏对象的我们通常需要一个引用比如一个GameObject或一个ID来在发生碰撞时知道是谁撞了谁。碰撞层级和标签不是所有物体都需要相互碰撞。玩家应该和敌人、墙壁碰撞但不应该和奖励道具发生物理碰撞而是触发。我们需要一个简单的过滤系统比如LayerMask和Tag。让我们来定义这个类using UnityEngine; /// summary /// 自定义的2D AABB碰撞体组件。 /// 建议挂载在需要检测碰撞的GameObject上。 /// /summary public class AABBCollider : MonoBehaviour { // 碰撞体的中心点相对于GameObject的Transform [SerializeField] private Vector2 _centerOffset Vector2.zero; // 碰撞体的半宽高从中心到边的距离 [SerializeField] private Vector2 _size Vector2.one; // 碰撞层级用于过滤 public LayerMask collisionLayerMask -1; // 默认与所有层碰撞 // 可选的标签用于更精细的过滤或识别 public string collisionTag ; // 公共属性方便外部获取计算后的世界坐标边界 public float MinX { get; private set; } public float MaxX { get; private set; } public float MinY { get; private set; } public float MaxY { get; private set; } // 一个标志位表示这个碰撞体是静态的如地形还是动态的如玩家、敌人 [SerializeField] private bool _isStatic false; public bool IsStatic _isStatic; /// summary /// 每帧更新碰撞体的世界坐标边界。 /// 对于静态物体可以在Start中计算一次以优化性能。 /// /summary public void UpdateBounds() { if (_isStatic) return; // 静态物体边界无需每帧更新 Vector2 worldCenter (Vector2)transform.position _centerOffset; // 注意这里假设碰撞体不随物体旋转而旋转符合AABB定义。 // 如果物体旋转了这个AABB就不再精确贴合这是AABB的局限性。 // 对于有旋转的物体需要重新计算一个能包裹住旋转后物体的、新的轴对齐包围盒计算会更复杂。 MinX worldCenter.x - _size.x; MaxX worldCenter.x _size.x; MinY worldCenter.y - _size.y; MaxY worldCenter.y _size.y; } // 在Scene视图中绘制Gizmos便于调试和设计关卡 private void OnDrawGizmosSelected() { Gizmos.color Color.green; // 计算当前在编辑器中的边界如果不在运行时则使用transform.position Vector2 currentCenter Application.isPlaying ? new Vector2((MinXMaxX)/2, (MinYMaxY)/2) : (Vector2)transform.position _centerOffset; Vector2 currentSize Application.isPlaying ? new Vector2(MaxX-MinX, MaxY-MinY) : _size * 2; Gizmos.DrawWireCube(currentCenter, currentSize); } }设计解析与注意事项UpdateBounds方法这是性能关键。动态物体每一帧都需要调用此方法来根据Transform的位置更新它的世界坐标边界。对于静态物体如地图块、墙壁我们可以在Start()或Awake()中计算一次然后标记为_isStatic true这样就能避免每帧无意义的计算这是巨大的性能优化点。旋转问题这是AABB最大的局限性。我们的UpdateBounds没有考虑物体的旋转。如果一个矩形精灵旋转了45度用一个轴对齐的矩形去包裹它会产生很大的空白区域即“包围盒”变得不紧凑导致检测精度下降可能出现“没碰到却判定碰撞”的情况。对于需要旋转的物体有更高级的包围盒如OBB Oriented Bounding Box或直接使用多边形碰撞检测。在项目初期如果旋转角度不大可以适当增大_size来补偿或者规定某些物体不允许旋转。Gizmos调试OnDrawGizmosSelected让我们在Unity编辑器里能直观地看到碰撞体的形状和范围对于关卡设计和调试至关重要。一定要用。接下来我们需要一个“碰撞管理器”。它负责持有所有活跃的AABBCollider并每帧执行碰撞检测。我们将其设计为一个单例类方便全局访问。4. 碰撞管理器的实现核心检测算法与性能优化碰撞管理器AABBCollisionManager是整个系统的大脑。它的核心任务很简单收集所有碰撞体然后检查它们两两之间是否发生了碰撞。但如何高效地完成“两两检查”是区分业余和职业实现的关键。最 naive 的方法是双重循环for (int i 0; i colliders.Count; i) { for (int j i 1; j colliders.Count; j) { CheckCollision(colliders[i], colliders[j]); } }如果场景中有N个碰撞体时间复杂度是O(N²)。当N100时需要检测4950次N1000时是499500次即使单次检测再快这个数量级也是无法接受的。因此我们必须引入空间划分Spatial Partitioning来优化。对于2D游戏最常用且简单的就是网格划分Grid或四叉树Quadtree。这里我们先实现一个简单的基于网格的粗筛它足以应对大多数中小型2D项目并且理解起来更直观。思路将游戏世界划分为一个个固定大小的格子Cell。每个AABBCollider根据其位置被放入一个或多个格子中如果碰撞体很大可能跨越多个格子。检测时我们只需要检查同一个格子内以及相邻格子内的碰撞体对从而避免与距离很远的物体进行不必要的检测。让我们开始构建管理器using System.Collections.Generic; using UnityEngine; public class AABBCollisionManager : MonoBehaviour { public static AABBCollisionManager Instance { get; private set; } // 所有注册的动态碰撞体静态碰撞体有单独的处理方式 private ListAABBCollider _dynamicColliders new ListAABBCollider(); // 所有静态碰撞体通常用于环境 private ListAABBCollider _staticColliders new ListAABBCollider(); // 网格划分参数 [Header(Spatial Grid Settings)] [SerializeField] private Vector2 _gridWorldSize new Vector2(50, 50); // 网格覆盖的世界范围 [SerializeField] private float _cellSize 5f; // 每个格子的大小 private int _gridSizeX, _gridSizeZ; private ListAABBCollider[,] _grid; private void Awake() { if (Instance ! null Instance ! this) { Destroy(this); } else { Instance this; InitializeGrid(); } } private void InitializeGrid() { _gridSizeX Mathf.CeilToInt(_gridWorldSize.x / _cellSize); _gridSizeZ Mathf.CeilToInt(_gridWorldSize.y / _cellSize); // 注意我们用y表示世界高度对应网格的Z维度 _grid new ListAABBCollider[_gridSizeX, _gridSizeZ]; for (int x 0; x _gridSizeX; x) { for (int z 0; z _gridSizeZ; z) { _grid[x, z] new ListAABBCollider(); } } Debug.Log($碰撞网格初始化: {_gridSizeX}x{_gridSizeZ}, 单元格大小{_cellSize}); } // 注册和注销碰撞体 public void RegisterCollider(AABBCollider collider) { if (collider.IsStatic) { _staticColliders.Add(collider); // 静态物体也需要放入网格但只需要放一次 AddColliderToGrid(collider); } else { _dynamicColliders.Add(collider); } } public void UnregisterCollider(AABBCollider collider) { if (collider.IsStatic) { _staticColliders.Remove(collider); RemoveColliderFromGrid(collider); } else { _dynamicColliders.Remove(collider); RemoveColliderFromGrid(collider); } } // 核心更新循环 private void Update() { // 1. 清空动态物体所在的旧网格单元简单做法每帧清空整个网格并重新分配对于动态物体不多的情况可以接受 ClearDynamicCells(); // 2. 更新所有动态碰撞体的边界 foreach (var collider in _dynamicColliders) { collider.UpdateBounds(); AddColliderToGrid(collider); } // 3. 执行碰撞检测 PerformCollisionChecks(); } private void ClearDynamicCells() { for (int x 0; x _gridSizeX; x) { for (int z 0; z _gridSizeZ; z) { _grid[x, z].RemoveAll(c !c.IsStatic); // 只移除动态物体保留静态物体 } } } private void AddColliderToGrid(AABBCollider collider) { // 计算碰撞体覆盖了哪些网格单元 int startX Mathf.FloorToInt((collider.MinX _gridWorldSize.x / 2) / _cellSize); int endX Mathf.FloorToInt((collider.MaxX _gridWorldSize.x / 2) / _cellSize); int startZ Mathf.FloorToInt((collider.MinY _gridWorldSize.y / 2) / _cellSize); // MinY对应世界Z int endZ Mathf.FloorToInt((collider.MaxY _gridWorldSize.y / 2) / _cellSize); // 钳制到网格范围内 startX Mathf.Clamp(startX, 0, _gridSizeX - 1); endX Mathf.Clamp(endX, 0, _gridSizeX - 1); startZ Mathf.Clamp(startZ, 0, _gridSizeZ - 1); endZ Mathf.Clamp(endZ, 0, _gridSizeZ - 1); for (int x startX; x endX; x) { for (int z startZ; z endZ; z) { if (!_grid[x, z].Contains(collider)) { _grid[x, z].Add(collider); } } } } private void RemoveColliderFromGrid(AABBCollider collider) { /* 实现逻辑与Add类似反向操作 */ } // 核心碰撞检测逻辑 private void PerformCollisionChecks() { // 我们遍历所有网格单元 for (int x 0; x _gridSizeX; x) { for (int z 0; z _gridSizeZ; z) { var cellColliders _grid[x, z]; int count cellColliders.Count; // 检查当前单元内所有碰撞体对 for (int i 0; i count; i) { for (int j i 1; j count; j) { var colA cellColliders[i]; var colB cellColliders[j]; // 层过滤 if (((1 colA.gameObject.layer) colB.collisionLayerMask) 0) continue; if (((1 colB.gameObject.layer) colA.collisionLayerMask) 0) continue; // 执行AABB检测 if (CheckAABBCollision(colA, colB)) { // 碰撞发生这里可以触发事件 OnCollisionDetected(colA, colB); } } } } } } // 纯数学的AABB碰撞检测函数这是整个系统的核心 public static bool CheckAABBCollision(AABBCollider a, AABBCollider b) { // 使用分离轴定理的简化版检查在X轴和Y轴上是否都重叠 bool overlapX a.MinX b.MaxX a.MaxX b.MinX; bool overlapY a.MinY b.MaxY a.MaxY b.MinY; return overlapX overlapY; } // 当检测到碰撞时通知双方这里用简单的消息发送你可以替换成自己的事件系统 private void OnCollisionDetected(AABBCollider a, AABBCollider b) { a.gameObject.SendMessage(OnMyCollisionEnter, b, SendMessageOptions.DontRequireReceiver); b.gameObject.SendMessage(OnMyCollisionEnter, a, SendMessageOptions.DontRequireReceiver); // 更推荐的做法是使用C#事件Event或委托Delegate实现一个观察者模式这样耦合度更低。 // 例如a.OnCollisionEnter?.Invoke(b); } }代码详解与优化点网格坐标转换AddColliderToGrid方法中的计算(collider.MinX _gridWorldSize.x / 2) / _cellSize是为了将世界坐标假设原点在网格中心转换为网格索引。你需要根据你的游戏世界坐标系来调整这个公式。如果你的世界原点在左下角公式会更简单。每帧清空网格为了简化我们对动态物体采用了每帧清空所属网格再重新添加的策略。这在动态物体数量不多比如少于200个时效率尚可。如果动态物体非常多更高效的做法是记录每个物体上一帧所在的网格只更新发生变化的位置。检测范围我们的PerformCollisionChecks只检查了同一个网格单元内的物体。这可能会漏掉那些刚好位于两个大网格边界两侧的、发生碰撞的小物体。一个更健壮的做法是检查一个物体所在网格及其周围8个邻居网格。这对于大小不超过一个网格单元的物体来说是足够的。我们的代码中因为AddColliderToGrid已经将大物体放入所有覆盖的网格所以同一单元检测对于完全在单元内的碰撞是有效的但对于跨单元边界的碰撞需要检查邻居单元。作为优化和简化我们假设物体大小相对网格较小且我们的检测频率每帧可以容忍偶尔漏过一帧的边界碰撞或者可以通过稍微增大碰撞体来补偿。对于要求精确的项目实现邻居检测是必要的。层过滤PerformCollisionChecks中的层过滤代码((1 colA.gameObject.layer) colB.collisionLayerMask) 0是Unity LayerMask的标准用法。它检查colB的层掩码是否包含了colA所在的层。我们要求双方都允许与对方碰撞碰撞才会发生这提供了最大的灵活性。5. 碰撞响应与信息处理检测到了然后呢检测到碰撞只是第一步更重要的是碰撞发生后该做什么。这就是碰撞响应。在Unity物理系统中响应如弹开、摩擦是自动计算的。在我们的自定义系统里我们需要自己来处理。通常碰撞响应分为几个步骤获取碰撞信息不仅仅是知道“撞了”还要知道“撞得有多深”、“从哪个方向撞的”。这对于把物体推开解决穿透至关重要。解决穿透在离散的帧检测中物体经常已经相互嵌入穿透了。我们需要计算一个最小的位移向量将物体分开。应用物理效果根据物体的属性如弹性、质量和碰撞信息计算速度的变化如反弹。触发游戏逻辑比如玩家碰到敌人掉血吃到金币加分。让我们先实现第一步和第二步计算碰撞法向量和穿透深度。这对于实现一个简单的角色控制器比如不让玩家穿墙是核心。我们在AABBCollisionManager中添加一个更强大的检测函数它不仅能返回是否碰撞还能返回碰撞的详细信息。public struct CollisionInfo2D { public AABBCollider other; public Vector2 normal; // 碰撞法线指向从A指向B的方向通常用于将A推离B public float depth; // 穿透深度 public Vector2 point; // 碰撞点近似值对于AABB通常是重叠矩形的中心 } public static bool CheckAABBCollisionDetailed(AABBCollider a, AABBCollider b, out CollisionInfo2D info) { info new CollisionInfo2D { other b }; // 计算在X轴和Y轴上的重叠量 float overlapX Mathf.Min(a.MaxX, b.MaxX) - Mathf.Max(a.MinX, b.MinX); float overlapY Mathf.Min(a.MaxY, b.MaxY) - Mathf.Max(a.MinY, b.MinY); // 如果没有重叠返回false if (overlapX 0 || overlapY 0) return false; // 计算穿透深度和法线选择重叠量较小的轴作为分离轴 if (overlapX overlapY) { // X轴重叠更小从左右两侧推开 info.depth overlapX; info.normal a.Center.x b.Center.x ? Vector2.left : Vector2.right; // A的中心在B的左边则法线向右推A info.point new Vector2( (a.Center.x b.Center.x) ? a.MaxX : a.MinX, // 近似碰撞点 (a.MinY a.MaxY b.MinY b.MaxY) / 4 // Y方向取中点 ); } else { // Y轴重叠更小从上下两侧推开 info.depth overlapY; info.normal a.Center.y b.Center.y ? Vector2.down : Vector2.up; // A的中心在B的下边则法线向上推A info.point new Vector2( (a.MinX a.MaxX b.MinX b.MaxX) / 4, // X方向取中点 (a.Center.y b.Center.y) ? a.MaxY : a.MinY ); } return true; }这个函数是解决穿透的关键。它通过比较X和Y方向的重叠量找出“最容易分开”的方向即重叠量最小的轴然后给出一个沿着该轴的法线向量和需要移动的深度。例如如果一个玩家矩形从上方落到一个平台矩形上Y方向的重叠会小于X方向法线会是Vector2.up深度就是嵌入的像素值。我们只需要将玩家的位置加上info.normal * info.depth就能瞬间将其推到平台表面解决穿透。现在让我们创建一个简单的PlayerController来演示如何使用这套系统实现移动和碰撞。using UnityEngine; [RequireComponent(typeof(AABBCollider))] public class SimplePlayerController : MonoBehaviour { public float moveSpeed 5f; public float jumpForce 10f; private Vector2 _velocity; private bool _isGrounded false; private AABBCollider _myCollider; private void Start() { _myCollider GetComponentAABBCollider(); if (_myCollider null) { Debug.LogError(SimplePlayerController 需要 AABBCollider 组件); } // 注册到管理器AABBCollider的Start或Awake中应该已经注册了这里确保一下 // 通常注册应在AABBCollider中完成这里只是演示逻辑。 } private void Update() { // 处理输入 float horizontal Input.GetAxis(Horizontal); _velocity.x horizontal * moveSpeed; if (_isGrounded Input.GetButtonDown(Jump)) { _velocity.y jumpForce; _isGrounded false; } // 应用重力简单的模拟 _velocity.y Physics.gravity.y * Time.deltaTime * 2; // 乘以2让重力强一点 // 计算位移 Vector2 displacement _velocity * Time.deltaTime; // 在移动前进行碰撞检测和解决 MoveAndCollide(displacement); } private void MoveAndCollide(Vector2 displacement) { // 我们采用多次迭代步进的方式来解决碰撞提高稳定性尤其是高速移动时 int iterations 0; int maxIterations 5; // 防止死循环 while (displacement.magnitude 0.001f iterations maxIterations) { iterations; // 假设我们暂时移动到这个位置 Vector2 proposedPosition (Vector2)transform.position displacement; // 更新碰撞体边界到假设位置这里简化实际应创建一个临时的边界计算 // 更严谨的做法是使用射线投射Ray Casting或连续碰撞检测CCD这里用离散的多次迭代模拟 _myCollider.UpdateBounds(); // 先基于当前位置更新 // 这里我们需要从管理器中获取所有可能与玩家碰撞的静态和动态物体简化假设管理器提供了方法 // 例如var potentialColliders AABBCollisionManager.Instance.GetNearbyColliders(_myCollider); // 为了示例我们假设有一个列表 allColliders bool collisionOccurred false; CollisionInfo2D earliestCollision new CollisionInfo2D(); float earliestTime 1f; // 归一化时间1表示本帧结束 // 遍历所有潜在碰撞体这里简化实际应用空间划分优化后的列表 foreach (var otherCollider in AABBCollisionManager.Instance.GetAllColliders()) { if (otherCollider _myCollider) continue; if (!AABBCollisionManager.CheckAABBCollision(_myCollider, otherCollider)) continue; CollisionInfo2D info; if (AABBCollisionManager.CheckAABBCollisionDetailed(_myCollider, otherCollider, out info)) { // 这是一个简单的响应立即将玩家从穿透中推离 transform.position (Vector3)(info.normal * info.depth * 1.01f); // 多推一点确保分离 collisionOccurred true; // 根据法线方向更新状态例如如果法线向上则认为落地 if (info.normal.y 0.5f) { // 法线大致向上 _isGrounded true; _velocity.y Mathf.Min(_velocity.y, 0); // 重置Y速度防止继续下沉 } // 如果撞到左右墙壁X速度清零 if (Mathf.Abs(info.normal.x) 0.5f) { _velocity.x 0; } // 撞到头顶Y速度清零防止“顶头”后还向上飞 if (info.normal.y -0.5f) { _velocity.y Mathf.Min(_velocity.y, 0); } // 由于位置被修正需要重新计算剩余位移这是一个复杂话题这里简化处理跳出循环下一帧再处理 // 更完善的实现需要计算碰撞时间TOI并滑动剩余位移。 break; } } if (!collisionOccurred) { // 没有碰撞安全移动 transform.position (Vector3)displacement; break; // 移动完成退出循环 } // 如果发生碰撞且被推开我们需要基于新的位置重新计算可能与剩余位移产生的碰撞。 // 这里简化处理直接清零位移防止无限循环。实际项目需要更精细的“滑动”处理。 displacement Vector2.zero; } // 迭代次数用尽警告可能卡在角落 if (iterations maxIterations) { Debug.LogWarning(Player collision resolution reached max iterations.); } } // 接收碰撞事件的方法需要与AABBCollisionManager.OnCollisionDetected中发送的消息名匹配 private void OnMyCollisionEnter(AABBCollider other) { // 这里可以处理游戏逻辑比如碰到敌人掉血 // Debug.Log(${gameObject.name} 撞到了 {other.gameObject.name}); } }这段控制器代码是一个高度简化的示例它演示了如何将碰撞检测集成到移动逻辑中。但它也暴露了自定义物理的复杂性穿透解决Collision Resolution我们采用的方法是“发现穿透立即推开”。这可能导致物体被“弹开”而不是平滑滑动。更高级的方法是计算“碰撞时间Time of Impact”和“滑动向量Sliding Vector”。多次迭代while循环用于处理一帧内可能发生的多次碰撞比如同时撞到墙和地面。maxIterations防止在复杂角落卡死。性能GetAllColliders()在物体很多时非常慢。在实际项目中你必须使用前面提到的网格系统只获取玩家所在网格及相邻网格的碰撞体列表。管理器应该提供一个GetPotentialColliders(AABBCollider)的方法。地面检测我们通过法线y 0.5f来判断是否落地。这是一个简单粗暴但有效的方法。更稳健的做法是使用“地面射线”或记录上一帧是否在地面。6. 高级话题、优化与避坑指南实现了一个基础可用的系统后我们来探讨一些进阶内容和实战中必然会遇到的“坑”。6.1 处理高速移动物体隧道效应Tunneling这是离散碰撞检测每帧检查一次的经典问题。如果物体速度太快一帧移动的距离超过了自身的尺寸它就可能从另一个薄物体比如一颗子弹从一堵墙中间“穿”过去因为上一帧在墙前下一帧在墙后中间帧没有检测到碰撞。解决方案连续碰撞检测CCD不是检测两个静态形状是否重叠而是检测从上一帧位置到当前帧位置形成的“运动线段”或“扫掠体Swept Volume”是否与目标碰撞体相交。对于AABB就是检测一个“扫掠的矩形”。实现复杂度较高。子步采样Sub-stepping将一帧的时间分成多个小步如4步每小步移动距离按比例减少并进行碰撞检测。这能大大降低隧道效应发生的概率但计算量成倍增加。扩大碰撞体Proxy Geometry对于高速运动的物体如子弹使用一个比视觉模型更大的碰撞体或者根据其速度动态延长碰撞体在运动方向上的范围。射线投射Raycasting对于子弹这类小物体可以不用AABB而是用射线检测。从上一帧位置向当前帧位置发射一条射线检测与环境的碰撞。这是处理高速小物体的最佳实践。在我们的AABB系统中对于玩家和普通敌人使用子步采样是一个在效果和性能间不错的折中。可以在MoveAndCollide函数中将位移分成2-4份循环处理。6.2 优化空间划分从网格到四叉树当游戏世界很大物体分布极度不均匀时均匀网格的效率会下降很多空格子。四叉树Quadtree是更适合2D动态场景的空间划分数据结构。它会根据区域内物体的密度动态地细分或合并节点。四叉树的基本思想定义一个根节点覆盖整个游戏世界。如果一个节点内的物体数量超过某个阈值比如10个就将该节点均等分成四个子节点。将物体重新分配到子节点中。检测时从根节点开始递归地只检查与查询范围如另一个碰撞体的包围盒相交的节点内的物体。实现一个完整的、支持动态更新的四叉树比网格复杂但它能更好地适应物体分布在大型开放世界或物体聚集区域如大量敌人的场景下性能优势明显。Unity的Physics2D系统内部就使用了类似的空间划分结构通常是BVH包围盒层次树。6.3 常见问题与调试技巧物体抖动当两个物体持续碰撞如玩家站在地上每帧的穿透解决可能会因为浮点数精度或法线计算的微小差异导致物体在1个像素的范围内上下抖动。解决方法引入一个微小的容差值如0.001f当穿透深度小于该值时不进行位移修正或者引入“睡眠”状态对静止的物体停止物理计算。卡在角落如PlayerController示例中如果同时撞到两面墙的夹角简单的逐轴推开可能会让物体卡住。需要更智能的解决策略比如同时考虑多个碰撞约束或者使用物理引擎中常见的“顺序脉冲解析Sequential Impulse Resolution”的简化版。Gizmos不显示确保AABBCollider脚本的OnDrawGizmosSelected方法正确且在编辑器场景视图中勾选了“Gizmos”并选择了对应的游戏对象。性能分析在Unity Profiler中观察AABBCollisionManager.Update和PerformCollisionChecks的耗时。如果耗时过高首先检查是否对静态物体进行了每帧更新不应该其次检查网格大小和数量是否合理格子太小格子太多管理开销大格子太大每个格子内物体多检测次数多。可以尝试在编辑器中可视化你的碰撞网格。6.4 何时该用何时不该用适合使用自定义AABB系统的场景对性能有极致要求的移动端2D游戏如休闲超休闲游戏。游戏物理需求简单主要是阻挡和触发如平台跳跃、RPG、卡牌游戏。需要实现非常特定、Unity物理系统难以实现的碰撞行为如自定义的像素完美碰撞、网格地形碰撞。作为学习项目深入理解游戏物理和优化。不建议使用自定义AABB系统的场景需要复杂物理模拟的游戏如愤怒的小鸟、物理沙盒。Unity的物理系统经过多年优化在稳定性和功能上远胜自研简单系统。3D游戏。3D的AABB即AABB但扩展到三维原理类似但复杂度上升且3D物理需求通常更复杂。项目时间紧迫且物理不是核心玩法。Unity物理系统能快速搭建原型。自己实现碰撞检测是一次深刻的修炼。它让你从“使用者”变为“创造者”真正理解游戏世界中物体交互的底层逻辑。虽然初期会遇到各种边界情况和性能挑战但由此获得的系统设计能力、优化思维和对细节的掌控力是单纯使用现成引擎无法比拟的。希望这篇长文能为你打下坚实的基础当你下次在Unity中遇到物理系统的瓶颈时能有信心掏出C#打造一套更贴合自己项目需求的解决方案。