拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

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

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

第一次用Pygame写游戏,是好几年前的事了。当时我刚学完Python的基础语法,正愁不知道拿这门语言做点什么,写完作业只能盯着黑底白字的控制台发呆。后来在论坛上看到有人用Pygame做出了一个火箭躲避小游戏,我照着教程一行行敲,敲到半夜,屏幕上终于出现了一个会在窗口里动来动去的方块——那种“我能靠代码改变世界”的错觉,瞬间就上头了。

Pygame是Python生态里最适合新手入门的2D游戏开发库。它基于SDL(Simple 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_env

Windows下激活虚拟环境:

game_env\Scripts\activate

macOS或Linux下:

source game_env/bin/activate

激活成功以后,命令行前面会多出一个小括号提示,说明你现在已经在一个独立的环境里了。然后安装Pygame:

pip 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上进行,你可以把它理解成一块可以任意涂改的大画布。

主循环是整个游戏的心脏,它的作用是反复执行以下三件事:

  1. 处理事件:监听并响应用户的输入,比如关闭窗口、按键盘、点鼠标。
  2. 更新游戏状态:根据输入改变变量值,比如玩家的位置、敌人的血量、计分数字。
  3. 重新绘制:把所有物体的新位置画到画布上,再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.py:

import 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:同时又在循环里remove,Python会报错或漏项,正确的做法是用for apple in apples[:]遍历副本,或者倒序遍历。

6.3 排查思路:从现象到根因

我给新手的建议,永远是“最小化复现”。当你发现一个Bug,先别急着在完整代码里翻,试着把它简化成一个最小程序:只保留主循环和出问题的那个功能。比如苹果不动,那就把画出苹果的代码单独提出来,固定死在屏幕上位置输出——如果显示正常,问题在更新逻辑;如果显示都不正常,问题在绘制逻辑。这样一层层缩小范围,比我隔空猜你要快得多。

我自己调试时,最常用的工具其实就是print(),往关键位置打印变量值。Pygame本身没有内置强大的断点调试工具,但这种逐行打印的方式对小项目完全够用。等将来项目变大,你会发现单元测试和结构化日志更有效,但那是后话,你现在只需要记住:先跑通主干,再逐层加功能。

7. 我的个人经验与再往前走一步的方向

做完这个“接苹果”之后,我的第一个感觉是:“原来做游戏的关键不在于画出多好看的图,而在于让循环转得漂亮。”这个循环思维,后面在学AI、写后端服务时,反复都有用。Pygame虽然只适合做小项目,但它逼你理解程序运行的本质,这是它送给你最值钱的东西。

如果文章中这个简单版本你已经跑通了,我个人推荐的三个扩展方向是:

  • 给苹果加随机下落速度,甚至让少数苹果变成“坏苹果”——碰到会扣分,增加策略性。
  • 加入开始界面和结束界面,用状态机管理游戏阶段。这是很多小游戏进化的关键一步。
  • 学习和使用pygame.sprite.Group重构代码,把挡板和苹果都变成精灵,代码结构会清晰很多,也为以后做更复杂的游戏打基础。

最后分享一个小技巧,如果你以后想要一个能发给朋友玩的独立程序,可以用pyinstaller把Python脚本打包成exe。打包时记得把pygame相关的资源文件一起带上,另外打出来的包可能会比较大,因为Pygame自带的依赖占了不少体积,不要惊讶。我做第一个成品的时候,看到打包完的文件夹整整几十兆,愣是被吓了一跳。

Pygame这条路,入门不难,难的是把一个简单玩法打磨出“手感”。我至今还记得第一次让苹果“啪”地撞上挡板、分数跳上去那一刻的快乐。如果你也把代码敲出来了,跑去试试那几行参数,找到你的第一版“手感”,那种快乐你马上就能体会到了。

返回列表