☰
Pygame入门实战:从安装到用Python写一个接苹果小游戏
2026/9/29 16:18:40 网站建设 项目流程

先说结论:只要你想用Python写一个"键盘能控制、画面会动、玩起来有反馈"的小程序,Pygame就是性价比最高的选择。它不是功能最强的引擎,跟Unity、Godot比不了画面表现力,但它的定位从来不是做大制作,而是让你用最少的代码量,把游戏循环、事件处理、碰撞检测这些所有游戏都绕不开的核心机制彻底搞明白。这篇文章就从pygame安装开始,一路做到一个完整的接苹果小游戏——玩家可以左右移动、苹果从天上掉下来、接住加一分、漏掉三次就结束、中途还带音效和难度递进。代码全程不超过两百行,你跟着跑完,就会拥有第一个真正属于自己的小游戏。

1. Pygame安装:很多人还没写第一行代码就卡在这

"pygame安装"一直是我见过搜索量最高的Python入门词之一,原因很简单:官方文档把安装写得像一行命令的事,但实际操作里Python环境、pip、网络、权限四处都可能出问题。我不想一上来就丢一句"pip install pygame"给你,先把环境这关彻底过掉。

1.1 前置检查:Python版本与pip的配合

安装之前先确认三件事,缺一不可。第一,Python版本,建议用Python 3.8到3.12之间的稳定版,Pygame 2.x对Python 3.8以上支持最稳,太老的版本比如3.6以前,会遇到某些预编译轮子缺失的问题。第二,确认pip可用,在终端里执行python --version和python -m pip --version,两条命令都能正常输出版本号,再往下走。第三,检查Python是64位还是32位,我的建议是无脑用64位,32位环境在部分系统上会有莫名其妙的内存限制和兼容性毛病。

有些人是装了Anaconda的,我给游戏开发单独建一个干净的虚拟环境,别把pygame往基础环境里塞。Anaconda自带的环境通常依赖很多,装游戏库的过程中万一和conda的包管理器冲突,排查起来相当费时间。用这条命令建环境,进去之后安装会清爽很多:

python -m venv game_env # Windows下进入环境 game_env\Scripts\activate # macOS / Linux下进入环境 source game_env/bin/activate

1.2 安装命令、镜像源与三次常见报错

在虚拟环境下,安装命令就是这一条:

python -m pip install pygame

直接敲pip install pygame也行,但用python -m pip更稳妥,它可以避免"pip命令指向了另一个Python"这种诡异问题。国内网络环境下直接安装偶尔会超时,我一般顺手加上清华镜像源:

python -m pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple

安装过程中最常遇到的三类报错,我整理成一张表,方便你对照排查:

报错表现根本原因处理方式
ReadTimeoutError / timeout连默认源网络不畅加-i镜像源参数重新安装
Could not find a version that satisfies the requirementpip版本太旧,或Python版本过老先执行python -m pip install --upgrade pip
Permission denied系统目录无写入权限Windows用管理员终端;macOS/Linux加--user参数

还有个细节很容易忽略:安装完之后,很多人会去pip list里找pygame确认,但有时候pip list显示安装成功,import却报错。这种"装上了却不能用"的假象,通常是因为你import用的Python和你pip装包的不是同一个环境。所以验证安装是否成功,别只看列表,要直接跑一段import代码。

1.3 验证安装:跑一个最小窗口

新建一个文件,叫test_pygame.py,写上这段:

import pygame pygame.init() screen = pygame.display.set_mode((640, 480)) running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False pygame.quit()

这段能弹出一个640x480的黑色窗口,并且点关闭按钮后能正常退出,说明安装链路全通了,可以正式进入开发环节。我看到不少新手在这儿遇到"窗口一闪而过"或者"进程不退出"的问题,前者的原因通常是代码最后没加pygame.quit(),后者通常是事件循环里忘了处理QUIT事件。这段不是浪费时间的形式,它同时验证了Python解释器、Pygame动态库、显示设备三个层面的连通性。

2. 游戏循环是Pygame的发动机:搞懂它等于学会一半

接苹果游戏虽然小,但它五脏俱全。在写完整代码之前,我想先把Pygame最核心的机制讲透,因为很多人的第一个游戏不是难在API,而是难在"程序到底是怎么做到画面持续更新、还能同时响应键盘的"。

