☰
Python游戏中的碰撞检测实现:从AABB到像素级精度
2026/10/6 17:24:41 网站建设 项目流程

我最近用 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 True

Pygame 的 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 从零实现碰撞检测 Demo

3.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(f"Score: {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²)。n=150 时,每帧要做超过一万次检测,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 跑一遍,比看十篇文章都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询