ARTICLE DETAIL

资讯详情

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

Pygame实战:从零实现‘猫咪摘椰子’休闲小游戏

Pygame实战:从零实现‘猫咪摘椰子’休闲小游戏 刷到过儿童搞笑动画的朋友大概率见过这种套路一只胖乎乎的猫咪被树上掉下来的椰子砸得晕头转向或者一路抱着椰子狂奔。“猫咪摘椰子”这个看起来只有五秒钟笑点的卡通设定如果换个思路其实是一个天然的休闲游戏原型玩家控制猫咪移动椰子不断从高处掉落接住得分漏掉丢命。标题里的 Funny Cat Coconut Challenge、Golpo Kutir、TOON 字样更像是一个卡通频道或动画 IP 的命名组合这里不讨论它的背景而是把这个卡通课题迁移到真正的游戏开发中来。我的判断是用 Pygame 实现这个“猫咪摘椰子”小游戏最大的价值不是做出一个多么华丽的成品而是把游戏开发最常见的骨架——游戏循环、键盘输入、对象移动、碰撞检测、计分和结束判定——用最小成本串起来。对刚学完 Python 基础语法、想尝试图形界面和游戏开发的新手来说这是一个比“学生管理系统”更有画面感、也更容易坚持下去的练手项目。无论你是给孩子做教学演示给学生出课设题目还是给自己的休闲小游戏想法做原型验证本文的代码都可以直接作为起点。读完本文你会得到一份可运行的 Pygame 源码掌握从游戏设计、编码实现到运行排错的完整链路同时理解几个后续做任何游戏都必须用到的核心概念。文章不会堆砌复杂框架也不会甩给你一个几千行的工程而是用一个晚上就能跑通的最小可玩版本帮你把“游戏到底是怎么做出来的”这件事讲清楚。1. 为什么要用 Pygame 做“猫咪摘椰子”这个小游戏1.1 从卡通创意到游戏原型卡通和游戏之间并没有天然边界。一个搞笑的卡通场景核心往往是“角色面对一个不断变化的障碍或挑战”这正是游戏机制生长的土壤。猫咪想摘椰子椰子会从天而降猫咪需要移动身体去接住它们——这已经天然具备了操作输入、分数累积、失败条件三要素。很多新手在拿到这样的题目时容易陷入一个误区一上来就想着做 3D 建模、骨骼动画、物理引擎。但实际开发中一个创意从概念到可玩需要的往往是最朴素的循环不断处理输入、不断更新对象位置、不断判断碰撞、不断绘制画面。这个骨架在任何游戏引擎里都一样Pygame 的优势在于它足够薄你能直接看见每一行代码在做什么。用卡通创意做游戏原型不是幼稚而是一种非常高效的学习方式画面简单才能把注意力集中在游戏逻辑本身。1.2 谁最适合读这篇文章先明确受众边界再决定要不要往下读。适合读这篇文章的人包括已经学过 Python 的 if、while、函数和类但还没做过图形界面程序的人。想给学生或初学者设计一个“有完整玩法、有反馈、有结束条件”的教学项目的人。想验证某个休闲小玩法是否有乐趣想快速搭原型的产品和独立开发者。想接 Pygame但又不想从贪吃蛇和俄罗斯方块开始的人。不适合读这篇文章的人也很清楚如果你要做的是复杂商业游戏、3D 动作游戏或需要多人联网的项目Pygame 不是一个合理选择那不是本文要解决的事情。这里要解决的问题是用最短的代码量建立一个可交互、可计分、可结束、可扩展的小游戏流程。1.3 为什么是 Pygame 而不是 Unity、Godot这个话题在社区里经常被问。我的观点是工具选择要看目标。Unity 和 Godot 是完整的游戏引擎它们解决的是资产管线、场景编辑、光照、物理模拟、跨平台打包等工业化问题。对于“学习编程基础 理解游戏逻辑”这件事它们的学习曲线反而更陡。你打开 Unity 的第一眼面对的是一个庞大的编辑器界面而不是一段能看懂的代码。Pygame 不是引擎它是一层游戏开发库只做窗口创建、图形绘制、事件处理、声音播放这些基础事情。游戏逻辑全部由你自己写这意味着你被迫去理解每一块拼图。这种“从零搭起”的过程对学习非常重要。当你以后去学 Unity 或 Godot 时你会发现只是换了一套表达方式那些循环、状态、碰撞概念在 Pygame 里早就建立过了。另外一个实际原因Pygame 是纯 Python 库安装只需一条 pip 命令一个文件就能跑起来对课设演示和教学演示都非常友好。2. 游戏开发核心概念先把这几个概念搞懂写代码之前先花一点时间理解五个核心概念。它们不局限于 Pygame在几乎所有游戏框架里都存在。2.1 游戏循环游戏本质上是一个无限循环每一轮循环做四件事读取玩家输入键盘、鼠标、手柄。更新所有游戏对象的状态猫咪移动、椰子下落。检测对象之间的关系碰撞、出界。把所有对象绘制到屏幕上。这个循环每秒钟要重复很多次通常用 FPS 控制次数。本文代码设置的 FPS 是 60也就是每秒循环 60 次。你屏幕上看到的“猫咪在动”其实是一帧一帧的静态画面快速切换和动画片原理完全一样。新手最容易犯的错误是在循环里写了耗时操作比如网络请求、复杂文件读取、大量日志输出导致一帧执行时间过长游戏画面就会出现卡顿。这是游戏循环最基本的原则不要让单帧耗费太多时间。2.2 精灵对象与矩形碰撞游戏里每一个可见或可交互的对象比如猫咪、椰子都可以抽象成一个“精灵”。精灵通常拥有位置、尺寸、图片或绘制方法以及一个用于碰撞检测的矩形区域。在 Pygame 里pygame.Rect是最常用的碰撞模型。它会记录一个对象的左上角坐标、宽度和高度并提供colliderect()方法判断两个矩形是否重叠。本文代码里猫咪用Cat类表示椰子用Coconut类表示各自都实现get_rect()方法然后在主循环里调用cat.get_rect().colliderect(coconut.get_rect())这句话的意思是“猫咪的矩形区域是否撞上了椰子的矩形区域”。如果为真就说明接住了椰子。矩形碰撞是最简单的碰撞模型它不够精确比如圆形物体在斜角处会被判定为碰撞。但对“猫咪接椰子”这个游戏来说矩形碰撞已经足够而且性能极高。2.3 帧率与时间控制clock pygame.time.Clock()用来控制游戏速度。每帧末尾调用clock.tick(60)Pygame 就会让循环尽量保持每秒 60 帧。如果你不控制帧率程序会跑得飞快椰子在眨眼间就落到底部根本没法玩。控制帧率不仅是保证体验也是让游戏在不同电脑上表现一致的重要手段。性能更好的电脑不会让游戏速度更快这符合玩家预期。2.4 事件驱动与持续按键游戏输入有两种常见方式事件驱动当按键被按下或释放时pygame.event.get()会返回对应的事件对象。适合处理“按一下空格开始游戏”“按 R 重新开始”这类一次性交互。持续按键pygame.key.get_pressed()返回一个表示按键当前状态的数组。适合处理“按住左右方向键让猫咪持续移动”这类连续操作。本文代码两种都会用到。开始画面使用事件驱动游戏主循环使用pygame.key.get_pressed()读取方向键。如果你只用事件驱动来处理移动你会遇到按住方向键但角色不连续移动的诡异问题这里提前说明后面不再反复强调。3. 环境准备与项目初始化3.1 安装 Python 与环境检查本文代码面向 Python 3.8 及以上版本Pygame 2.x 版本。建议先检查本机 Python 版本python --version如果你还没有 Python需要先去官网下载安装包。Windows 用户安装时务必勾选“Add Python to PATH”否则命令行里会找不到 python 命令。推荐在项目目录中使用虚拟环境避免污染全局 Python 环境。在命令行进入项目目录后执行python -m venv venvWindows 下激活虚拟环境venv\Scripts\activatemacOS 或 Linux 下激活source venv/bin/activate激活成功后命令行前面会出现(venv)提示说明环境已就绪。3.2 安装 Pygame激活虚拟环境后执行pip install pygame安装完成后可以用一行命令检查版本python -c import pygame; print(pygame.version.ver)看到类似2.6.1的输出说明安装成功。如果这一步报错通常是网络问题或 Python 版本过低可以考虑更换 pip 镜像源例如pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple3.3 项目目录结构本文只需要一个文件但一个规范的目录结构对未来扩展很有帮助coconut-challenge/ ├── main.py # 游戏主程序 ├── images/ # 后续放角色图片 ├── sounds/ # 后续放音效 └── venv/ # 虚拟环境images和sounds目录现在可以留空后续扩展时使用。本文为了降低上手成本不使用任何外部素材所有图形都用 Pygame 自带的绘图函数生成。3.4 验证 Pygame 是否可用在项目目录下新建一个名为 main.py 的文件先写一个最简窗口测试import pygame import sys pygame.init() screen pygame.display.set_mode((800, 600)) pygame.display.set_caption(Pygame Test) while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() pygame.display.flip()运行python main.py如果出现一个 800x600 的窗口并且可以关闭说明环境和显卡驱动都没有问题。这一步排查很重要因为后续所有代码都建立在这个最基本的基础上。4. 游戏设计先定规则再写代码很多初学者拿到题目后第一反应是打开编辑器写代码这是一种低效的方式。游戏开发的第一步应该是把玩法规则写清楚哪怕只是几行文字。设计阶段想得越清楚写代码时就越少返工。4.1 玩法规则本文实现的“猫咪摘椰子”规则如下游戏场景设在一座海岛上猫咪位于画面底部椰子从顶部随机位置下落。玩家按住左、右方向键控制猫咪水平移动让猫咪接住掉落的椰子。每接住一个椰子得分增加 10 分。每漏掉一个椰子椰子落出屏幕底部生命值减少 1。初始生命值为 3生命值归零时游戏结束。这套规则和许多经典接物类小游戏一致简单但完整。卡通化的包装只是换了一层皮玩家在接的不是金币而是椰子角色不是默认方块而是一只长方形猫。4.2 游戏状态的拆分一个休闲游戏通常有三个状态开始画面展示游戏标题和操作提示等待玩家按空格键开始。游戏进行中猫咪移动、椰子生成和下落、碰撞检测、计分循环进行。结束画面展示最终得分等待玩家按 R 重新开始或按 ESC 退出。三个状态在代码里分别对应三个函数主函数负责串联它们。这样设计的优点是逻辑清晰后续加暂停、加胜利条件都方便。4.3 数值与难度曲线一个接物类游戏如果永远只有同一速度玩家很快就会腻。本文在规则里增加了一个简单难度曲线总分椰子生成间隔帧椰子下落速度范围0-49503-650-99404-7100-199304-8200-299255-9300以上206-10生成间隔越小椰子越密集下落速度越快玩家反应时间越短。这是一个非常简单的“动态难度”模型。它的核心意义在于随着玩家表现变好游戏持续提供挑战避免一波新手福利通关后无事可做。4.4 为什么先写设计再做代码把设计写清楚还有一个作用当你几个月后再回来看自己的代码你会立刻明白当时为什么要这样设计。很多个人项目烂尾不是因为代码写得烂而是因为需求一直在脑子里变代码也随之失控。一个可运行的最小版本搭配一份清晰的设计说明比一个看似完善却永远跑不起来的半成品有价值得多。5. 完整代码实现与逐段解析下面给出完整代码。为了便于复制运行我把所有代码放在同一个main.py文件中。代码整体约两百行注释已经尽量详细你可以直接保存运行也可以参照后续小节理解每一块的职责。 main.py Funny Cat Coconut Challenge - 猫咪摘椰子 Python Pygame 实现的极简休闲小游戏 import pygame import random import sys # ---------- 初始化 ---------- pygame.init() WIDTH, HEIGHT 800, 600 screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(Funny Cat Coconut Challenge - 猫咪摘椰子) clock pygame.time.Clock() FPS 60 # ---------- 颜色 ---------- SKY_BLUE (135, 206, 235) SAND (255, 228, 181) GREEN (34, 139, 34) BROWN (139, 69, 19) LIGHT_BROWN (205, 133, 63) COCONUT_BROWN (101, 67, 33) ORANGE (255, 165, 0) BLACK (0, 0, 0) WHITE (255, 255, 255) YELLOW (255, 215, 0) RED (220, 20, 60) def load_fonts(): 优先加载系统中的中文字体找不到时退回默认字体。 for font_name in (simhei, microsoftyahei, pingfangsc, notosanscjk): if pygame.font.match_font(font_name): return pygame.font.SysFont(font_name, 32), pygame.font.SysFont(font_name, 52) return pygame.font.Font(None, 32), pygame.font.Font(None, 52) font, title_font load_fonts() def draw_text(text, color, x, y, fntfont, centerFalse): 在屏幕上绘制文字centerTrue 时以(x, y)为中心居中显示。 surface fnt.render(text, True, color) rect surface.get_rect() if center: rect.center (x, y) else: rect.topleft (x, y) screen.blit(surface, rect) def draw_background(): 绘制简单的海岛背景沙滩和椰子树。 # 沙滩 pygame.draw.rect(screen, SAND, (0, HEIGHT - 80, WIDTH, 80)) # 椰子树树干 pygame.draw.rect(screen, BROWN, (80, HEIGHT - 250, 20, 170)) # 椰子树冠 pygame.draw.polygon(screen, GREEN, [ (50, HEIGHT - 250), (130, HEIGHT - 250), (90, HEIGHT - 320) ]) class Cat: 猫咪角色玩家通过左右方向键控制。 def __init__(self): self.width 80 self.height 60 self.x (WIDTH - self.width) // 2 self.y HEIGHT - self.height - 20 self.speed 8 def move(self, keys): if keys[pygame.K_LEFT]: self.x - self.speed if keys[pygame.K_RIGHT]: self.x self.speed # 限制猫咪不能移出画面 self.x max(0, min(self.x, WIDTH - self.width)) def get_rect(self): return pygame.Rect(self.x, self.y, self.width, self.height) def draw(self, surf): # 身体 pygame.draw.ellipse(surf, ORANGE, (self.x, self.y, self.width, self.height)) # 耳朵 pygame.draw.polygon(surf, ORANGE, [ (self.x 5, self.y 10), (self.x 20, self.y - 15), (self.x 30, self.y 10) ]) pygame.draw.polygon(surf, ORANGE, [ (self.x self.width - 30, self.y 10), (self.x self.width - 20, self.y - 15), (self.x self.width - 5, self.y 10) ]) # 眼睛 pygame.draw.circle(surf, BLACK, (self.x 30, self.y 30), 6) pygame.draw.circle(surf, BLACK, (self.x 50, self.y 30), 6) # 鼻子 pygame.draw.circle(surf, BLACK, (self.x 40, self.y 38), 4) # 胡须 pygame.draw.line(surf, BLACK, (self.x 8, self.y 45), (self.x 25, self.y 42), 2) pygame.draw.line(surf, BLACK, (self.x 72, self.y 45), (self.x 55, self.y 42), 2) class Coconut: 从顶部随机位置落下的椰子。 def __init__(self): self.radius 18 self.x random.randint(self.radius, WIDTH - self.radius) self.y -self.radius self.speed 5 def fall(self): self.y self.speed def get_rect(self): return pygame.Rect( self.x - self.radius, self.y - self.radius, self.radius * 2, self.radius * 2 ) def off_screen(self): return self.y HEIGHT self.radius def draw(self, surf): # 椰子主体 pygame.draw.circle(surf, COCONUT_BROWN, (self.x, self.y), self.radius) # 高光 pygame.draw.circle(surf, LIGHT_BROWN, (self.x - 4, self.y - 4), self.radius - 8) # 表面的三个小点 pygame.draw.circle(surf, BROWN, (self.x - 7, self.y 5), 3) pygame.draw.circle(surf, BROWN, (self.x 2, self.y 8), 3) def get_difficulty(score): 根据当前得分返回(椰子生成间隔, (最小速度, 最大速度))。 if score 300: return 20, (6, 10) if score 200: return 25, (5, 9) if score 100: return 30, (4, 8) if score 50: return 40, (4, 7) return 50, (3, 6) def show_start_screen(): 开始画面显示标题等待按空格键。 screen.fill(SKY_BLUE) draw_text(Funny Cat Coconut Challenge, BROWN, WIDTH // 2, HEIGHT // 2 - 80, title_font, centerTrue) draw_text(Press SPACE to Start, BLACK, WIDTH // 2, HEIGHT // 2 10, centerTrue) draw_text(Use LEFT / RIGHT to move the cat, BLACK, WIDTH // 2, HEIGHT // 2 50, centerTrue) draw_text(Catch coconuts and earn points!, BLACK, WIDTH // 2, HEIGHT // 2 80, centerTrue) pygame.display.flip() while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() if event.type pygame.KEYDOWN and event.key pygame.K_SPACE: return def run_game(): 游戏主循环控制猫咪生成椰子处理碰撞和计分。 cat Cat() coconuts [] score 0 lives 3 spawn_timer 0 while lives 0: clock.tick(FPS) # 事件处理 for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() # 按键控制 keys pygame.key.get_pressed() cat.move(keys) # 根据当前得分计算难度 spawn_gap, (min_speed, max_speed) get_difficulty(score) # 生成新椰子 spawn_timer 1 if spawn_timer spawn_gap: coconut Coconut() coconut.speed random.randint(min_speed, max_speed) coconuts.append(coconut) spawn_timer 0 # 更新所有椰子并检测碰撞 for coconut in coconuts[:]: coconut.fall() if cat.get_rect().colliderect(coconut.get_rect()): coconuts.remove(coconut) score 10 elif coconut.off_screen(): coconuts.remove(coconut) lives - 1 # 绘制 screen.fill(SKY_BLUE) draw_background() cat.draw(screen) for coconut in coconuts: coconut.draw(screen) draw_text(fScore: {score}, BLACK, 50, 30) draw_text(fLives: {lives}, BLACK, WIDTH - 100, 30) pygame.display.flip() return score def show_game_over(score): 结束画面展示最终得分等待重新开始或退出。 screen.fill(SKY_BLUE) draw_text(Game Over, RED, WIDTH // 2, HEIGHT // 2 - 80, title_font, centerTrue) draw_text(fFinal Score: {score}, BLACK, WIDTH // 2, HEIGHT // 2 - 10, centerTrue) draw_text(Press R to Restart, YELLOW, WIDTH // 2, HEIGHT // 2 60, centerTrue) draw_text(Press ESC to Quit, YELLOW, WIDTH // 2, HEIGHT // 2 100, centerTrue) pygame.display.flip() while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() if event.type pygame.KEYDOWN and event.key pygame.K_r: return True if event.type pygame.KEYDOWN and event.key pygame.K_ESCAPE: return False def main(): 游戏入口串联开始画面、游戏主循环和结束画面。 while True: show_start_screen() score run_game() restart show_game_over(score) if not restart: break pygame.quit() sys.exit() if __name__ __main__: main()5.1 初始化与常量定义代码顶部的pygame.init()负责初始化 Pygame 的所有模块。WIDTH和HEIGHT定义了窗口大小screen是游戏画布。FPS设置帧率上限clock用于控制循环速度。颜色用 RGB 元组定义这样在代码中只要写SKY_BLUE就知道是天空蓝不需要反复记忆(135, 206, 235)是哪个颜色。这个习惯在项目大了以后很重要常量命名让代码可读性大幅提升。load_fonts()的作用是解决中文乱码和字体找不到的问题。它先尝试查找常见的系统字体如果找到了就返回对应的字体对象否则退回 Pygame 默认字体。这样在不同操作系统上都能正常显示文字。窗口标题里的中文不依赖 Pygame 字体系统会自行渲染所以保留中文也没问题。5.2 Cat 类的设计与实现Cat类负责猫咪的属性和行为。属性包括位置、尺寸、速度行为包括移动、绘制、返回碰撞矩形。move()方法接收pygame.key.get_pressed()的返回值判断左、右方向键是否被按住然后修改猫咪的横坐标。最后一行做了边界限制self.x max(0, min(self.x, WIDTH - self.width))这行代码等价于“如果猫咪要移出左边就停在左边要移出右边就停在右边”。它很简洁但新手常常忽略边界限制导致角色跑出屏幕后找不回来。边界处理是游戏开发中一个很小但很重要的细节。draw()方法没有加载图片而是用椭圆、多边形、圆形和线段画出一只卡通猫。这是刻意为之开发游戏的最低成本是用几何图形替代美术资源先把玩法验证通再替换正式素材。你后续完全可以把这段绘图逻辑替换成surf.blit(cat_image, (self.x, self.y))不需要改其他代码。5.3 Coconut 类的设计与实现Coconut类的属性和行为比 Cat 类简单。它只需要随机横坐标、下落速度、下落方法、绘制方法和碰撞矩形。有一个细节值得注意off_screen()判断的是self.y HEIGHT self.radius。为什么不直接判断y HEIGHT因为椰子的圆心坐标可能已经超出窗口底部但圆形本身还没有完全离开画面。加上半径可以保证椰子完全消失后才判定为漏掉视觉效果更自然。这在游戏开发中叫“判定区域”它不一定和绘制区域完全一致。很多游戏把碰撞判定矩形做得比可视物体略小就是出于类似的原因给玩家一点宽容度手感更好。5.4 碰撞检测与列表删除的坑主循环中更新椰子时使用了下面这段代码for coconut in coconuts[:]: coconut.fall() if cat.get_rect().colliderect(coconut.get_rect()): coconuts.remove(coconut) score 10 elif coconut.off_screen(): coconuts.remove(coconut) lives - 1关键点在于coconuts[:]。它创建了原列表的一份拷贝然后遍历拷贝在原列表上执行删除操作。如果你写成for coconut in coconuts:在循环内部调用coconuts.remove(coconut)就会导致遍历过程中列表长度发生变化可能出现跳过某个元素甚至报错的情况。这是 Python 列表遍历的经典陷阱很多初学游戏开发的人在这里卡半天。碰撞检测方面colliderect()方法接收一个pygame.Rect返回布尔值。猫咪的矩形来自cat.get_rect()椰子的矩形来自coconut.get_rect()。矩形重叠即判定为接住。这个逻辑虽然朴素但在本游戏中足够准确而且一帧只会执行一次检测性能可以忽略不计。关于是否把判定区域缩小、是否用圆形碰撞这些都是可以后续优化的方向但目前不需要。5.5 游戏状态的切换main()函数是游戏的总调度器while True: show_start_screen() score run_game() restart show_game_over(score) if not restart: break程序先显示开始画面玩家按空格后进入run_game()。当生命值归零run_game()返回最终得分。接着显示游戏结束画面如果玩家按 Rmain()会回到show_start_screen()重新开始如果按 ESC则退出整个程序。这种状态机写法把“开始-进行-结束”三个状态分离开来逻辑非常清晰。后续如果要加“暂停状态”或“胜利状态”你只需要再增加一个函数在适当的位置跳转即可不需要动其他部分的代码。6. 运行、测试与效果验证6.1 运行方法保存好main.py后在激活虚拟环境的终端执行python main.py如果一切正常会依次出现开始画面蓝色天空背景沙滩和椰子树标题文字提示按空格开始。按下空格后画面底部出现橙色的猫咪顶部开始不定期掉落棕色椰子。按住左右方向键猫咪可以水平移动。接住椰子后左上角 Score 增加 10。漏掉椰子后右上角 Lives 减少 1。Lives 归 0 时进入 Game Over 画面。这个执行流程本身就是一个冒烟测试每个环节都不需要额外工具。6.2 如何判断游戏是否“正常”从功能角度验证标准可以列成一张检查清单验证项预期表现是否通过窗口能否正常打开和关闭800x600 窗口标题正常显示是/否开始画面能否进入游戏按空格后进入主循环是/否猫咪能否移动左右方向键对应左右移动是/否椰子能否生成和下落顶部随机位置出现并往下移动是/否接住椰子能否得分猫咪碰到椰子后 Score 增加 10是/否漏掉椰子能否扣生命椰子落出底部后 Lives 减少 1是/否游戏能否正常结束Lives 为 0 时进入 Game Over是/否建议自己在一次游戏中完整验证这七项。如果哪一项不符合直接跳到下一章的排查表。6.3 手动测试的一个小技巧如果你想快速测试生命归零的场景不用每次都玩很久。可以把初始lives临时改成 1或者把get_difficulty()返回的生成间隔改成 10让椰子变得密集。这样几秒钟就能验证游戏结束流程是否正常。改完记得还原这种临时改动在开发过程中很正常。6.4 给代码加一个简单日志如果发现问题不好定位可以在主循环里临时添加打印print(fscore{score}, lives{lives}, coconuts{len(coconuts)})把它放在绘制之前。这样你能从终端看到每一帧的状态变化。不过日志打印本身会拖慢帧率定位完问题后要立刻删除不要留在最终代码里。7. 常见问题与排查方法实际运行中读者最容易遇到下面几个问题。每个都是一线开发中常见的情况我按“现象、原因、排查、解决”四个维度整理成表再补充重点说明。问题现象可能原因排查方式解决方案窗口打开后立刻闪退代码存在 SyntaxError或路径写错导致文件找不到在终端运行python main.py查看报错信息根据报错定位到对应行修正后重跑pip install pygame 失败网络波动或 pip 源不稳定重新安装或更换镜像源使用国内镜像源pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple按住方向键猫咪不移动在事件驱动中处理移动而非读取持续按键状态检查 move 里用的是pygame.key.get_pressed()还是 KEYDOWN使用pygame.key.get_pressed()不要用 KEYDOWN 移动移动太快或太慢没有设置固定 FPS或电脑性能差异大检查循环里是否调用clock.tick(FPS)每帧调用clock.tick(60)中文文字显示成方框系统中文字体缺失查看 Pygame 返回的字体信息把界面文字改成英文或手动指定已安装的中文字体画面很卡椰子一顿一顿单帧耗时过长循环里有阻塞操作检查是否有打印、文件读取等高耗时操作删除临时日志保证帧内只做必要更新游戏分数不动椰子矩形未与猫咪矩形重叠检查猫咪的 y 坐标和椰子下落终点用print(cat.get_rect(), coconut.get_rect())打印矩形位置漏掉椰子却不扣生命off_screen()判断条件写得过宽确认y HEIGHT radius的逻辑改为y - radius HEIGHT或保留带半径判断下面挑几个容易出现理解偏差的说明。第一个是“按住方向键不移动”。这个问题的根源是事件机制和持续按键机制的区别。pygame.event.get()的事件类型包括 KEYDOWN 和 KEYUP它们代表“按下的那一刻”和“松开的那一刻”。如果你只依赖 KEYDOWN 来移动猫咪那你只有按下瞬间才移动一下按住不放是没有连续效果的。解决方法是主循环里用keys pygame.key.get_pressed()每一帧都读取当前有哪些键被按住再传给cat.move(keys)。本文代码已经按正确方式实现如果你改成事件驱动后出问题请回到这个思路检查。第二个是“中文显示成方框”。Pygame 默认字体是 FreeSansBold它对中文的支持有限。即使你在代码里写了中文窗口标题能正常显示但画面内的文字很可能变成豆腐块。本文代码里load_fonts()会检测系统中是否有 simhei、microsoftyahei 等字体找到就用找不到就退回默认字体。因此稳妥的做法是先把界面文字保留英文优先保证功能跑通。等确认系统字体支持中文后再把显示文本替换成中文。第三个是“椰子突然消失或分数错乱”。如果遍历列表时直接用原列表删除会破坏迭代顺序。假设列表里有三个椰子第一个被删除后第二个会顶到第一个的位置循环却继续按原来的索引走于是有一个椰子被跳过。严重的还会导致列表越界或运行错误。所以统一使用coconuts[:]拷贝遍历这是最省心且不易出错的做法。8. 从小游戏到工程化扩展与最佳实践游戏跑通以后千万不要急着加上一堆新功能先做一件更重要的事情把代码读一遍确认自己是否理解每一行的作用。在此基础上可以从下面几个方向继续演进。8.1 用真实图片替换几何图形当前代码用几何图形绘制猫咪和椰子好处是零素材依赖坏处是不够好看。当你准备进入“好看”阶段时可以把Cat类里的draw()方法改成图片加载cat_image pygame.image.load(images/cat.png).convert_alpha()然后在绘制时surf.blit(cat_image, (self.x, self.y))图片素材要注意几点图片尺寸最好和self.width、self.height一致不一致时用pygame.transform.scale()调整PNG 透明图使用convert_alpha()可以保留透明通道素材来源要注意版权不要随意使用不明来源的游戏图片。8.2 增加音效反馈一个游戏的“手感”很大程度来自音效。接住椰子时播放一个清脆的碰撞音漏掉椰子时播放一个低沉的坠落音玩家的反馈感会明显增强。Pygame 用pygame.mixer.Sound播放音效catch_sound pygame.mixer.Sound(sounds/catch.wav) catch_sound.play()在碰撞检测为真的分支里调用即可。注意pygame.mixer.init()需要先初始化通常pygame.init()会一并完成。8.3 引入状态机管理游戏流程目前的开始、进行、结束三个状态分布在三个函数中逻辑简单时很清晰。如果后续要加入暂停、结算动画、关卡切换、设置界面建议引入一个简单的状态机对象用状态枚举控制当前活跃状态class GameState: START start PLAYING playing PAUSED paused GAME_OVER game_over主循环根据状态调用不同的更新和绘制函数。这是很多商业游戏的常见设计也是从“一个小文件”走向“一个工程”的第一步。8.4 渲染性能和对象池本文每次生成椰子都用Coconut()创建新对象删除时用list.remove()。在最多同时几十个椰子的场景下这完全没问题。但如果你的游戏需要同时存在几百个对象创建和销毁对象的开销会变大。此时可以让椰子对象复用生成时把 y 重置到顶部而不是新建对象。这个概念叫对象池。对象池的做法可以作为一个进阶练习但现阶段不必过早优化。游戏开发的正确原则是先让功能正确再用数据证明性能瓶颈在哪里而不是凭猜测优化。8.5 打包成可执行文件发给别人玩而不用安装 Python可以用 PyInstaller 打包pip install pyinstaller pyinstaller -F -w main.py-F表示打包为单文件-w表示不显示控制台窗口。打包生成的 exe 放在dist目录下可以发给其他人直接运行。注意如果你在后面用了图片和音效打包时需要用--add-data把素材一并打进去否则运行时会找不到资源。8.6 一些值得养成的工程习惯从小项目开始养成好习惯比以后改习惯容易得多常量集中定义不要散落在代码各处。每个类和函数只做一件事职责清晰。每帧更新的逻辑和绘制的逻辑尽量分开方便后续替换渲染方式。临时调试代码用注释或日志标记完成后及时清理。文件名、类名、变量名保持统一风格不中英混杂。代码风格虽然有个人偏好但在团队中应该统一。哪怕是一个人写项目几个月后再看也需要自己的代码清晰可读。9. 总结与实践建议这篇文章想说明的事情其实很简单一个卡通创意变成可玩的游戏并不需要复杂的引擎和美术资源。你只需要一个游戏循环、两个对象类、一次碰撞检测、一个计分变量和一个结束条件。两百行左右的 Python Pygame 代码就可以跑通一个完整且可玩的小游戏。建议你把代码自己敲一遍而不是直接复制运行。敲的过程中你会注意到很多细节为什么Cat的 y 坐标要留 20 像素而不是贴着底部为什么椰子用coconuts[:]遍历为什么get_difficulty()返回值要用元组解包这些问题在复制粘贴时不会出现但当你自己写的时候它们会成为你的经验。下一步的扩展方向按难度递增排序给猫咪和椰子换成真实图片素材加入接住和漏掉的音效增加暂停功能和胜利条件把椰子生成逻辑改成对象池用 PyInstaller 打包成 exe 发给朋友玩甚至在熟悉之后尝试移植到 Web 平台。每一步都可以在现有代码基础上完成不需要推翻重来。最后再提醒一点不要把目标定成“做一个完美的商业游戏”。把卡通创意变成能玩的游戏关键不在于引擎多华丽、画面多精致而在于你把循环、输入、绘制、碰撞这套骨架跑通。这是游戏开发最底层的逻辑也是最值得花时间掌握的东西。
返回列表