我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Python游戏碰撞检测全攻略:从AABB到空间分区与优化实践

Python游戏碰撞检测全攻略:从AABB到空间分区与优化实践 做游戏时角色穿墙、子弹打不中敌人、明明看到图形重叠却判定为没碰上……这一系列问题根源几乎都指向同一个东西碰撞检测。我在写Python小游戏的头两年里被这四个字折磨得够呛后来才慢慢摸清它涉及的几种算法、边界条件和优化手段。这篇文章会把我做项目过程中沉淀下来的经验完整梳理一遍从最基础的矩形相交判断到圆形检测、空间分区优化再到几个非常容易踩的坑一次讲透。1. 先搞懂碰撞的本质坐标、形状与重叠判断很多刚接触Python游戏开发的人第一步就卡在“怎么判断两个东西碰上了”。这事的本质其实很简单在两个物体占据的二维区域之间找数学上的交集。只要能构造出区域的数学描述剩下就是比较运算。1.1 AABB矩形碰撞游戏里最常用的基础AABBAxis-Aligned Bounding Box轴对齐包围盒是所有碰撞检测算法里最直白、最容易上手的一种。它的核心思想是把游戏里的角色、障碍物、道具都用矩形包起来矩形的四条边都平行于坐标轴然后用边界值来判断是否相交。每个AABB矩形只需要四个数字就能完整描述left左边界x坐标right右边界x坐标top上边界y坐标bottom下边界y坐标矩形A和矩形B发生碰撞的条件是def aabb_collision(a, b): # 两个矩形不相交的条件一个在另一个的左边、右边、上边或下边 if a.right b.left or a.left b.right: return False if a.bottom b.top or a.top b.bottom: return False return True这段代码的原理用生活场景来类比就很好理解两个人并肩站在一起如果甲的最右侧都没碰到乙的最左侧那两人中间一定有缝隙根本没挨上。四个方向的判断都通过说明水平和垂直方向上都有重叠两个矩形才算真正碰上了。我最初写碰撞检测时用的不是这种四个边界值的方式而是用中心坐标加宽高去推结果每次都要做两次除法和加减法代码里还容易出现正负号搞混的情况。后来全部改成left、right、top、bottom四值结构逻辑一下就清爽了。AABB之所以在游戏开发中这么流行就是因为它只涉及四次数值比较性能极高。1.2 圆形碰撞距离判断带来的另一个维度矩形碰撞做不了所有场景。比如场景里的球形障碍物、圆形爆炸范围、扇形弹幕如果强行用矩形包视觉上会非常违和——明明圆和圆之间还有一大块空白的角落却判定为碰撞了。这时候就要换成圆形碰撞检测。圆形之间的碰撞判定甚至比矩形更简单算两个圆心之间的距离如果这个距离小于两个半径之和就是碰上了。import math def circle_collision(c1, c2): dx c1.x - c2.x dy c1.y - c2.y distance_sq dx * dx dy * dy radius_sum c1.r c2.r return distance_sq radius_sum * radius_sum注意我在这里特意用了“距离平方”而不是直接算距离。原因是math.sqrt()开根号是一个相对昂贵的运算在几百个对象同时做碰撞检测时每次多开一次根号累计的性能开销相当可观。而比较距离平方和半径平方数学上是完全等价的却省掉了开根号。这个案例值得记在心里游戏开发里凡是能避免的浮点运算都存在优化空间。碰撞检测函数往往每一帧要调用成百上千次微观上的每次少算一步宏观上就是帧率从40帧到60帧的区别。1.3 坐标系与碰撞方向的陷阱Python游戏库Pygame、Arcade等的屏幕坐标系有一个特点y轴是向下的。也就是左上角是(0,0)y值越大位置越低。这个细节对碰撞检测的影响很大。我见过多个新手拿数学课上学到的“y轴向上”思维去写碰撞结果上下方向永远反着。实际开发中还有一个更隐蔽的问题碰撞后要做位置修正时到底应该往哪个方向弹开这需要我们在碰撞发生时额外判断两个矩形重叠区域的重心相对于运动物体的方位。一个常用的做法是取重叠矩形的宽度和高度哪个小就从哪个方向把物体推出去。比如物体向右移动撞墙重叠区域通常高度远大于宽度那就说明碰撞发生在水平方向应该把物体的x坐标修正回墙的左边。这些位置的细节处理我会在后面的实战章节里专门展开讲。先把这个概念记住碰撞检测只是回答“碰没碰上”而“怎么处理碰撞”才是游戏手感的核心。2. 一个可以跑起来的Pygame碰撞检测示例理论说再多不如一个能亲手跑起来的Demo。这个章节我用Pygame写一个完整的小例子屏幕上有若干个障碍物方块玩家用方向键控制一个方块移动一旦碰到障碍物障碍物会变红并且玩家会被阻挡住不能继续朝那个方向前进。2.1 环境准备Python和Pygame的安装在正式写代码前先把环境搭好。Pygame是目前Python游戏开发里最主流的库之一底层用C语言实现性能比纯Python绘图好得多API也相当亲民。安装过程就两步# 第一步确认Python已安装建议3.8以上版本 python --version # 第二步安装Pygame pip install pygame如果pip提示找不到命令可以换成python -m pip install pygame。国内的网络环境下如果下载速度慢可以在命令后面加上-i https://pypi.tuna.tsinghua.edu.cn/simple使用清华镜像源加速。这里插一句很多人一上来就用Anaconda管理Python环境其实做小游戏项目完全没必要。Anaconda是为数据科学准备的里面打包了大量用不到的科学计算库对游戏开发来说反而造成了环境冗余。直接用系统Python加虚拟环境就够了。# 建虚拟环境Windows、macOS、Linux通用 python -m venv mygame_env # 激活虚拟环境 # Windows: mygame_env\Scripts\activate # macOS/Linux: source mygame_env/bin/activate2.2 核心代码解析移动与碰撞事件下面是我实际项目里写的一个简化版本保留了碰撞检测的核心逻辑。每一处关键代码后面我会紧扣原理做解释。import pygame import sys # 初始化Pygame pygame.init() # 屏幕设置 SCREEN_WIDTH 800 SCREEN_HEIGHT 600 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(碰撞检测演示) clock pygame.time.Clock() # 颜色定义 WHITE (255, 255, 255) BLUE (0, 100, 255) RED (255, 50, 50) GRAY (120, 120, 120) # 玩家矩形用Rect对象存储自带碰撞检测方法 player pygame.Rect(100, 100, 50, 50) player_color BLUE speed 5 # 障碍物列表 obstacles [ pygame.Rect(300, 200, 80, 80), pygame.Rect(550, 400, 100, 100), pygame.Rect(150, 450, 60, 120), ] # 碰撞状态记录每个障碍物当前是否被碰撞 collision_flags [False] * len(obstacles) class Player: def __init__(self, rect): self.rect rect self.color BLUE self.can_move True def handle_input(self, keys): # 先试探性地移动 move_x, move_y 0, 0 if keys[pygame.K_LEFT]: move_x -speed if keys[pygame.K_RIGHT]: move_x speed if keys[pygame.K_UP]: move_y -speed if keys[pygame.K_DOWN]: move_y speed # 移到新位置 new_rect self.rect.move(move_x, move_y) # 边界限定防止冲出屏幕 new_rect.clamp_ip(screen.get_rect()) # 更新 self.rect new_rect def draw(self, surface): pygame.draw.rect(surface, self.color, self.rect) player_obj Player(pygame.Rect(100, 100, 50, 50)) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False keys pygame.key.get_pressed() # 移动玩家 player_obj.handle_input(keys) # 逐帧重置碰撞标志 for i in range(len(collision_flags)): collision_flags[i] False # 逐个检测玩家与障碍物 for i, obstacle in enumerate(obstacles): if player_obj.rect.colliderect(obstacle): collision_flags[i] True # 发生碰撞时把玩家从重叠中推出来 # 原理比较重叠区域的宽高决定以哪个方向修正 dx player_obj.rect.right - obstacle.left dy player_obj.rect.bottom - obstacle.top # 如果dx和dy是同号还是异号根据运动方向做修正 overlap_left player_obj.rect.right - obstacle.left overlap_right obstacle.right - player_obj.rect.left overlap_top player_obj.rect.bottom - obstacle.top overlap_bottom obstacle.bottom - player_obj.rect.top min_x min(overlap_left, overlap_right) min_y min(overlap_top, overlap_bottom) if min_x min_y: # 水平方向弹出 if overlap_left overlap_right: player_obj.rect.right obstacle.left else: player_obj.rect.left obstacle.right else: # 垂直方向弹出 if overlap_top overlap_bottom: player_obj.rect.bottom obstacle.top else: player_obj.rect.top obstacle.bottom else: pass # 没碰撞就不管 # 绘制 screen.fill(WHITE) player_obj.draw(screen) for i, obstacle in enumerate(obstacles): color RED if collision_flags[i] else GRAY pygame.draw.rect(screen, color, obstacle) pygame.display.flip() clock.tick(60) pygame.quit() sys.exit()这段代码里最核心的部分是colliderect方法这是Pygame为Rect对象内置的AABB碰撞检测原理就是我前面写的那个四值比较。接下来是碰撞后的位置修正这段逻辑展开说物体向左移动撞到障碍物时重叠区域中水平方向的重叠量往往比垂直方向的重叠量小。比如玩家方块以5像素的速度向左移动撞上一个静止方块在撞上那一帧两个方块的横向重叠最多是5像素而纵向则可能重叠了整整50像素。这时候比较min_x和min_y发现水平重叠远小于垂直重叠就说明碰撞发生在水平方向应该横向推回。具体推回时我们还要判断是从障碍物左边推还是右边推——看哪边的重叠量更小就说明物体是从那边撞进来的。我在实际项目中用这个逻辑做了几个游戏原型手感整体顺滑原因是它只需要局部数据就能决定推开方向不依赖全局速度方向适合绝大多数2D场景。2.3 效果与扩展方向运行上面的代码你会看到玩家方块可以自由移动碰到灰色障碍物时障碍物变红并且玩家不会穿进去而是被结结实实地“堵”在外面。这个Demo本身没什么游戏性但它是所有2D动作游戏的基础脚手架。基于这个Demo扩展方向非常丰富改成子弹与敌机的碰撞子弹可以是高速运动的细长矩形需要额外考虑高速穿透问题后面专门讲改成拾取系统碰撞后不一定阻止移动而是触发事件比如金币消失、分数增加改成平台跳跃竖直方向的碰撞处理要更细致通常分为“站在平台顶”“头顶撞墙”“侧面撞墙”三种情况我自己拿这个项目练手时还顺手做了一个敌人巡逻的AI让敌人在两个障碍物之间来回走玩家碰到敌人会掉血。这样整个Demo立刻变成了一个可玩性还不错的微型游戏。3. 当场景里的物体变多从O(n²)到空间分区上面的Demo里只有几个障碍物逐对检测完全没问题。但做游戏的人都知道真实的游戏世界不可能只有这么几个物体。子弹十几颗、敌人几十个、粒子系统几百个再加上可破坏的建筑碎片如果每一帧都把所有物体两两配对做检测复杂度是O(n²)当n达到上千时帧率会肉眼可见地崩掉。3.1 为什么暴力遍历会卡假设场景有500个物体每帧需要做的两两配对检测数量是$$ C(500, 2) 500 \times 499 / 2 124750 $$也就是说一帧里要跑大约12万次矩形相交判断。在Pygame这种Python解释器环境下这个量级已经能感受到卡顿了。当物体数量升到2000时这个数字变成接近200万次基本告别实时。实际游戏里物体数量通常没这么夸张但碰撞逻辑往往不止是矩形相交判断还要跟着做位置修正、事件触发、音效播放每个被判定为碰撞的物体对还要再走一遍处理逻辑。所以真正消耗性能的不是判断本身而是判断之后产生的大量事件处理。空间分区的目的就是宁可多花一点计算量在一个粗粒度的分区上也要把精确判断的目标数量大幅降下来。3.2 网格空间分区的实现思路空间分区的实现思路非常朴素把游戏世界划分为大小相等的格子每个格子维护一个列表记录里面有哪些物体。检测碰撞时只取物体所在格子以及相邻格子里的物体来做精确判断。这一步分两个阶段粗检测阶段把物体归入格子也可以用指针引用避免复制精检测阶段对同一格内的物体做碰撞检测格子的尺寸选择有技巧。格子太小每个格子里物体少但跨格子的物体要同时属于多个格子维护成本上升格子太大每个格子里塞的物体太多退化回暴力遍历。我的经验是把格子尺寸设为场景里典型物体尺寸的2到4倍。这样平均每个格子里的物体数量维持在个位数效果最理想。class SpatialGrid: def __init__(self, cell_size, world_width, world_height): self.cell_size cell_size self.cols world_width // cell_size 1 self.rows world_height // cell_size 1 self.grid {} def _key(self, col, row): return (col, row) def clear(self): self.grid.clear() def insert(self, rect): # 物体可能横跨多个格子需要全部插入 left_col rect.left // self.cell_size right_col rect.right // self.cell_size top_row rect.top // self.cell_size bottom_row rect.bottom // self.cell_size for col in range(left_col, right_col 1): for row in range(top_row, bottom_row 1): key self._key(col, row) if key not in self.grid: self.grid[key] [] self.grid[key].append(rect) def get_neighbors(self, rect): left_col rect.left // self.cell_size - 1 right_col rect.right // self.cell_size 1 top_row rect.top // self.cell_size - 1 bottom_row rect.bottom // self.cell_size 1 result [] for col in range(left_col, right_col 1): for row in range(top_row, bottom_row 1): key self._key(col, row) if key in self.grid: result.extend(self.grid[key]) return result这里我用了字典而不是二维数组来存储格子原因是游戏世界里很多格子完全没物体用字典天然避免了为空白格子分配内存的问题。3.3 实测为什么这个方法值得用我自己写过一个测试实验500个矩形在屏幕内随机运动每帧做碰撞检测。用暴力遍历和用网格分区两种方式对比暴力遍历大约每帧耗时30毫秒网格分区只需要2到3毫秒性能提升了十倍左右。关键是网格分区写起来也没多复杂20行代码而已。这个优化对小游戏来说也许不是刚需但它是构建大型场景的基础能力。做游戏开发脑子里永远要有这样一笔账这个操作的调用量是多少能不能在更高层级先剔除掉明确的“非碰撞”对网格分区就是经典的“先粗后精”策略。4. 我踩过的碰撞坑穿透、卡墙与抖动修正前两章讲的是“正常怎么做”这一章讲的是“不正常的做法会带来什么后果”。碰撞检测作为游戏逻辑里最容易被忽略又最影响手感的部分坑是真的多。这些都是我做项目时一次一次踩出来的写下来希望你能绕过去。4.1 高速穿透当移动步长大于物体尺寸这是个物理概念但在游戏里同样存在。想象一个速度很快的子弹一帧移动20像素而一个障碍物只有10像素厚。碰撞检测的执行顺序通常是先移动到新位置再检测碰撞。那么子弹会直接从障碍物“头顶”飞过去完全检测不到碰撞。这是因为碰撞检测的采样是离散的它只能看到这一帧的瞬间位置看不到两帧之间的运动路径。这就好比你用照相机拍一个快速飞行的球只要快门够慢拍到的每一帧里球都在不同位置甚至可能完全错过一个较窄的柱子。解决方案有好几种各有适用场景步长细分法把一帧的大步长拆成多个小步长比如一次走20像素就拆成4次每次5像素走完一次就检测一次。这个方法直观但成本高速度越快拆得越多。连续碰撞检测用射线或扫掠矩形从旧位置到新位置画一个覆盖路线的矩形来做碰撞。Pygame的pygame.Rect没有直接支持这个但原理不难自己实现。锁定帧率上限把速度限制在每帧最大不超过最小物体尺寸的1/3用填表的方式保证稳定。这个方法适合对物理精度要求不高的游戏。实际项目中我常把第一种方法和第三种方法结合起来限制最大速度同时对高速物体做两三次细分。既能保证不穿透性能也基本可控。4.2 卡墙与抖动碰撞后修正的顺序问题卡墙这个现象玩过2D游戏的人应该都遇到过角色被卡在墙边怎么按方向键都纹丝不动或者轻微按一下就疯狂抖动。抖动产生的原因是玩家撞上墙之后碰撞检测逻辑把玩家推回墙外——但推回后的位置仍然和墙重叠了一个像素于是下一帧又检测到碰撞再次推回形成了一个每帧都在“推回去”但方向冲突的循环。这个循环表现在渲染上就是角色在墙边以极高频抖动。解决这个问题的关键是要把碰撞后的位置修正做得彻底而不是“差不多推回去就行”。我调试时发现最常见的抖源其实是同时对多个方向做了碰撞修正。比如玩家同时按住右和上角色斜向运动撞到墙角系统先水平推开又垂直推开两个修正互相打架。一个实用的修正原则是一次帧内最多只处理一个方向的推开。先判断玩家主要是水平运动还是垂直运动比较位移量然后优先修正主要运动方向上的碰撞另一个方向等下一帧再处理。这样做会有极轻微的“碰墙时微微停在角落”的视觉瑕疵但游戏手感完全消除了抖动。另一个容易忽视的问题是推开碰撞时使用了player.rect.right obstacle.left这样的直接赋值但下一帧玩家仍然会尝试向右移动于是又撞上。如果你希望角色在撞墙后有一小段“贴墙减速”效果可以在检测到碰撞时把速度向量的对应分量清为零# 伪代码水平方向碰撞后清零水平速度 if collided_horizontally: velocity_x 0这从根本上解决了“每帧都在撞墙”的问题。4.3 四方向检测与死角处理在平台跳跃类游戏里碰撞检测通常是分方向的——玩家站在平台顶上、头顶撞到砖块、左右撞到墙壁三种情况处理逻辑完全不同。绝大多数实现方式是把物体拆成水平和垂直两步来移动先把物体在x轴上移动检测水平方向的碰撞做水平修正再把物体在y轴上移动检测垂直方向的碰撞做垂直修正这个顺序很重要永远不要一步到位同时移动x和y后再做碰撞检测。因为那样你无法区分碰撞发生在哪个方向修正起来只能靠重叠宽高去猜容易出bug。分成两步之后还有一个经典死角为什么站在平台边缘时明明人的一半悬空却不会掉下去这是因为碰撞体通常是矩形矩形的最底边还搭在平台上。很多游戏为了解决“站在悬崖边不掉”的违和感还会额外做边缘检测或者把碰撞体宽度缩窄模拟出角色体积比视觉形象略窄的效果。我做平台跳跃时特别喜欢用一个小技巧把玩家的碰撞矩形宽度设置为视觉宽度的70%底部对齐不变。这样既不会让玩家觉得碰撞区域卡顿又能在视觉上容忍一部分身体探出平台。5. 进阶方向像素级碰撞与多边形碰撞体AABB和圆形碰撞已经能解决大部分2D游戏的需求但总有一些场景它们力不从心。比如两个箭头形的物体AABB矩形包出来会有一个很大的空白直角区域明明视觉上没碰到却被判定为碰撞了。这时候就需要更精细的碰撞检测方案。5.1 像素级碰撞检测用掩码算精确相交像素级碰撞Pixel Perfect Collision的思路是把每个物体的图像作为一张二值掩码——有像素的位置记为1透明的位置记为0。两个物体发生矩形重叠时逐像素比较对应位置的掩码值如果同一位置上两个掩码都是1就说明发生了真正的像素级重叠。这个算法的优点不言而喻精度极高能感知到图形轮廓的细微接触。缺点是性能开销很大逐像素比较在物体多或图形大的场景下会拖慢帧率。因此实践中的做法几乎永远是两阶段检测先做AABB粗检测绝大部分碰撞对在这一层就被滤掉了只有AABB重叠的物体对才进入像素级精检测Pygame里自带掩码模块实现起来不算复杂import pygame # 假设surface1和surface2是两张带透明通道的图像 mask1 pygame.mask.from_surface(surface1) mask2 pygame.mask.from_surface(surface2) # 先获取两个矩形的位置 rect1 surface1.get_rect(topleft(x1, y1)) rect2 surface2.get_rect(topleft(x2, y2)) # 计算偏移量 offset_x rect2.x - rect1.x offset_y rect2.y - rect1.y # 精确碰撞检测 if mask1.overlap(mask2, (offset_x, offset_y)): print(像素级碰撞发生了)mask.overlap这个方法的内部实现专门做了优化。它不会傻乎乎逐像素遍历整个图形而是从两个掩码的边界交叉区域开始扫描多数情况下只需要比较少量像素就能判断出结果。但即使这样像素级检测的花费仍然显著高于AABB一般用在子弹打Boss、拾取物与角色的精细交互这类低频但高精度的场景中。5.2 组合碰撞体复杂物体的低成本近似复杂的游戏物体比如一辆卡车、一架飞机、一条龙如果用一个单独的AABB矩形去包精度太差如果用像素级碰撞性能又扛不住。折中的方案是组合碰撞体用一个物体定义多个碰撞组件组件之间用圆、矩形或椭圆组合任何一个组件检测到碰撞就视为物体发生了碰撞。这种方式几乎完全模拟了视觉轮廓性能也不错。写一个碰撞筛选逻辑时经常需要标识出同一个物体下的多个碰撞体都归属于谁。做法是在碰撞检测函数中优先做组件归属排查——先快速找出哪些碰撞体分别归属哪个物体再去查归属物体的逻辑。这个分离对性能帮助非常大。我自己写坦克大战类游戏时坦克由车体和炮管组成我用两个矩形组合车体是主碰撞体炮管窄而长单独一个矩形。炮管可以越过车体边缘探出去被子弹打中一样判定为坦克受损。这种结构如果只用像素级检测性能又要掉一大截。5.3 向量与速度方向的进阶玩法到这里碰撞检测其实还没结束。真正让游戏“活起来”的是碰撞之后的物理响应反弹、摩擦、弹性、旋转。这些内容属于物理模拟范畴但经常和碰撞检测纠缠在一起简单提一下反弹根据碰撞法线方向把速度向量沿法线做反射变换。这个可以用向量运算轻松实现。速度传递移动平台给玩家一个附加速度玩家站在平台上前进时不被拖拽。碰撞摩擦碰撞后沿碰撞面切向方向的速度分量乘以一个摩擦系数让物体在地面上逐渐停下。如果这些需求变大建议直接引入现成的物理引擎。Python生态里Pymunk是Box2D的Python绑定接口简洁文档齐全。我在做物理类小游戏时用Pymunk处理刚体、碰撞响应和关节自己在上面管事件逻辑体验很好。它内部做了大量碰撞检测优化而且支持圆形、线段、多边形等多种碰撞体比从零写一套物理系统稳定得多。6. 实战中的经验总结与调试技巧最后这部分把散落在各章里的实战经验统一收一下再补充几个调试碰撞检测时的独家技巧。6.1 调试碰撞检测的“可视化”三板斧碰撞检测最难的是定位问题到底是检测逻辑写错了还是物体的位置数据不对我的习惯是调试时把碰撞相关的所有信息直接画在屏幕上。这个方法看起来原始却是我用过效率最高的手段。具体做法有三步画出碰撞体轮廓所有参与碰撞的矩形、圆形用高亮颜色描边和视觉贴图区分开。这样你能立刻看出来碰撞体的实际位置是否和贴图对齐。标记碰撞事件发生碰撞的物体把轮廓色变成醒目的红色并持续几帧。这样碰撞触发频率一目了然之前遇到过“每帧都在碰撞但视觉上看不出来”的诡异问题就是这么定位的。显示速度向量在每个物体中心画一条短线表示当前速度方向和大小。很多不自然的“卡顿”或“穿透”本质是速度向量不对看到向量就明白了。我调试碰撞时几乎从来不用print输出坐标值——屏幕上画出来的一帧数据比终端里刷屏的数字直观一万倍。6.2 分层碰撞用碰撞层管理复杂交互当游戏里的可交互对象变多两个物体是否发生碰撞往往不是物理上允许不允许而是游戏规则上允许不允许。子弹可以打敌人但不能打玩家自己玩家可以和地面碰撞但不能和弹幕碰撞……这时候给碰撞检测引入“层”的概念很有必要。每一类物体分配一个碰撞层并维护一张层与层之间“是否检测”的关系表代码结构就干净多了。# 简单定义几个碰撞层 PLAYER 1 ENEMY 2 BULLET 3 TERRAIN 4 # 定义层间检测关系 collision_matrix { (PLAYER, TERRAIN): True, # 玩家与地面检测 (PLAYER, ENEMY): True, # 玩家与敌人检测 (BULLET, ENEMY): True, # 子弹与敌人检测 (BULLET, TERRAIN): True, # 子弹与墙壁检测 (BULLET, PLAYER): False, # 子弹不与自己人碰撞 (ENEMY, ENEMY): False, # 敌人之间重叠不处理 } def should_collide(a_layer, b_layer): key (a_layer, b_layer) return collision_matrix.get(key, collision_matrix.get((b_layer, a_layer), False))这样的设计让碰撞规则的新增和维护变成了改表而不是在代码里处处写if判断。我在一个相对复杂的射击游戏项目里用这套方案后新增敌人类型时完全不需要改动碰撞检测主逻辑只需要在新敌人的初始化函数里注册所在层就行。6.3 性能预算为碰撞检测设一个预期做了几年游戏以后最深刻的体会之一是不要等到卡顿才去优化。应该在项目一开始就明确性能预算。“这个游戏物体的数量上限会是几百个”“碰撞检测阶段最多允许耗时3毫秒”——带着这些指标去设计碰撞策略很多架构决策会变得清晰。比如知道物体数上限是200那暴力遍历每帧需要计算约2万次矩形相交在Python中大约耗时几毫秒。这个数字还在可以接受的范围内那就不必一上来就写网格分区这种优化方案——YAGNI原则你不会需要它You Arent Gonna Need It在游戏开发里同样适用。但如果目标是做成千上万个粒子的弹幕效果那就必须从一开始规划空间分区或网格优化。最后再分享一个小技巧写碰撞检测函数时尽量保证它是“纯函数”——输入两个物体的位置信息输出是否碰撞的布尔结果。不要在碰撞检测函数内部直接修改物体的位置。这一条原则让我避开了大量难以追踪的bug。碰撞检测只负责判断位置修正交给单独的逻辑层各自职责清晰代码也容易测试。我用这套体系先后完成了好几个小游戏平台跳跃、射击、迷宫探索每到一个新项目碰撞检测部分基本都是直接复用并稍作调整。花在这一块上的时间换来的是游戏手感稳定和调试效率大幅提升这笔投入非常划算。如果你正在做Python游戏建议你按上面的顺序先写出一个最小可运行版本然后把调试可视化那三板斧加上去很快就能摸清门道。
返回列表