ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PyGame碰撞检测与绘制技巧:从Rect到Mask全解析

PyGame碰撞检测与绘制技巧:从Rect到Mask全解析 写PyGame游戏最绕不开的一关就是碰撞检测。我见过太多人画面漂漂亮亮一跑起来子弹打在敌人身上像被空气弹开或者角色明明没碰到门却卡在门口进不去——十有八九都是碰撞这块出了问题。这篇内容就围绕PyGame里的碰撞检测与绘制技巧完整展开从最基本的Rect矩形判定到圆形碰撞、像素级Mask检测再到如何把碰撞框画出来让它不再当“玄学”。无论你是刚开始写贪吃蛇、弹球游戏的新手还是已经在做完整小游戏但被bug折磨的进阶玩家这套东西都能直接用起来。1. 碰撞检测到底在解决什么问题1.1 游戏里没有“真实的接触”只有计算出来的交集电脑游戏里的场景其实只是一堆数字在内存里跑来跑去。角色位置是一对坐标物体尺寸是一个宽高敌人也是一个矩形或者圆形。所谓“碰到了”在程序层面就是两个几何形状的边界产生了交集而我们做的就是把这个交集判断在每一帧里快速算出来。这话听起来像废话但很多人一开始就是绕不过弯。你看到画面里两张图叠在一起了就觉得“它们应该撞到了”可代码里如果没有显式的碰撞判断角色和墙壁就会像鬼魂一样穿身而过。所以我习惯把碰撞检测理解为“答题卡上的对答案动作”每一帧问一次你的框和我的框有没有重叠重叠就做响应不重叠就继续各走各的。拿生活里的事打比方你推着购物车过闸机闸机其实不是在“看”你这个人而是检测“购物车的外框”和“闸机的感应区”有没有重合重合就开门。游戏里的碰撞框就是那辆购物车的外框只是它默认不会画出来需要你用绘制技巧把它可视化才能知道这个框是大是小、靠左靠右。为什么不用“直接看两张图片有没有重叠的像素”来判断原理上讲是可以的但性能扛不住。一张800x600的图都几十万像素两两比较的算法想都不敢想。所以游戏引擎普遍用矩形、圆形这类廉价几何形状做近似先快速排除大多数情况必要时再用精确的遮罩去复核。想清楚这一层后面再看Rect、Mask这些API思路就通了。1.2 PyGame能给什么不能给什么PyGame本质上是SDL的Python封装面向2D游戏开发。它给你准备现成的pygame.Rect、pygame.sprite.Sprite、pygame.mask这些数据结构其中Rect自带一堆碰撞方法这是绝大多数碰撞检测的起点。但也得清醒一点PyGame没有内置物理引擎它不会帮你处理反弹、摩擦、加速度这些。碰撞检测在PyGame里就是纯粹的“判断函数”你调用它得到True/False或者重叠点后面怎么响应——弹开、消失、加分、播放音效——都得自己写。我第一次从Unity转过来时特别不适应因为Unity里挂个碰撞体拖个Rigidbody就完事而PyGame里连“碰到地面就停下来”这种都需要自己写逻辑。但换个角度想这也是好事因为碰撞检测的底层原理会被你摸得明明白白后面换任何引擎都不慌。这篇文章的主线就是把PyGame里最实用的那几类碰撞判断挑出来逐个讲透再配合绘制技巧帮你把整个流程调到舒服的状态。适合什么人看刚上手PyGame一两个星期、准备做第一个完整小游戏的初学者以及已经能做简单demo但总被碰撞bug缠住的人。我会尽量避免教科书式的罗列API而是给出一套直接能抄、能跑、能改的写法。2. 常用碰撞检测方案逐个拆解2.1 Rect矩形碰撞最常用的入门选择在PyGame里pygame.Rect是最核心的东西没有之一。不管是图片的截取区域、窗口里的方框、还是鼠标点击的判定区域归根到底都是一个Rect。矩形碰撞的原理也最直观两个矩形如果在水平和垂直方向上的投影范围都有交集就说明撞上了。反过来只要横向或者纵向上错开一个像素矩形就不重叠。用起来更是傻瓜式。假设你有一个玩家的矩形player_rect和一个道具的矩形item_rectif player_rect.colliderect(item_rect): print(碰到了)Rect内置的几个常用碰撞接口都值得盘一下colliderect(other_rect)判断两个矩形是否重叠返回布尔值。collidepoint(x, y)判断某个点是否落在这个矩形内部鼠标点击、子弹命中坐标判断非常常用。collidelist(rect_list)传入一个矩形列表返回第一个发生碰撞的索引没有则返回 -1。collidedict(rect_dict)在字典里找第一个碰上的键值对比较少用但做对象分组统计时挺方便。我用得最多的是colliderect和collidelist。比如做贪吃蛇时贪吃蛇的头每走一步就和整个身体列表collidelist一次只要返回的索引不是 -1就说明蛇咬到了自己。这样几行代码就把死亡判定做完了。一个容易踩的点Rect没有“速度”概念很多人误以为设置了rect.x 5后它自带碰撞后的反弹效果其实完全不是。你移动矩形后下一轮碰撞判断依然是从当前坐标重新开始的。还有PyGame的很多方法分“原地修改”和“返回新对象”两种move(x, y)返回一个新的Rect原对象不动move_ip(x, y)是原地修改ip就是in-place。新手经常搞混写成rect rect.move_ip(5, 0)结果move_ip返回None后面全崩。这个坑几乎每个PyGame玩家都踩过。2.2 圆形碰撞处理球体、飞弹的常用方案矩形碰撞虽然简单但遇到圆形物体就很尴尬。比如一颗弹珠用正方形框去套四角明显是多出来的空区域图片是圆的框是方的经常出现“明明还差半个角没碰上系统却说碰到了”的错觉。这时候圆形碰撞就顶上来了。圆形碰撞的原理比矩形还简单两个圆相撞当且仅当两圆心之间的距离小于等于两个半径之和。换成数学公式就是sqrt((x1 - x2)^2 (y1 - y2)^2) r1 r2。代码里直接用math.hypot可以省去手动开方import math def circle_collide(p1, r1, p2, r2): x1, y1 p1 x2, y2 p2 return math.hypot(x1 - x2, y1 - y2) r1 r2 # 用法 if circle_collide(player_pos, 25, bullet_pos, 8): print(子弹命中)math.hypot(dx, dy)做的事情就是sqrt(dx*dx dy*dy)唯一的好处是写法更干净而且参数比较大时数值稳定性更好一点。如果你在大量对象里做圆形碰撞开方运算会带来一点开销想压性能的话可以直接比较距离的平方也就是dx*dx dy*dy (r1r2)*(r1r2)这样省掉开方数据量大的时候差别是能感觉出来的。圆形碰撞经常和矩形配合用。先是两队矩形做粗筛凡是在矩形阶段就没碰上的直接用False结果不用再算距离矩形碰上了但不放心再用圆形公式精确判断。这种两级判定看起来多写了几行代码实测下来在大规模子弹对射场景里能省下不少计算量。2.3 Mask遮罩碰撞像素级精确检测有些游戏物体形状特别不规则比如地图上一块裂纹密布的岩石、一个带着翅膀的精灵用矩形判断会差太多圆形更搭不上边。这时PyGame的遮罩Mask系统就派上用场了。Mask的原理是先把带透明通道的图像转成一个二维布尔数组里面每个值为1表示这个像素点是“有效实体”的值为0表示透明背景。碰撞检测时比较两个Mask在重叠区域的像素只要有一对有效像素同时存在于同一位置就认定发生碰撞并且overlap方法会告诉你第一个碰撞点的坐标mask1 pygame.mask.from_surface(image1) mask2 pygame.mask.from_surface(image2) offset (rect2.x - rect1.x, rect2.y - rect1.y) collision_point mask1.overlap(mask2, offset) if collision_point: x, y collision_point # 这个坐标是相对于 mask1 的左上角的这里有个非常容易搞错的点offset是第二个物体相对于第一个物体的偏移而不是绝对坐标。很多人写代码时直接把两个物体的坐标差值搞反结果明明重叠了也检测不到。我的建议是每次写Mask碰撞都先从简单的两个方块开始验证确认offset计算无误后再套用到精灵身上。Mask的代价也很明显像素级判断比矩形判定慢一到两个数量级。如果场景里几十个精灵都在用Mask碰撞帧率很容易肉眼可见地掉。所以我的经验是Mask只用于那些真正需要精确碰撞的少数对象比如玩家被技能命中的部位判定、特别造型的Boss其他能用矩形的坚决用矩形。另外mask.from_surface有个默认阈值为127意思是透明通道小于127的像素会被当成透明这个值可以通过第二个参数调整做半透明特效时多留意否则你期望的“实心区域”和Mask实际生成的可能会不一样。2.4 精灵组与辅助碰撞函数如果你写的是用面向对象组织的游戏pygame.sprite.Sprite和pygame.sprite.Group几乎绕不开。Sprite类默认只有一个rect属性但你可以在子类里加速度和碰撞逻辑。Group的作用像一个集合容器统一管理所有同类型的精灵并且提供两个非常常用的碰撞辅助pygame.sprite.spritecollide(sprite, group, dokill)拿一个单独的精灵去和整个组的精灵做碰撞检测。dokill设为True时被碰到的精灵会直接从组里移除做子弹打敌人再顺手不过。pygame.sprite.groupcollide(group1, group2, dokill1, dokill2)两组成员互相碰撞判断返回一个字典键是group1中的精灵值是与它相撞的group2精灵列表。做子弹组的遍历和敌方组的遍历一行代码解决所有判定。hits pygame.sprite.groupcollide(bullets_group, enemies_group, True, True) for bullet, enemy_list in hits.items(): score 10 * len(enemy_list)我个人很喜欢spritecollide因为它省去了手写colliderect循环的麻烦。但要注意它默认使用的就是rect.colliderect做判定你也可以传入第三个参数自定义碰撞函数比如改成圆形判定或Mask判定。这个扩展能力很强却经常被人忽略。还有个大坑Sprite的rect如果忘记初始化Group在绘制或碰撞时就会直接报错或者碰撞永远不触发。写Sprite类时第一件事就是在构造函数里把self.rect建出来。方案精度性能适用场景Rect矩形中高方块、平台、按钮、通用判定圆形较高高弹珠、爆弹、球形角色Mask遮罩像素级低复杂不规则造型、局部命中点精灵组辅助取决于底层中大量同类型对象批量判定3. 绘制技巧把碰撞过程可视化3.1 碰撞框可视化调试写碰撞检测最怕的一件事就是“感觉对了”其实完全不对。尤其是图片本身和碰撞框不匹配的时候你只看画面根本看不出问题出在哪。我的习惯从第一天开始就没变过任何时候在做碰撞逻辑都要先把碰撞框画出来。对矩形就是描一圈线对圆形就是画一个边框圆不要觉得丑调试完再关掉也不迟。用pygame.draw画碰撞框非常简单# 画矩形碰撞框 pygame.draw.rect(screen, (255, 0, 0), entity.rect, width2) # 画圆形碰撞框 pygame.draw.circle(screen, (0, 255, 0), entity.center, entity.radius, width2)width2让线条有2像素宽比默认的实心填充更能看出边界位置。我还会在调试模式下给不同对象用不同颜色玩家碰撞框用绿色可交互物品用黄色危险物体用红色。这样一眼扫过去哪个碰撞框过大、过小、偏移了马上就能发现。这里有个绘制细节pygame.draw系列本身就是直接画到目标Surface上的无法带透明度混合。如果你想画半透明的调试框常规做法是创建一个带SRCALPHA的临时Surface在上面绘制后blit到主画面上。比如debug_surface pygame.Surface((40, 40), pygame.SRCALPHA) debug_surface.fill((255, 0, 0, 80)) screen.blit(debug_surface, (entity.rect.x, entity.rect.y))调试框绘制尽量放在screen.fill()之后、正式道具绘制之前保证它被业务内容压住一点不干扰视觉判断但又不至于完全被盖住。启动时用一个全局变量或者类的属性控制是否显示比如DEBUG True跑完再批量关掉别让这个代码残留在发布版本里拖累渲染。还有一点如果你用了pygame.transform.scale对图片进行缩放碰撞框也要同步缩放。很多人把图片放大了碰撞框还停留在原始尺寸就会出现“角色变大但判定不变”的怪异手感。每次对图片做变换都记得检查一下rect是否要对齐新尺寸。3.2 图片绘制与碰撞框坐标同步在PyGame里图片绘制用screen.blit(image, dest)碰撞判断用rect二者必须时刻保持一致。新手最常出现的问题是图片明明加载好了但blit的位置和rect的位置用了两套不同的变量结果画面上角色在左边碰撞判定却跟着右边的数据走玩家就会觉得“我怎么被空气打中”。更稳的写法是让Rect承担“唯一位置来源”的职责self.image pygame.image.load(player.png).convert_alpha() self.rect self.image.get_rect() self.rect.topleft (100, 100) # 每帧绘制 screen.blit(self.image, self.rect) # 之后对rect做移动 self.rect.x self.vx只要blit传的是rect本身位置就永远同步。当你需要让碰撞框比图片显示区域小一圈时这是调手感时的常见操作可以不要直接修改rect的尺寸而是单独维护一个hitbox_recthitbox self.rect.inflate(-20, -20)inflate(-20, -20)返回一个以原矩形中心为中心、宽高各缩小20的新矩形。这样视觉上图片还是原来的大小碰撞却更贴合实际给玩家的反馈也不会出现“明显看不出来碰到了却判定成功”的违和感。PyGame还有一个好用的点是Rect自带一系列对准辅助属性center、midbottom、midtop、topleft等。比如要把图片放在窗口底部居中直接rect.midbottom screen.get_rect().midbottom不用手动算坐标。这些属性在绘制时尤其省心特别是在做HUD和多人角色布局时能少写一堆移位代码。3.3 绘制顺序和层叠关系游戏画面不是一鼓脑把所有东西画上去就完事的。PyGame里所有绘制都是基于后绘制覆盖先绘制的规则背景画在最前面角色盖在背景上对话框盖在角色上。如果你把背景放在最后画那所有内容都会消失只剩一张图。这个顺序问题我调试过好多次每次都是画面全白或者角色被擦掉才反应过来。合理的绘制顺序一般来说是screen.fill()清屏铺背景色。绘制背景图层比如地图、静态装饰。绘制动态物体玩家、NPC、敌人、子弹、掉落物。绘制特效层粒子、碰撞框调试框。绘制UI层分数、血条、提示文字。如果你用精灵组管理对象可以用Group.draw(screen)批量绘制内部就是对每个精灵调用blit。但注意Group.draw只能按加入组的顺序逐个画如果不同精灵之间需要有前后遮挡关系——比如角色跑到树后身体被树干挡住——光靠一个Group就不够了。这时候要么用多个Group按顺序draw要么给精灵提个layer属性排序后再画。还有个小技巧绘制区域尽量只画屏幕内可见的内容。虽然PyGame的blit本身有一定裁剪能力但如果你一个场景里有几千个物体把所有物体全blit一遍仍会造成不必要的开销。简单的做法是判断对象rect是否与屏幕的rect有交集有交集才画这个判断用rect.colliderect(screen_rect)一秒钟的事。4. 完整实操做一个接物小游戏的碰撞体4.1 项目初始化与基础框架理论讲了一堆最后还是要落在代码上。我准备写一个最简单但五脏俱全的接物小游戏底部一个蓝色方块代表玩家顶部持续掉落红色圆形物品玩家用方向键左右移动接到物品加分并消失漏掉就继续。这个Demo覆盖了Rect碰撞、圆形碰撞、绘制碰撞框、精灵与碰撞反馈这几个核心技巧。第一步是搭好环境import pygame import random pygame.init() WIDTH, HEIGHT 800, 600 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(PyGame碰撞检测与绘制技巧 Demo) clock pygame.time.Clock()pygame.init()在引入后一定要调用它会初始化音频、显示、事件系统等一堆模块。如果你跳过了这一步大概率会在运行到一半时碰到“video system not initialized”之类的报错。窗口大小用常量写在顶部后面所有布局和碰撞范围都引用它改分辨率时不用到处找魔法数字。4.2 玩家对象与物品对象用类来组织对象会更清晰。玩家类持有一个Rect、移动速度和颜色三样东西class Player: def __init__(self, x, y, width100, height20): self.rect pygame.Rect(x, y, width, height) self.speed 6 self.color (70, 130, 230) def update(self, keys): if keys[pygame.K_LEFT] and self.rect.left 0: self.rect.x - self.speed if keys[pygame.K_RIGHT] and self.rect.right WIDTH: self.rect.x self.speed物品类稍微复杂一点因为这里我想同时演示矩形碰撞框和圆形碰撞掉落物视觉上是一个圆碰撞上也用圆形判定。为了统一逻辑我让它同时维护一个Rect作为位置参考圆的圆心就是rect.center一个半径字段radius另加一个速度vyclass FallingItem: def __init__(self): radius random.randint(12, 24) self.radius radius self.rect pygame.Rect( random.randint(radius, WIDTH - radius), -radius * 2, radius * 2, radius * 2 ) self.vy random.randint(2, 5) self.color (230, 120, 70) def update(self): self.rect.y self.vy def draw_collision_circle(self, screen, debugFalse): pygame.draw.circle(screen, self.color, self.rect.center, self.radius) if debug: pygame.draw.circle(screen, (255, 255, 255), self.rect.center, self.radius, 2)我把radius直接作为碰撞圆的半径同时也是绘制圆的半径这样视觉和判定天然同源不会出现“画的是大圆但判定是小圆”的错位。很多初学者把绘制尺寸和碰撞尺寸分开维护画着画着就忘了更新问题反而多。如果你确实需要视觉比判定大也应该在同一个数据源上派生而不是开两套互相不知情的变量。4.3 碰撞逻辑与画面反馈主循环里物品生成我用了随机数控制这样不用做定时器也能让掉落节奏自然晃动player Player(WIDTH // 2 - 50, HEIGHT - 50) items [] score 0 missed 0 font pygame.font.SysFont(None, 32) running True while running: dt clock.tick(60) for event in pygame.event.get(): if event.type pygame.QUIT: running False keys pygame.key.get_pressed() player.update(keys) if random.randint(0, 25) 0 and len(items) 30: items.append(FallingItem()) for item in list(items): item.update() if item.rect.top HEIGHT: items.remove(item) missed 1 continue # 矩形粗筛先判断外接矩形有没碰到速度最快 if player.rect.colliderect(item.rect): # 圆形精判只有矩形碰上了再算圆心距离 px, py player.rect.center ix, iy item.rect.center if math.hypot(px - ix, py - iy) item.radius min(player.rect.width, player.rect.height) / 2: items.remove(item) score 1这里我特意把“矩形碰撞”和“圆形碰撞”放在同一段逻辑里演示先用colliderect做粗筛矩形相交了再用距离公式精判。因为玩家是一个扁矩形我把玩家的等效半径粗略定为矩形短边的一半。如果需要更精确可以给玩家也维护一个圆形碰撞数据或者干脆给玩家的四个角各做一次圆形判定。Demo里这个近似已经够用代码也更好读。响应逻辑也很简单碰到就移除物品并加分。加分这件事其实也是“动作响应”现实中你可能会在这里播放音效、更新连击、加特效但位置都放在碰撞成立的分支里这是游戏事件响应的核心骨架。4.4 完整可运行代码与效果检查绘制阶段按之前说的顺序来最终完整代码长这样直接复制就能跑import pygame import random import math pygame.init() WIDTH, HEIGHT 800, 600 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(PyGame碰撞检测与绘制 Demo) clock pygame.time.Clock() font pygame.font.SysFont(None, 32) class Player: def __init__(self, x, y, width100, height20): self.rect pygame.Rect(x, y, width, height) self.speed 6 self.color (70, 130, 230) def update(self, keys): if keys[pygame.K_LEFT] and self.rect.left 0: self.rect.x - self.speed if keys[pygame.K_RIGHT] and self.rect.right WIDTH: self.rect.x self.speed class FallingItem: def __init__(self): self.radius random.randint(12, 24) self.rect pygame.Rect( random.randint(self.radius, WIDTH - self.radius), -self.radius * 2, self.radius * 2, self.radius * 2 ) self.vy random.randint(2, 5) self.color (230, 120, 70) def update(self): self.rect.y self.vy def draw(self, screen, debugFalse): pygame.draw.circle(screen, self.color, self.rect.center, self.radius) if debug: pygame.draw.circle(screen, (255, 255, 255), self.rect.center, self.radius, 2) player Player(WIDTH // 2 - 50, HEIGHT - 50) items [] score 0 missed 0 running True while running: clock.tick(60) for event in pygame.event.get(): if event.type pygame.QUIT: running False keys pygame.key.get_pressed() player.update(keys) if random.randint(0, 25) 0 and len(items) 30: items.append(FallingItem()) for item in list(items): item.update() if item.rect.top HEIGHT: items.remove(item) missed 1 continue if player.rect.colliderect(item.rect): px, py player.rect.center ix, iy item.rect.center player_radius min(player.rect.width, player.rect.height) / 2 if math.hypot(px - ix, py - iy) item.radius player_radius: items.remove(item) score 1 screen.fill((20, 22, 30)) for item in items: item.draw(screen, debugTrue) pygame.draw.rect(screen, player.color, player.rect) pygame.draw.rect(screen, (255, 255, 255), player.rect, width2) score_text font.render(fScore: {score}, True, (240, 240, 240)) missed_text font.render(fMissed: {missed}, True, (200, 120, 120)) screen.blit(score_text, (15, 10)) screen.blit(missed_text, (15, 40)) pygame.display.flip() pygame.quit()到这里别急着跑完就收工。我每写完一版碰撞逻辑都会先开调试框跑几分钟专门观察几个容易被忽略的地方物品从屏幕边缘掉落时会不会一出生就和玩家重叠判定掉落速度变化时碰撞是哪一帧触发的分数变化的时机和画面视觉是否一致。不出意外的话刚写完的版本总有一两个细节需要微调比如生成位置有概率出现在玩家正上方导致玩家站着不动就开始连续得分——这种情况下我会让生成位置避开屏幕底部区域或者在items列表更新前先做一次碰撞检查。5. 常见问题、性能优化与排查实录5.1 我踩过的坑一张排查速查表PyGame碰撞问题反反复复就那么几类我把常见症状和对应解法整理在一张表里方便你直接对号入座症状可能原因解决方案明明没有碰到却返回True矩形碰撞框大于视觉图像用inflate(-w, -h)缩小碰撞框或启用调试图观察明显重叠了却不返回True两个Rect坐标不在同一参照系检查move是否写成了move_ip检查blit的dest用了哪套坐标检测偶发、不稳定物体移动速度过快一帧跨过碰撞区域做“扫掠”判断或用上一帧位置到当前位置的线段与目标Rect相交精灵组碰撞完全不触发Sprite没设置rect构造函数里self.rect self.image.get_rect()Mask碰撞位置偏移offset参数方向或差值搞错Mask用固定偏移公式先做两个静态方块验证碰撞后物体双方都消失了把两个对象都移除了明确dokill参数确定只有该消失的一方会从组里删除帧率骤降大量Mask碰撞或两两遍历先用矩形粗筛只在必要时用Mask记录碰撞范围减少遍历报错video system not initialized忘记调用pygame.init()在程序开头初始化定时检查是否写在主循环外5.2 碰撞穿透问题我特别说一下“碰撞穿透”这个高频bug。比如子弹速度每秒1200像素帧率为60的话一帧子弹就走了20像素。如果子弹宽10像素敌人宽30像素本来应该命中但子弹在这一帧的起点还在敌人左侧终点已经在敌人右侧两帧之间完全没有某一帧出现矩形相交于是判定就直接错过了看起来就像是子弹穿过了敌人。解决穿透的思路有三种第一种是减小步长——把更新拆成多步每步不超过一定像素但这在高速物体上很笨重。第二种更优雅的思路是“射线/线段检测”记录物体上一帧位置和本帧位置然后判断“一段线段”是否和目标矩形相交。PyGame本身没有内置线段与Rect的碰撞方法但可以用游戏循环里做分段检测steps max(1, int(speed / max_span)) for _ in range(steps): bullet.rect.move_ip(velocity_x / steps, velocity_y / steps) if bullet.rect.colliderect(enemy.rect): kill() break第三种是给高速物体用一个“长条形的碰撞覆盖”把子弹的上/下帧位置之间连成一个细长矩形用这个细长矩形和敌人做碰撞。这种技巧在2D射击游戏里非常常见。它的核心思想是把时间维度上也压成几何形状碰撞检测就不依赖某一帧的离散位置了。5.3 性能优化与实测心得碰撞检测性能最怕两两比较。如果有N个物体彼此都要判断复杂度是O(N^2)N到100以上就开始卡。优化的第一准则永远是“提前排除”先用矩形粗筛矩形都没碰上后面的一切精度算法都不用跑了。我在之前Demo里已经展示过这种组合。再进一步可以用空间划分把屏幕按格子分块物体只跟同一格的邻居比较。实现思路不难每个格子存一个列表每帧把物体按中心点放进对应格子碰撞时先取同格子的物体列表。这样大规模场景的碰撞检测能从N^2骤降到很小的常数范围内。另外一个细节是碰撞检测的“频率”。不是所有物体的碰撞都需要每帧判断。比如处于冷却阶段的技能可以加一个计时器2帧检测一次或者只对屏幕内的物体做检测。这种“精细调度”看起来小在实战里对帧率提升非常明显。我经常说碰撞检测和绘制是游戏性能的两个大头绘制靠裁剪和批量碰撞靠分层和粗筛把这两条做到位PyGame跑几百个对象也能维持稳定帧率。写到最后说点我个人的体会。来来回回折腾PyGame发现碰撞检测最难的不是API记不住而是“手感”这个东西没法用代码抄。同一个Rect碰撞有人觉得碰一下就判定太灵敏有人觉得要重合一半才有反馈。调碰撞框的过程其实是在调游戏的“呼吸感”。我会在写完功能后专门花半小时只动数值——碰撞框缩小几个像素、移动速度改几分之一、圆形半径加加减减——直到自己玩起来觉得顺再交付给别人。这个流程没有捷径但配合好碰撞框绘制调试你会看得见每一处改动带来的差异。也正因为PyGame把碰撞检测拆得这么底层你更能理解其他引擎里的“碰撞体”到底是个什么东西之后再回去写Unity、Godot会快得多。
返回列表