Python+pygame实现“小人接金币”小游戏:从零到完整代码

发布时间:2026/9/7 15:49:56
Python+pygame实现“小人接金币”小游戏:从零到完整代码 做小游戏这件事听起来好像挺有门槛但实际上放到 Python 里也就是一百多行代码的事。今天要分享的“小人接金币”就是这样一个小项目屏幕上方不停往下掉金币底部一个小人跟着你的键盘左右移动去接金币接得越多得分越高漏掉 5 个金币游戏就结束。玩法简单到不需要设计文档但“麻雀虽小五脏俱全”——游戏循环、事件监听、碰撞检测、随机生成、计分状态这些做正经游戏才用得上的基础知识它全占了。特别适合刚学完 Python 基础语法、想找个项目练手的朋友一两晚上就能写完写完你会对类、对象、循环和事件驱动有完全不一样的感觉。我这个项目最初也就是给学员上课时写的演示代码后来发现很多朋友对着教程抄完语法一写程序还是不知道从哪儿下手。与其硬啃那些抽象的概念不如亲手做一个看得见、玩得了的小游戏。这篇文章我会把从环境配置、思路拆解到完整代码、踩坑经验全部摊开来讲你跟着操作最后一定能得到一个能双击运行、能拿出去“炫耀”的小游戏。1. 项目整体设计与核心思路1.1 为什么选 Python pygame 而不是别的方案先说结论对于这种 2D 小游戏pygame 是目前最适合 Python 新手的库没有之一。我知道有人会问Python 不是擅长数据分析、爬虫和 AI 吗做游戏是不是有点“不务正业”别急Python 做游戏确实不是它的最强项但这里有个重要前提——我们做的是教学练手项目不是要开发 3A 大作。“小人接金币”这种游戏每帧需要处理的图形和逻辑极其有限Python 的运行效率完全够用就像用家用轿车跑城市道路没必要开赛车。pygame 的好处在于它把游戏开发最常见的需求都封装好了。比如精灵Sprite类它帮你管理图片的位置、大小和绘制精灵组Group可以让你一次性更新、绘制所有对象碰撞检测模块能直接判断两个角色有没有碰在一起。这些能力在 tkinter、PyQt 这类普通 GUI 库里面要么没有要么用起来很别扭。tkinter 更适合做工具型软件界面你要强行用它做游戏碰撞检测和动画刷新都得手写代码量翻好几倍。1.2 游戏规则与玩法的设计思路这个游戏的规则一开始我跟朋友讨论过好几个版本。最早我设想的是“接金币 躲炸弹”屏幕上掉金币的同时也掉红色炸弹碰到炸弹扣一条命接住金币加分。后来试玩发现对新手来说同时处理两类物体有点分心而且代码里要多写一套炸弹类的逻辑。为了保持“简单又有趣”这个核心目标我把规则砍成了最干净的一套屏幕上方每隔一小段时间掉下一枚金币金币落到底部就算“漏接”。玩家用键盘左右方向键控制底部的小人移动。小人接到金币得分 1。漏接次数达到 5 次游戏结束显示最终得分。你会发现这套规则里没有关卡、没有血量、没有道具但玩起来并不会无聊。原因在于我加入了两个隐藏的动态变量金币生成速度会随着得分提高而加快金币的下落速度本身也是随机分布的。这两个变量让游戏始终处于“有一点挑战但又不至于绝望”的状态这种难度曲线设计思路其实很多商业游戏都在用。1.3 代码的整体架构规划在动手写代码之前我习惯先在脑子里把程序的结构画出来。“小人接金币”虽然小但该有的部分一个不能少我把整个项目分成了四个层次第一层是初始化模块负责启动 pygame、创建游戏窗口、设定颜色和字体。第二层是角色类包括玩家类 Player 和金币类 Gold每个类负责自己的位置、外观和行为逻辑。第三层是主循环也就是游戏的心脏每一帧它都按固定的顺序执行读入用户输入、更新所有角色状态、检测碰撞、绘制画面。第四层是辅助逻辑比如得分统计、漏接判定、游戏结束条件。这种“分层”的思想特别重要。哪怕你现在只是写 100 行的练习项目也建议从一开始就养成这种习惯。等以后做更复杂的项目你会发现这种结构让你的代码非常好维护——想加一个新角色只需要多写一个类想改游戏规则只需要动主循环里的几行代码。2. 环境准备把 Python 和 pygame 装好2.1 Python 环境安装与验证很多朋友第一次写 Python 小游戏还没开始写代码就先卡在环境上。我见过太多“代码明明一模一样为什么我的就报错”的案例最后发现是 Python 版本太老或者压根没装对。先去 Python 官网下载最新稳定版推荐 3.8 以上的版本可以是 3.10 或者 3.12都能完美支持 pygame。这里有一个特别容易踩的坑Windows 系统下安装时第一屏里一定要勾选“Add Python to PATH”很多人的报错“python 不是内部或外部命令”十有八九就是没勾这一项。如果你已经装完了才发现没勾也不用重装手动把 Python 的安装目录和 Scripts 子目录加到系统环境变量里就行具体操作网上搜“添加 PATH 环境变量”就可以。装完之后打开终端Windows 按 Win R输入 cmd 回车输入python --version如果能看到类似Python 3.12.3的输出说明环境已经就绪。再输入pip --version验证包管理工具可用这两个命令都通过环境这关你就过了。2.2 安装 pygame 及镜像源加速环境就绪后安装 pygame 其实只要一条命令pip install pygame但新手在国内网络环境下经常会遇到下载速度堪比蜗牛爬、甚至中途超时报错的情况。这时候不要硬等换用国内镜像源速度立竿见影pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple清华源、阿里源、豆瓣源都可以选一个离你近的。安装成功之后可以在 Python 交互环境里验证一下import pygame pygame.version.ver如果输出了一个版本号说明 pygame 已经可以正常导入了。注意如果你电脑上同时装了多个 Python 版本比如一个 3.8 一个 3.12一定要确认 pip 装到了你用到的那个版本上。保险的做法是在终端里用python -m pip install pygame而不是直接用pip install pygame这样可以确保库装进当前 python 对应的环境里。2.3 编辑器的选择与运行方式编辑器这里我推荐两种方案按你自己的习惯选。第一种是 VSCode轻量、免费、插件生态丰富。装好之后在扩展市场里搜“Python”安装微软官方那个插件发布者是 Microsoft它会帮你做代码高亮、自动补全和错误检查。然后打开项目文件夹新建一个main.py文件右上角会出现一个“运行”按钮点击就能直接运行。第二种是 PyCharm如果你喜欢“开箱即用”的体验推荐用社区版就够。PyCharm 的好处是新建项目时会自动帮你创建虚拟环境依赖管理更清晰对于刚入门的人来说不容易把环境搞乱。我个人的建议是如果只是写今天这种小项目VSCode 足够如果你打算后续深入学习 Python可以尝试 PyCharm。不管用哪个编辑器运行游戏时我推荐直接在终端里执行python main.py这样能看到完整的运行日志遇到报错信息也能第一时间复制出来排查比在编辑器里点一次运行然后窗口一闪而过要好得多。3. 核心代码实现从零写一个能玩的游戏3.1 初始化窗口与主循环骨架所有 pygame 游戏的第一步都是初始化。我们创建一块画布并设置窗口标题import pygame import random import sys pygame.init() SCREEN_WIDTH 480 SCREEN_HEIGHT 720 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(小人接金币) clock pygame.time.Clock()这段代码里有几个关键点。第一个是pygame.init()它会初始化 pygame 内部所有的模块包括显示、声音、字体等这是所有 pygame 程序的默认启动步骤不写的话后面会报错。第二个是set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))注意这个方法的参数是一个元组很多初学者写成set_mode(480, 720)就会报错因为函数只接收一个表示尺寸的元组。第三个是Clock对象它的作用后面马上讲。游戏的核心是一个“死循环”通常叫主循环main loop。每一帧循环都干四件事处理用户事件、更新角色位置、绘制画面、控制帧率。可以用下面的骨架来理解while True: # 1. 处理事件比如点击关闭按钮、按键盘按键 # 2. 更新所有角色状态移动、下落、碰撞 # 3. 绘制背景和角色到屏幕 # 4. 控制循环速度帧率这个循环会一直跑下去直到用户关闭窗口或者游戏结束。pygame 的图形刷新逻辑全在这里面你可以把它理解成一个动画播放器每一帧就是一张画面只要一秒钟刷新 60 帧看起来就是流畅的动画。3.2 玩家类让小人听你的指挥接下来写主角。我选择用继承 pygame 精灵类的方式来定义玩家这样它可以无脑塞进精灵组里享受统一的绘制和更新接口class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.width 80 self.height 60 self.image pygame.Surface((self.width, self.height)) self.image.fill((70, 130, 230)) self.rect self.image.get_rect() self.rect.centerx SCREEN_WIDTH // 2 self.rect.bottom SCREEN_HEIGHT - 30 self.speed 7 def move(self, keys): if keys[pygame.K_LEFT] and self.rect.left 0: self.rect.x - self.speed if keys[pygame.K_RIGHT] and self.rect.right SCREEN_WIDTH: self.rect.x self.speed这个类里面有几个地方值得仔细讲讲。self.image和self.rect是 pygame Sprite 的两个核心属性image 是什么样rect 是在哪儿pygame 在绘制时会按照这两个属性来工作。我这里先用pygame.Surface创建了一个纯色矩形当作小人的“形象”运行起来是一个蓝色的方块看起来确实朴素但好处是零素材依赖不涉及路径问题。等你想换图片只需用pygame.image.load(player.png)代替这一行代码其他部分完全不用改。移动逻辑用到了keys[pygame.K_LEFT]这种方式来判断按键状态。这里有一个新手常纠结的问题用事件处理KEYDOWN和用pygame.key.get_pressed()有什么区别简单说get_pressed 返回的是每一帧所有按键的实时状态适合“按住持续移动”的场景而 KEYDOWN 事件只在按钮按下的那一刻触发一次适合跳跃、射击这种“按一下触发一次”的操作。接金币需要小人持续左右移动所以用 get_pressed 更合理。边界判断是另一个容易漏掉的地方。如果不做限制小人会直接跑出屏幕再也找不回来。左右边界判断要基于rect.left和rect.right与屏幕边缘比较而不是基于rect.x和rect.x width因为 rect 属性已经帮我们算好了这样写更清晰也不容易出错。3.3 金币类从天而降的金币雨金币的设计思路和玩家类似但多了一个“下落”的行为。我让金币从屏幕上方随机水平位置出现然后每一帧向下移动一段距离class Gold(pygame.sprite.Sprite): def __init__(self): super().__init__() self.radius 15 self.image pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA) pygame.draw.circle(self.image, (255, 215, 0), (self.radius, self.radius), self.radius) self.rect self.image.get_rect() self.rect.x random.randint(0, SCREEN_WIDTH - self.rect.width) self.rect.y -self.rect.height self.speed random.randint(4, 8) def update(self): self.rect.y self.speed if self.rect.top SCREEN_HEIGHT: self.kill() return True return False这里我做了一个设计决定金币的初始位置y设为负数也就是金币从屏幕上方看不见的地方冒出来这样不会出现“凭空出现”的突兀感。下落速度speed用random.randint(4, 8)随机生成区间最小值 4、最大值 8这个数值不是随手写的——我测试了下来速度低于 4 金币几乎悬浮在空中玩家会玩得昏昏欲睡高于 8 则反应不过来体验偏紧张。你如果觉得难度不合适可以调整这两个数字。创建图片的时候我用pygame.Surface((...), pygame.SRCALPHA)创建了一个带透明通道的表面然后用pygame.draw.circle在表面上画了一个圆。这里不加SRCALPHA的话金币周围会带着一块黑色矩形底看起来很难看。这一点特别想提醒你新手做图形程序时透明通道问题能劝退一堆人。update方法里有一个关键逻辑金币掉出屏幕底部时调用self.kill()把自己从精灵组里移除同时返回一个True提示主循环“这枚金币漏接了”。用返回值来传递状态比在外面额外判断要简洁得多。3.4 主循环把角色串起来到这里玩家和金币都定义好了剩下的工作就是在一个主循环里把所有的逻辑串起来def main(): player Player() all_sprites pygame.sprite.Group() all_sprites.add(player) gold_group pygame.sprite.Group() font pygame.font.SysFont(SimHei, 32) score 0 missed 0 max_missed 5 spawn_count 0 running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() keys pygame.key.get_pressed() player.move(keys) spawn_count 1 if spawn_count % 25 0: gold_group.add(Gold()) for gold in list(gold_group): if gold.update(): missed 1 if missed max_missed: running False hit_golds pygame.sprite.spritecollide(player, gold_group, True) score len(hit_golds) screen.fill((0, 0, 0)) all_sprites.draw(screen) gold_group.draw(screen) score_text font.render(f得分: {score}, True, (255, 255, 255)) missed_text font.render(f漏接: {missed}/{max_missed}, True, (200, 60, 60)) screen.blit(score_text, (20, 20)) screen.blit(missed_text, (SCREEN_WIDTH - 200, 20)) pygame.display.flip() clock.tick(60)主循环里最核心的是三个点。第一是碰撞检测。pygame.sprite.spritecollide(player, gold_group, True)会检测玩家是否和金币组里的金币产生碰撞第三个参数True表示碰撞到的金币直接从组里移除这一步同时完成了“记录加分”和“让金币消失”两个操作。返回值是一个被消除的金币列表len(hit_golds)就是本次循环新增的得分。第二是计时生成。我定义了一个spawn_count计数器每循环一次加 1每满 25 次就生成一个新金币。这种“计数法”比用time.time()计时更稳定因为游戏主循环的帧率固定为 60 FPS所以 25 帧就相当于 0.4 秒生成一个。想加快游戏节奏把 25 调成 15 就行。第三是游戏结束条件。金币update()返回True表示漏接此时missed加 1达到 5 次就置running False循环退出。退出后我在屏幕上显示了最终得分等 3 秒自动关闭窗口。3.5 完整代码直接可以运行上面拆开了讲可能有点碎。下面我把完整代码贴出来你新建一个main.py复制进去直接跑就能玩import pygame import random import sys # ---------- 初始化 ---------- pygame.init() SCREEN_WIDTH 480 SCREEN_HEIGHT 720 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(小人接金币) clock pygame.time.Clock() BLACK (0, 0, 0) WHITE (255, 255, 255) GOLD (255, 215, 0) RED (200, 60, 60) BLUE (70, 130, 230) class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.width 80 self.height 60 self.image pygame.Surface((self.width, self.height)) self.image.fill(BLUE) self.rect self.image.get_rect() self.rect.centerx SCREEN_WIDTH // 2 self.rect.bottom SCREEN_HEIGHT - 30 self.speed 7 def move(self, keys): if keys[pygame.K_LEFT] and self.rect.left 0: self.rect.x - self.speed if keys[pygame.K_RIGHT] and self.rect.right SCREEN_WIDTH: self.rect.x self.speed class Gold(pygame.sprite.Sprite): def __init__(self): super().__init__() self.radius 15 self.image pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA) pygame.draw.circle(self.image, GOLD, (self.radius, self.radius), self.radius) self.rect self.image.get_rect() self.rect.x random.randint(0, SCREEN_WIDTH - self.rect.width) self.rect.y -self.rect.height self.speed random.randint(4, 8) def update(self): self.rect.y self.speed if self.rect.top SCREEN_HEIGHT: self.kill() return True return False def main(): player Player() all_sprites pygame.sprite.Group() all_sprites.add(player) gold_group pygame.sprite.Group() font pygame.font.SysFont(SimHei, 32) score 0 missed 0 max_missed 5 spawn_count 0 running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() keys pygame.key.get_pressed() player.move(keys) spawn_count 1 if spawn_count % 25 0: gold_group.add(Gold()) for gold in list(gold_group): if gold.update(): missed 1 if missed max_missed: running False hit_golds pygame.sprite.spritecollide(player, gold_group, True) score len(hit_golds) screen.fill(BLACK) all_sprites.draw(screen) gold_group.draw(screen) score_text font.render(f得分: {score}, True, WHITE) missed_text font.render(f漏接: {missed}/{max_missed}, True, RED) screen.blit(score_text, (20, 20)) screen.blit(missed_text, (SCREEN_WIDTH - 200, 20)) pygame.display.flip() clock.tick(60) screen.fill(BLACK) over_text font.render(游戏结束, True, WHITE) score_final font.render(f最终得分: {score}, True, GOLD) screen.blit(over_text, (SCREEN_WIDTH // 2 - 80, SCREEN_HEIGHT // 2 - 60)) screen.blit(score_final, (SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2)) pygame.display.flip() pygame.time.wait(3000) pygame.quit() if __name__ __main__: main()这个代码量非常克制120 行左右但已经是一个能反复游玩、有得分、有失败条件的完整小游戏了。运行起来你会发现虽然画面很简陋但游戏本身还挺上头的这是“玩法设计”的功劳跟画面精美程度没有必然关系。4. 常见问题与排查技巧4.1 新手最容易踩的几个坑我在写这个项目时曾经故意在班里让学员玩“找 bug”游戏他们踩过的坑我都整理成了表格贴出来供你对照排查。错误现象可能原因解决办法运行报错No module named pygamepygame 没装好或装到了其他 Python 环境用python -m pip install pygame重新安装并注意终端里python指向的版本窗口一闪而过直接退出了主循环写错了或while条件一开始就不成立检查running变量初始值及循环内是否被执行尝试在循环开头加print(run)看输出小人一按方向键就跑出屏幕没做边界判断按文章里的代码判断self.rect.left 0和self.rect.right SCREEN_WIDTH金币碰到小人没有消失碰撞检测的对象不对或玩家/金币不在同一个组确认碰撞检测用的是player和gold_group且玩家没有被误加到gold_group中文文字显示成方块或乱码系统没有对应字体或字体名写错改用pygame.font.SysFont(SimHei, 32)或用pygame.font.Font(字体文件路径, 32)游戏速度忽快忽慢没加clock.tick(60)主循环末尾一定调用clock.tick(60)它能把循环固定在一秒 60 帧不同机器上体验才会一致金币一次性掉一大片金币生成时机判断错误检查spawn_count % 25 0是否写在主循环内正确的位置4.2 调试思路与调参心得调试游戏程序和调试普通业务代码不太一样因为很多逻辑是循环往复的一口气跑几千帧。我的经验是先别急着用断点调试器用最简单粗暴的print大法定位问题所在。比如你想知道金币到底有没有生成可以在生成那一行后面加一句print(spawn a gold at, x, y)跑几秒钟看终端输出就能确认生成逻辑有没有执行、位置合不合理。再比如碰撞检测不生效你可以在spritecollide之后加print(hit count:, len(hit_golds))如果一直输出 0说明碰撞的区域可能根本没重叠这时候就该检查玩家和金币的 rect 坐标了。调参也是一个体力活。游戏手感好不好往往取决于几个数字下落速度、生成间隔、玩家移动速度。我建议你写代码时把这些数字单独抽出来放在文件头部不要直接散落在各个逻辑里。比如PLAYER_SPEED 7、GOLD_SPEED_MIN 4、GOLD_SPEED_MAX 8、SPAWN_INTERVAL 25想调难度时只需要改这几个常量不需要去几十行代码里找那个数字。这种习惯放到任何项目里都适用。还有一个在调试阶段很有用的技巧按顺序拆逻辑。如果不知道自己写的代码哪一步有问题就把更新和绘制拆开。比如先不生成金币只让玩家能左右移动跑一下确认没问题再只让金币下落不实现碰撞跑一下最后再把碰撞和计分加起来。每一步都能独立验证问题自然就暴露在最小范围内。5. 玩法扩展与打包发布5.1 把方块变成真正的“小人”和“金币”代码跑通之后最想做的事情大概率是让画面更好看。最简单的方式是准备两张 PNG 图片一张小人、一张金币然后用pygame.image.load加载并替换掉Surface方块。需要注意三点第一图片格式推荐用 PNG它支持透明背景第二图片大小要控制好可以在加载后调用pygame.transform.scale缩放比如统一缩放到 80x60 和 30x30第三加载之后要重新获取rect因为图片的尺寸和原来的方块不一样。修改后的金币类如下class Gold(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image pygame.image.load(gold.png) self.image pygame.transform.scale(self.image, (30, 30)) self.rect self.image.get_rect() self.rect.x random.randint(0, SCREEN_WIDTH - self.rect.width) self.rect.y -self.rect.height self.speed random.randint(4, 8)5.2 加炸弹、加道具、加难度曲线如果想要游戏更好玩不需要大改架构因为精灵类和主循环的框架都是现成的。加炸弹最容易写一个Bomb类逻辑和Gold几乎一样区别是外观是红色并且update()返回的语义不同——碰到炸弹不减分而是直接结束游戏或者在碰撞检测时额外写一个spritecollide判断。加生命值也简单在Player类里加一个lives属性漏接时减生命归零才结束游戏。想加难度曲线可以在主循环里根据得分动态调整生成间隔spawn_interval max(8, 25 - score // 10) if spawn_count % spawn_interval 0: gold_group.add(Gold())这样每得 10 分金币生成间隔就会缩短一点游戏节奏逐渐加快玩家会一直保持新鲜感。这类“数值驱动难度”思路在商业游戏里太常见了你亲自实现一遍会很有成就感。5.3 打包成 exe 发给朋友玩自己电脑上运行没问题之后很多人想把它发给朋友。这时候需要打包成独立可执行文件。用 PyInstaller 就能做到pip install pyinstaller pyinstaller --onefile --noconsole main.py命令跑完在dist目录下就能找到一个单独的 exe 文件双击即可运行朋友的电脑上不需要安装 Python 也能玩。这里有几条经验。第一如果用了外部图片和音频打包时默认不会自动包含它们需要用--add-data参数把文件一起打进去或者把资源文件复制到 exe 同目录下。第二--noconsole参数用于隐藏命令行窗口如果不加运行时会弹出一个黑乎乎的终端很影响体验。第三exe 体积会比较大一个 120 行的游戏打出来可能有 30-40MB放心这是 PyInstaller 把 Python 解释器一起打进去了不是代码有问题。我打包过好多次最常遇到的问题就是杀毒软件误报。解决方案是设置 PyInstaller 加--noupx参数或者用 UPX 前先测试哪个版本不会被误报。当然自己用或给熟人玩一般问题不大。写到这里这个“小人接金币”就已经从无到有完整跑通了。回想我最初接触 pygame 写这个项目的时候最大的教训就是急着写完整代码而不理解主循环的“更新-绘制”节奏后来踩过几次坑才明白游戏程序不是一个从头到尾执行的脚本而是一个以帧为单位一直心跳运行的引擎。如果你自己动手写的时候卡住了记住那句话先跑起来再改细节。把最小的循环跑通再一点点往里面加东西这是我觉得最靠谱的入门方法。