恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python游戏开发必知:碰撞检测原理与优化实践
首页
资讯中心
/
Python游戏开发必知:碰撞检测原理与优化实践
Python游戏开发必知:碰撞检测原理与优化实践
发布时间:2026/10/14 4:07:59
从“弹一下球”到“做一个能玩的游戏”碰撞检测几乎是绕不过去的一道坎。我自己早年用Python写小游戏时最初以为“碰到没碰到”就是比一比坐标结果真上手才发现里面有矩形相交、圆形距离、像素遮罩、高速穿透、空间分区一堆讲究。这篇东西就是我在实际项目里反复踩坑之后整理出来的经验适合刚用Python写过几个小Demo、想把碰撞检测做得更扎实的人也适合那些总觉得“明明写了检测却不生效”的朋友。我会把原理讲清楚代码给完整怎么调、怎么查、怎么优化也一并聊透。1. 再谈碰撞检测不只是“碰到没碰到”1.1 碰撞检测在游戏里的地位碰撞检测表面上是一个“判断两个物体是否相交”的问题但它在游戏里承担的东西远比“相交”两个字要复杂。它决定了角色能不能穿过墙壁、子弹能不能命中敌人、玩家有没有踩到陷阱、道具是否可以被拾取。可以说除了渲染和输入碰撞检测就是游戏逻辑里最核心的一块基石。我在做一个俯视角射击原型的时候最先写的不是射击逻辑反而是碰撞检测的框架。原因是所有其他系统都要依赖它。敌人要能挡路子弹要能判定命中玩家的移动要受地图边界限制。如果碰撞检测不可靠后面所有逻辑都像盖在流沙上的房子。而如果你用的是像Pygame这种库它本身就内置了一部分碰撞检测但很多人只会用最简单的矩形相交遇到特殊形状、高速运动、大量物体同时碰撞的场景就不知道怎么办了。我建议你先把碰撞检测理解成一个“精度与性能的权衡问题”。你要检测的物体是什么形状、移动速度多快、场景里同时有多少个物体需要互相检测这三个变量决定了你该选哪一套方案。没有万能的碰撞检测只有当前场景下最合适的碰撞检测。1.2 三种主流碰撞体的取舍思路在2D游戏里最常见的碰撞体有三种轴对齐包围盒AABB也就是矩形、圆形、像素遮罩。它们的精度和性能是阶梯式递增的理解清楚了选型就不会错。先说AABB。这是Pygame里pygame.Rect默认使用的碰撞方式也是绝大多数2D游戏的基础。它的核心特征是矩形永远不旋转各边永远和坐标系轴平行。这让相交判断变得非常简单——两个矩形相交当且仅当它们在x轴上的投影区间有重叠同时在y轴上的投影区间也有重叠。这种检测速度极快每帧几千个Rect互相检测都不成问题。但缺点是它无法贴合非矩形物体。比如一个圆形的敌人用矩形框住它四个角会有明显的“空判定区”玩家明明没碰到圆却因为你碰了矩形的角而被判定为命中体验会相当差。再说圆形碰撞体。判断两个圆是否相交只需要计算两圆心距离是否小于等于两圆半径之和。这个算法比矩形稍微多一点计算量涉及一次开平方但整体还是很快。圆形碰撞体非常适合用来描述角色、子弹、小球这类形状接近圆形的物体。它没有“空角”问题视觉上一对一贴合。最后是像素级碰撞。Pygame里通过Mask实现可以精确到每个像素。它适合形状非常不规则的物体或者需要精确判定是否命中特定区域的场景。但性能开销也是最大的除非场景物体数量很少否则不建议每帧对所有物体做像素级检测。把三者放在一起看是一条从“快但粗糙”到“慢但精细”的光谱。我见过不少新手一上来就用像素级碰撞结果帧率掉了一半这就是没有做选型权衡的典型表现。碰撞体类型判定方式性能开销适用场景典型缺陷AABB矩形投影区间重叠极快大多数2D游戏的基础碰撞不贴合非矩形物体圆形圆心距离与半径和比较快角色、子弹、球体无法描述长方形物体Mask像素逐像素扫描慢精确判定、形状不规则物体大量物体时性能堪忧2. 基础碰撞检测的落地写法2.1 Pygame里的矩形碰撞Pygame的pygame.Rect是自带的碰撞检测工具强烈建议熟练掌握它比很多人想象中能干。它的坐标系原点在左上角x轴向右y轴向下Rect本身同时记录了左上角坐标、宽度和高度并提供了一堆现成的碰撞检测方法。最常用的是colliderect判断两个矩形是否相交import pygame rect_a pygame.Rect(10, 10, 50, 50) rect_b pygame.Rect(40, 40, 50, 50) if rect_a.colliderect(rect_b): print(矩形相交了) else: print(没有相交)这个写法非常简单但很多人不知道Rect还提供了另外几个有用的方法。collidepoint可以判断一个点是否在矩形内部适合做鼠标点击检测collidelist可以拿一个Rect去和一组Rect逐一比较返回第一个碰撞到的索引找不到返回-1collidelistall则返回所有碰撞的索引列表。这几兄弟配合起来非常方便。obstacles [pygame.Rect(100, 100, 30, 30), pygame.Rect(200, 200, 40, 40)] player pygame.Rect(120, 120, 20, 20) # 拿到第一个碰撞的障碍物索引 hit_index player.collidelist(obstacles) if hit_index ! -1: print(f碰到了一个障碍物索引为{hit_index}) # 拿到所有碰撞的障碍物索引 hit_all player.collidelistall(obstacles) print(f共碰撞了{len(hit_all)}个障碍物)我自己在实际项目里的习惯是所有需要参与碰撞的对象不管视觉素材多花哨内部都维护一个Rect属性作为碰撞体。例如一个角色类它可能有一个self.image用于渲染还有一个self.rect用于碰撞。这样写的好处是后续所有逻辑都可以统一围绕rect进行计算不用每次单独取坐标、比对尺寸。有一点必须提醒Rect的坐标是整数。如果你做的是平滑移动物体坐标可能带小数直接用Rect存储位置会有精度损失。常见解法是单独用一个浮点坐标self.x和self.y维护真实位置每帧把它转成Rect再做渲染和碰撞。我自己踩过这个坑角色移动一会儿就出现一顿一顿的感觉排查半天才发现是Rect把坐标取整了。2.2 圆形碰撞与核心推导圆形碰撞体在Pygame里没有内置函数但自己实现起来非常容易。原理是基于欧几里得距离公式两个圆的圆心分别是(x1, y1)和(x2, y2)半径分别是r1和r2。当两圆不相交时圆心距离一定大于半径之和相交或相切时圆心距离小于等于半径之和。import math def check_circle_collision(x1, y1, r1, x2, y2, r2): # 计算圆心距离的平方 distance_sq (x1 - x2) ** 2 (y1 - y2) ** 2 radius_sum r1 r2 # 比较距离平方和半径和的平方避免开平方 return distance_sq radius_sum ** 2这里有一个很关键的性能小技巧比较距离时不要直接开平方。因为比较sqrt(distance_sq)和radius_sum的大小关系与比较distance_sq和radius_sum ** 2的大小关系完全一致而开平方的代价比乘法大得多。在几百个圆形物体同时碰撞的场景下这个小优化能让帧率有明显差异。圆形碰撞体非常适合描述玩家和敌人。我做过一个弹幕类小游戏玩家是一个八方向平滑移动的小飞机机身形状接近椭圆用矩形判定会有明显的冤枉判定最后改用圆形碰撞体手感立刻对了。这里还有一个小细节如果物体形状是椭圆而非正圆可以用两个半径不同的圆叠加或者把碰撞判断写成“圆心距离的平方与两个半轴长度平方的加权比较”但一般情况下一个正圆就够用了。Pygame的Sprite类在2.0版本之后提供了pygame.sprite.collide_circle函数如果你用的是精灵组可以直接传这个函数作为碰撞回调它内部会自动从精灵的rect里提取圆心和半径。但需要注意它默认把半径取为rect宽度的一半如果你的精灵不是正方形这个默认半径可能不准需要自己在精灵类里重写radius属性。2.3 像素级碰撞什么时候才用得上像素级碰撞是2D碰撞检测的终极武器它把精度推到了像素级别。Pygame里用pygame.mask来实现。它可以把一个Surface对象转换成一个Mask对象Mask记录的是表面中哪些像素是不透明的。然后通过两个Mask的overlap方法来检测是否有重叠区域并返回重叠的坐标。import pygame def pixel_collision(surface_a, rect_a, surface_b, rect_b): # 从Surface创建Mask mask_a pygame.mask.from_surface(surface_a) mask_b pygame.mask.from_surface(surface_b) # offset是b相对于a的位置偏移 offset (rect_b.x - rect_a.x, rect_b.y - rect_a.y) # 返回None表示没有重叠 overlap_point mask_a.overlap(mask_b, offset) return overlap_point is not None需要注意from_surface这个函数是有开销的不要每帧都去创建Mask否则性能会很容易崩。常见的做法是在加载图片时就预先创建Mask并缓存起来。像素级碰撞也有升级玩法。比如实现一个精确但高效的判断先用Rect做一次粗检测如果矩形都没相交那肯定没碰撞直接跳过只有矩形相交了再去做像素级检测。这样就避免了大部分无谓的像素扫描。什么时候才用像素级碰撞我觉得是“必须精确到形状”的时候。比如游戏里有一把弯刀刀刃是弯曲的用矩形去框会产生很大误差玩家会觉得自己明明躲开了刀刃却被砍中。这种场景下用Mask来做精确判定就很有必要。但如果你的游戏物体都是方块、球体、胶囊形状用AABB和圆形基本就够了不要为了一点点精度牺牲大量性能。我曾接过一个项目里面有一个体型特别奇怪的Boss是一个带锯齿触角的怪物玩家子弹要能精准判定是否命中触角还是命中身体。最后就是先用矩形碰撞过滤大部分子弹再把确实接近的子弹和Boss的Mask做精确比较实测性能和精度都很理想。3. 让碰撞检测“跑得快”空间划分3.1 网格法把地图切块当你场景里的物体数量多起来后最朴素的全员相互遍历会迅速变成性能瓶颈。假设场景里同时有500个物体两两检测意味着大约12万次比较每帧12万次换算到每秒60帧就是700多万次。哪怕每次比较都很快总时间也相当可观。空间划分就是用来解决这个问题的。网格法的思路非常朴素把游戏地图按照固定大小切割成格子每个格子记录落在其中的物体列表。每一帧更新时重新把物体分配进格子。检测碰撞时只对“处于同一个格子或相邻格子”的物体两两检测其他物体直接忽略。class SpatialGrid: def __init__(self, cell_size, map_width, map_height): self.cell_size cell_size self.cols map_width // cell_size 1 self.rows map_height // cell_size 1 self.cells {} def clear(self): self.cells.clear() def add_object(self, obj): # 计算物体占据哪些格子 left obj.rect.left // self.cell_size top obj.rect.top // self.cell_size right obj.rect.right // self.cell_size bottom obj.rect.bottom // self.cell_size for row in range(top, bottom 1): for col in range(left, right 1): if row 0 and row self.rows and col 0 and col self.cols: key (col, row) self.cells.setdefault(key, []).append(obj) def get_nearby_objects(self, obj): left obj.rect.left // self.cell_size top obj.rect.top // self.cell_size right obj.rect.right // self.cell_size bottom obj.rect.bottom // self.cell_size nearby set() for row in range(top - 1, bottom 2): for col in range(left - 1, right 2): if row 0 and row self.rows and col 0 and col self.cols: for candidate in self.cells.get((col, row), []): if candidate is not obj: nearby.add(candidate) return nearby这个实现里比较关键的一步是get_nearby_objects在获取自己所在格子时往外多取了一圈。原因是一个物体可能刚好在格子边界上另一个物体在相邻格子里两者肉眼可见会发生碰撞但如果不查相邻格子就直接漏掉了。很多人写网格法第一次就栽在这里。网格尺寸的选择也有讲究。格子太大比如地图的宽度直接作为一个格子空间划分就退化成全员遍历格子太小物体可能同时出现在几十个格子里维护开销急剧增加。我通常把格子大小设置为常见碰撞体尺寸的两倍到四倍比如角色大小为32x32格子设成64x64或128x128实测效果比较平衡。3.2 四叉树与更进阶的取舍四叉树是网格法的一种动态优化。它不需要提前把地图切成均匀的格子而是根据物体分布动态划分区域当一个区域里的物体数量超过阈值就把这个区域分裂成四个子区域物体被继续分配到子区域里。这样做的优势是物体密集的区域划分细物体稀疏的区域划分粗空间利用率更高。四叉树的实现比网格法复杂不少需要处理插入、分裂、查询、合并一系列操作。我在多数项目里其实直接用网格法就够了。四叉树真正能发挥优势的场景是物体分布极度不均比如某个角落挤满了上百个物体其他地方空荡荡。网格法在这种情况下会在密集区域产生大量重复的网格分配而四叉树能自适应地处理。从工程角度我建议如果你只是做一个小游戏物体数量在几百以内连空间划分都不用做直接全员遍历完全可以接受。只有当物体会超过几百个并且帧率出现明显下降时再上网格法。四叉树可以作为后续优化手段不必一开始就套上去。与其追求高级算法不如保证检测逻辑本身准确、稳定、易调试。我用网格法做过一个两千多个粒子互相碰撞的模拟项目在普通电脑上也能稳定跑到60帧完全够用。4. 常见问题与排查实录4.1 子弹穿透与隧穿效应隧穿效应是2D游戏中最经典的碰撞异常。子弹速度非常快时上一帧还在墙壁左侧下一帧已经跑到墙壁右侧连续两帧都没有检测到相交于是子弹直接穿墙而过。这个问题我在做射击类原型的时候真的被困扰过很久。用矩形碰撞举例子墙的x范围是100到150子弹每帧向右移动20像素。帧1子弹位置在110和墙重叠帧2子弹位置在170已经越过墙体。如果某一帧移动过多可能恰好跳过重叠区间。解决方案是扫掠检测Swept Collision Detection。原理是不要把子弹当成一个点或矩形直接与墙体比较而是把子弹从当前位置到目标位置的“移动轨迹”浓缩成一个扫掠体再用这个扫掠体去与墙比较。有一个简单的近似做法假设墙是静止的把子弹的旧位置和新位置连线看成一条线段去检测这条线段是否与墙壁矩形相交。Pygame里可以用pygame.Rect.clipline方法它能直接判断线段与矩形是否相交并返回交点。import pygame def check_swept_collision(bullet_rect_old, new_x, new_y, wall_rect): # 子弹的起点和终点 start (bullet_rect_old.centerx, bullet_rect_old.centery) end (new_x, new_y) # clipline返回None表示无线段在矩形内返回线段端点表示有碰撞 clipped_line wall_rect.clipline(start, end) if clipped_line: print(子弹轨迹穿过了墙) return True return False需要注意这个方案是“子弹够小可以被近似成点”时才准确。如果子弹本身是一个大矩形更准确的做法是做Minkowski和把墙体矩形沿子弹运动方向膨胀然后用膨胀后的矩形去检测一个点或者把子弹移动到目标位置后的最终矩形与墙体做“扫掠矩形相交”计算。对于大多数小游戏clipline已经足够用了。4.2 碰撞响应抖动与堆叠比“检测不到碰撞”更烦人的是“检测到了但处理得很糟糕”。经典场景是玩家角色落到地面的一瞬间画面开始抖动或者角色被卡进墙里反复弹跳。抖动的原因通常是检测和响应放在了同一帧且响应过于机械。比如玩家向下移动时你检测到与地面矩形相交就把玩家往上一顶顶回没碰撞的位置。如果玩家这一帧移动了5像素而顶回去只顶了3像素下一帧又检测到相交再顶回3像素角色就会在视觉上轻微上下抖动。堆叠问题出现在多个物体同时互相挤压的场景处理顺序不同结果也不同。我的做法是分离“运动更新”和“碰撞响应”。每帧先根据玩家输入更新位置然后用一个统一的函数去处理所有碰撞而不是在移动函数里零散地响应碰撞。碰撞响应本身也遵循一个原则先修正“位置”再更新“速度”。对于角色和地面的情况可以用“最小平移向量”的思路算出矩形相交后找到要把玩家推离所需的最小方向和距离一次性修正位置。还有一个容易被忽略的坑不要在处理碰撞的过程中修改被遍历的列表。假如你一边遍历所有敌人一边检查碰撞如果碰撞时把敌人从列表中删除了遍历就会出错可能会跳过某些敌人或者导致索引越界。正确做法是先收集要删除的物体遍历结束之后再统一删除。4.3 调试技巧可视化永远是第一位的碰撞检测是“看不见摸不着”的逻辑出了问题非常难直接定位。我的第一条调试经验是把碰撞体绘制出来。Pygame里可以用pygame.draw.rect或pygame.draw.circle把物体的碰撞体形状画在屏幕上仔细观察碰撞体是否和视觉素材匹配。如果你发现角色明明图像还离墙很远但已经被判定为碰撞大概率是碰撞体比图像大。把碰撞体画出来一眼就能看出来。我见过很多项目角色图像已经缩小了但rect没有跟着缩导致玩家面对一大片看不见的空气墙这种问题没有可视化调试是极难发现的。第二条经验是在关键碰撞事件里加一行日志输出但要注意控制频率。比如每帧检测都输出会很刷屏可以使用一个计数器每60帧输出一次或者在碰撞发生时只输出一次。日志里要包含碰撞双方的标识、位置、矩形信息把时间线和空间关系串起来。第三条经验是用颜色区分碰撞状态。正常状态下碰撞体是绿色的发生碰撞时变成红色。这样你在游戏运行过程中瞟一眼屏幕就知道哪些地方在产生碰撞哪些地方没有。实测这个方法对排查“明明该碰撞却没反应”的问题非常有效。我还习惯在项目里保留一个“调试开关”在开发时打开可视化发布前关掉。比如一个DEBUG_COLLISION全局变量为True时额外绘制碰撞体、显示FPS和物体数量。这些看似微不足道的小工具在游戏开发的后期调试中能省下大量时间。我个人体感碰撞检测这件事最怕的不是不懂算法而是“不知道为什么不对”。算法原理翻文档都能看懂但工程里的坑永远是热乎乎的Rect的整数坐标、Mask创建开销、遍历时修改列表、高速物体的隧穿。把这些坑提前排掉你的游戏手感才会真正扎实起来。如果你正在做一个Python小游戏不妨先做一个小场景一个玩家、一堵墙、一颗高速子弹把本文里提到的矩形检测、圆形检测、扫掠检测和可视化调试全部过一遍。这套流程走下来你后续再写更大规模的碰撞逻辑都会有底。