猫咪狂奔记Simons Cat这类卡通跑酷游戏难点不在画面而在跳跃重力、障碍物生成和碰撞反馈这三件事如何协同。用 Python 和 Pygame 仿写一个最小版本可以发现游戏开发入门其实比想象中更接近工程问题需要拆模块、设置常量、处理帧率、排查碰撞。这篇文章会带读者完成一个可直接运行的“猫咪狂奔记”同人学习版并说明从“在编辑器里能跑”到“打包给别人能玩”之间还需要补上哪些工程意识。文章里的代码只用于学习 Pygame 游戏循环不包含 Simons Cat 官方图片、音效或素材。本地练习时可以用占位色块和自制图形发布或商用前需要替换成合规素材。1. 先想清楚“猫咪狂奔记”的游戏核心是什么1.1 从动画到跑酷玩法的映射“猫咪狂奔记Simons Cat”这类卡通跑酷游戏表面上考验反应速度背后的核心机制只有三个玩家角色持续向右或向前移动视觉上表现为背景和障碍物向左滚动。玩家通过按键让角色跳跃跳跃过程受重力影响。角色碰到障碍物时游戏结束按路程或障碍物数量计算得分。动画追求的是有趣的角色表演跑酷游戏追求的是“预判、起跳、落地、继续循环”的反馈节奏。做代码实现时不需要先做复杂美术而要先能把一条游戏循环跑起来。Pygame 里做跑酷游戏最合适的模型是“一个主循环 多个状态”。主循环负责接收事件、更新数据、绘制画面状态机负责区分待开始、游戏中、结束重来三个阶段。游戏状态玩家操作系统行为进入方式ready按空格显示开始提示不生成障碍物程序启动或重开后playing按空格跳跃重力、障碍物、分数同时更新从 ready 按空格over按空格重开停止更新显示结束提示发生碰撞后1.2 用最小闭环定义开发目标在开始写代码前先定义一个可验收的最小版本窗口大小固定为 960x540。玩家是一个地面上的方块按下空格向上跳松开后受重力落回地面。障碍物每隔固定时间从右侧生成向左移动。玩家和障碍物矩形相交时游戏结束。屏幕显示当前分数结束后按空格重新开始。这个闭环覆盖了 Pygame 游戏最主要的四部分事件、更新、绘制、状态切换。后续要加动画、音效、道具、菜单都是在这个闭环上扩展。2. 环境准备与项目骨架要先对齐否则后面配置都会出错2.1 Python 和 Pygame 环境检查Pygame 是 Python 的第三方游戏库安装并不复杂但版本不一致会导致运行结果有差异。开始前先确认 Python 版本和 pip 是否可用。python --version pip --version确认 Python 环境正常后安装 Pygamepip install pygame安装完成后建议先打印版本号确认导入成功python -c import pygame; print(pygame.version.ver)这里要注意如果电脑里同时装了多个 Python 版本python和pip可能指向不同环境。推荐使用虚拟环境隔离项目依赖。python -m venv venvWindows 下进入虚拟环境venv\Scripts\activate在 macOS 或 Linux 下进入虚拟环境source venv/bin/activate然后再次执行 pip 安装。这样不会污染全局环境也方便后续根据requirements.txt重建依赖。pip freeze requirements.txt2.2 项目目录与资源文件规划Pygame 项目很容易把代码和图片全部堆到一个文件里。前期跑通没问题但一旦要加音效、角色动画和打包发布就会非常痛苦。推荐从第一天就按下面结构组织文件cat_run/ ├── main.py ├── config.py ├── game.py ├── player.py ├── obstacle.py ├── utils.py └── assets/ ├── images/ │ ├── cat.png │ ├── obstacle.png │ └── background.png └── sounds/ ├── jump.wav └── hit.wav模型对象拆分为独立文件config.py存放所有可调常量。player.py玩家跳跃、重力、碰撞矩形。obstacle.py障碍物生成和移动。game.py游戏状态、事件处理、更新和绘制。main.py程序入口负责初始化窗口和主循环。assets 目录在初始阶段可以只建空目录。如果图片缺失代码里可以生成占位色块避免程序直接崩溃。2.3 最小窗口程序先跑通不急着写完整游戏先写一个最小窗口程序确认 Pygame 环境、窗口创建、事件循环都没有问题。import pygame WIDTH 960 HEIGHT 540 FPS 60 pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(猫咪狂奔记Simons Cat 风格学习版) clock pygame.time.Clock() running True while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False screen.fill((135, 206, 235)) pygame.display.flip() clock.tick(FPS) pygame.quit()这个程序只做三件事创建窗口、保持事件循环、按 60 FPS 刷新画面。如果这一步能正常关闭窗口说明后续代码可以继续。如果这里都报错问题大概率出在环境安装而不是游戏逻辑。注意不要直接在所有代码写完后才运行。先跑通最小窗口再逐步加上玩家、障碍物、碰撞和状态切换可以把问题隔离在更小的范围内。3. 实现猫咪狂奔记的核心模块3.1 玩家角色按键跳跃与重力模拟跑酷游戏里跳跃手感最重要。跳跃不是直接把角色移动一段距离而是给角色一个向上的初速度然后每一帧都叠加一个向下的重力加速度。在config.py中先定义好全局常量WIDTH 960 HEIGHT 540 FPS 60 GRAVITY 0.8 JUMP_SPEED -15 GROUND_Y 460 PLAYER_X 120 OBS_MIN_SPEED 6 OBS_MAX_SPEED 10 OBS_INTERVAL 1400 COLOR_BG (135, 206, 235) COLOR_CAT (255, 180, 0) COLOR_OBS (80, 80, 80)其中GRAVITY是每帧重力加速度JUMP_SPEED是起跳瞬间的垂直速度。因为屏幕坐标 y 轴向下向上跳时初速度是负数。玩家类可以这样写import pygame from config import * class Player: def __init__(self): self.reset() def reset(self): self.x PLAYER_X self.y GROUND_Y self.vy 0 self.size (50, 50) self.rect pygame.Rect( self.x, self.y - self.size[1], self.size[0], self.size[1] ) def jump(self): if self.rect.bottom GROUND_Y: self.vy JUMP_SPEED def update(self, dt): self.vy GRAVITY * dt self.rect.y self.vy * dt if self.rect.bottom GROUND_Y: self.rect.bottom GROUND_Y self.vy 0 def draw(self, screen): pygame.draw.rect(screen, COLOR_CAT, self.rect)关键点有两个。第一jump里判断了self.rect.bottom GROUND_Y这是为了避免玩家在空中连续按空格反复起跳。实际项目中这个判断可以换成“允许二段跳”等不同玩法。第二update里最后把rect.bottom钳制到地面位置保证角色不会掉到地面下面。地面线不是 yHEIGHT而是 GROUND_Y是为了给地面留出视觉空间。3.2 障碍物随机生成与滚动障碍物不需要一开始就做复杂纹理。先让一个矩形从右侧向左移动验证移动逻辑再替换成图片。import random import pygame from config import * class Obstacle(pygame.sprite.Sprite): def __init__(self): super().__init__() self.w random.randint(30, 60) self.h random.randint(60, 100) self.image pygame.Surface((self.w, self.h)) self.image.fill(COLOR_OBS) self.rect self.image.get_rect() self.rect.x WIDTH random.randint(0, 60) self.rect.bottom GROUND_Y self.speed random.randint(OBS_MIN_SPEED, OBS_MAX_SPEED) def update(self, dt): self.rect.x - self.speed * dt这里OBS_MAX_SPEED和OBS_MIN_SPEED控制障碍物难度。障碍物越高玩家需要起跳的时机越精确障碍物越宽越容易碰撞。实际调优时可以分别调整。障碍物生成频率由 Pygame 的自定义事件控制。在Game初始化时设置定时器pygame.time.set_timer(pygame.USEREVENT 1, OBS_INTERVAL)每隔OBS_INTERVAL毫秒事件队列里就会多出一个pygame.USEREVENT 1事件。在这个事件里生成一个障碍物就可以避免在主循环里手写计时器。3.3 碰撞检测用矩形近似代替像素检测Pygame 的Sprite自带矩形碰撞检测最常用的是colliderect。它的含义是判断两个矩形区域是否有交集。if self.player.rect.colliderect(obstacle.rect): self.state over但这里有个重要问题如果用整张透明图片做矩形透明区域也算碰撞范围会出现“明明没碰到猫身体却判定死亡”的情况。推荐做法是给玩家使用一个稍小的碰撞框让判定更接近真实视觉轮廓。hitbox self.player.rect.inflate(-10, -10) if hitbox.colliderect(obstacle.rect): self.state overinflate(-10, -10)会把矩形四周各缩小 10 像素。这样做的好处是手感比较宽松玩家体验更好。3.4 分数、音效和游戏状态流转核心游戏循环放在game.py中负责整合玩家、障碍物、分数和状态。import pygame from config import * from player import Player from obstacle import Obstacle class Game: def __init__(self, screen): self.screen screen self.running True self.score 0 self.state ready self.player Player() self.obstacles pygame.sprite.Group() pygame.time.set_timer(pygame.USEREVENT 1, OBS_INTERVAL) def handle_event(self, event): if event.type pygame.QUIT: self.running False elif event.type pygame.KEYDOWN: if event.key pygame.K_SPACE: if self.state ready: self.state playing elif self.state playing: self.player.jump() elif self.state over: self.reset() elif event.type pygame.USEREVENT 1: if self.state playing: self.obstacles.add(Obstacle()) def reset(self): self.score 0 self.player.reset() self.obstacles.empty() self.state ready def update(self, dt): if self.state ! playing: return self.player.update(dt) self.obstacles.update(dt) for obstacle in self.obstacles: hitbox self.player.rect.inflate(-10, -10) if hitbox.colliderect(obstacle.rect): self.state over return if obstacle.rect.right 0: obstacle.kill() self.score 1 def draw(self): self.screen.fill(COLOR_BG) pygame.draw.line( self.screen, (0, 0, 0), (0, GROUND_Y), (WIDTH, GROUND_Y), 2 ) if self.state playing or self.state over: self.player.draw(self.screen) self.obstacles.draw(self.screen) self.draw_score() if self.state ready: self.draw_text(Press SPACE to Start, WIDTH // 2, HEIGHT // 2) if self.state over: self.draw_text( Game Over - Press SPACE to Restart, WIDTH // 2, HEIGHT // 2 60 ) pygame.display.flip() def draw_score(self): font pygame.font.Font(None, 36) text font.render(fScore: {self.score}, True, (0, 0, 0)) self.screen.blit(text, (20, 20)) def draw_text(self, content, x, y): font pygame.font.Font(None, 48) text font.render(content, True, (255, 255, 255)) rect text.get_rect(center(x, y)) self.screen.blit(text, rect)状态机是整个游戏最核心的控制点。update只在playing状态更新handle_event根据不同状态处理空格键。reset负责清理分数、玩家位置和障碍物避免重开时残留上一局数据。入口文件main.py只需要做初始化import pygame from config import WIDTH, HEIGHT, FPS from game import Game def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(猫咪狂奔记Simons Cat 风格学习版) clock pygame.time.Clock() game Game(screen) game.run(clock) if __name__ __main__: main()Game类里需要补充一个run方法def run(self, clock): while self.running: dt clock.tick(FPS) / 1000.0 * 60 for event in pygame.event.get(): self.handle_event(event) self.update(dt) self.draw() pygame.quit()dt是帧率归一化因子。如果程序稳定运行在 60 FPSdt约等于 1如果降到 30 FPSdt约等于 2。这样角色和障碍物移动速度不会因为帧率波动而明显变化。4. 关键参数解释与运行验证4.1 核心常量速查表刚接触 Pygame 的人往往会到处改数字结果手感改乱。推荐把可调参数集中在config.py改完立刻运行验证。参数默认值作用调大影响调小影响FPS60控制帧率更流畅CPU 占用更高更省资源但画面可能卡顿GRAVITY0.8控制下落加速度下落更快手感更重下落更慢手感更轻JUMP_SPEED-15控制起跳初速度跳得更高跳得更矮GROUND_Y460地面 y 坐标地面更低地面更高OBS_MIN_SPEED6障碍物最低移速整体更难整体更简单OBS_MAX_SPEED10障碍物最高移速随机难度上限高难度更稳定OBS_INTERVAL1400障碍物生成间隔ms障碍物更少障碍物更密PLAYER_X120玩家固定 x 坐标更靠近右侧反应时间短更靠近左侧反应时间长4.2 运行结果与验证方法在项目根目录执行python main.py预期结果出现 960x540 窗口背景为浅蓝色。初始显示 “Press SPACE to Start”。按空格进入游戏出现黄色玩家方块。按空格角色上跳随后落回地面。右侧每隔一段距离生成灰色障碍物。碰到障碍物后显示结束提示。按空格重新开始分数归零。验证时不能只看“窗口能打开”要单独确认每一项跳跃是否只能在地面触发。角色落地后不会穿到地面下方。障碍物是否持续向左移动。障碍物离开左边界后是否会被移除并加分。重开后旧障碍物是否全部清空。4.3 常见运行报错和解决方法报错信息常见原因处理方式ModuleNotFoundError: No module named pygame没有安装 pygame或安装到了另一个 Python 环境确认当前虚拟环境后重新pip install pygamepygame.error: video system not initialized在pygame.init()之前创建窗口或加载资源保证所有 Pygame 调用都发生在初始化之后ValueError: subsurface rectangle not contained within surface图片尺寸与代码中矩形不匹配检查图片路径和实际尺寸游戏速度与显示器刷新率不一致只按帧数控制速度没有使用 dt统一使用clock.tick(FPS) / 1000.0 * 60窗口一闪而过pygame.quit()前没有循环阻塞检查 while running 循环是否正常进入5. 从“能跑”到“能发布”的工程化改造5.1 配置外置与资源加载在纯学习项目里常量写在config.py已经足够。但如果要交付给别人使用建议把窗口大小、速度、障碍物间隔等参数抽到外部配置文件里例如 JSON 或 INI避免每次修改都改代码。{ width: 960, height: 540, fps: 60, gravity: 0.8, jump_speed: -15, ground_y: 460, obs_interval_ms: 1400 }加载资源时尽量不要直接假设图片一定存在。可以写一个安全加载函数文件缺失时用占位色块替代并打印警告日志。import os import pygame def load_image(path, sizeNone, fallback_color(255, 180, 0)): if os.path.exists(path): try: image pygame.image.load(path) if size: image pygame.transform.scale(image, size) return image.convert_alpha() except pygame.error as exc: print(fload image failed: {path}, {exc}) image pygame.Surface(size, pygame.SRCALPHA) image.fill(fallback_color) return imageconvert_alpha可以保留 PNG 的透明通道。如果加载的是不透明 JPG可以用convert()速度更快。5.2 日志、异常处理和帧率控制学习代码不需要复杂日志但最好保留最基本的可见性。主循环外层加异常处理可以避免游戏崩溃时只留下一个黑窗口。def main(): pygame.init() screen pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption(猫咪狂奔记Simons Cat 风格学习版) clock pygame.time.Clock() game Game(screen) try: game.run(clock) except Exception: pygame.quit() raise这里在异常时先pygame.quit()再重新抛出异常方便看到完整堆栈同时避免进程卡在未清理的窗口中。生产环境还需要考虑帧率是否固定还是允许垂直同步。在低配机器上是否自动降低障碍物移动速度。日志文件是否写入磁盘。崩溃后是否需要上报错误信息。5.3 打包为可执行程序把 Python 项目给别人玩对方不一定装了 Python。使用 PyInstaller 可以打包成独立可执行文件。pip install pyinstallerWindows 下打包并带上 assets 目录pyinstaller -F -w --add-data assets;assets main.pymacOS 或 Linux 下--add-data的分隔符是冒号pyinstaller -F -w --add-data assets:assets main.py参数说明-F打包成单个文件。-w不显示控制台窗口。--add-data把资源目录一起打进去。打包后在dist目录里可以看到可执行文件。要注意 PyInstaller 的资源路径和源码运行时的路径不一样。建议在代码里使用sys._MEIPASS判断资源根目录。import sys import os def resource_path(relative): if hasattr(sys, _MEIPASS): return os.path.join(sys._MEIPASS, relative) return os.path.join(os.path.dirname(__file__), relative)注意-F单文件模式启动较慢而且杀毒软件容易误报。如果正式发布优先使用目录模式-D。6. 常见坑和排查链路6.1 图片透明背景变黑用 PNG 做角色时如果不调用convert_alpha()直接用pygame.image.load().convert()透明区域可能被填充成黑色。解决方式image pygame.image.load(cat.png).convert_alpha()或者创建带透明通道的 Surfacesurface pygame.Surface((50, 50), pygame.SRCALPHA)6.2 碰撞判定过于灵敏或失灵碰撞判定过于灵敏通常是直接使用原图矩形透明区域也算碰撞。碰撞判定失灵通常是玩家已经掉落到地面以下或者障碍物移动步长太大跨过了玩家矩形。处理方式给玩家矩形使用inflate(-10, -10)缩小碰撞框。保证GROUND_Y和障碍物 bottom 对齐。如果障碍物速度很快可以连续多次移动不要一次移动太大距离。6.3 不同显示器帧率和速度不一致如果在障碍物移动时直接写rect.x - 6那么 30 FPS 下移动速度只有 60 FPS 的一半。更合理的方式是使用dt做时间归一化。rect.x - speed * dtdt的计算放在主循环dt clock.tick(FPS) / 1000.0 * 60这样不同刷新率下每秒实际移动距离基本一致。6.4 排查链路从现象倒推原因如果游戏表现不符合预期按下面顺序排查确认是渲染问题还是逻辑问题。检查输入事件是否进入了正确分支。检查玩家位置、速度、地面阈值。检查障碍物生成事件是否被触发。检查碰撞矩形位置是否可见。检查pygame.display.flip()是否被调用。检查运行日志是否出现资源加载失败。现象可能原因检查方式处理建议按空格没反应事件类型判断错误或状态没进入 playing在 handle_event 里打印 event 和 state确认空格事件没有被二次封装角色一直下落GROUND_Y 没有生效或 rect 被改错打印 rect.bottom 和 GROUND_Y给地面边界增加钳制逻辑障碍物看不见图片为空或 rect 在屏幕外打印障碍物 rect 坐标检查生成位置是否在 WIDTH 右侧分数不增加障碍物离开边界后没有被 kill打印 obstacle.rect.right在 update 中增加越界清理重开后障碍物还在reset 没有清理 Group打印 len(self.obstacles)调用self.obstacles.empty()7. 可复用清单和学习扩展方向7.1 “猫咪狂奔记”开发检查清单[ ] Python 和 Pygame 版本已确认虚拟环境隔离依赖。[ ]config.py集中管理常量不需要改代码才能调参。[ ] 最小窗口程序先跑通再进入完整逻辑。[ ] 玩家跳跃有地面判断不能在空中无限起跳。[ ] 玩家落地后 y 坐标被钳制在地面之上。[ ] 障碍物使用 Sprite Group 管理移动使用 dt 归一化。[ ] 碰撞检测使用缩小后的 hitbox而不是原始图片矩形。[ ] 障碍物离开屏幕后被移除并累计分数。[ ] 游戏状态 ready、playing、over 流转完整。[ ] 图片资源加载有缺失时的 fallback不会直接崩溃。[ ] 音效文件缺失时不阻断主流程。[ ] 打包发布时 assets 被正确包含路径兼容 PyInstaller。[ ] 在 30 FPS、60 FPS、120 FPS 环境下分别验证速度一致性。7.2 后续扩展方向当前版本只是跑酷游戏的地基可以继续扩展的方向包括玩家动画使用多张图片按帧切换跑步和跳跃动画。道具系统增加加速、护盾、额外生命等游戏元素。难度曲线随着分数升高逐步降低障碍物生成间隔、提高移动速度。音效和背景音乐使用pygame.mixer.Sound和pygame.mixer.music。关卡背景使用视差滚动让背景层移动速度慢于障碍物层。数据存储把最高分保存到本地文件重开游戏后仍然存在。更好的碰撞模型从矩形升级为圆形或多圆组合减少误判。建议下一步把“最高分持久化”和“音效加载”两个功能先加上。它们不复杂却能让项目从“可以演示”变成“可以留着玩”。真实游戏项目里跑酷的核心从来不是代码量而是手感。手感来自重力和跳跃速度的反复调整。每次改一个数字就运行一次观察落地瞬间和起跳高度直到节奏符合直觉。回到最初的问题猫咪狂奔记Simons Cat风格的游戏能复现吗当然可以。但更重要的不是复现某个具体画面而是理解它背后的游戏循环如何工作。把这个循环理解清楚之后换成 Unity、Godot 或其他游戏框架也只是换一套 API 重新表达同样的逻辑。