ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Pygame实战指南:从零开发一个可玩的接苹果小游戏

Pygame实战指南:从零开发一个可玩的接苹果小游戏 第一次用Pygame写游戏是好几年前的事了。当时我刚学完Python的基础语法正愁不知道拿这门语言做点什么写完作业只能盯着黑底白字的控制台发呆。后来在论坛上看到有人用Pygame做出了一个火箭躲避小游戏我照着教程一行行敲敲到半夜屏幕上终于出现了一个会在窗口里动来动去的方块——那种“我能靠代码改变世界”的错觉瞬间就上头了。Pygame是Python生态里最适合新手入门的2D游戏开发库。它基于SDLSimple DirectMedia Layer封装用起来非常直接你画一个矩形它就出现在屏幕上你按一下方向键它就移动一格。它不会像Unity、Godot那样把渲染、物理、动画全都包装成黑盒而是把游戏运行最核心的“事件循环-状态更新-重新绘制”这套逻辑赤裸裸地摆在你面前。这篇文章我不打算讲高深的花活儿就把我实际做过的、踩过的坑、验证过好用的路子从环境准备到一个完整可玩的“接苹果”小游戏一步步拆给你看。如果你是零基础或者刚好学完Python但不知道下一步该干嘛这篇正好够用。1. Pygame到底是什么一个低调但好用的练手工具1.1 用生活类比理解Pygame的本质很多朋友一听到“游戏开发”脑子里就浮现出一堆炫酷的3D引擎、复杂的物理系统然后直接吓退。但Pygame完全不是这个路数。你大可以把Pygame理解成一套“手绘动画工具”就像小时候我们用透明纸一页一页画火柴人然后快速翻动画页让小人跑起来。Pygame做的就是这个事你把一张张画面画在一个叫Surface画布的东西上然后把这块画布显示到窗口里再把画面擦掉重画反复循环。只要循环够快——比如一秒钟60次——人类的视觉暂留就会把它看成连续动画。这套逻辑对应到代码里就是三件套任何一个Pygame程序都逃不掉初始化创建窗口、设置尺寸、准备字体和图片等资源。主循环不断监听键盘鼠标事件根据输入更新游戏里所有物体的位置和状态。绘制把所有物体画到屏幕上刷新显示进入下一帧。Pygame的价值在于它把所有底层的复杂操作——创建窗口、图像格式转换、键盘状态读取、定时器——都封装成了简单的函数和类你只需调用即可。它适合那些想理解“游戏到底是怎么跑起来的”的人比如刚开始学编程的学生、想做一些交互小项目的Python爱好者以及作为正式的图形界面编程入门之前的过渡工具。1.2 适合做什么不适合做什么我个人的建议是Pygame适合做2D小游戏、交互式教学演示、自己练手的项目原型比如贪吃蛇、打砖块、俄罗斯方块、接弹珠这类经典小游戏。它足够轻学习曲线平缓两天就能看到成果。而如果你想去开发3A大作、需要物理模拟的大型游戏那Pygame确实不太合适。它的渲染基于CPU光栅化性能天花板明显3D能力更是几乎没有。不过对新手而言“有天花板”反而是好事——在低地板、看清楚内部结构的领域里你能把游戏开发的基本功练扎实再转头去学Unity、Godot时很多概念其实是打通的。我遇到过不少朋友问既然将来要做商业游戏为什么不直接从Unity开始我的观点是直接上手大引擎当然可以但引擎会把很多概念抽象成“拖拽组件”和“可视化配置”你很容易学会了点按钮却没理解背后的机制。Pygame把概念还原得很原始它逼着你用代码思考“物体在哪里”“速度是多少”“是否发生了碰撞”这些基本功在大引擎里一样有用只是被隐藏了。所以Pygame最适合的定位是入门启蒙、把编程和图形界面结合、建立对游戏循环的直觉。2. 环境准备与安装别踩我踩过的坑2.1 安装前先确认Python版本安装Pygame本身是件简单事但对零基础的人卡在安装阶段是很常见的。先确认你已经安装了Python。建议用3.8到3.12之间的版本太老的版本可能没有对应Pygame的轮子太新的版本偶尔会遇上兼容性问题。可以用命令行验证python --version如果提示找不到命令说明Python不在环境变量里。在Windows上最简单的做法是重新安装Python并勾选“Add Python to PATH”macOS用户建议通过Homebrew安装Linux用户则一般自带或者用包管理器安装。版本确认无误后我强烈建议建一个虚拟环境别把包一股脑装到全局环境里。多个项目之间的依赖不会互相打架这个习惯越早养成越好python -m venv game_envWindows下激活虚拟环境game_env\Scripts\activatemacOS或Linux下source game_env/bin/activate激活成功以后命令行前面会多出一个小括号提示说明你现在已经在一个独立的环境里了。然后安装Pygamepip install pygame装完之后顺手验证一下能否正常导入import pygame print(pygame.version.ver)能打印出版本号比如2.6.1就说明安装成功了。这一步千万不能跳我见过至少三五个朋友安装时提示成功但一运行程序就报“ModuleNotFoundError”就是因为在全局环境里装的而解释器跑到虚拟环境里去了。2.2 安装太慢或失败的解决办法如果你是国内网络环境pip官方源的下载速度可能会让你怀疑人生甚至直接超时。这种情况不是Pygame的问题而是网络链路的原因。解决办法很简单换一个国内镜像源。使用清华源或者阿里源安装pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple我这里多说一句很多教程会让你永久修改pip默认源把配置文件改成一个镜像地址。但我觉得初学者不用一上来就改全局配置用一个临时参数-i即可后面如果确实每次都很慢再去永久配置也不迟。另外还有一个细节如果你看到网上有pygame-ce这个包它是Pygame社区维护的版本功能和接口基本兼容修复了很多老版本Bug某些场景下性能还更好。对新手而言直接用官方的pygame就行不用纠结。等以后你要用某个新特性发现官方包缺失时再切换到pygame-ce迁移成本也很低。2.3 开发工具代码编辑器和运行方式写Pygame代码并不需要多复杂的IDE。我自己开始用的是VSCode后来觉得对小项目而言Thonny这种极简编辑器对新手更友好因为它能直接看到变量值变化。当然你用任何能编辑Python文件的工具都行哪怕记事本也能跑但建议至少配一个带语法高亮的编辑器。运行Pygame程序时直接python 你的文件名.py即可。这里有个新手容易困惑的点如果你在IDE里点击“运行”按钮有的IDE会打开一个内置终端Pygame窗口可能显示不出来或无法交互。遇到这种情况优先检查是不是终端类型的问题有些IDE需要配置为“在外部终端运行”或“集成终端”。3. 搭建游戏骨架事件循环与主框架3.1 初始化窗口和主循环的底层逻辑Pygame程序最基础的结构长这样你先不用管每一行具体什么意思先整体感受一下import pygame pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(我的第一个Pygame程序) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False screen.fill((0, 0, 0)) pygame.display.flip() pygame.quit()这段代码虽然只有一个黑色窗口但它已经包含了你写的任何一个游戏都会有的骨架。pygame.init()负责初始化Pygame的所有模块包括显示、事件、字体、混音等虽然你也可以只初始化某个特定模块但新手阶段一把梭没毛病。pygame.display.set_mode创建窗口返回值是一个Surface对象你后续所有画面绘制都要在这个Surface上进行你可以把它理解成一块可以任意涂改的大画布。主循环是整个游戏的心脏它的作用是反复执行以下三件事处理事件监听并响应用户的输入比如关闭窗口、按键盘、点鼠标。更新游戏状态根据输入改变变量值比如玩家的位置、敌人的血量、计分数字。重新绘制把所有物体的新位置画到画布上再flip()刷新窗口显示。这三件事循环得越快游戏越流畅。3.2 理解事件Event、帧率FPS和时钟事件这个概念是Pygame里最核心的机制之一。你可以把事件理解成系统的“小纸条”用户点击了关闭按钮系统就说“我要退出啦”往事件队列里塞一张纸条用户按下方向键系统又塞一张“方向键被按下”的纸条。程序每轮循环通过pygame.event.get()把这些纸条一次性全取出来逐张处理。打个比方就像食堂打饭的窗口厨师把炒好的菜都放在出餐口服务员一次性端走分发而不是炒一个端一个。另一个关键概念是帧率FPS。上面那个循环虽然看起来是死循环但如果电脑性能好它一秒钟可能跑几百遍性能差一点就只有几十遍。这就导致一个问题如果运动速度是按“每帧移动距离”来计算的同样的代码在不同电脑上会跑出完全不同的速度。解决办法就是用pygame.time.Clock()来控制帧率比如强制让循环每秒最多跑60次clock pygame.time.Clock() # 在主循环末尾加上 clock.tick(60)tick(60)的意思是“让这一帧尽量控制在1/60秒左右”如果代码跑得太快它会主动休眠一小段跑得太慢它也不会加快只是让实际帧率低于目标值。我在实际开发中通常把60作为标准值它既能保证动画足够顺滑又不会让CPU无谓地跑满。3.3 画点什么到屏幕上draw与blit的区别代码里我用了screen.fill((0, 0, 0))把背景清成黑色这一步很多人会忽略但它是必须的。如果不清屏上一帧的内容会残留在画布上画面会变得跟涂鸦一样一团糟。清屏之后你就可以开始画各种图形和图像了。Pygame画图像的核心方法有两类一类是pygame.draw模块用来画基础图形比如矩形、圆形、直线另一类是blit()方法用来把一整张图片贴到画布上。区别在哪draw.rect是“用代码现画一个纯色矩形”而blit是“把一张已经存在图片抠下来贴上去”它的计算成本更高但能表达更多细节。新手阶段用draw画矩形和圆形就够了等以后再加图片资源也不迟。pygame.display.flip()会把整块画布一次性刷新到屏幕上这是双缓冲机制的标准操作。还有一个小兄弟叫pygame.display.update()它只刷新你指定的区域性能更优。但对小游戏来说flip()一个简单有效的选择不用纠结。4. 让游戏“活”起来输入、移动与碰撞4.1 使用pygame.event.get()处理单次按键搞定窗口之后下一步就是让游戏里的东西响应你的操作这可以说是从“静态演示”跨到“互动游戏”的分水岭。处理键盘输入有两条路单次按键和持续按键。单次按键用事件循环里的KEYDOWN事件适合那种“按一下触发一次动作”的场景比如按空格键跳跃、按回车确认菜单选项。示例代码for event in pygame.event.get(): if event.type pygame.KEYDOWN: if event.key pygame.K_SPACE: print(跳跃)这里的event.key就是事件携带的信息——你按下的是哪一个键。Pygame为所有键定义了常量比如K_LEFT是左方向键、K_a是字母A。4.2 使用pygame.key.get_pressed()处理持续按键但如果你玩的是赛车游戏或者飞机躲避游戏按住方向键期间物体要持续移动用KEYDOWN就会很别扭因为按键只会在按下的那一刻触发一次事件。正确的做法是用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这就像你按住水龙头开关不放水就一直流松开手水就停。让我用更形象的类比事件型按键像门铃按一下响一声持续型按键像水龙头开着就有水流。你需要预判自己的游戏逻辑属于哪一种或者两者结合——跳跃用事件型移动用持续型这是游戏开发的常见组合。移动速度这里直接写死player_x - 5在60帧的时钟下那这个方块一秒移动300像素。如果后面帧率变了移动速度也会变。更严谨的做法是把速度乘以dt上一帧到这一帧的时间间隔但对新手来说先设定固定帧率再写固定速度问题不大。4.3 碰撞检测矩形判定是最实用的选择游戏里几乎一定会遇到“两个物体碰到一起”的情况子弹击中敌人、苹果落进球筐、角色撞到墙壁。Pygame提供了基础的碰撞检测API其中最常用的是pygame.Rect的colliderect方法rect1 pygame.Rect(100, 100, 50, 50) rect2 pygame.Rect(120, 120, 30, 30) if rect1.colliderect(rect2): print(碰撞发生了)这里的逻辑是给每个物体一个矩形包围盒比如一个苹果占一个40x40的区域一个挡板占一个120x20的区域如果两个矩形有交集就说明它们碰上了。我知道你在想什么——那如果物体的形状不是矩形呢比如一个圆形苹果戳到不规则障碍物确实不够精确但绝大多数2D小游戏根本不需要像素级碰撞。把每个物体理解成一个“快递盒子”快递堆里只要两个盒子的外轮廓有重叠就可以认为碰撞了。这么做成本极低、实现简单玩家在高速画面中也根本感知不到那几像素的误差。另一个常用方法是pygame.sprite.spritecollide它处理“一个精灵跟一组精灵”的批量碰撞后面我会在完整案例里用到。4.4 用类组织代码精灵Sprite的妙处当你只有一个方块在跑时写一堆全局变量还能忍受。但游戏一旦超过三四个物体变量就会爆炸你会有player_x、apple_x1、apple_x2、enemy_speed……改一个变量名牵一发动全身迟早疯掉。这时候就该引入Pygame的精灵类pygame.sprite.Sprite了。精灵就是一个游戏角色的抽象概念。你可以定义一个类把它的坐标、速度、外观、更新逻辑全都打包一起class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.Surface((60, 20)) self.image.fill((0, 255, 0)) self.rect self.image.get_rect() self.rect.x 370 self.rect.y 550 def update(self): keys pygame.key.get_pressed() if keys[pygame.K_LEFT]: self.rect.x - 5 if keys[pygame.K_RIGHT]: self.rect.x 5这段代码里最关键的约定是所有精灵都必须有image和rect两个属性image是你画出来的或者加载的图片rect是它在场景中的位置矩形。Pygame的精灵组pygame.sprite.Group会自动调用组里每个精灵的update()方法并且能画出全组精灵这样主循环会变得极简all_sprites pygame.sprite.Group() player Player() all_sprites.add(player) # 主循环中 all_sprites.update() all_sprites.draw(screen)这种“对象化”的思维是游戏开发里的基本素养也是Pygame留给你最重要的东西之一。等以后转到其他框架或引擎你会发现概念是相通的。5. 从零写一个可玩的“接苹果”小游戏5.1 设计规则和素材准备前面讲了一堆概念现在我们来实战一把。我这次做的是一个“接苹果”小游戏屏幕顶部不断往下掉苹果底部有一个挡板玩家用左右方向键移动挡板接住苹果每接一个得1分漏掉一个扣1条命命掉光游戏结束。这个游戏麻雀虽小但五脏俱全它涵盖了移动、碰撞、计分、游戏状态切换这些基础机制最关键的是它只用十几行核心代码就能把完整流程跑通。素材方面我全部用pygame.draw画图搞定不需要准备任何外部图片文件这样也避免了图片路径加载的坑。苹果用红色圆代替挡板用绿色矩形背景用浅灰色。分数显示则调用pygame.font.SysFont。记住第一版能跑起来最重要美术美化永远是最后一步。5.2 关键功能分步实现我先拆开讲四个关键功能最后再给完整代码。挡板的移动其实就是对一个矩形的位置变量做增减。帧率固定为60的情况下速度设为7左右手感比较好import pygame pygame.init() SCREEN_WIDTH 600 SCREEN_HEIGHT 700 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) clock pygame.time.Clock() player_x 270 player_width 60 player_height 20 player_speed 7苹果的生成可以用一个简单的列表管理每个苹果是一个矩形和速度的组合。每隔一段时间就append一个新苹果然后在每帧中让它的y坐标增加如果超出屏幕底部就移除apples [] if frame_count % 30 0: apples.append(pygame.Rect(random.randint(20, SCREEN_WIDTH - 20), 0, 20, 20)) for apple in apples[:]: apple.y 5 if apple.y SCREEN_HEIGHT: apples.remove(apple) lives - 1计分和生命值的显示则需要一个能渲染文字的函数font pygame.font.SysFont(SimHei, 30) text_surface font.render(f分数: {score} 生命: {lives}, True, (0, 0, 0)) screen.blit(text_surface, (10, 10))font.render这个方法接收三个参数要显示的字符串、是否抗锯齿、颜色。注意它返回的也是一个Surface所以你依然用blit贴到屏幕上。5.3 完整代码整合下面是完整的可运行代码你可以直接复制到一个文件里跑建议文件名取catch_apple.pyimport pygame import random pygame.init() SCREEN_WIDTH 600 SCREEN_HEIGHT 700 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(接苹果小游戏) clock pygame.time.Clock() # 挡板参数 player_width 80 player_height 20 player_x (SCREEN_WIDTH - player_width) // 2 player_y SCREEN_HEIGHT - 30 player_speed 7 # 苹果参数 apple_width 20 apple_height 20 apple_speed 5 apples [] spawn_counter 0 score 0 lives 3 font pygame.font.SysFont(SimHei, 30) running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 挡板移动 keys pygame.key.get_pressed() if keys[pygame.K_LEFT] and player_x 0: player_x - player_speed if keys[pygame.K_RIGHT] and player_x SCREEN_WIDTH - player_width: player_x player_speed # 生成新苹果 spawn_counter 1 if spawn_counter % 30 0: apple_x random.randint(0, SCREEN_WIDTH - apple_width) apples.append(pygame.Rect(apple_x, 0, apple_width, apple_height)) # 更新苹果位置检测碰撞 player_rect pygame.Rect(player_x, player_y, player_width, player_height) for apple in apples[:]: apple.y apple_speed if apple.colliderect(player_rect): apples.remove(apple) score 1 elif apple.y SCREEN_HEIGHT: apples.remove(apple) lives - 1 # 游戏结束 if lives 0: running False # 绘制画面 screen.fill((220, 220, 220)) pygame.draw.rect(screen, (0, 180, 0), player_rect) for apple in apples: pygame.draw.rect(screen, (200, 0, 0), apple) text_surface font.render(f分数: {score} 生命: {lives}, True, (0, 0, 0)) screen.blit(text_surface, (10, 10)) pygame.display.flip() clock.tick(60) pygame.quit()跑一下试试。你应该能看到一个灰色窗口绿色的挡板在底部红色的苹果不停往下掉接住它分数增加漏掉它生命减少。虽然画面简陋但一个游戏该有的要素已经全齐了。5.4 运行测试与调参心得第一次跑通这个程序只需要几分钟但真正花时间的是调参数。我当时调了三个关键数值apple_speed从3调到5太快的话玩家反应不过来太慢又没挑战。spawn_counter % 30的30决定生成频率也就是每30帧生成一个苹果。调成20会明显更密集。player_speed从5调到7因为屏幕宽度只有600速度太慢会让玩家追不上苹果。我的建议是把这些参数都写在一个变量里别直接在代码里散着写5、30。这样你想改手感时不用满屏找数字。另外需要注意的是这个版本的苹果生成时机是固定的玩久了会形成节奏记忆。你可以在生成间隔里加一点随机量比如random.randint(20, 40)游戏的变数会多很多。6. 实战中常见问题与排查技巧6.1 安装与启动阶段的典型问题我自己刚起步时包括帮别人看代码时遇到过太多了。以下这几种情况是最典型的我直接做成一个速查表问题现象可能原因解决办法ModuleNotFoundError: No module named pygame装到了别的Python环境或没装成功pip list看是否安装确认虚拟环境是否激活窗口一闪而过自动关闭主循环没有正确写程序直接跑到pygame.quit()检查while running循环是否遗漏窗口出现但卡死无法关闭事件处理缺失或死循环里没有取事件确认pygame.event.get()在主循环内每帧调用字体无法显示中文系统默认字体不支持中文换成pygame.font.SysFont(SimHei, 30)或使用图片文字6.2 运行时表现异常与处理还有一种情况程序能跑但画面表现不对。比如苹果消失瞬间还保留残影那就是每帧开头没有清屏或者清除的位置不对。检查你的screen.fill是否在绘制之前调用。再比如游戏速度时快时慢多半是没有用clock.tick(60)锁帧显示器频率和程序循环速度不一致就会让动画一顿一顿的。我也遇到过不少逻辑Bug最典型的是“苹果被接住后score没有增加”。排查方法很简单在碰撞分支里加一行print(接到)看一下是否真的进了这段逻辑。很多时候不是碰撞判定有问题而是苹果列表在遍历时被修改导致漏判。比如for apple in apples:同时又在循环里removePython会报错或漏项正确的做法是用for apple in apples[:]遍历副本或者倒序遍历。6.3 排查思路从现象到根因我给新手的建议永远是“最小化复现”。当你发现一个Bug先别急着在完整代码里翻试着把它简化成一个最小程序只保留主循环和出问题的那个功能。比如苹果不动那就把画出苹果的代码单独提出来固定死在屏幕上位置输出——如果显示正常问题在更新逻辑如果显示都不正常问题在绘制逻辑。这样一层层缩小范围比我隔空猜你要快得多。我自己调试时最常用的工具其实就是print()往关键位置打印变量值。Pygame本身没有内置强大的断点调试工具但这种逐行打印的方式对小项目完全够用。等将来项目变大你会发现单元测试和结构化日志更有效但那是后话你现在只需要记住先跑通主干再逐层加功能。7. 我的个人经验与再往前走一步的方向做完这个“接苹果”之后我的第一个感觉是“原来做游戏的关键不在于画出多好看的图而在于让循环转得漂亮。”这个循环思维后面在学AI、写后端服务时反复都有用。Pygame虽然只适合做小项目但它逼你理解程序运行的本质这是它送给你最值钱的东西。如果文章中这个简单版本你已经跑通了我个人推荐的三个扩展方向是给苹果加随机下落速度甚至让少数苹果变成“坏苹果”——碰到会扣分增加策略性。加入开始界面和结束界面用状态机管理游戏阶段。这是很多小游戏进化的关键一步。学习和使用pygame.sprite.Group重构代码把挡板和苹果都变成精灵代码结构会清晰很多也为以后做更复杂的游戏打基础。最后分享一个小技巧如果你以后想要一个能发给朋友玩的独立程序可以用pyinstaller把Python脚本打包成exe。打包时记得把pygame相关的资源文件一起带上另外打出来的包可能会比较大因为Pygame自带的依赖占了不少体积不要惊讶。我做第一个成品的时候看到打包完的文件夹整整几十兆愣是被吓了一跳。Pygame这条路入门不难难的是把一个简单玩法打磨出“手感”。我至今还记得第一次让苹果“啪”地撞上挡板、分数跳上去那一刻的快乐。如果你也把代码敲出来了跑去试试那几行参数找到你的第一版“手感”那种快乐你马上就能体会到了。
返回列表