资讯详情

Python游戏开发碰撞检测从入门到实战:矩形、圆形与像素级mask详解

📅 2026/9/28 11:36:46 | 华诺云谱 👁 阅读
Python游戏开发碰撞检测从入门到实战:矩形、圆形与像素级mask详解
做Python游戏开发碰撞检测几乎是你绕不开的第一道坎。我自己第一次写弹球游戏时就撞过一堵无形的墙画面里两个物体明明已经重叠了程序却毫无反应有时候隔着一小段距离却提示碰到了。后来我才明白问题不在pygame而在碰撞检测的实现方式没选对、边界条件没写清。这篇文章想系统地讲一讲Python游戏中的碰撞检测实现从最简单的矩形判定到圆形判定、像素级mask再到碰撞后的位移回应用可直接上手的代码和思路帮刚接触Python游戏开发的人把这块地基打牢。文章里的示例都以pygame为主因为它在2D游戏开发中最常用碰撞检测相关的API也比较完整。但底层思路都是通用的你换成其他游戏框架或者自己写渲染引擎时同样适用。1. 先把碰撞检测拆开看游戏循环里它在哪个环节1.1 一次碰撞是怎么发生的游戏看起来是在连续播放画面实际上是在一个循环里不停刷新。pygame的标准循环大概是这样import pygame pygame.init() screen pygame.display.set_mode((800, 600)) clock pygame.time.Clock() running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 更新游戏逻辑 # 碰撞检测通常在这里做 # 绘制画面 pygame.display.flip() clock.tick(60)clock.tick(60)的作用是把帧率控制在每秒60帧。每一帧里都有一段更新游戏逻辑的代码碰撞检测就放在这个阶段。为什么不能放在绘制阶段因为绘制的职责只是把当前状态画出来如果在绘制时才做碰撞判断那么一个帧里要做判断又要改位置很容易出现画完了才处理碰撞结果物体已经穿进去了的尴尬情况。实际项目里碰撞检测的流程一般分成两步先遍历需要检测的物体判断它们是否相交如果相交了再根据需求处理位置修正、反弹、伤害计算或者音效触发。很多新手只关注第一步怎么判断相交忽略了第二步结果角色被撞到之后会陷入墙壁或者卡在模型里。1.2 离散检测与连续检测为什么会有穿透这回事按检测时机分碰撞检测可以分成离散检测和连续检测。离散检测是每帧做一次快照式的判断这一帧你在这个位置下一帧你到了另一个位置如果两帧之间物体移动距离太大就可能跳过中间本该碰撞的区域导致高速飞行的子弹直接穿透墙壁。连续检测则是把物体从本帧位置到下一帧位置的运动路径也纳入计算看路径上是否与目标相交。Python游戏、尤其是pygame开发的2D游戏绝大多数用的都是离散检测。它简单、运行快但要在意物体最大移动速度。一个角色一帧内移动了10像素而墙壁厚度只有4像素那这一帧前后都有可能直接跨过去。这不是碰撞检测算法错了而是检测时机的问题。处理手段通常有两种把帧率提高或者做更细的扫描。1.3 先搞清楚物体在游戏里怎么表示碰撞检测说到底是在做几何判断所以物体用什么几何形状表示很关键。pygame中最常用的有几种pygame.Rect矩形区域用x, y, width, height四个值描述。适合墙壁、平台、玩家的整体包围盒。圆心加半径用(x, y)和r描述适合圆形物体。多边形pygame里有pygame.draw.polygon可用于绘制但碰撞检测通常要借助其他几何算法。像素掩码pygame.mask类基于图片的非透明像素生成掩码可以做到像素级精确判断。形状越贴近物体实际轮廓碰撞检测越精确但计算代价也越高。所以实际开发里很少一上来就用像素级检测而是先选一个简单的包围形状再考虑要不要增加精度。2. 矩形碰撞Python游戏里最常用的方案2.1 pygame.Rect 的 colliderect 到底做了什么pygame里做矩形碰撞最简单的方式是Rect.colliderect。rect1 pygame.Rect(100, 100, 50, 50) rect2 pygame.Rect(130, 130, 50, 50) rect3 pygame.Rect(200, 100, 50, 50) print(rect1.colliderect(rect2)) # True两个矩形重叠了 print(rect1.colliderect(rect3)) # False只是贴边或分离colliderect看起来只是帮我判断两个矩形是否相交但它背后有一个非常关键的细节pygame的Rect只支持整数坐标。你传入浮点数它内部会帮你截断成整数这在做平滑移动时容易引发一种闪烁穿透的错觉。比如物体速度是(1.6, 0)每帧都移动1.6像素pygame会先把坐标截断成(1, 0)再逐步累加最后位置就不连续。这个问题可以在外部维护浮点坐标绘制和碰撞时再统一取整。colliderect还支持与矩形列表做批量判断比如pygame.Rect.collidelist和collideobjectsplayer pygame.Rect(50, 50, 20, 20) walls [pygame.Rect(0, 0, 100, 10), pygame.Rect(100, 0, 10, 100)] # 返回第一个相交的矩形下标没有则返回 -1 index player.collidelist(walls) print(index)这种接口在做角色撞墙这种场景时非常方便。2.2 手写矩形相交判定边界条件才是重点如果不用现成API两个矩形相交的核心条件是它们在x轴方向的投影有重叠且在y轴方向的投影也有重叠。假设rect1的左上角是(x1, y1)宽高是w1, h1rect2同理。它们相交的数学条件可以写成def rect_overlap(r1, r2): # 检查x轴方向是否有重叠 horizontal_overlap (r1.x r2.x r2.width) and (r1.x r1.width r2.x) # 检查y轴方向是否有重叠 vertical_overlap (r1.y r2.y r2.height) and (r1.y r1.height r2.y) return horizontal_overlap and vertical_overlap很多人容易写反比如写成r1.x r2.x and r1.x r1.width r2.x r2.width这表示的是r1完全包含r2不是它们相交。要判断重叠必须同时检查我的左边是否在对方的右边左边和我的左边是否在对方的右边左边这些交叉关系。一个常见的问题是边界相切。游戏里通常把贴边也当成碰撞因为两个物体正好差0像素不重叠下一帧很可能就会重叠没必要在边界上区分太细。colliderect的处理是相交即True边界相切不算相交。如果你需要把接触边界也算作碰撞就要手动放宽条件。2.3 用矩形碰撞搭建一个简单的玩家与障碍物检测下面这个例子演示了键盘控制玩家移动并在移动后检测是否与障碍物碰撞import pygame pygame.init() screen pygame.display.set_mode((800, 600)) clock pygame.time.Clock() player pygame.Rect(100, 300, 30, 30) obstacle pygame.Rect(400, 280, 200, 40) speed 5 running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False keys pygame.key.get_pressed() 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 # 先移动再检测碰撞 player.x move_x player.y move_y if player.colliderect(obstacle): # 这里只做简单回退 player.x - move_x player.y - move_y screen.fill((30, 30, 30)) pygame.draw.rect(screen, (0, 200, 0), player) pygame.draw.rect(screen, (200, 200, 200), obstacle) pygame.display.flip() clock.tick(60) pygame.quit()注意一个容易被忽略的细节这里先同时移动x和y再检测碰撞回退时把两个方向都回退。这样写代码简单但有一种斜向卡墙的问题物体贴墙移动时x方向已经撞了可y方向本来能继续动现在被一起回退手感会很木。更精细的做法是把x和y方向的移动分开检测player.x move_x if player.colliderect(obstacle): player.x - move_x player.y move_y if player.colliderect(obstacle): player.y - move_y这样至少能保证撞墙时还能沿着墙面滑动。这个先单轴移动再单轴回退的思路在平台跳跃游戏里特别实用。3. 圆形碰撞与混合碰撞处理更自然的物体形状3.1 圆心距离与半径之和如果游戏里有球、角色头顶、爆炸范围这类物体用矩形包一圈会显得很生硬。比如一个圆形角色和一面斜墙矩形包围盒在四个角上会出现空碰撞视觉上明明没碰到碰撞检测却告诉你碰上了。圆形碰撞的判定方式是如果两个圆的圆心距离小于或等于半径之和那么它们发生碰撞。import math def circle_collision(c1, r1, c2, r2): dx c1[0] - c2[0] dy c1[1] - c2[1] distance math.hypot(dx, dy) # 等价于 sqrt(dx*dx dy*dy) return distance r1 r2math.hypot比手动写math.sqrt(dx ** 2 dy ** 2)更稳也更不容易因为数值精度翻车。这里有个优化点判断是否碰撞时其实不需要真的开平方。因为开平方是单调递增的比较距离和半径之和可以改为比较距离的平方与半径之和的平方def circle_collision_fast(c1, r1, c2, r2): dx c1[0] - c2[0] dy c1[1] - c2[1] return dx * dx dy * dy (r1 r2) * (r1 r2)在Python这种解释型语言里少一次sqrt调用在大量物体检测时能明显省时间。我在做几百个粒子互相碰撞的项目时用平方比较是基本的性能习惯。3.2 矩形与圆形混合碰撞找矩形上离圆心最近的点游戏里更常见的是圆形角色去碰矩形砖块比如玩家是圆滚滚的小球地图却有方形的平台。矩形和圆形的碰撞检测有一个经典做法先找到矩形范围内离圆心最近的点再判断这个点到圆心的距离是否小于等于半径。最近点的计算方法比较直观把圆心的x坐标限制在矩形的左右边界之间把圆心的y坐标限制在矩形的上下边界之间。限制后得到的点就是离圆心最近的矩形上的点。def circle_rect_collision(circle_center, radius, rect): closest_x max(rect.left, min(circle_center[0], rect.right)) closest_y max(rect.top, min(circle_center[1], rect.bottom)) dx circle_center[0] - closest_x dy circle_center[1] - closest_y return dx * dx dy * dy radius * radiusmin和max的组合看起来有点绕但本质就是把点往矩形范围里推。圆心在矩形内部时最近点就是圆心自己距离为0肯定碰撞圆心在矩形上方时最近点的y会被限制成矩形的top这样判断的就是圆心到矩形上边的距离。这个函数的用途很广。比如一个圆形子弹和一个方形掩体或者玩家是圆形、场景里的许多物体是矩形碰撞体都能用。3.3 不同形状碰撞的取舍几种常见碰撞判断的代价从低到高大致是检测方式计算成本适用场景矩形与矩形很低几次比较方形物体、场景墙体、包围盒圆形与圆形低几次乘法加法球、粒子、爆炸范围矩形与圆形中有限制运算加距离判断圆形角色配矩形平台像素级mask高逐像素遍历美术资源形状不规则且要求精确选哪种形状其实是在精度和性能之间做取舍。开始不用追求绝对精确能先用矩形解决的场景就先上矩形等出现了明显的视觉误差再针对具体物体升级碰撞算法。4. 像素级精确碰撞当矩形和圆形都不够用的时候4.1 pygame.mask 的基本用法矩形和圆形再怎么组合也无法精确还原一个形状复杂的精灵图。比如一个角色手上拿着一把剑剑刃细长用矩形包围盒的话剑没碰到敌人包围盒已经先碰到了。这个时候可以用pygame.mask做像素级精确碰撞。pygame.mask.from_surface会根据图像表面的alpha通道或颜色关键字生成一个掩码。掩码是一张和图像同样大小的布尔网格透明像素是0不透明像素是1。mask1 pygame.mask.from_surface(player_image) mask2 pygame.mask.from_surface(enemy_image)然后调用overlap传入另一个掩码和二者位移偏移offset (enemy_rect.x - player_rect.x, enemy_rect.y - player_rect.y) overlap_point mask1.overlap(mask2, offset)这里的offset是enemy相对于player的位置偏移。overlap返回重叠区域的第一个点坐标如果没有重叠返回None。4.2 配合 Rect 使用别让 mask 裸奔mask本身没有位置信息它的坐标全靠 Rect 来承载。所以像素碰撞的基本步骤一定是先用Rect做粗检测如果Rect相交了再用mask做细检测。if player_rect.colliderect(enemy_rect): offset (enemy_rect.x - player_rect.x, enemy_rect.y - player_rect.y) if player_mask.overlap(enemy_mask, offset): # 真正的像素级碰撞发生 pass这一步非常关键。如果直接拿两个mask.overlap跑而它们的Rect相距很远offset会很大overlap内层仍然要遍历像素性能就白白浪费了。4.3 像素碰撞的性能陷阱像素碰撞最怕的是频繁调用from_surface。图像每改变一次帧比如角色走路动画每帧换一张图就重新生成一次mask这个开销很容易让游戏掉帧。比较好的做法是在加载图片时就生成mask并缓存起来除非精灵图本身发生了变化。另外mask的overlap算法是遍历较小掩码的每个像素再映射到另一个掩码上查找。如果两个掩码都很大比如256x256每一帧做几十次这样的检测计算量会非常可观。实际项目里常见做法是分层检测第一层矩形包围盒粗检排除明显不相交的物体。第二层对可能相交的物体再做大粒度的圆形检测或缩小的矩形检测。第三层最后才用mask做像素级确认。我曾经在一个小型塔防游戏里把敌人都换成mask碰撞结果100个敌人加上溅射攻击判定帧率从60直接掉到20多。后来改成先做矩形粗检只有矩形重叠时再算mask帧率才恢复正常。这个优化思路在物体数量多的时候收益非常明显。4.4 什么时候不要用像素碰撞像素碰撞不是万能的。它有很明显的两个问题一是计算量大二是它只能判断有没有碰到很难得到碰撞发生在哪个方向、应该推多远的信息因为重叠区域是一堆零散像素点不像矩形那样能直接算出深度。如果你的需求只是碰到之后弹开一点、碰到之后减速矩形和圆形已经能搞定。像素级检测更适合那些对视觉效果要求极高的场景比如独立游戏里精细的子弹判定、美术风格强烈的怪物流血判定、或者项目中有透明背景的异形角色。如果你做的是平台跳跃游戏碰撞主体其实依然可以用矩形只是把玩家身体拆分成长宽比合适的几个矩形区域比如头、身体、脚三个碰撞盒这种做法在业界也很常见因为它比全像素检测稳定得多。5. 碰撞之后的处理重叠修正、反弹与穿透问题5.1 最小平移向量把物体推出去很多初学者在实现碰撞后只会做一件事检测到碰撞就把位置恢复成上一帧的位置。这个方案能应付简单场景但在角色与斜坡、多物体堆叠、快速移动时就会出现抖动和卡顿。更专业的做法是计算最小平移向量。所谓最小平移向量就是把两个相交的物体分开所需要的最小位移方向与大小。比如矩形碰撞可以通过比较两个矩形重叠部分的宽度和高度来决定往哪个方向推def get_collision_push(rect1, rect2): overlap_x min(rect1.right, rect2.right) - max(rect1.left, rect2.left) overlap_y min(rect1.bottom, rect2.bottom) - max(rect1.top, rect2.top) if overlap_x overlap_y: # 水平方向重叠更小从水平方向推出 if rect1.centerx rect2.centerx: rect1.right rect2.left else: rect1.left rect2.right else: # 垂直方向重叠更小从垂直方向推出 if rect1.centery rect2.centery: rect1.bottom rect2.top else: rect1.top rect2.bottom这里选择重叠较小的轴来推离物体能减少视觉上突跳的感觉。你可能会疑惑为什么重叠小就推哪个轴因为重叠小意味着物体在这个方向上侵入得浅推出去的距离最自然玩家不会感觉角色被弹飞。5.2 反弹、速度改变与物理响应碰撞检测的下游是碰撞响应。一个常见的简单响应是反弹根据碰撞方向反转速度分量。比如一个球碰到了上方的平台那么它的垂直速度应该反转if ball_rect.colliderect(platform_rect): if ball_speed_y 0 and ball_rect.bottom - platform_rect.top 10: ball_speed_y -ball_speed_y这里加了条件ball_speed_y 0是为了避免已经在下落碰到平台顶部结果又把方向反了的反复横跳。还有一个关键点是判断碰撞方向时不要只依赖colliderect返回的布尔值。布尔值只告诉你碰了没告诉你碰的是哪个方向。你需要借助相对位置和重叠深度来判断这就是上一节最小平移向量的作用。另一个容易踩的坑是碰撞检测时物体已经重叠了但重叠量很大。高速移动的物体每帧移动10像素碰撞发生时可能已经侵入对方15像素。如果只是简单地把速度反向物体下一帧还会再撞进来看起来就像在墙体边缘颤抖。正确做法是先把物体推离到刚好接触的位置再反转速度。5.3 高速穿透与连续碰撞检测的解决思路前面提到离散检测会漏掉高速小物体。最简单的解决思路是细分步长把一次移动拆成几次小步移动每次移动后都做一次碰撞检测。def move_with_collision(obj, dx, dy, obstacles, step2): # 把dx、dy拆成多个小步 distance max(abs(dx), abs(dy)) if distance 0: return steps max(1, int(distance // step)) step_x dx / steps step_y dy / steps for _ in range(steps): obj.x step_x obj.y step_y for wall in obstacles: if obj.colliderect(wall): # 撞到了回退一步并停止 obj.x - step_x obj.y - step_y return这个做法牺牲了一些性能但能让子弹不容易穿过薄墙。另一种思路是用扫掠检测把物体从起点到终点的移动轨迹看成一个矩形判断这个轨迹矩形是否与目标相交。pygame本身没有内置扫掠检测但我们可以手动构造轨迹矩形def swept_rect(rect, dx, dy): if dx 0: x rect.x width rect.width dx else: x rect.x dx width rect.width - dx if dy 0: y rect.y height rect.height dy else: y rect.y dy height rect.height - dy return pygame.Rect(x, y, width, height)用这个轨迹矩形去检测是否碰撞能避免大多数高速穿透。如果你做的是子弹类游戏我建议至少对子弹做扫掠检测因为子弹通常体积小、速度大是最容易穿透的一类物体。5.4 碰撞分组与碰撞矩阵当场景里物体种类很多时你需要明确谁和谁才会发生碰撞。比如玩家子弹和敌人碰撞敌人子弹和玩家碰撞但玩家子弹不与其他玩家子弹碰撞。这种需求可以用碰撞矩阵实现。collision_matrix { (player, enemy): True, (player_bullet, enemy): True, (enemy_bullet, player): True, (enemy, enemy): False, } def can_collide(type1, type2): return collision_matrix.get((type1, type2)) or collision_matrix.get((type2, type1))在遍历时先判断碰撞矩阵是否允许发生碰撞再进行几何检测能少做很多无谓的计算。尤其当实体数量多到几千个时不加矩阵的全遍历会让游戏在后期越来越卡。6. 我踩过的坑调试碰撞检测的几个实用经验6.1 看清碰撞体可视化调试是第一步我调试碰撞检测时有个习惯把所有碰撞体用半透明色块画出来覆盖在原始图像上。collider_color (255, 0, 0, 80) # 半透明红色 pygame.draw.rect(screen, collider_color, player_rect) pygame.draw.circle(screen, collider_color, circle_center, radius, 1)这样你能直接看到碰撞体比角色大多少、小了哪里。很多我明明没碰到却判定碰撞的问题一看半透明色块就明白了其实是碰撞体比角色图像大了一圈。还有为了调试方便我会在碰撞发生时把重叠点画出来肉眼确认算法是否正确。6.2 Rect的坐标陷阱x、y到底是左上角还是中心pygame的Rect用x, y表示左上角而很多游戏素材在绘制时喜欢以中心点定位比如角色图片中心是脚底位置。于是经常出现一种问题图片看起来站在平台上Rect却悬空或者陷进地板里。我在写平台游戏时习惯维护一份真实坐标和一份渲染坐标。真实坐标可以用浮点数存储物体的中心点渲染时再把Rect的中心点设置到真实坐标上entity.x velocity_x entity.y velocity_y entity_rect.center (int(entity.x), int(entity.y))碰撞检测用entity_rect但位置累计用entity.x和entity.y。这样既避免了Rect整数截断带来的精度丢失又能保证绘制位置和碰撞位置一致。6.3 别小看遍历顺序反向遍历与列表删除游戏里经常出现碰撞后销毁物体的需求比如子弹命中后要移除。遍历列表时边遍历边删除是很多新手容易踩的坑for bullet in bullets: if bullet.colliderect(enemy): bullets.remove(bullet) # 可能会跳过下一个子弹原因在于删除当前元素后列表元素会前移循环索引会跳过下一个未遍历的元素。解决方法是反向遍历或者先把要删除的对象收集起来再统一删除for bullet in bullets[:]: if bullet.colliderect(enemy): bullets.remove(bullet)这里对bullets做了一次浅拷贝遍历的是副本删除的是原列表。这种方式简单稳定在处理大量物体时也是常用手段。6.4 从能跑到能用我之前建议的落地路径如果你是刚入门我建议按这个路径来练先用pygame.Rect.colliderect做两个方块之间的碰撞然后给方块加上移动做一个撞墙回退和反弹再把其中一个方块换成圆形用距离判定做一次圆形碰矩形最后如果有精力再研究pygame.mask和扫掠检测。这个路径的每一步都在解决一个具体问题而不是空谈算法。等你能把上面这些代码串起来做一个简单的打砖块或者躲避小游戏就已经很稳了。我最初写第一个Python小游戏的时候也是从矩形碰撞开始后来为了做精细判定才逐步引入圆形和mask。你会发现游戏开发里真正复杂的不是某个函数怎么调用而是你怎么把不同碰撞方式组合起来让玩家感觉世界是可信的。最后分享一个小经验碰撞检测的代码通常会散落在多个地方建议你单独建一个collision.py把矩形、圆形、混合碰撞、mask碰撞这些函数都集中起来。这样调试和替换算法都方便也不会让主循环文件变成一团乱麻。游戏越做越大之后这个习惯会帮你省下大量时间。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