2.1 初始化与窗口创建的底层逻辑

pygame.init()这行听起来像句废话,实际做的是把Pygame背后所有模块统一初始化,包括显示、音频、字体、时钟等。它执行一次就够;如果你单独用音频或字体,也可以分别调用pygame.mixer.init()和pygame.font.init(),但一般不需要这么细。

真正关键的是pygame.display.set_mode((width, height))。这行代码会调用底层的SDL,在操作系统里创建一个真正的窗口,同时返回一个Surface对象,也就是"画布"。Pygame里所有可见的东西——矩形、圆形、图片、文字——本质上都是画到这张画布上,最后再一次性提交到屏幕上。理解这一点,你就懂了为什么每次绘制前要用screen.fill()清屏:因为上一帧的图案还留在画布上,不清掉就会产生拖影。

搞明白Surface的概念之后,你再看网上那些"加载图片"、"渲染文字"的代码就不会懵了。pygame.image.load()返回的是一张Surface,font.render()返回的也是一张Surface,它们都只是"画布上的图层",用blit()贴到主画布上,你的脑子里要始终有这个画面:所有东西都是往同一张画布上贴。

2.2 事件队列:键盘和鼠标是怎么进来的

窗口创建完毕,程序就进入一个死循环,这就是游戏循环。每轮循环里做三件事:处理输入、更新逻辑、绘制画面。Pygame把所有外部输入(按键、鼠标、关窗、缩窗)都打包成"事件"放进一个队列,你的程序通过pygame.event.get()把这个队列里的东西取出来。

事件分两大类。第一类是离散事件,比如"按了一下空格键"或者"点了关闭按钮",这类事件用for event in pygame.event.get()判断event.type;第二类是连续状态,比如"左箭头键一直按着",这时更适合用pygame.key.get_pressed(),它返回一个包含所有按键当前状态的元组,每帧查这个元组,就能实现按住持续移动的效果。

有个新手常掉的坑:把键盘事件写成if event.key == pygame.K_LEFT就直接移动一格。event是从事件队列里取出来的"那一下",不是"持续的状态",所以这种写法只能按一下动一下。要实现连续移动,必须用key.get_pressed(),这也是接苹果游戏玩家控制的核心。

2.3 帧率与update/draw分离

游戏画面不是凭空出现的,它是由无数张静态画面快速切换形成的。每个循环周期就是一帧,clock.tick(60)的作用是把每秒帧数限制在60次。它内部会计算上一帧到现在的时间差,然后sleep掉多余的时间,避免游戏在120Hz显示器上跑得像火箭一样快,也避免不同性能的机器表现不一致——有的电脑上苹果掉得飞快,有的电脑上慢吞吞,这个体验差异就是用tick统一帧率解决。

一个典型的循环骨架是这样的:

while running: # 1. 处理事件 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 2. 更新逻辑:根据键盘状态改变位置、生成苹果、检测碰撞 # 3. 绘制画面:先填充背景,再画所有图形 # 4. 提交画面 pygame.display.flip() clock.tick(60)

pygame.display.flip()是把画布内容真正推到屏幕的命令,相当于摄影里的"快门释放"。我习惯把"更新逻辑"和"绘制画面"分成两个函数写,后期加功能时改动不会互相干扰。你在网上看到的很多教程会把游戏对象包装成类,不管形式怎么变,内核都是这个四步循环。

3. 接苹果小游戏:从空目录到一个能玩的完整程序

3.1 先拆需求:四个模块,一个都不能少

动手写码之前,先把"接苹果"翻译成程序能理解的模块。我习惯拆成四块:玩家,一个可以在底部左右移动的矩形,代表篮子;苹果,从屏幕顶部随机位置出现、匀速下落的圆形;规则,接住一个苹果加一分,漏掉三个游戏结束;反馈,屏幕左上角实时显示得分。分完模块,写代码就是四个小功能拼装,心智负担小很多。很多人一上来就画美术图、写音乐,结果核心逻辑还没跑通,这就是本末倒置。

3.2 玩家移动:连续按键与边界钳制

