做2D坦克游戏翻车率最高的功能往往不是贴图渲染也不是敌人AI而是最底层的实体移动。坦克大战3.0这个版本我干的第一件大事就是把运动系统整个推翻重写核心目标只有一个防重叠。坦克不能穿墙、坦克之间不能互相穿过、炮弹命中时也不能从目标身体里透过去。整个工程前后折腾了一周有差不多一半时间花在处理边界条件上比如墙角卡位、高速子弹、两辆坦克同时挤向同一个格子的情况。这篇文章就把这套防重叠运动方案从设计思路到代码实现再到排坑实录完整讲一遍。想复现这套方案的朋友只需要有一个能画矩形的2D环境再借一套键盘输入剩下的碰撞逻辑按文中的路子走就行。如果你是第一次写2D游戏这篇文章能帮你少踩一大半碰撞相关的老坑如果你已经在用Box2D这类物理引擎也可以看看手写一个简单碰撞系统时到底在思考什么。下面的内容全部围绕一个真实项目展开代码是C#风格的伪代码换到C、Java、Python都只是语法翻译的问题。1. 项目拆解防重叠运动到底在解决什么问题1.1 防重叠的三个层次检测、响应、预防先从问题本身说起。所谓防重叠指的是任何游戏实体在运动过程中不出现“你中有我、我中有你”的穿插状态。拿现实生活打比方你在走廊里迎面走过来一个人正常情况下你俩不会叠在一起因为你会提前绕开他。但游戏里的坦克不会绕它只会在每一帧里按照玩家输入的方向移动固定距离所以必须由程序主动检查“我下一步要去的地方到底能不能站人”。这里要区分三个很容易混淆的概念碰撞检测、碰撞响应、碰撞预防。碰撞检测回答的问题是“两个物体现在是否相交”碰撞响应回答的是“如果相交了该怎么办”碰撞预防则更进一步在物体移动之前就判断“我能不能往这里走”。很多老版本游戏的运动系统是“先移动再检测最后把位置推回去”属于典型的“事后纠正”。这种做法不是不能用但在精度要求高的场景里就会露馅高速移动时经常推不回去或者推回去的一瞬间产生肉眼可见的抖动。坦克大战3.0改用的事前检测思路才是防重叠运动的真正核心。除此之外防重叠还有一个容易被忽视的方向同一帧内多个实体同时运动时的处理。比如两辆坦克面对面冲锋如果各自独立检测可能这一帧双方的新位置还没有相交下一帧却已经互相穿过去了。这就需要在运动顺序和碰撞优先级上做文章这一点我放到后面专门讲。1.2 3.0版本为什么单独重写运动逻辑坦克大战3.0这个项目玩法是经典坦克大战的框架地图上有砖墙和钢墙玩家控制一辆坦克消灭敌军敌方坦克会主动追击和射击。前两个版本虽然能跑通但玩家反馈里出现频率最高的不是难度问题而是“手感不对劲”——说直白一点就是穿模。两辆坦克叠在一起子弹穿过砖墙打到玩家身上坦克贴着墙走的时候不是卡死就是被弹飞。这些问题归根结底都指向同一个模块运动与碰撞。所以3.0版本我没有急着加新玩法先把移动、碰撞、出生、子弹命中这些基础逻辑统一进一套新的运动框架里。这样做的收益是很大的后续加新坦克、新子弹、新地图时只要套用同一套碰撞规则就不会再出现“新单位破坏了旧规则”的情况。对于一款小游戏而言基础系统的一致性比功能数量重要得多。1.3 这套方案适用的边界在动手之前先给这套方案画个边界。它适合的场景有几个特征第一实体形状基本是轴对齐的矩形也就是长方形方向跟坐标轴平行坦克顶多横平竖直地转向不会任意旋转第二同屏实体数量在几十到几百这个量级不需要上大规模物理引擎第三游戏对碰撞的精确性要求高于对物理真实感的要求不需要反弹、摩擦、重力这类效果。如果你的游戏里有大量圆形的角色、需要像弹射游戏那样的物理手感或者实体数量达到上千那么下面这套AABB防重叠方案只适合作为入门版本你需要在这个基础上继续扩展。2. 碰撞检测方案选型为什么是AABB而不是物理引擎2.1 AABB的数学原理看起来朴素用起来扎实AABB全称是Axis-Aligned Bounding Box轴对齐包围盒。所谓轴对齐指的是这个矩形的四条边分别与坐标轴平行不会旋转。如果一个矩形斜过来了那它就不叫AABB了。两个AABB怎么判断是否重叠规则其实就一句话两个矩形在X轴方向的投影如果重叠并且在Y轴方向的投影也重叠那它们就相交只要有一个轴的投影不重叠就一定不相交。用代码写出来是这个样子public bool Overlaps(Rect a, Rect b) { // 只要有一个轴的投影没有重叠就返回false if (a.Right b.Left || a.Left b.Right) return false; if (a.Bottom b.Top || a.Top b.Bottom) return false; return true; }这里我把条件分成了两行就是为了强调“两个轴要同时满足”。见过不少新手把这里的逻辑写成“X轴或者Y轴重叠就返回真”结果就是两个矩形明明一上一下错开着程序却认为它们撞上了。这个低级错误我后文还会再提一次因为哪怕是有经验的开发者也偶尔会被运算符优先级坑到。坦克大战为什么适合AABB因为这类游戏的坦克是四方向转向的贴图本身跟坐标轴平行碰撞体直接用一个矩形就能精确覆盖车身。如果用圆形碰撞体反而会出现四个角露在外面——也就是说一个圆形的碰撞体在靠近墙角时会提前判定碰撞虽然更圆滑但不够还原玩家对“坦克车身”的直觉。2.2 为什么不直接用物理引擎可能有朋友会问现在项目都用Unity了里面自带Box2D物理引擎直接挂个碰撞体组件不就行了确实可以但坦克大战3.0的选择是自己写。这里把两种方案的取舍摆出来聊一聊。物理引擎的核心能力是模拟力、速度、动量、摩擦、关节约束这些内容它解决的问题是“物体在力的影响下如何运动”。而坦克大战需要的其实是“物体是否能够移动到某个位置”这种更简单的逻辑。用物理引擎当然能实现碰撞但实现“坦克贴墙滑动”的时候你得去调整摩擦系数、反弹系数、碰撞响应回调调完之后手感还未必对。相比之下自己维护一个碰撞检测函数几十行代码就能给出确定性的结果。维度自写AABB碰撞物理引擎Box2D等代码量几百行足够引入完整依赖调试难度可一行行追黑盒回调需理解引擎机制运动控制完全自主天然支持“能不能走”的判断需要阻止引擎自动响应额外配置性能开销极低纯几何运算有扫描排序等预处理开销扩展能力需要自己写旋转、多边形等高级检测自带多边形、关节、传感器适合场景网格地图、矩形碰撞体、确定性强物理模拟感强的游戏就不说多平台移植的问题了手写方案在任何渲染框架下都能跑逻辑完全不依赖引擎版本。当然了如果项目规模上去了或者后续玩法需要诸如“炮弹击退坦克”“爆炸把坦克掀飞”这类带物理规则的玩法再引入物理引擎也不迟。关键是想清楚当前版本的边界不要拿大炮打蚊子也不要等踩坑了才后悔没上引擎。2.3 网格碰撞地图把墙体检测变成查表坦克大战的地图是典型的网格地图墙体的位置可以用一张二维数组来表达比如0代表空地1代表砖墙2代表钢墙。这样做的最大好处是墙体碰撞检测从“遍历所有墙体矩形”降成了“查几个格子”。具体做法是取得坦克的碰撞体矩形算出这个矩形覆盖了哪些格子然后逐个检查这些格子的值。由于坦克的尺寸远大于地图格子一次要查的格子数量是有限的通常只有四到九个。这一段代码是整篇的核心之一public bool IntersectsWall(CollisionMap map, Rect bounds) { int cellSize map.CellSize; // 碰撞体矩形覆盖的格子范围 int minCol bounds.Left / cellSize; int maxCol (bounds.Right - 1) / cellSize; int minRow bounds.Top / cellSize; int maxRow (bounds.Bottom - 1) / cellSize; // 越界直接视为墙避免坦克跑出地图 if (minCol 0 || maxCol map.Cols) return true; if (minRow 0 || maxRow map.Rows) return true; for (int c minCol; c maxCol; c) { for (int r minRow; r maxRow; r) { if (map.Grid[c, r] ! 0) return true; } } return false; }这里有一个极其容易踩坑的细节maxCol计算的时候为什么要在bounds.Right上减一因为如果坦克的右边恰好落在某个格子的边界线上比如x等于32而格子大小是16那它只占到第1格的一部分并没有真正进入第2格。如果直接拿32除以16会得到第2格的下标从而把并不存在的碰撞也算进去坦克就会莫名其妙地被卡住。这个减一的操作本质上是把“矩形与格子的交集”处理成半开区间保证边界情况不会判错。实测下来大部分“坦克卡在砖墙边缘”的Bug最后都能追到这句代码上。地图网格除了做墙体检测还方便做地图编辑器和出生点校验。我给每个格子都区分了碰撞属性砖墙、钢墙、水域各有各的值以后要加河流、冰面这类特殊地形只需要增加对应的碰撞属性判断完全不需要改动运动框架本身。3. 防重叠运动核心实现从原理到落地3.1 先检测后移动重塑运动流程整个防重叠运动的第一个原则就是“先检测后移动”。听起来像废话但很多实现根本做不到位。正确的流程是这样的每一帧根据方向键和速度算出目标位置先拿着“目标位置的碰撞体”去检测墙体和其他坦克只有确认完全不碰撞才真正更新坦克坐标。我给出一个完整的TryMove函数这是整个系统的心脏public bool TryMove(Tank tank, float dx, float dy, CollisionMap map, ListTank allTanks) { // 目标位置 float newX tank.X dx; float newY tank.Y dy; // 目标位置的碰撞体 Rect newBounds new Rect( newX - tank.HalfWidth, newY - tank.HalfHeight, tank.Width, tank.Height); // 第一道检测地图墙体 if (IntersectsWall(map, newBounds)) return false; // 第二道检测其他坦克 foreach (Tank other in allTanks) { if (other tank) continue; if (Overlaps(newBounds, GetBounds(other))) return false; } // 只有通过全部检测才真正移动 tank.X newX; tank.Y newY; return true; }注意函数里所有检测用的都是“目标位置”的碰撞体而不是当前位置。这保证了在位置更新之前问题就已经被拦截。如果你在某个项目里看到了“先改位置、再检测、测出了重叠再把位置改回去”这种写法趁早把它改掉因为事后纠正方案在复杂场景里几乎必然会出抖动和穿透问题。另外返回值要不要提供取决于使用场景。如果移动失败的时候需要播放撞击声或者触发动画那么返回一个布尔值就很有用如果只是简单阻止移动不返回值也行。我在项目里保留了这个布尔值方便调试时打印“为什么没走动”。3.2 滑动响应贴墙移动不再卡死如果仅仅做到“撞上就禁止移动”那手感会非常僵硬。玩家按住右下方向键坦克碰到墙之后如果整个移动被禁止它就连贴着墙往前滑都做不到游戏体验会直接崩掉。正确的做法是引入滑动响应当完整位移被阻挡时把位移拆成X轴和Y轴两个方向分别尝试。实现方式非常直接先用完整位移尝试失败后再分别尝试两个轴向public void MoveWithSlide(Tank tank, float dx, float dy, ...) { // 完整位移优先 if (TryMove(tank, dx, dy, ...)) return; // 分成两个轴向先X后Y if (TryMove(tank, dx, 0, ...)) return; if (TryMove(tank, 0, dy, ...)) return; // 两个方向都撞墙说明卡在角落保持原地 }这个思路的来源是分离轴思想一个二维位移可以分解为两个一维位移分别检测就等价于完整检测只是“部分成功”时允许保留一个轴的方向移动。具体案例是玩家坦克顶着一面竖直墙往左上走完整位移被墙挡住但X轴方向可以走过去的话就只执行X轴位移坦克看起来就是在贴墙滑动。反过来如果先尝试X轴失败再尝试Y轴还能处理顶墙往上的情况。这里有一个实战经验分离轴尝试的顺序会影响手感通常先试X轴再试Y轴符合玩家“左右优先于上下”的操作直觉。但如果你在做一个纵向地图为主的关卡可以考虑改成先试Y轴。另外滑动响应在斜角输入时会有阶梯感这是几乎所有2D游戏都会遇到的取舍属于可接受的成本。3.3 坦克互撞优先级规则怎么定防重叠不只针对墙体坦克与坦克之间的互撞更要细抠。设想两辆坦克面对面冲锋双方每帧各自计算出新位置单看任何一辆它前方另一辆坦克的位置都没有挡住它——但等两辆都移动了结果就是重叠。这就是同一帧内“同步移动”带来的检测漏洞。我在3.0里采用的规则是分步移动加优先级判定。具体做法是把本帧所有需要移动的坦克排成固定顺序依次执行TryMove。先移动的坦克会占住新位置后移动的坦克检测时就会发现前方有障碍从而停下来。这套逻辑很简单但效果很稳定永远不会出现两辆坦克互相穿过的画面。那“固定顺序”怎么定我用的方案是玩家永远优先移动敌坦克按创建顺序排序。这个规则的意义在于当玩家坦克和敌坦克同时往同一个格子挤的时候玩家优先敌坦克让路。这个设计符合直觉玩家不会因为“我明明先按了移动键却被看不见的规则挡住”而疑惑。如果你希望更公平也可以改为按坐标先后排序但务必保证顺序在每一帧内是确定的不要在移动过程中动态排序否则会出现“你让我、我让你”的抖动死循环。这里再说一个延伸需求坦克推挤。有些玩家反馈希望坦克能把另一辆坦克推开但经典坦克大战里坦克是绝对碰撞的推挤会明显破坏玩法节奏所以我默认关掉了推挤。如果你想做推挤效果实现方式也没那么复杂当A坦克的目标位置被B坦克占据时把B坦克按A的运动方向平移同样距离然后递归检查B的新位置是否合法。递归推挤的深度要限住不然会有连锁反应和死循环风险。3.4 高速穿透与帧率无关性步进切割坦克大战3.0里子弹的移动速度比坦克快得多如果不做特殊处理子弹很可能在一个16毫秒的帧里从一堵薄砖墙的左边穿越到右边碰撞检测完全来不及发现。这种情况在圈内有个专门的词叫“隧穿”Tunneling是高速运动碰撞的头号杀手。最简单的解决方案是“步进切割”把一帧内的位移人为切分成多段逐段做碰撞检测。比如子弹速度为每帧20像素墙厚8像素那就把这段位移切成每段4像素一共5段这样第3段就能检测到墙体。核心代码是public void MoveWithSteps(Tank tank, float dx, float dy, ...) { // 限制单步最大位移不超过碰撞体最小尺寸的一半 float maxStep Math.Min(tank.HalfWidth, tank.HalfHeight) * 0.5f; float distance Mathf.Sqrt(dx * dx dy * dy); int steps Mathf.CeilToInt(distance / maxStep); float stepX dx / steps; float stepY dy / steps; for (int i 0; i steps; i) { if (!TryMove(tank, stepX, stepY, ...)) break; } }这里单步最大位移的取值直接决定了防穿透的可靠性。我推荐取碰撞体最小尺寸的一半用坦克来举例坦克宽32像素高28像素最小尺寸的一半是14像素那么单步最大位移14像素任何比14像素薄的墙体都能被准确拦截。步数越多运动轨迹越平滑性能开销也越低实际调的时候留意一个平衡即可。如果将来子弹速度进一步加快还可以把系数降到四分之一甚至更小一帧分十几次检测在2D游戏里依然是零感知的开销。还有一个与帧率相关的细节要一起解决移动速度的单位应该统一成“像素/秒”再乘以每帧的真实时间差deltaTime这样60帧和144帧的屏幕上坦克移动速度才能一致。但帧率很低时deltaTime会变得很大导致一帧内移动距离非常长所以MoveWithSteps的步进切割需要同时兜住这个风险。我的习惯是deltaTime超过1/30秒时强制走步进逻辑低于这个阈值时走普通的TryMove即可省掉不必要的计算。4. 实操过程与核心代码实现4.1 搭建最小验证场景一张地图两辆坦克理论说了不少接下来是实操。我在项目里搭了一个最小的验证场景用来反复测试防重叠逻辑一张8乘8的网格地图格子大小为16像素地图数据用二维数组直接写死一辆玩家坦克用方向键控制一辆敌方坦克设定为在固定路线上来回巡逻。这个场景虽然简单但已经能覆盖墙体防重叠、坦克互撞、滑动响应、高速穿行这些核心场景。地图数据大概是这样的0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0我特意在地图中间留了一条窄通道并且布置了几个T型墙角用来测试滑动响应的边界表现。你会发现很多碰撞Bug只有在墙角和高低错落的墙体组合里才会暴露出来所以测试地图一定要有意识地制造这些地形而不是只用空旷场地。坦克的实体信息统一存在一个Tank结构体里中心坐标、半宽、半高、速度、朝向、阵营。这里选择用“中心点加半宽半高”而不是“左上角加宽高”来表示矩形是有讲究的当你要围绕中心旋转或者计算对称碰撞时“中心点加半宽半高”的写法明显更不容易出错四方向转向后只需要改朝向变量不用去重算各顶点坐标。4.2 核心代码把防重叠逻辑串起来整个防重叠运动框架在项目里分为几块Tank结构体、CollisionMap类、移动函数库、主循环调用。主循环每帧做的事情非常固定// 主循环内的更新逻辑伪代码 void Update(float deltaTime) { // 1. 收集所有坦克本帧的移动意图 ListMoveIntent intents CollectMoveIntents(); // 2. 按固定优先级排序 intents.SortByPriority(); // 3. 逐个执行带滑动和步进的移动 foreach (var intent in intents) { float dx GetDx(intent.Direction) * intent.Speed * deltaTime; float dy GetDy(intent.Direction) * intent.Speed * deltaTime; MoveWithSlide(intent.Tank, dx, dy, ...); } // 4. 处理子弹与坦克走同一套碰撞规则额外检测命中 UpdateBullets(deltaTime); // 5. 渲染 Render(); }这套分层结构的好处是碰撞规则只存在于TryMove和IntersectsWall这两个函数里其他代码不需要关心碰撞逻辑。无论你往游戏里加什么新角色、新移动模式只要最后都调MoveWithSlide就不可能出现“新增功能绕过防重叠规则”的情况。实际开发里我强烈建议从第一天就把“移动意图收集”和“实际位移执行”分成两步。因为一个完整的帧内可能有多辆坦克同时产生移动意图而它们彼此影响的判定顺序必须在统一的排序环节里定下来。如果每个坦克在自己的Update里直接改坐标防重叠规则就永远是一盘散沙——你会发现在运动顺序上打的补丁一个比一个丑陋。4.3 调试可视化把碰撞体画出来代码写得再认真没有可视化辅助很多碰撞问题依然查不出来。我在调试阶段给每个坦克和子弹都单独绘制了一层半透明的碰撞体矩形并且用颜色区分状态绿色表示本帧成功移动黄色表示发生了滑动红色表示完全被阻挡。这个小小的可视化开关帮我找到了一大批逻辑问题。举个例子坦克顶着墙角的时候如果分离轴尝试的顺序不对你会看到坦克的碰撞体在墙角位置来回抖动颜色在红色和黄色之间反复跳动一眼就能看出是移动顺序或者边界算错了。另一个非常实用的调试技巧是“预测位置绘制”在每帧移动前把目标位置的碰撞体用虚线画出来这样你能直观看到坦克的前进意图以及它是被墙体挡住的还是被其他坦克挡住的。绘制这些辅助图形不需要额外引库DirectX和OpenGL都有基础线段绘制接口Unity里用Debug.DrawRectSDL里就是SDL_RenderDrawRect。关键不是绘制本身而是建立“先看碰撞体再看逻辑”的排查习惯这比任何代码审查都高效。5. 常见问题与排查技巧实录写这套系统的过程中我几乎把能踩的坑都踩了一遍。为了方便后面自己查阅也为了方便读者快速定位问题我把高频问题整理成一张速查表后面的小节再逐个展开讲排坑过程。现象根因排查方向坦克卡在墙角出不来格子范围算错或移动回退逻辑有缺陷打印碰撞状态检查格子范围减一逻辑高速子弹穿墙单帧位移大于墙体厚度引入步进切割限制最大步长坦克互相穿过同帧移动顺序混乱各自独立改坐标统一收集移动意图按固定优先级分步移动两个矩形贴边时判定冲突边界点归属约定不一致统一半开区间约定写好单元测试同屏实体多时卡顿全量两两检测组合爆炸网格分区做粗检测5.1 坦克卡在T型墙角里出不来这是滑动响应最常见的Bug。现象是玩家坦克斜向顶进一个T型墙角松开方向键后发现坦克已经不在活动范围里了或者按下反方向键要好几帧才能挣脱。原因通常有两个一是碰撞体矩形在目标位置检测时把本不该算进来的一格墙算进来了也就是前面提到的减一问题二是分离轴尝试的两个轴向都失败后代码直接把坦克位置改成了“上一帧位置”但这个上一帧位置实际上已经处于重叠状态下一帧继续尝试移动时又被相同逻辑卡住。排查方法是逐帧打印碰撞状态方向、目标位置、命中的格子列表、两个轴向各自的结果。我当时遇到的是第二种情况解决方案是把MoveWithSlide改造成“在任何移动失败时优先回退到本帧开始前的位置并且跳过本帧的位置更新”。另外如果你用网格地图还有一招很管用的预处理在生成地图时把所有可能产生卡位的角落格子标记一个特殊属性调试时直接把这个格子高亮显示出来。别小看这个土办法它比对着代码猜快得多。5.2 子弹高速穿透薄砖墙当子弹速度提到每帧20像素以上后穿透现象开始出现。我一开始以为是碰撞体太小把子弹碰撞体改大了一圈结果更糟——子弹还没碰到墙呢就提前消失了。后来才反应过来问题根本不在碰撞体尺寸而在检测粒度子弹一帧内移动的距离远大于墙体厚度单次检测永远会在墙的左右两侧都看不到墙。解决方案就是我前面写的步进切割。实测下来速度300像素每秒的子弹在60帧率下单帧位移5像素已经小于常见墙体厚度理论上不会穿墙但一旦帧率掉到30单帧位移就变成了10像素薄一点的墙可能就会穿透。所以我的规则是不管当前帧率多少只要单帧位移超过碰撞体最小尺寸的一半就强制步进。这个阈值写进框架之后穿透问题就再也没有出现过。顺带一提处理完子弹穿透之后我还顺手修复了一个相关Bug子弹命中坦克后爆炸特效的播放位置应该是命中点而不是子弹最终停下的位置这两个点在高速度下可能隔了十几像素。5.3 碰撞判断的经典低级错误AND和OR的恩仇写这个框架的时候我统计了一下AABB判断相关的Bug里有接近一半都出在运算符上。Overlaps函数里的逻辑是“两个轴都不分离才返回true”用代码写就是// 错误写法看起来顺理成章实际完全错误 if (a.Right b.Left || a.Left b.Right || a.Bottom b.Top || a.Top b.Bottom) return true;这个错误写法的迷惑性在于把“条件不成立”翻译成了“条件成立”四个不等式少一不注意就全漏了。正确的写法要么像我最开始给的那样把所有分离条件用或连接结果为真就代表不重叠要么用求交集的思路检查左右、上下的重叠区间是否都存在。我的建议是把Overlaps函数单独封装、写好注释、加单元测试别让它散落在业务代码里随手复制。还有一个同类Bug是边界点归属不统一。比如矩形A的右边界是100矩形B的左边界也是100这时它们算不算碰撞如果算那就意味着两个矩形可以刚好贴住但同一像素只能属于一个矩形如果不算坦克之间的缝隙可能会比预期多出一像素。这个问题没有标准答案但对同一个项目必须保持一致。我在项目里的约定是右边界和下边界算开区间即x小于等于other.Left才认为分离这样两个矩形贴边时不会判定为重叠同时杜绝了像素缝隙。5.4 同屏实体变多之后的性能问题坦克大战3.0基础阶段同屏坦克最多七八辆子弹十来发全量两两检测也就是一百多次比较性能完全不是问题。但等到加了不少敌方坦克子弹数量上去之后我还是把性能优化提前做了原因是防患于未然。优化手段是“网格分区”做粗检测broad phase把整个地图按照一定大小划分成格子每帧把所有实体登记到它们所在的格子中两两检测只发生在同一格子或相邻格子内的实体之间。这个思路跟墙体检测的网格化是同源的。实测在20辆坦克、60发子弹的场景下全量两两检测每帧需要做上百的组合比较而网格分区后只需要三四十次收益已经很明显。如果你的实体数量更大还能换用四叉树或者空间哈希不过坦克大战这种规模用网格分区就足够了。这里多说一句做出网格分区之前一定要确保防重叠逻辑本身已经稳定不要在错误的基础上做无谓的性能优化。6. 个人复盘这套方案的边界与扩展方向写到这核心内容基本讲完了最后聊聊我在这套方案里的一些体会以及后续可以怎么扩展。先说边界。这套防重叠方案建立在“AABB加网格地图”的假设上一旦游戏出现旋转体、圆形子弹散射或者高低差地形这套基础逻辑就不够用了。比如你想做那种炮塔可以360度旋转的坦克坦克本体的碰撞体仍然可以是AABB但炮弹的碰撞体如果要做成任意角度的细长矩形就需要引入SAT分离轴定理计算任意凸多边形的相交代码量会上一个台阶。再说扩展。未来如果要加道具系统比如星星、护盾、地雷只需要在碰撞规则里增加“碰撞属性”这个维度每个实体带一个碰撞掩码墙体检测时判断掩码是否包含墙属性坦克间检测时判断阵营是否敌对。这套设计在坦克大战3.0里已经预留了接口我后面考虑加“河流阻挡履带但允许炮弹通过”这种特殊地形也是同样的思路。最后一点也是我最大的体会在写防重叠运动之前我花了整整一周把前两个版本的运动代码全部删掉当时很舍不得。但重构完之后游戏手感反而干净利落了很多。很多看似简单的游戏机制背后的实现细节远比想象中多而把基础系统做干净永远是后面所有玩法开发的地基。如果你也在写类似的2D游戏建议按这篇文章的顺序走一遍先理清栅格地图再写AABB检测然后补滑动和步进最后用可视化调试反复验证边界情况。等你把这些都跑通了再加什么玩法都会顺手很多。