
我最近用 Python 做了一个 2D 平台跳跃小游戏项目的核心部分正好就是标题里写的“Python游戏中的碰撞检测实现”。说实话碰撞检测听起来是个入门级概念但真正把它做好能把一个小项目的完成度拉高一大截角色踩没踩到砖块、子弹有没有命中敌人、金币吃没吃到、敌人会不会穿墙全部要依靠“两个物体是否重叠”的判断来撑。这篇文章会从零讲清楚几种主流碰撞检测方案的原理、Pygame 里的具体写法以及我在实际调试中踩过的坑。适合正在用 Python 做小游戏、或者刚入门想弄懂碰撞检测底层逻辑的读者尤其是被“物体乱穿”折磨过的新手这篇应该能帮上忙。1. 项目概述与整体设计思路1.1 碰撞检测在游戏里到底解决什么问题先说一个认知层面的东西碰撞检测不只是在问“撞没撞”它还要回答“撞上之后怎么办”。只做检测不做响应你会看到角色和障碍物重叠在一起却没有反馈感觉就像物体互相穿过。完整方案通常分成两步第一步做几何相交判定第二步做碰撞响应比如推挤、扣血、得分、播放音效。我用一个生活化类比帮你理解你伸手去拿桌上的杯子眼睛看到手和杯柄接触这是检测手指顺势握住杯子这是响应。游戏里每一帧都在模拟这套过程而且还要跟帧率、精度、性能扯上关系。游戏越复杂碰撞检测的边界情况越多这也是为什么很多资深开发者会说碰撞系统写得好不好直接影响游戏手感和玩家评价。1.2 为什么这个项目选择 Python 而不是其他方案选择 Python 来做碰撞检测实验不是因为它在性能上有多强而是因为它能让你把注意力集中在“算法本身”而不是“工程细节”上。Pygame 内置了 Rect、Sprite、Group、mask 这些数据结构写碰撞逻辑非常顺手几分钟就能把一段可运行的检测代码跑出来。对于教学、原型验证、参赛 Demo、独立小游戏来说Python 的效率优势很值。但你也得认清它的短板Python 是解释型语言逐帧做大量像素级检测时会有性能瓶颈。解决办法不是回避而是用分层策略先用廉价算法做大范围粗筛再用昂贵算法做小范围精测。这也是很多游戏引擎内部的做法只不过引擎帮你封装好了。本文后面会有大量这种思路的具体实现。1.3 主流的碰撞检测方案怎么选在动手写代码之前我建议先有一张“选型图谱”明确不同方案适合什么对象。方案适用对象精度性能实现难度AABB 矩形方块、角色、贴图矩形中等极高低圆形检测球、圆饼、大体轮廓中等极高低像素 Mask不规则精灵、透明贴图高中中SAT 多边形凸多边形高中高物理引擎复杂场景、多刚体高中集成成本高选型不是越精确越好因为精度越高通常意味着性能开销越大。好的工程做法是“层级式碰撞检测”先用最便宜的 AABB 把所有候选对象过一遍排除掉绝大多数不可能碰撞的物体再对少数候选做圆形、mask 或复杂多边形精测。这个思想会贯穿本文后面的代码实现。1.4 本项目的预期效果与技术范围我计划做一个小 Demo目标很明确一个 640×480 的窗口玩家用方向键控制一个 40×40 的绿色方块场景里有随机生成的金币和固定的红色障碍物。玩家碰到金币加 1 分碰到障碍物则回到出生点。代码里预留一个 DEBUG 开关打开后可以把碰撞盒、圆心、半径都画出来方便肉眼验证碰撞边界。技术范围限定在 2D 矩形与圆形碰撞机制、mask 精确碰撞、固定步长防穿透三块。2. 核心碰撞检测原理与代码细节2.1 屏幕坐标系与游戏物体的碰撞边界Pygame 的坐标系以窗口左上角为原点x 轴向右增大y 轴向下增大。一个矩形通常用 (x, y, width, height) 表示注意这里的 y 是顶部坐标不是底部。很多新手第一次写碰撞时会把 y 方向搞反导致检测结果看起来“差了一个身位”。另一个新手常踩的坑是把精灵的视觉边缘当成碰撞边界。默认情况下Pygame 的 Rect 是覆盖整个贴图矩形的而贴图往往包含透明区域。比如一张角色贴图背后有披风特效披风周围的透明像素也都在矩形内部结果就是角色明明没碰到敌人却因为透明区域重叠被判为碰撞。理解“碰撞边界是几何近似不是美术轮廓”这一点能帮你少调半天 bug。2.2 AABB 矩形碰撞四个比较定生死AABB 也叫轴对齐包围盒它的核心原理是两个矩形只要在 x 方向或 y 方向任意一个方向上没有重叠就一定没有碰撞。判断条件写成代码非常短。def rect_collision(ax, ay, aw, ah, bx, by, bw, bh): # 左边界在右边或右边界在左边则不相交 if ax aw bx or bx bw ax: return False # 上边界在下边或下边界在上边则不相交 if ay ah by or by bh ay: return False return TruePygame 的 Rect 类型自带colliderect方法底层逻辑和上面一样。为什么说 AABB 快因为它只需要几次整数比较没有开方、没有三角函数。在一帧里检测几百个矩形也毫无压力是所有碰撞检测方案的性能底座。2.3 圆形碰撞检测用距离的平方代替开方圆形碰撞检测的数学效率是一个经典优化案例。判断两个圆是否碰撞最直观的方式是计算圆心距离再和半径之和比较但距离公式带一个平方根运算。平方根是相对昂贵的浮点操作每帧检测大量圆形时这个开销会被放大。优化方法是不开方直接比较距离的平方和半径之和的平方。def circle_collision(x1, y1, r1, x2, y2, r2): dx x2 - x1 dy y2 - y1 return dx * dx dy * dy (r1 r2) * (r1 r2)这样只用了四次乘法、两次减法、一次加法、一次比较非常快。圆形碰撞适合炮弹、金币、角色头顶的判定区域精度通常比矩形更符合直觉因为很多游戏对象的轮廓都接近圆形。2.4 像素级 Mask 碰撞让不规则物体也能精确命中AABB 对不规则形状的误判问题可以用 Pygame 的 mask 模块来解决。pygame.mask.from_surface会根据 Surface 的 alpha 通道或 color key 生成一张遮罩然后overlap方法可以判断两个 mask 在某个偏移位置是否有重叠像素甚至返回重叠点的坐标。用 mask 碰撞后子弹打中飞机机翼才算命中打在透明背景上不会误爆精度能到像素级。代价是逐像素检查性能比矩形和圆形都慢。所以在实际项目里我几乎从来不会直接拿所有物体做 mask 检测而是先做一次 AABB 粗测只有粗测命中后再用 mask 精测。这就是“粗测 精测”的分层思路。2.5 碰撞检测之后的响应逻辑推离与最小重叠轴检测出重叠之后游戏需要给出反馈。最简单的反馈是扣血或加分但如果你想做一个手感正常的平台跳跃游戏那就要处理“推离”问题角色和墙壁重叠后不能继续往里走得推回合法位置。推离的核心是计算重叠深度。假设 A 是静止障碍物B 是移动角色检测到重叠后分别计算 x 方向重叠宽度和 y 方向重叠高度。哪一个方向的深度更小就认为碰撞主要来自那个方向并把物体沿该方向推出去。代码思路大致是这样def resolve_overlap(player, obstacle): overlap_x min(player.right, obstacle.right) - max(player.left, obstacle.left) overlap_y min(player.bottom, obstacle.bottom) - max(player.top, obstacle.top) if overlap_x overlap_y: if player.centerx obstacle.centerx: player.right obstacle.left else: player.left obstacle.right else: if player.centery obstacle.centery: player.bottom obstacle.top else: player.top obstacle.bottom这套逻辑是很多 2D 游戏物理的基础自己手写过一次再去看游戏引擎里的碰撞响应就会觉得熟悉。3. 完整实操用 Pygame 从零实现碰撞检测 Demo3.1 环境准备Python 版本与 Pygame 安装建议使用 Python 3.8 及以上版本Pygame 用 2.1 以上版本。安装只需要一条命令pip install pygame如果下载速度慢可以使用国内 PyPI 镜像源比如清华镜像pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple装完后用一行命令验证是否成功python -c import pygame; print(pygame.version.ver)项目结构我建议先单文件跑通所有代码放在main.py里等逻辑完整后再拆成多个模块。小项目过度设计是最常见的问题跑通功能永远优先。3.2 初始化窗口与游戏循环Pygame 的基本框架有三件事初始化窗口、处理事件、更新与绘制。下面是最小可运行的代码模板import pygame import random pygame.init() WIDTH, HEIGHT 640, 480 screen pygame.display.set_mode((WIDTH, HEIGHT)) clock pygame.time.Clock() running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False screen.fill((30, 30, 30)) pygame.display.flip() clock.tick(60) pygame.quit()窗口背景是深灰色固定 60 FPS。注意clock.tick(60)的作用是限制循环速度避免 CPU 空转到 100%。3.3 定义玩家、金币与障碍物对象我习惯用简单的类或直接使用 Rect。为了演示这里用类封装方便后面扩展。玩家是绿色方块金币用圆形障碍物用矩形。class Player: def __init__(self, x, y): self.rect pygame.Rect(x, y, 40, 40) self.color (0, 200, 0) self.speed 5 def move(self, dx, dy): self.rect.x dx self.rect.y dy class Coin: def __init__(self, x, y): self.rect pygame.Rect(x, y, 24, 24) self.radius 12 self.color (255, 220, 0) class Obstacle: def __init__(self, x, y, w, h): self.rect pygame.Rect(x, y, w, h) self.color (220, 60, 60)注意 Coin 虽然也用 Rect 存储位置但真正做碰撞检测时用的是圆形这样演示两种方案可以同时出现。3.4 主循环中的三类碰撞响应玩家移动使用方向键每帧调用一次move。碰撞检测分三类玩家与金币、玩家与障碍物、玩家与屏幕边界。player Player(100, 100) coins [Coin(random.randint(50, WIDTH-50), random.randint(50, HEIGHT-50)) for _ in range(5)] obstacle Obstacle(300, 200, 30, 120) score 0 while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False keys pygame.key.get_pressed() dx, dy 0, 0 if keys[pygame.K_LEFT]: dx -player.speed if keys[pygame.K_RIGHT]: dx player.speed if keys[pygame.K_UP]: dy -player.speed if keys[pygame.K_DOWN]: dy player.speed player.move(dx, dy) # 玩家与金币圆形碰撞检测 for coin in coins: if coin.rect.colliderect(player.rect): # 这里先用矩形粗测再精确用圆形距离检测 player_center player.rect.center coin_center coin.rect.center distance_sq (player_center[0] - coin_center[0]) ** 2 (player_center[1] - coin_center[1]) ** 2 radius_sum player.rect.width // 2 coin.radius if distance_sq radius_sum ** 2: score 1 coin.rect.topleft (random.randint(50, WIDTH - 50), random.randint(50, HEIGHT - 50)) # 玩家与障碍物AABB 推离 if player.rect.colliderect(obstacle.rect): resolve_overlap(player.rect, obstacle.rect) # 玩家与屏幕边界 player.rect.left max(0, player.rect.left) player.rect.right min(WIDTH, player.rect.right) player.rect.top max(0, player.rect.top) player.rect.bottom min(HEIGHT, player.rect.bottom) screen.fill((30, 30, 30)) pygame.draw.rect(screen, obstacle.color, obstacle.rect) for coin in coins: pygame.draw.circle(screen, coin.color, coin.rect.center, coin.radius) pygame.draw.rect(screen, player.color, player.rect) # 显示分数 font pygame.font.Font(None, 36) text font.render(fScore: {score}, True, (255, 255, 255)) screen.blit(text, (10, 10)) pygame.display.flip() clock.tick(60)这里有一个容易被忽略的细节金币碰撞时我先用coin.rect.colliderect(player.rect)做了粗测再用圆形公式精测。虽然当前对象数量少省不了多少时间但这是良好的分层习惯后面对象多了直接受益。3.5 碰撞盒可视化调试开关调试碰撞最有效的工具是“把碰撞区域画出来”。我给代码加一个DEBUG变量打开后所有矩形边框和圆形轮廓都会绘制到屏幕上。DEBUG True # 在主循环绘制阶段之后追加绘制调试信息 if DEBUG: pygame.draw.rect(screen, (255, 255, 0), player.rect, 2) pygame.draw.rect(screen, (255, 255, 0), obstacle.rect, 2) for coin in coins: pygame.draw.circle(screen, (255, 255, 0), coin.rect.center, coin.radius, 2)为什么要强调可视化调试因为肉眼看到碰撞盒你才能分辨是“美术没对齐”还是“坐标算错”。我见过不少新手用 print 日志调半天都看不出问题把碰撞盒画出来一眼就明白了。这个开关应该从项目第一天就打开而不是等到出 bug 再临时加。3.6 固定时间步长解决高速穿透的核心方法碰撞穿透的根源是两次检测之间物体移动了太多距离。比如一帧里角色向右移动 10 像素而障碍物宽度只有 6 像素那一帧角色还在障碍左侧下一帧已经在右侧看起来就是直接穿过去了。解决办法之一是固定时间步长。不要让每帧的移动量随机涨而是让游戏逻辑按照固定间隔更新比如每秒 60 次。即使屏幕刷新率达到 120 FPS逻辑更新依然保持稳定的 60 次/秒。STEP 1 / 60 accumulator 0 while running: dt clock.tick(120) / 1000 accumulator dt while accumulator STEP: update() check_collisions() accumulator - STEP固定步长的另一个好处是物理表现稳定在 60 FPS 的电脑和 120 FPS 的电脑上物体速度和碰撞结果一致。做联网同步或录回放时这几乎是必须的。4. 常见问题与排查技巧实录4.1 物体直接穿过目标物体这是最经典的问题。我复盘过的原因主要有三种第一移动速度太快一帧内跨越目标厚度第二帧率不稳定帧间隔变大导致一帧位移过大第三检测逻辑只覆盖了移动后的位置没有考虑移动路径上的情况。排查路径也很固定先打开碰撞盒可视化确认目标物体确实是实心的再打印物体移动前后的坐标计算一帧位移量如果位移量确实超过目标厚度就加入固定步长或子步进。子步进的做法是把一次大位移拆成几次小位移每次都做碰撞检测相当于让检测频率高于移动频率。4.2 碰撞体积比画面大一圈很多精灵贴图是带透明边距的比如一个角色贴图 64×64但实际可见角色只有中间 32×48。直接用 Rect 检测等于把贴图四角的透明区域也当成了碰撞体积玩起来就会觉得“明明没碰到却被挡住”。最简单的修法是手动缩小碰撞盒。Pygame 的Rect.inflate可以按中心放大或缩小矩形传入负值就是缩小# 原矩形 40x40缩小为 34x36 small_rect player.rect.inflate(-6, -4)如果还需要更精确就上 mask。不过要注意mask 检测性能开销高适合用在 Boss、特殊道具上普通小兵建议用缩小的矩形盒。4.3 多个物体同时碰撞时的抖动我调试过一个非常典型的问题角色站在两个障碍物之间画面一抖一抖的。原因是同一帧内物体被多个碰撞点来回推动推离方向反复横跳。你刚把它从左边障碍推出来又碰到右边障碍被推回去视觉上就是高频抖动。解法是改变处理顺序先移动所有物体再统一做碰撞检测最后统一处理推离不要在移动一个物体的过程中立刻对它做响应。如果一次推离后仍然重叠可以做有限次迭代求解比如最多推 2 次超过就停止避免死循环。4.4 性能突然变差尤其是金币和粒子多起来之后当场景里有 100 个金币、50 个敌人每个物体都跟另一个物体做检测时复杂度是 O(n²)。n150 时每帧要做超过一万次检测Python 就开始吃力了。优化策略有三个层级粗检测先用距离或网格快速排除远处的物体只对近处的做矩形/圆形检测。空间网格把屏幕分成若干格子只检测同一格子和相邻格子的物体大幅减少配对数量。使用 Pygame 的碰撞工具pygame.sprite.groupcollide经过一定优化比自己写双重 for 循环要快。空间网格的原理不复杂比如把 640×480 的屏幕划分成 64×64 的格子一个物体只属于一个格子检测时只看它所在的格子以及周围最多 8 个格子。这比全局遍历少得多。4.5 手写碰撞系统与物理引擎的边界在哪如果项目需要重力、反弹、摩擦力、关节连接我不建议继续手写。Pygame 本身不提供物理引擎但可以配合 pymunk 使用。pymunk 内置了刚体、碰撞形状、碰撞回调安装也很简单pip install pymunk什么时候继续手写需求简单、教学目的、想完全掌控检测算法的时候。什么时候转引擎物理交互密集、需要弹簧/链条/复杂碰撞过滤、要求稳定物理表现的时候。记住一点手写的优势是理解与掌控引擎的优势是功能与稳定。4.6 快速排错速查表症状可能原因解决方案物体穿透速度过快 / 帧间隔过大固定步长、子步进、限制最大速度碰撞比画面大矩形包含透明区域缩小 Rect、使用 mask抖动乱跳触发顺序错误 / 推离方向不稳先移动、再检测、再统一推离性能卡顿O(n²) 遍历空间网格、粗检测、groupcollide检测不到碰撞坐标系理解错误打印 rect 数据、画碰撞盒5. 一些提升体验的扩展方向5.1 把“碰撞”做成事件系统如果你觉得直接在游戏循环里写碰撞响应不够干净可以引入一个事件系统。碰撞发生后不立刻执行具体行为而是抛出一个事件比如on_coin_collect(coin_id)、on_player_hit()由其他模块监听并处理。这样做的好处是逻辑解耦金币模块管金币行为音效模块管音效得分模块管得分彼此不干扰。对于更大的项目事件系统是必然选择。我这个 Demo 里虽然没做但它是碰撞响应代码演进的重要方向。5.2 从碰撞检测走向游戏机制设计碰撞检测不只是“物理引擎”的专属它还广泛存在于游戏机制中。平台跳跃游戏的“落脚点判定”ARPG 的“受击判定”潜行游戏的“视野锥检测”本质都是几何相交计算。你掌握了矩形、圆形、mask 这些基本工具后等于有了游戏开发里最常用的一块地基。比如你可以用圆形检测做“范围感知”让敌人感知进入圆形范围的玩家用矩形检测做“攻击区域”一个长方形区域代表剑的攻击范围玩家站在里面就算被击中。这些机制都很容易用前面讲过的代码实现。5.3 从 2D 向 3D 与物理引擎迁移的思路2D 碰撞检测的思维模型在 3D 里依然成立。3D 同样有包围盒AABB 的立体版、包围球、凸包检测同样需要粗测和精测的分层。只不过维度从 2 个方向变成了 3 个方向计算量更大对数据结构的要求更高所以 3D 项目几乎都会直接用物理引擎比如 Bullet、PhysX、Unity 的物理系统。因此我建议你先耐心把手写 2D 检测跑通理解“层级检测”“最小重叠轴推离”“掩码精测”这几个核心概念之后再去学引擎就非常轻松。引擎只是帮你把这些概念高效实现并封装逻辑内核并没有变。最后说点我自己的真实体会。我在这个项目上最大的跟头就是觉得碰撞检测很简单结果反复遇到穿透和抖动后来才意识到真正核心的不是那几行判断代码而是“更新的节奏”和“响应的顺序”。如果你想长期做游戏开发建议从一开始就把碰撞盒可视化调试打开它是我目前见过成本最低的排错方式。另一个小技巧是粗测用矩形精测用 mask更新用固定步长重叠用最小深度推离这套组合拳能撑起绝大多数 2D 项目。等你确实需要大量物理交互时再考虑接入 pymunk 也不迟。希望这篇内容能让你少走几步弯路动手把 Demo 跑一遍比看十篇文章都管用。