玩家只需要两个逻辑:根据键盘方向移动,以及不跑出屏幕。位置用player_x表示,每帧根据按下的方向增减player_speed,然后用max和min把坐标限制在屏幕范围内:

keys = pygame.key.get_pressed() if keys[pygame.K_LEFT]: player_x -= player_speed if keys[pygame.K_RIGHT]: player_x += player_speed player_x = max(0, min(WIDTH - player_w, player_x))

这一行max(0, min(...))就是边界钳制。有些游戏会让玩家角色"穿墙",从左边消失、右边出现,那是一种风格;但接苹果是"守着篮子接东西"的玩法,边界钳制更符合直觉。WIDTH和player_w分别是屏幕宽度和篮子宽度,这样写代码,屏幕尺寸改动时,玩家的活动范围会自动跟着适配。

3.3 苹果的生成与下落:从随机数到列表

苹果的要点是"生成频率"和"下落速度"。每一帧我用random.random()生成一个0到1之间的随机数,当它小于某个阈值时就生成一个新苹果。这个阈值是生成概率,0.02表示平均每帧有2%的概率冒出一个新苹果,60帧每秒换算下来大约每秒出一两个。这个值就是后续调整难度的重要手柄:

if random.random() < 0.02: apple_x = random.randint(apple_radius, WIDTH - apple_radius) apple_list.append([apple_x, 0])

这里的apple_x预留了圆形半径的边距,否则苹果会卡在屏幕边缘,视觉上半颗圆被裁掉,很出戏。苹果的y坐标从0开始,每帧加apple_speed,就实现了匀速下落。为什么用列表存苹果而不是单独一个变量?因为游戏允许屏幕上同时存在多个苹果,列表天然适合管理这种数量不固定的对象。后面如果要加"金苹果"、"炸弹"之类的新元素,也是同样的思路,往列表里塞新类型就行。

3.4 碰撞检测:矩形相交的精度够用吗

接苹果的核心判定是玩家的篮子是否接住了苹果。Pygame给矩形提供了现成的colliderect方法,所以我用玩家的矩形和苹果的外接矩形做判断:

player_rect = pygame.Rect(player_x, player_y, player_w, player_h) fruit_rect = pygame.Rect(apple[0] - apple_radius, apple[1] - apple_radius, apple_radius * 2, apple_radius * 2) if player_rect.colliderect(fruit_rect): apple_list.remove(apple) score += 1

这里的精度问题在于:苹果是个圆形,用外接矩形去碰撞,会让"空的四个角"也算命中。实际游戏里这个误差几乎没人感知得到,苹果直径也就30像素左右,四个角最多影响两三像素的判定,完全可以接受。如果你追求精准,可以改用圆心距离判断:math.hypot(apple_center_x - player_center_x, apple_center_y - player_center_y)是否小于半径之和。但对于第一个小游戏,colliderect的简洁和性能就足够了,先跑起来,再谈精度。

还有一个小坑要注意:遍历列表的同时删除列表元素,容易跳过元素甚至报错。我的写法是for apple in apple_list[:],先复制一份遍历,在原件里删除,这是Python入门阶段非常实用的小技巧。

3.5 完整代码串联与运行效果

把所有模块拼到一起,完整代码就是下面这段,每段功能注释都写在代码里:

import pygame import random pygame.init() WIDTH, HEIGHT = 640, 480 screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("接苹果小游戏") player_w, player_h = 80, 20 player_x = WIDTH // 2 - player_w // 2 player_y = HEIGHT - player_h - 30 player_speed = 8 apple_list = [] apple_radius = 15 apple_speed = 3 score = 0 font = pygame.font.SysFont("microsoftyahei", 28) clock = pygame.time.Clock() 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]: player_x -= player_speed if keys[pygame.K_RIGHT]: player_x += player_speed player_x = max(0, min(WIDTH - player_w, player_x)) if random.random() < 0.02: apple_x = random.randint(apple_radius, WIDTH - apple_radius) apple_list.append([apple_x, 0]) for apple in apple_list[:]: apple[1] += apple_speed if apple[1] > HEIGHT: apple_list.remove(apple) print("游戏结束!得分:", score) running = False player_rect = pygame.Rect(player_x, player_y, player_w, player_h) for apple in apple_list[:]: fruit_rect = pygame.Rect(apple[0] - apple_radius, apple[1] - apple_radius, apple_radius * 2, apple_radius * 2) if player_rect.colliderect(fruit_rect): apple_list.remove(apple) score += 1 screen.fill((30, 30, 40)) pygame.draw.rect(screen, (200, 200, 200), (player_x, player_y, player_w, player_h)) for apple in apple_list: pygame.draw.circle(screen, (220, 60, 60), (apple[0], apple[1]), apple_radius) score_surface = font.render("得分: " + str(score), True, (255, 255, 255)) screen.blit(score_surface, (10, 10)) pygame.display.flip() clock.tick(60) pygame.quit()

