Python 学到能写爬虫、能调 Pandas 之后,很多人会陷入一种奇怪的尴尬:语法都认识,示例代码能跑,但让自己从零做一个“拿得出手”的东西,却完全不知道从哪里下手。如果你正处在这个阶段,我建议你花一个下午做一个皮卡丘主题的桌面小游戏。项目名就叫《皮卡丘,我们来咯》。
我的判断很直接:Pygame 不是生产级游戏引擎,但它是 Python 初学者完成“从语法到项目”跨越时性价比最高的工具之一。它把函数封装、面向对象、事件机制、坐标运算、碰撞检测这些平时散落的知识点,全部缝进了一个有角色、有反馈、有分数的程序里。改一个数值,屏幕上立刻有视觉变化;写错一个坐标,角色直接飞出窗口。这种强反馈循环,是刷一百道语法题都换不来的。
这篇文章的落点很具体:先讲清楚 Pygame 的核心概念,再带你从零写完一个可玩的“皮卡丘接精灵球”小游戏,包含完整代码、运行验证、常见排错和工程化建议。读完你不仅能跑通这个项目,还能顺着扩展思路把它改成自己的作品。
1. 为什么写一个皮卡丘小游戏,而不是继续刷语法题
很多 Python 教程的节奏是:字符串、列表、字典、函数、类、文件操作……每一步都有例子,但例子之间没有关联。学完后面忘掉前面,最大的原因不是你记忆力差,而是知识没有形成一个可以运行的整体。游戏开发恰好能把大部分 Python 核心语法串起来:类的继承用来管理角色和道具,函数用来封装绘制逻辑,字典和列表用来保存游戏状态,文件读写可以用来做存档,异常处理用来兜底崩溃场景。
选择皮卡丘主题也有现实考量。做项目最怕中途放弃,而一个你有情感认同的主题,会显著拉高你把它做完的概率。看到黄色小怪兽在屏幕上移动、碰到精灵球时闪出电光,这种正反馈比“成功打印一行 Hello World”强太多了。而且皮卡丘的辨识度高,拿去给同学演示,对方一眼就知道你在做什么。
不过要说清楚一点:Pygame 适合入门和快速原型,但不适合做复杂物理、3D 渲染或大型商业游戏。它的定位是“让你理解游戏底层是怎么运转的”。你用 Pygame 写完一个游戏,再去学 Unity、Godot 或者 Cocos,会发现很多概念是相通的:场景循环、精灵渲染、碰撞检测、输入响应。先用 Pygame 把游戏主循环刻进脑子里,后面的路会顺很多。
还有一个容易被忽略的点:皮卡丘是受版权保护的角色形象。本文不会提供任何官方素材文件,而是用 Pygame 绘图函数生成一个“程序化占位版皮卡丘”。这个占位角色足以承载全部游戏逻辑教学,如果你之后想发布或商用,记得替换成自己原创的美术素材,这是开发者应该有的版权意识。
2. Pygame 核心概念:窗口、事件循环、精灵与碰撞
写 Pygame 之前,必须先建立几个核心概念。不需要背下来,但至少要理解它们在游戏循环中的角色。
2.1 坐标系:左上角是原点
Pygame 的窗口坐标系和我们上学时学的笛卡尔坐标系不一样。原点在窗口左上角,x 轴向右增大,y 轴向下增大。也就是说,角色的 y 坐标越大,它在屏幕上越靠下。很多新手一上来就搞反方向,结果按“上箭头”时角色往下跑。
游戏中的位置和碰撞不处理像素坐标,而是处理矩形。每个可见对象都能用一个矩形包起来,这个矩形就是 Rect 对象,它由x、y、width、height四个值描述。Pygame 的碰撞检测也基于 Rect 之间的相交判断,这比逐像素检测快得多。
2.2 Surface 与 blit
一个 Surface 就是一块内存中的画布。整个窗口是一个 Surface,每个贴到屏幕上的图片、文字,也都可以是一个 Surface。绘制游戏对象的标准流程是:先在独立 Surface 上画好内容,再调用screen.blit(surface, 坐标)把它“贴”到窗口上。blit 这个名字来自 Bit Block Transfer,本质是一块像素数据到另一块的复制。
2.3 事件循环:游戏不是从上往下执行完就结束
普通 Python 脚本从上往下执行完就退出,游戏程序不一样。它需要持续运行,每秒钟重新绘制几十次画面,同时不断处理玩家的按键、鼠标和窗口关闭请求。这个持续运行的循环叫主循环,也叫事件循环。
游戏每一帧大概要做四件事:处理输入事件、更新游戏状态、绘制画面、控制帧率。Pygame 中pygame.event.get()负责取出当前帧发生的所有事件,比如点击了关闭按钮、按下了某个键。而像方向键这种需要持续按住才能移动的操作,更适合用pygame.key.get_pressed()获取按键状态。
2.4 帧率与 Clock
游戏画面是快速切换的静态帧。帧率 FPS 表示每秒刷新多少次,通常设置为 60。Pygame 的pygame.time.Clock()可以控制循环速度,clock.tick(60)表示这一帧最多执行 1/60 秒,剩余时间让程序休眠。没有这一步,游戏会在高配电脑上快得没法玩。
2.5 Sprite 与 Group
Pygame 提供了一个轻量级的精灵基类pygame.sprite.Sprite。把游戏对象写成 Sprite 子类后,每个对象都有image和rect两个属性,一个管绘制内容,一个管位置和碰撞。再用Group把多个精灵放在一起,就能一次性完成更新和绘制。
传统控制台程序和 Pygame 游戏程序的执行方式有本质区别,可以参考下面的对比:
| 维度 | 传统控制台程序 | Pygame 游戏程序 |
|---|---|---|
| 执行方式 | 顺序执行,跑完退出 | 主循环驱动,每帧重复执行 |
| 用户输入 | input() 阻塞等待 | 事件队列,非阻塞轮询 |
| 状态变化 | 变量直接打印 | 修改对象属性,重绘画面 |
| 时间控制 | 无 | Clock 控制帧率 |
| 程序退出 | 执行到最后一行 | 收到 QUIT 事件或 running=False |
3. 环境准备与项目结构
本文示例代码面向 Python 3.10 及以上版本,Pygame 2.x。具体版本以你本机环境为准,核心思路不变。
3.1 安装 Pygame
建议先创建虚拟环境,再安装依赖。Windows 命令行或 macOS/Linux 终端执行:
python -m venv venv激活虚拟环境。Windows:
venv\Scripts\activatemacOS / Linux:
source venv/bin/activate然后安装 Pygame:
pip install pygame如果网速较慢,可以换国内镜像源:
pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后验证:
python -c "import pygame; print(pygame.version.ver)"能输出版本号说明安装成功。
3.2 项目目录结构
我建议从一开始就按可分文件的方式组织代码,而不是把所有逻辑塞进一个文件。本文的项目结构如下:
pikachu-game/ ├── config.py # 游戏配置常量 ├── sprites.py # 角色与道具精灵类 ├── main.py # 主程序入口 ├── requirements.txt # 依赖清单 └── README.md # 项目说明config.py集中管理窗口尺寸、颜色、速度等常量,这样改配置时不需要翻游戏逻辑代码。sprites.py只放角色类。main.py里放游戏主类和启动入口。项目规模不大时,这种分层看起来“多此一举”,但等你想加音效、加关卡、加开始画面时,会发现它非常好扩展。
4. 核心流程拆解:从零搭建“皮卡丘,我们来咯”
正式写代码之前,先把游戏流程拆成几个清晰的步骤。这一步很重要,如果一开始就埋头写代码,很容易写着写着把自己绕晕。
4.1 初始化窗口与游戏配置
Pygame 使用的第一步是调用pygame.init(),它会初始化显示器、音频、事件等所有模块。然后用pygame.display.set_mode((宽, 高))创建窗口,用set_caption设置标题。窗口创建完成后,游戏就有了一个可以绘制的画布。
4.2 设计精灵类
整个游戏有两个核心精灵:玩家控制的皮卡丘,以及需要去碰触的精灵球。我把它们都设计为pygame.sprite.Sprite的子类。
皮卡丘这个类需要实现:
- 在
__init__中生成自己的image和rect - 提供一个
update方法,根据按键状态更新坐标 - 用
clamp_ip把角色限制在窗口范围内,防止跑出屏幕
精灵球类相对简单,只需要:
- 随机出现在屏幕内部
- 在更新时检查是否存活过久,过期后自动消失
这样设计的好处是:主循环只需要调用all_sprites.update()和all_sprites.draw(screen),所有类型的对象都会自动更新和绘制,完全不用在主循环里写一大堆针对单个对象的判断。
4.3 玩家移动
玩家移动使用pygame.key.get_pressed(),它返回一个元组,记录每个键盘按键当前是否被按下。在update方法里,每一帧都检查方向键和 WASD 键的按下状态,然后累加位置偏移。
为什么不用事件循环里的KEYDOWN来处理移动?因为KEYDOWN只在按键刚按下去的瞬间触发一次。如果你想实现“按住方向键持续移动”,用事件循环就需要自己维护按键状态集合,非常麻烦。key.get_pressed()直接返回每一帧的实时按键快照,处理持续移动最合适。
4.4 精灵球生成与碰撞
精灵球的生成需要一个节流器。最简单的做法是记录上一次生成精灵球的时间,当前时间减去上次时间大于设定间隔时,生成一个新的精灵球。
碰撞检测直接用pygame.sprite.spritecollide(player, pokeballs, True)。第三个参数为 True,表示碰撞后自动删除被碰到的精灵球。这个函数会返回所有被碰撞到的精灵球列表,我们遍历列表累加分数即可。
4.5 文字提示与游戏反馈
为了让游戏更有“皮卡丘,我们来咯”的主题感,每次碰到精灵球时,在窗口顶部显示一句“皮卡丘,我们来咯!”。实现方式是在碰撞发生时记录当前时间,绘制画面时判断这句提示文字是否还在显示时间内。这比直接打印到终端更有游戏反馈感。
4.6 主循环组装
最后在主循环里把以上所有模块串起来。每一帧执行:
- 处理退出事件
- 按节流规则生成精灵球
- 更新玩家和精灵球状态
- 检测碰撞并更新分数
- 绘制背景、精灵、文字
- 调用
clock.tick(60)控制帧率
主循环是一个游戏程序的心脏,所有逻辑都挂在这条时间轴上执行。
5. 完整可运行代码实现
下面直接给出完整代码。按顺序创建文件即可,不需要额外安装其他库。
5.1 依赖清单requirements.txt
在项目根目录创建:
pygame>=2.05.2 配置文件config.py
所有可调整的常量都集中在这里:
# 文件路径:config.py # 窗口尺寸 WIDTH = 900 HEIGHT = 640 # 帧率 FPS = 60 # 游戏标题 GAME_TITLE = "皮卡丘,我们来咯" # 精灵球生成间隔(毫秒) SPAWN_INTERVAL = 1200 # 皮卡丘移动速度 PLAYER_SPEED = 5 PLAYER_BOOST_SPEED = 8 # 颜色定义 YELLOW = (255, 215, 0) DARK = (45, 45, 45) RED = (220, 50, 50) WHITE = (245, 245, 245) BLUE = (70, 130, 240) BG_COLOR = (120, 190, 90) TEXT_COLOR = (35, 35, 35)5.3 精灵与绘制函数sprites.py
这个文件包含皮卡丘和精灵球的绘制函数、精灵类。
# 文件路径:sprites.py import random import pygame from config import ( BLUE, DARK, HEIGHT, PLAYER_BOOST_SPEED, PLAYER_SPEED, RED, WIDTH, YELLOW, WHITE, ) def create_pikachu_surface(size: int = 100) -> pygame.Surface: """用绘图函数生成一个简化的皮卡丘占位图。 这里不使用官方素材,而是用几何图形拼出一个 黄色为主、有耳朵和腮红的角色,用于演示精灵逻辑。 """ surf = pygame.Surface((size, size), pygame.SRCALPHA) # 身体:黄色椭圆 body_rect = pygame.Rect(int(size * 0.2), int(size * 0.3), int(size * 0.6), int(size * 0.62)) pygame.draw.ellipse(surf, YELLOW, body_rect) pygame.draw.ellipse(surf, DARK, body_rect, 2) # 左耳 left_ear = [(int(size * 0.22), int(size * 0.34)), (int(size * 0.06), int(size * 0.05)), (int(size * 0.18), int(size * 0.20))] pygame.draw.polygon(surf, YELLOW, left_ear) pygame.draw.polygon(surf, DARK, left_ear, 2) # 左耳黑尖 left_tip = [(int(size * 0.08), int(size * 0.10)), (int(size * 0.06), int(size * 0.05)), (int(size * 0.14), int(size * 0.10))] pygame.draw.polygon(surf, DARK, left_tip) # 右耳 right_ear = [(int(size * 0.78), int(size * 0.34)), (int(size * 0.94), int(size * 0.05)), (int(size * 0.82), int(size * 0.20))] pygame.draw.polygon(surf, YELLOW, right_ear) pygame.draw.polygon(surf, DARK, right_ear, 2) # 右耳黑尖 right_tip = [(int(size * 0.92), int(size * 0.10)), (int(size * 0.94), int(size * 0.05)), (int(size * 0.86), int(size * 0.10))] pygame.draw.polygon(surf, DARK, right_tip) # 眼睛 pygame.draw.circle(surf, DARK, (int(size * 0.34), int(size * 0.52)), int(size * 0.04)) pygame.draw.circle(surf, DARK, (int(size * 0.66), int(size * 0.52)), int(size * 0.04)) # 腮红 pygame.draw.circle(surf, RED, (int(size * 0.18), int(size * 0.66)), int(size * 0.06)) pygame.draw.circle(surf, RED, (int(size * 0.82), int(size * 0.66)), int(size * 0.06)) # 嘴巴 mouth_rect = pygame.Rect(int(size * 0.40), int(size * 0.60), int(size * 0.20), int(size * 0.12)) pygame.draw.arc(surf, DARK, mouth_rect, 0, 3.14, 2) return surf def create_pokeball_surface(radius: int = 24) -> pygame.Surface: """绘制一个精灵球图标。""" surf = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA) # 上半部分红色,下半部分白色 pygame.draw.circle(surf, RED, (radius, radius), radius) pygame.draw.rect(surf, WHITE, (0, radius - 4, radius * 2, radius + 4)) # 黑色中线 pygame.draw.rect(surf, DARK, (0, radius - 4, radius * 2, 6)) # 中心圆 pygame.draw.circle(surf, WHITE, (radius, radius), radius // 2) pygame.draw.circle(surf, DARK, (radius, radius), radius // 2, 3) # 外圈描边 pygame.draw.circle(surf, DARK, (radius, radius), radius, 3) return surf class Pikachu(pygame.sprite.Sprite): """玩家角色:皮卡丘""" def __init__(self, x: int, y: int): super().__init__() self.image = create_pikachu_surface() self.rect = self.image.get_rect(center=(x, y)) self.speed = PLAYER_SPEED self.boost_speed = PLAYER_BOOST_SPEED def update(self, keys) -> None: dx, dy = 0, 0 speed = self.boost_speed if keys[pygame.K_SPACE] else self.speed if keys[pygame.K_LEFT] or keys[pygame.K_a]: dx -= speed if keys[pygame.K_RIGHT] or keys[pygame.K_d]: dx += speed if keys[pygame.K_UP] or keys[pygame.K_w]: dy -= speed if keys[pygame.K_DOWN] or keys[pygame.K_s]: dy += speed self.rect.x += dx self.rect.y += dy # 防止角色跑出窗口 self.rect.clamp_ip(pygame.Rect(0, 0, WIDTH, HEIGHT)) class Pokeball(pygame.sprite.Sprite): """可捕获的精灵球道具""" def __init__(self): super().__init__() self.image = create_pokeball_surface() x = random.randint(60, WIDTH - 60) y = random.randint(100, HEIGHT - 60) self.rect = self.image.get_rect(center=(x, y)) self.born_time = pygame.time.get_ticks() self.lifetime = 120 # 存活帧数 def update(self) -> None: # 存活超过寿命的精灵球自动消失 frame_alive = max(1, self.lifetime) if pygame.time.get_ticks() - self.born_time > frame_alive * 16: self.kill()说明一下,create_pikachu_surface用几何图形画了一个简化版角色,这不是官方形象,只是用来承载移动、碰撞、绘制这些游戏教学逻辑。如果你有自己画的像素图或购买的素材,可以直接加载图片替换掉绘制函数。
5.4 主程序main.py
# 文件路径:main.py import pygame from config import ( BG_COLOR, FPS, GAME_TITLE, HEIGHT, SPAWN_INTERVAL, TEXT_COLOR, WIDTH, ) from sprites import Pikachu, Pokeball class Game: def __init__(self): pygame.init() self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(GAME_TITLE) self.clock = pygame.time.Clock() self.running = True self.score = 0 self.combo = 0 self.spawn_timer = 0 self.hit_text = "" self.hit_text_time = 0 self.player = Pikachu(WIDTH // 2, HEIGHT - 80) self.pokeballs = pygame.sprite.Group() self.all_sprites = pygame.sprite.Group(self.player) # 字体:优先使用系统中文字体 self.font = pygame.font.SysFont("simhei", 28, bold=True) self.small_font = pygame.font.SysFont("simhei", 20) def handle_events(self) -> None: for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: self.running = False def spawn_pokeball(self) -> None: now = pygame.time.get_ticks() if now - self.spawn_timer > SPAWN_INTERVAL: self.spawn_timer = now ball = Pokeball() self.pokeballs.add(ball) self.all_sprites.add(ball) def update(self) -> None: keys = pygame.key.get_pressed() self.player.update(keys) self.pokeballs.update() # 碰撞检测,碰到精灵球则加分 hits = pygame.sprite.spritecollide(self.player, self.pokeballs, True) for _ in hits: self.score += 10 self.combo += 1 self.hit_text = "皮卡丘,我们来咯!" self.hit_text_time = pygame.time.get_ticks() def draw(self) -> None: self.screen.fill(BG_COLOR) # 绘制草地点缀 for y in range(0, HEIGHT, 40): for x in range(0, WIDTH, 40): pygame.draw.circle(self.screen, (100, 165, 75), (x + 10, y + 20), 4) self.all_sprites.draw(self.screen) score_surface = self.font.render(f"得分: {self.score}", True, TEXT_COLOR) self.screen.blit(score_surface, (20, 20)) combo_surface = self.small_font.render(f"连击: {self.combo}", True, TEXT_COLOR) self.screen.blit(combo_surface, (20, 58)) # 显示捕获成功提示 if self.hit_text and pygame.time.get_ticks() - self.hit_text_time < 800: tip = self.font.render(self.hit_text, True, (255, 255, 255)) tip_rect = tip.get_rect(center=(WIDTH // 2, 80)) self.screen.blit(tip, tip_rect) pygame.display.flip() def run(self) -> None: while self.running: self.handle_events() self.spawn_pokeball() self.update() self.draw() self.clock.tick(FPS) pygame.quit() def main(): game = Game() game.run() if __name__ == "__main__": main()主循环的顺序是有讲究的:先处理事件,再生成新精灵球,再更新所有精灵,最后绘制画面。如果你把绘制放在更新之前,这一帧的画面就会比状态慢一帧,虽然在小游戏里影响不明显,但进入复杂项目后会成为很难排查的 bug。
5.5 运行方法
在项目根目录执行:
python main.py然后就可以用方向键或 WASD 控制皮卡丘移动。碰到红色精灵球会加分,屏幕顶部显示“皮卡丘,我们来咯!”提示。按 ESC 退出游戏。
6. 运行结果与效果验证
启动后在窗口中你应该能看到:
- 一个绿色草地风格的背景,上面有草点装饰
- 一只黄色为主、带耳朵和腮红的角色位于窗口下方中央
- 每隔 1.2 秒左右,屏幕随机位置出现一个红白配色的精灵球
- 移动角色碰触精灵球后,分数增加 10 分,顶部出现提示文字
判断游戏是否成功,不只看画面,还要确认几个核心逻辑是否正常:
| 验证点 | 预期表现 |
|---|---|
| 窗口创建 | 标题为“皮卡丘,我们来咯”,尺寸 900x640 |
| 移动控制 | 方向键和 WASD 均可控制角色移动 |
| 边界限制 | 角色不能移出窗口 |
| 加速移动 | 按住空格时角色移动明显变快 |
| 精灵球生成 | 间隔约 1.2 秒生成一个新精灵球 |
| 碰撞计分 | 碰到精灵球后分数 +10,精灵球消失 |
| 提示文字 | 碰撞后 0.8 秒内显示“皮卡丘,我们来咯!” |
| 退出机制 | ESC 或关闭窗口均能正常退出 |
如果发现精灵球生成过快或过慢,在config.py里调整SPAWN_INTERVAL,例如 1800 表示 1.8 秒生成一个,数值越大越困难。如果角色移动太飘,调整PLAYER_SPEED。
调试时可以临时在update方法里加print输出,例如每次碰撞时打印分数,这能帮助你确认碰撞逻辑是否真的执行:
for _ in hits: self.score += 10 self.combo += 1 print(f"[DEBUG] catch pokeball, score={self.score}")看到输出再关掉,比盲目猜测效率高得多。
7. 常见问题与排查思路
新手运行 Pygame 项目时,最容易在环境、路径、字体和坐标系上出问题。我把高频问题整理成一张表,方便你直接对照排查。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
ModuleNotFoundError: pygame | Pygame 未安装或未激活虚拟环境 | 检查终端提示符前缀是否显示 venv | 运行pip install pygame并激活虚拟环境 |
| 窗口一闪而过立刻退出 | 主函数书写错误或代码中有未捕获异常 | 用命令行运行查看完整报错 | 确认main()被调用,检查异常堆栈 |
| 窗口出现但看不到角色 | 精灵坐标超出窗口范围,或被背景覆盖 | 在 update 中打印 rect 坐标 | 检查clamp_ip是否生效,调整初始坐标 |
| 中文显示为方框 | 系统缺少指定中文字体 | 打印pygame.font.get_fonts()查看可用字体 | 改用pygame.font.match_font("pingfang")等系统字体 |
| 游戏运行太卡或太快 | 未调用clock.tick(FPS)或 FPS 设置异常 | 查看是否每帧都执行了 tick | 将self.clock.tick(FPS)放入主循环末尾 |
| Linux 下窗口显示不出来 | SDL 视频驱动不匹配 | 查看终端报错信息 | 设置SDL_VIDEODRIVER=x11后重试 |
| 精灵球一瞬间生成太多 | 生成间隔判断写在更新前且未更新 timer | 检查spawn_timer赋值位置 | 在生成操作后立即更新self.spawn_timer |
| 按住方向键没有反应 | 使用了事件循环的 KEYDOWN 而不是按键状态 | 添加打印确认事件是否触发 | update中改用pygame.key.get_pressed() |
这里重点说下中文显示问题。pygame.font.SysFont("simhei", ...)在 Windows 下通常能直接显示中文,但在 macOS 上不一定能找到 simhei。更稳妥的做法是先用pygame.font.get_fonts()查看当前系统可用字体,再选择一个中文字体名称。即使没有中文文字,也不影响游戏玩法,因为计分数字的渲染不受字体影响。
还有一个常见误区是:把self.spawn_timer = now写在判断条件外面。这会导致计时器每一帧都刷新,永远满足不了“间隔大于 SPAWN_INTERVAL”的条件,结果精灵球永远不生成。我刚学 Pygame 时在这个问题上卡了半小时。
8. 最佳实践与工程建议
这个小游戏虽然只有三个文件,但已经足够用来验证一些工程化习惯。如果你打算把它扩展成更大的项目,下面这些建议越早养成越好。
第一,配置和逻辑分离。所有窗口尺寸、颜色、速度、间隔都放config.py。你想调整游戏难度时,只改配置文件,不需要深入游戏逻辑。等游戏复杂到配置项超过几十个时,可以考虑用dataclass或配置文件统一管理。
第二,使用 Sprite 和 Group 组织对象,而不是用列表散装管理。Pygame 的 Group 不仅能统一更新和绘制,还能结合spritecollide做高效碰撞检测。一旦你开始同时管理敌人、子弹、道具、特效,这种设计带来的收益会非常明显。
第三,注意清理生命周期结束的对象。我在Pokeball.update里做了超时自动销毁,避免精灵球积压导致性能下降。实际开发中,子弹飞出屏幕、敌人死亡后的特效粒子,都要在合适的时机调用kill()释放。
第四,素材版权是真实存在的问题。皮卡丘的图像、名称都受版权保护,个人学习演示问题不大,但发布到公共平台或商用需要谨慎。建议学习时使用程序绘制或原创像素画,等作品成熟后再替换素材,这是一个技术博主应该承担的传播责任。
第五,考虑打包分发。想把这个小游戏发给朋友玩,可以用 PyInstaller 打包成 exe 或可执行文件:
pip install pyinstaller pyinstaller -F -w main.py-F表示打包成单文件,-w表示不显示控制台窗口。注意打包后需要验证字体和素材路径是否正确,因为打包会改变资源文件的相对路径。更推荐的做法是在项目内创建一个assets目录,并使用os.path.join(Path(__file__).parent, "assets")来定位资源。
第六,调试信息要能快速开关。可以在config.py里加一个DEBUG_MODE = True,然后在代码里用条件控制打印输出。这样开发时打开调试,发布前统一关闭,比频繁删改 print 语句干净得多。
9. 扩展方向:如何把它改造成真正的个人作品
游戏跑通只是第一步。要把它变成能写进简历或者展示给朋友的作品,可以从下面几个方向依次升级。
游戏机制方面,目前的设计是“碰到即得分”。你可以增加负面障碍,碰到后扣生命值;可以记录最高分并存到本地 JSON 文件;可以让精灵球生成间隔随分数提高而缩短,形成难度曲线;还可以增加道具种类,例如金色精灵球得 50 分、会逃跑的精灵球。
画面表现方面,可以给皮卡丘做多帧动画。准备几张不同步伐的帧图,在移动时按帧率切换,角色跑动时会有动画效果。碰撞时生成闪电特效,用一片短暂的明黄色闪电形状叠加显示。背景可以换成有渐变层次的地图,用地砖数组绘制。
声音交互方面,Pygame 的pygame.mixer支持播放 wav 或 ogg。碰撞时播放一个短音效,移动时播放环境音,能极大增强游戏反馈。开发时可以把音效总开关做成配置项,避免办公环境突然出声吓到同事。
技术深度方面,如果你想继续提升,可以尝试把游戏改成状态机模式,显式区分开始画面、游戏中、暂停、结算等状态。每一关的状态变化用独立的类管理,这就开始接近商业游戏引擎里的场景管理思路了。再往后学 Unity 或 Godot 时,你会发现这套思维方式可以直接迁移。
我自己写这个小游戏最大的感触是:代码量不大,但它逼着你去思考“程序为什么需要主循环”“对象之间怎么协作”“性能问题会藏在哪里”。这些问题的答案,不是靠背语法能悟出来的,是真的写完一个能跑、能玩、能被朋友评价“有点意思”的程序之后才懂的。
建议你先按文章代码把最小版本跑起来,然后从改一个数值开始,比如把精灵球生成间隔改成 500 试试游戏难度变化。等你想清楚要加什么功能,再动手改代码。每加一个功能前,先记下“这个功能需要哪些类、哪些状态、哪些新文件”,把设计写下来再动手,你会比直接改代码顺利得多。