ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Pygame从入门到实战:环境搭建、游戏循环与碰撞检测完整指南

Pygame从入门到实战:环境搭建、游戏循环与碰撞检测完整指南 说实话我第一次真正上手Pygame就是在大学宿舍里照着教程复制粘贴了一个贪吃蛇。那时候什么都不懂只知道pip install pygame敲下去等了一分多钟看到Successfully installed就觉得这库挺靠谱。后来工作里做了一些图形界面和轻量级工具回过头再看Pygame才发现它其实是个被严重低估的东西——很多人以为它只是个玩具但用它写小游戏、做原型、甚至给学生讲编程思想都是非常顺手的工具。今天这篇文章就围绕用Pygame开发你的第一个小游戏这个主题把从环境搭建、核心循环、碰撞检测到界面封装、移动端运行的完整链路都过一遍。无论你是刚接触Python的新手还是想找个周末项目练手的老玩家都能从这里拿到可以直接照着写的方案。我也会把安装过程中那些让人抓狂的问题——比如安装pygame时建立车轮失败——单独拿出来讲清楚毕竟环境配不好后面全是白搭。1. Pygame的游戏框架是怎么一回事1.1 它是一个库不是一个引擎很多人会拿Pygame和Unity、Godot这类游戏引擎比这其实不太公平。引擎是给你一个完整的房子你往里面摆家具而Pygame的定位更像是给你一堆砖头水泥房子得自己砌。它本身不提供场景编辑器、不提供可视化拖拽、也不帮你管理资源它给你的是一整套底层能力的Python封装窗口创建、图像加载与绘制、键盘鼠标事件、音频播放、碰撞检测以及最核心的——游戏循环的控制。这个定位反而很适合学习。因为当你用引擎拖一个碰撞体出来时你其实不知道背后发生了什么但用Pygame写一行rect.colliderect()你是在真正理解两个矩形区域是否相交这个几何判断。先理解原理再使用封装好的工具学习路径会更扎实。1.2 底层其实是SDLPygame的底层是C语言写的SDLSimple DirectMedia Layer库Pygame只是把它包了一层Python接口。这也是为什么Pygame的性能比纯Python的图形方案好很多因为真正耗时的图像绘制、像素操作都发生在C层。Python负责逻辑C负责渲染各干各的活。这里有个很关键的概念叫Surface。你可以把Surface理解成一块画布屏幕上显示的内容是主画布你加载的每张图片、每个文字渲染结果也都是一个Surface。游戏里做的所有事情本质上就是把这些Surface往主画布上摆然后一次性显示出来。理解了Surface你就理解了Pygame一半的绘图模型。1.3 什么场景适合用Pygame我自己的经验是下面这三类场景选Pygame非常合适。第一个是教学。给编程零基础的人讲循环事件状态机用文字讲十遍不如让他改一改游戏里的小球的移动速度来得直观。第二个是快速原型。领导说我想做个简单的小工具带个棋盘界面你用PyQt可能要查半天样式表用Pygame半小时就能拖出一个能点的交互界面。第三个是2D小游戏本身。2048、扫雷、俄罗斯方块、飞机大战这些经典游戏Pygame都能很舒服地实现——这些游戏的逻辑复杂度刚好在一个甜点区不太简单但也远没到需要引擎的程度。2. 环境准备先把安装这关过了2.1 安装前的两个检查先说结论Pygame对Python版本的要求并不苛刻官方建议Python 3.6以上实际上我在Python 3.9、3.10、3.11上都跑过都没问题。如果你用的是3.12或者更新版本可能会遇到某些第三方依赖还没适配的情况这时候优先建议装一个3.10或3.11的虚拟环境来用。安装前先做两个检查能省掉后面很多麻烦。第一个检查是确认pip本身能正常工作。打开终端输入pip --version如果提示找不到pip说明Python安装时没勾选添加环境变量或者用的是系统自带的Python。我遇到过好几次这种情况最简单的解法就是去Python官网重新安装一遍安装时务必勾选Add Python to PATH。第二个检查是确认网络能正常访问PyPI。国内用户如果直接pip装不动不是代码问题是网络问题。这时候换清华或阿里云的镜像源就行pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple2.2 安装pygame时建立车轮失败怎么破这个报错是热搜词里的重点我要单独拿出来讲。完整的报错信息一般是这样的Building wheel for pygame (pyproject.toml) ... error error: subprocess-exited-with-error很多人一看到Building wheel就慌了以为是自己代码有问题。其实这个提示的含义是pip在下载了Pygame的源码包之后发现当前环境里没有编译好的二进制安装包也就是wheel于是退而求其次想现场编译一个结果编译环境不齐全编译失败了。那问题来了为什么pip不直接用编译好的wheel非要现场编译这通常有两个原因。第一个原因是Python版本太新。新版本Python刚发布时Pygame的官方wheel还没来得及跟进pip只能退回到源码安装。第二个原因是操作系统架构比较特殊比如你在macOS上用了非Intel芯片或者在某些Linux发行版上官方没有提供对应的wheel。知道了原因解决办法就很明确了。首选方案是换一个Python版本比如3.10或3.11这个版本下Pygame的wheel非常齐全一行命令装完python3.11 -m pip install pygame第二个方案是安装编译前的系统依赖。在Linux上通常需要libsdl2、libsdl2-image、libsdl2-mixer、libsdl2-ttf这几个开发包在Ubuntu系上执行sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev装完再pip install就能编译成功了。Windows上如果遇到这个问题大概率是Python版本的问题换3.11基本能解决。还有个更省事的办法直接安装预编译版本。Pygame官方有一个包含所有依赖的完整包叫pygame-ce它是社区维护的版本同样支持Pygame的API安装体验更顺滑pip install pygame-ce如果只是体验学习用pygame-ce完全没问题代码写法跟Pygame一模一样。2.3 用conda或虚拟环境隔离我个人的习惯是但凡涉及图形库尽量在虚拟环境里装。因为pygame、pillow这类库依赖的底层动态库版本很容易跟其他项目冲突用虚拟环境隔离一劳永逸。用venv创建虚拟环境python3.11 -m venv mygame_envWindows下激活mygame_env\Scripts\activatemacOS/Linux下激活source mygame_env/bin/activate激活后pip install pygame就不会污染全局Python环境出问题了删掉文件夹重建就行成本很低。3. 先跑通第一个窗口程序3.1 别急着写游戏先把主循环跑起来Pygame里面最核心的概念不是某个函数而是游戏主循环这个流程。你写的所有游戏代码本质上都是在这个循环里反复执行的。我见过很多新手写代码喜欢把逻辑一条条顺序写下去结果发现窗口一打开就无响应或者闪一下就没了。原因就是缺少主循环——窗口的显示、事件的响应都需要在一个持续运行的循环里不断处理。一个最精简的主循环骨架长这样import pygame import sys pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(我的第一个Pygame窗口) # 主循环 while True: # 1. 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() # 2. 更新游戏状态暂时为空 # 3. 绘制画面 screen.fill((30, 30, 30)) pygame.display.flip()这个循环里做了三件事处理事件、更新状态、绘制画面。这三步就是整个游戏的心跳。事件是用户操作点关闭按钮、按键盘、动鼠标状态更新是游戏逻辑的计算小球位置移动、分数增加、血量减少绘制则是把最新状态画到屏幕上。3.2 帧率控制为什么需要Clock对象我最初写Pygame的时候没控制帧率结果小球的移动速度时快时慢——在性能好的电脑上快到看不清在性能差的电脑上慢到让人着急。原因很简单这个while循环的运行速度取决于CPU性能循环越快小球每次循环移动的固定距离就被执行得越频繁整体速度就越快。解决办法是用pygame.time.Clock()控制帧率clock pygame.time.Clock() while True: # ... 事件处理、状态更新、绘制 ... clock.tick(60)clock.tick(60)的意思是让这个循环每秒最多执行60次也就是说游戏帧率被锁定在60FPS。这样不管电脑性能如何游戏速度都是一致的。这里有个小技巧状态更新里的移动距离应该根据帧率来设计。比如想让小球每秒移动300像素而帧率是60FPS那么每帧移动的距离就是300/605像素。算清楚这个换算关系你就掌握了游戏速度控制的核心逻辑。3.3 键盘事件读取用户的输入一个游戏如果窗口一直安静地显示那还不算游戏。加上键盘交互就是一个分水岭。Pygame的键盘事件有两种写法。一种是事件触发式在循环里检查event.typefor event in pygame.event.get(): if event.type pygame.KEYDOWN: if event.key pygame.K_SPACE: print(空格键被按下了)另一种是状态查询式用pygame.key.get_pressed()获取当前所有按键的状态keys pygame.key.get_pressed() if keys[pygame.K_LEFT]: player_x - 5 if keys[pygame.K_RIGHT]: player_x 5两种方式的使用场景不同。需要响应按下这个瞬间动作的比如跳跃、发射子弹、暂停游戏用事件触发式需要持续判断某个键正在被按住的比如方向键移动角色用状态查询式。很多游戏里两者会混合使用这个习惯从一开始就可以养成。4. 做出第一个能玩的小游戏接球游戏4.1 游戏设计先想清楚再写代码讲完了基础我们做一个完整的小游戏。我选接球游戏而不选贪吃蛇原因是它涉及的要素足够全面但逻辑又足够简单一个挡板、一个下落的小球、一个得分计数器再加上游戏结束条件。游戏设计先写清楚玩家控制一个挡板左右方向键移动小球从屏幕顶部随机位置下落小球碰到挡板得分加1小球重新从顶部下落小球落到底部游戏结束这个设计涵盖了游戏开发里的几个核心概念玩家输入、物体的运动、碰撞检测、计分与游戏状态管理。做完这个后面做任何2D小游戏都八九不离十了。4.2 使用Sprite类管理游戏对象直接把游戏数据写成普通变量当然也能跑但游戏对象一多代码就会变得难以维护。Pygame提供了pygame.sprite.Sprite这个类用来封装游戏对象。我写游戏的习惯是每一个有独立行为的对象都继承Sprite。以挡板为例import pygame class Paddle(pygame.sprite.Sprite): def __init__(self, x, y, width100, height15): super().__init__() self.image pygame.Surface((width, height)) self.image.fill((80, 180, 250)) self.rect self.image.get_rect() self.rect.x x self.rect.y y self.speed 7 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 800: self.rect.x self.speed这里的关键是image和rect这两个属性。image是对象长什么样rect是对象在哪、占多大区域。Pygame的所有碰撞检测都是基于rect进行的。我给挡板设置了左右边界判断防止它移出屏幕这是很细节但很重要的体验点。小球稍微复杂一点因为它除了位置还有速度方向import random class Ball(pygame.sprite.Sprite): def __init__(self, width, height): super().__init__() self.image pygame.Surface((20, 20)) self.image.fill((200, 60, 60)) self.rect self.image.get_rect() self.rect.x random.randint(50, width - 50) self.rect.y 20 self.speed_y 5 def update(self): self.rect.y self.speed_y4.3 碰撞检测与分数逻辑碰撞检测是游戏编程绕不开的一环。Pygame里最常用的碰撞检测是矩形碰撞判断两个对象的rect是否相交。对2D小游戏来说矩形碰撞完全够用而且性能非常好。在接球游戏里每一帧都要判断两件事小球是否碰到了挡板小球是否超出了底部边界。if ball.rect.colliderect(paddle.rect): score 1 ball.rect.y 20 ball.rect.x random.randint(50, 750) if ball.rect.top 600: running False这段逻辑看起来简单但有个细节值得注意重新生成小球位置时我用random.randint重置了x坐标这样下一轮小球的落点位置是不同的游戏才有变化性。完整的游戏代码组织是这样的import pygame import sys import random pygame.init() WIDTH, HEIGHT 800, 600 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(接球小游戏) clock pygame.time.Clock() paddle Paddle((WIDTH - 100) // 2, HEIGHT - 30) ball Ball(WIDTH, HEIGHT) score 0 score_font pygame.font.Font(None, 36) running True while running: # 事件处理 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 更新 keys pygame.key.get_pressed() paddle.update(keys) ball.update() # 碰撞检测与计分 if ball.rect.colliderect(paddle.rect): score 1 ball.rect.y 20 ball.rect.x random.randint(50, WIDTH - 50) if ball.rect.top HEIGHT: running False # 绘制 screen.fill((20, 20, 30)) screen.blit(paddle.image, paddle.rect) screen.blit(ball.image, ball.rect) score_text score_font.render(f分数: {score}, True, (255, 255, 255)) screen.blit(score_text, (10, 10)) pygame.display.flip() clock.tick(60) pygame.quit() sys.exit()别看这个游戏小它已经把游戏循环、精灵、碰撞检测、事件处理、文本渲染这些核心模块都串起来了。把这个逻辑吃透后面往任何方向扩展都有底气。4.4 音效一个函数的事很多初学者忽略音效但游戏有没有声音体验完全是两个档次。Pygame的音频处理非常简单sound pygame.mixer.Sound(hit.wav) sound.play()在碰撞检测成功时调用sound.play()游戏的反馈感立刻就有了。音效文件可以使用免费的素材网站也可以用工具自己生成一段简单的提示音。注意音频格式Pygame对wav格式兼容性最好mp3在某些环境下可能会出问题。5. Pygame GUI给游戏加个像样的菜单界面5.1 用pygame.draw硬画UI是一种怎样的体验做游戏就绕不开界面。开头界面要有开始游戏退出按钮游戏结束要弹出重新开始提示。很多新手第一反应是用pygame.draw.rect画几个按钮然后判断鼠标点击位置。这个思路没错但实际写起来非常痛苦。因为你得手动管理按钮的位置、大小、颜色、鼠标悬停效果、点击判定一套UI写下来代码比游戏逻辑还多。而且这只是个开始——等你需要输入玩家名字、做设置界面的时候手写UI的工作量会成倍增长。我的建议是如果界面只是简单的几个按钮手写没问题一旦界面逻辑多了直接用现成的Pygame GUI库。5.2 常用的三个Pygame GUI库第一个是pygame-gui这是目前比较活跃的一个库提供按钮、文本框、下拉框、进度条等常用控件。它自带主题系统改样式不需要碰代码比较适合做游戏内的设置界面。第二个是pyperclip搭配pygame适合做文本输入比如排行榜输入玩家名字。pyperclip本身只是处理系统剪贴板配合自绘的输入框能快速实现可复制粘贴的文本输入功能。第三个是DearPyGui严格来说它不是Pygame专属但很多游戏工具类项目会拿它做调试面板。如果你的游戏开发流程里需要实时调参数小球速度、挡板大小用它比手搓调参界面高效得多。对于第一个游戏项目我建议先手写两三个按钮感受一下原理然后立刻上pygame-gui把省下来的时间花在游戏逻辑本身。6. 关于Pygame手机版免费真的能跑吗6.1 Pydroid 3Android上的Python环境搜Pygame手机版的人很多是想在手机上跑Python跑Pygame。先说结论能但体验和电脑端差很多适合演示和应急不适合作为主力开发。Android上最常用的方案是Pydroid 3。这个App自带Python解释器可以直接从内置界面里pip安装pygame然后在手机上运行Pygame程序。它的原理是在Android上构建了一个Linux运行环境SDL等底层库也能正常工作。我实测过简单的接球游戏帧率可以跑到30到60FPS完全能玩。不过要提醒几个坑。第一手机屏幕触控和电脑键鼠是完全不同的输入方式你在电脑上用方向键写的代码到手机上根本没法操作需要额外加触控逻辑比如pygame的touch事件或者读取手机传感器的接口。第二pygame窗口在Pydroid里的显示方式会有一定延迟写实时性要求高的游戏会有些不跟手。第三中文输入和文件管理在手机上比较别扭代码纠错也不方便。6.2 想真正在手机上跑还是得打包如果想把做好的游戏发给朋友在手机上安装那不能靠Pydroid得用打包方案。Android上最常用的免费方案是Buildozer配合Kivy一起用。Kivy本身跨平台能力强Pygame的代码迁移到Kivy需要改不少东西。也有一个比较成熟的工具叫pgs4aPygame Subset for Android是老项目了对新版Pygame支持有限不建议新手尝试。我的看法是如果目标是手机端就应该从一开始用Kivy或者Godot来开发而不是先写Pygame再想办法打包——技术选型这件事方向选对了比什么都重要。7. 常见问题与排查技巧实录7.1 Pygame窗口打开就闪退这是新手遇到最多的问题。排查顺序是这样先看代码里有没有pygame.quit()被提前调用再看主循环是否真的被进入。很多人在初始化窗口之后直接写了绘制代码但没有进入while循环窗口显示一帧就退出了。还有一个隐蔽的原因是pygame.init()没写。如果不调用初始化很多功能会直接报错或者窗口异常。写代码的固定顺序是import pygame、pygame.init()、创建窗口、进入主循环这个顺序别打乱。7.2 图片加载总是报错加载图片报错十次里有九次是路径问题。Pygame加载图片用的是相对路径或者绝对路径如果你在代码里写的是image.png但图片实际不在运行目录下就会报找不到文件。我的习惯是游戏的资源文件单独放一个assets文件夹然后代码里统一用os.path来拼接路径import os base_path os.path.dirname(__file__) image_path os.path.join(base_path, assets, image.png)这样不管在哪个目录运行路径都能找到。另一个常见问题是图片格式Pygame对png支持最好jpg也可以但要注意图片通道数RGBA的png才能显示透明效果。7.3 小球穿透挡板碰撞检测失效碰撞检测失效的情况我遇到过两次症状是小球速度很快的时候偶尔会出现穿过挡板直接掉出屏幕的情况。原因是小球移动速度太快两帧之间移动的距离超过了挡板的高度导致这一帧小球还没碰到挡板下一帧小球已经在挡板后面了。矩形碰撞检测是基于离散帧的它只知道这一帧和下一帧的位置中间发生了什么它不关心。解决这个问题的标准方案有三种。第一种是降低小球速度治标不治本。第二种是提高帧率比如从60FPS提高到120FPS能缓解但不彻底。第三种是使用连续碰撞检测即根据小球的移动向量判断它在移动轨迹上是否与挡板相交。Pygame里有pygame.sprite.spritecollide和pygame.sprite.collide_rect等函数但本质都是判断当前帧的位置。对于复杂项目可以考虑把移动追加减半来处理或者用扫掠线检测。对初学者来说我的建议是把速度控制在合理范围内同时保证每帧移动距离小于挡板的高度基本就不会遇到穿透问题。等以后做更复杂的物理逻辑时再引入专门的碰撞处理方案。最后说一点我个人的体会。Pygame被很多人低估恰恰是因为它上手太容易容易到让人觉得这不是正经做游戏的方式。但正因为它容易你才有机会把注意力完全放在游戏逻辑和编程思维上——事件驱动、状态管理、对象封装、碰撞几何这些技能迁移到任何语言、任何游戏引擎里都是通用的。我见过不少人从Pygame出发后来转做Unity、Godot或者前端游戏回头看都承认这段基础打得很值。如果你能把上面这个接球游戏完整写出来并且动手改一改小球速度、挡板宽度、计分规则你就算正式迈进游戏开发的门了。下一步可以试着加多几个小球、做关卡难度递增或者把这个思路迁移到经典游戏复刻上。周末花几个小时从环境搭建到一个小游戏跑起来这个成就感键盘敲下去那一刻你就知道了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进