运行时你会看到:一个灰色篮子跟随方向键左右移动,红色苹果从顶部随机落下,接住后左上角分数加一,漏掉任何一个苹果,程序打印得分并退出。到这里,你已经有了一个结构完整、能跑能玩的Pygame小游戏。骨架没问题了,接下来就是往里面加"游戏感"。

4. 从"能玩"到"好玩":三个最容易忽略的细节

4.1 中文字体渲染:别看小,坑却很深

很多初学Pygame的人第一次用font.render显示文字都成功了,但一换成中文,屏幕上直接出现方框或者乱码。原因是Pygame默认字体不支持中文。我上面代码里用了pygame.font.SysFont("microsoftyahei", 28),这在Windows上能正常显示微软雅黑,但在macOS和Linux上大概率失败。

更通用的写法是先让系统帮你匹配一个可用的中文字体:

chinese_font = pygame.font.match_font(["microsoftyahei", "simhei", "notosanscjk"]) font = pygame.font.Font(chinese_font, 28) if chinese_font else pygame.font.Font(None, 28)

match_font会按你给的字体名列表逐个查找,找到第一个系统里存在的字体;如果返回None,再用默认字体兜底。也可以用pygame.font.get_fonts()把所有可用字体打印出来,对着自己的系统环境挑一个。这个坑我在第一次写中文游戏时就踩过,当时还纳闷"英文能显示中文为什么不能",后来查了一圈才知道是字体文件的问题。涉及中文显示的项目,我建议直接把这个字体匹配函数封装成工具,每次都用同一段逻辑,能省不少事。

4.2 音效的最小实现:用pygame.mixer搞定

音效和音乐是"游戏感"的重要来源,但Pygame的音频部分被很多人跳过。最简路径是:在pygame.init()之外单独初始化mixer,然后加载.wav或.ogg文件,在事件触发时播放:

pygame.mixer.init() catch_sound = pygame.mixer.Sound("catch.wav") miss_sound = pygame.mixer.Sound("miss.wav") # 接住苹果时: catch_sound.play() # 漏掉苹果时: miss_sound.play()

需要注意两点。第一,pygame.mixer.Sound支持.wav和.ogg,不直接支持mp3,想用mp3得靠pygame.mixer.music.load,但music模块同一时间只允许一个音乐流。第二,init时如果返回错误码或者抛异常,多半是系统没有音频设备,在服务器或者某些精简版虚拟机上会遇到,本地桌面环境基本无障碍。实在找不到音频素材,可以先写一段正弦波生成最简易的提示音,网上搜"numpy生成wav"有很多现成代码,够用就好。

4.3 难度递进与一局游戏的节奏

现在的玩法是漏一个就死,你可以改成三条命,漏掉三个苹果才结束;同时分数每涨10分,就给苹果下落速度、生成概率加一点系数:

difficulty = 1 + score // 10 * 0.1 apple_speed = 3 * difficulty

这样游戏会形成明显的节奏曲线:前几秒轻松,慢慢开始紧张,高分段时玩家必须非常专注。别小看这么一行系数,它直接决定玩家会不会在"差一点就破纪录"的懊恼中主动再来一局。好的小游戏,难度曲线远比画面重要。我之前给朋友试玩这个接苹果时,第一版漏一个就死,大家玩两把就烦了;改成三条命加难度递增之后,好几个人开始互相攀比分数。同样的玩法,体验天差地别,这就是节奏设计的价值。

还有一点值得做:把"漏一个就退出"改成"漏三个才结束,并在屏幕中央显示剩余生命数"。这一步能让玩家的挫败感大幅降低,成就感逐步累积,是让demo变成"能给人玩的作品"的关键差别。

5. 卡顿排查、代码优化与打包发布

5.1 "越玩越卡"是列表泄漏的经典症状

如果你按我前面说的,把玩法从"漏一个就死"改成三条命,这里就很容易遇到那个著名的坑。三条命的逻辑需要改成"苹果掉出屏幕时移除苹果并扣一条命",而不是直接退出游戏;可一旦你忘了移除那行,苹果就会永远留在列表里,每帧重复绘制、重复移动,列表越滚越大,游戏自然越来越卡。

排查这种问题很简单:跑一段时间后,在游戏循环里打印len(apple_list),如果数量只增不减,就是清理逻辑漏了。"越玩越卡的列表"几乎是所有Pygame新手都会经历的必修课。但别觉得丢人,游戏开发里的内存泄漏,很多时候就是这种"该移出集合的对象没有移出",跟编程语言的高级特性没关系。

5.2 什么时候值得引入精灵组(Sprite Group)

目前为止我用list管理苹果,在苹果数量很少的情况下完全够用。但如果你打算把游戏做大,比如屏幕上同屏几十个敌人、每个都有独立行为,就该考虑用Pygame的精灵系统了。pygame.sprite.Sprite配合pygame.sprite.Group,自带add、remove、draw、update方法,更重要的是内置了groupcollide、spritecollide这类高效碰撞检测,比手写双重for循环快得多。

一个最简单的改造是定义Apple类:

class Apple(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image = pygame.Surface((30, 30), pygame.SRCALPHA) pygame.draw.circle(self.image, (220, 60, 60), (15, 15), 15) self.rect = self.image.get_rect() self.rect.x = random.randint(0, WIDTH - 30) self.rect.y = 0 def update(self): self.rect.y += apple_speed

然后用group.update()统一更新位置、group.draw(screen)统一绘制,代码结构会更清晰。但我不建议新手第一步就直接上Sprite,先把裸列表版的逻辑跑通,再重构,你会对"精灵组到底帮你省了什么"有更直观的认识。直接上高级抽象很容易迷路,因为你根本不知道那些魔法方法在背后替你解决了什么问题。

5.3 用PyInstaller打包成可分发的小程序

写完游戏之后另一个高频需求是:怎么把它发给别人玩,总不能让人先装Python吧。常规方案是用PyInstaller打包:

python -m pip install pyinstaller pyinstaller -F -w game.py

-F表示打成一个单独的exe,-w表示运行时隐藏控制台窗口。打包完之后,dist目录下会有一个可执行文件,直接双击就能运行。

打包有几个常见坑要提前说。第一,如果游戏里用了图片、音频等外部文件,PyInstaller不会自动打包进去,需要把文件放在代码同目录,运行时按相对路径查找。第二,打包出来的文件体积普遍偏大,PyInstaller会把整个Python解释器和Pygame全部塞进去,通常几十MB很正常,不用慌。第三,如果你在macOS下打包,默认只能在本机系统运行;想跨平台分发,得在对应的系统上分别打包,或者用CI托管跨平台构建。

我个人的体会是,打包不是学习的终点,它只是让你能把手上的成果递给朋友看。真正有意思的部分,仍然是游戏循环里那套"输入-更新-绘制"的节奏感,以及你自己亲手加上的每一条规则。做第一个Pygame游戏,最大的建议就是:不要贪多,不要一上来就规划"吃豆人"或"马里奥"。把一个接苹果这样的小玩法完整跑通,比看十篇教程都能学到更多。我最早写这类游戏的时候也走过不少弯路,一会儿想着加皮肤,一会儿想着做关卡编辑器,最后光靠整理代码和修复bug就花了一周。后来养成的习惯是:先跑起来再优化,把"能玩"当成第一里程碑。

如果你照着这篇把接苹果写完了,建议立刻尝试三个小改动:把苹果换成字母下落做成打字小游戏,把玩家改成鼠标控制,或者增加苹果上限做成生存模式。每一项改动都会让你对Pygame的理解加深一层。等你玩明白了,再去看Pygame官方文档里的examples目录,那里有更多值得拆解的小项目。

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

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

立即咨询