最近在补 Pygame 游戏开发的入门实践想到《Simons Cat》里那只总在饿肚子边缘疯狂试探的黑白猫手感特别适合做成一个 2D 跑酷小游戏。于是动手写了一个叫“猫咪狂奔记”的横版跳跃项目猫咪持续向右奔跑玩家用空格键控制起跳躲避栅栏、纸箱这类障碍物同时尽量吃到飞在空中的小鱼来加分。本文会从游戏设计思路讲起完整拆解用 Python Pygame 实现这个游戏的全过程。所有代码都放在一个main.py里方便直接复制运行适合刚学完 Python 基础、想通过小项目巩固面向对象和事件循环的读者也适合想快速做一个课设样例的开发者。代码虽然简单但游戏循环、重力模拟、碰撞检测、随机生成这些核心概念都会覆盖到。为什么用 Simons Cat 这个题材Simons Cat 是英国动画师西蒙·托菲尔德Simon Tofield创作的系列动画短片主角是一只黑白相间、表情丰富的小猫。它的动作夸张经常为了吃的做出各种捣蛋行为非常适合用来做跑酷游戏的主角。用这个题材做游戏主要出于三个考虑角色辨识度高。黑白猫形象简单即使不用美术素材用 Pygame 的基础绘图函数也能画出一个“神似”的猫咪。动画风格天然适配跑酷玩法。猫咪奔跑、扑食、被绊倒的肢体语言转换成游戏动画和碰撞反馈都很自然。开发成本低。这个项目完全可以不依赖外部图片所有角色、障碍物、食物都用几何图形绘制代码自包含新手也能跑起来。1. 背景与核心概念1.1 2D 跑酷游戏的基本构成“猫咪狂奔记”本质上是一个横版跑酷游戏。这一类游戏的核心逻辑可以归纳成下面几条人物自动向右侧移动或者在原地奔跑、场景向左移动。玩家需要控制跳跃和落地时机躲避障碍物。场景中随机出现加分道具鼓励玩家主动冒险。随着时间推移游戏难度逐渐提升玩家最终会因为一次失误而结束游戏。本文采用的实现方案是“人物原地奔跑场景物体向左移动”。这样做的好处是猫咪的坐标变化简单只需要维护一个垂直方向的 y 坐标地面和跳跃逻辑好算也不容易出坐标漂移的 bug。1.2 Pygame 在其中的作用Pygame 是 Python 非常经典的 2D 游戏开发库它封装了窗口创建、键盘鼠标事件、图形绘制、字体渲染和音频播放等能力。你不需要关心底层 OpenGL 或者窗口系统只需要把注意力放在游戏逻辑上。这个项目会用到的 Pygame 核心模块包括模块作用pygame.display创建游戏窗口刷新画面pygame.event接收键盘、退出等事件pygame.draw绘制矩形、圆形、多边形等基础图形pygame.Rect表示矩形区域用于碰撞检测pygame.time.Clock控制游戏帧率pygame.font在画面上绘制文字理解这些模块就能理解一个 Pygame 项目的大部分结构。1.3 这个项目的学习收益完成这个项目你会掌握游戏循环的基本写法事件处理、状态更新、画面渲染。基于帧的物理模拟重力、跳跃、落地。基于矩形区域的碰撞检测。面向对象思想在游戏中的实际应用。随机生成关卡元素和难度递增机制。这些知识点可以平移到很多 2D 游戏项目中哪怕是以后去接触 Unity、Godot这一套游戏循环思想也是通用的。2. 环境准备与项目结构2.1 Python 与 Pygame 安装本文代码基于 Python 3.8 及以上版本Pygame 使用 2.x 系列。如果你电脑里还没有 Python可以先去官网下载安装包。装好之后在命令行里确认版本python --version然后安装 Pygamepip install pygame如果下载速度慢可以使用国内镜像源pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后可以验证python -c import pygame; print(pygame.version.ver)能看到版本号就说明环境没问题。2.2 版本兼容性说明本文示例代码使用pygame.Rect的border_radius参数来画圆角矩形这是 Pygame 2.0 之后才支持的特性。如果你安装的是 Pygame 1.9.x运行代码可能会报got an unexpected keyword argument border_radius之类的错误。解决办法要么是升级 Pygamepip install --upgrade pygame要么把手动把代码里的border_radiusxx参数删掉。本文下面的代码以 Pygame 2.x 为准。2.3 项目结构设计为了让新手更容易上手这个项目采用单文件结构。所有游戏代码都写在main.py中。如果你后面想扩展可以按职责拆分成多个文件比如cat_run/ ├── main.py # 游戏入口与主循环 ├── config.py # 常量配置 ├── cat.py # 猫咪角色类 ├── obstacle.py # 障碍物类 ├── food.py # 食物类 └── utils.py # 通用工具函数本文先以单文件为主在文章后面会给出拆分建议。3. 游戏核心机制拆解3.1 游戏循环的三大步骤几乎所有 Pygame 程序都遵循同一个循环结构处理事件判断用户是否点击关闭窗口、是否按键。更新状态根据当前输入和时间更新角色位置、障碍物位置、分数等变量。绘制画面把当前状态画到窗口上然后刷新显示。这个循环每秒会执行多次具体次数由clock.tick(FPS)控制。本文设定FPS 60也就是每秒循环 60 次这样可以保证在不同性能的电脑上游戏速度基本一致。3.2 帧率、速度与坐标系统Pygame 的窗口坐标系以左上角为原点x 轴向右增大y 轴向下增大。在这个项目中猫咪的 x 坐标固定不变保持在窗口左侧。障碍物和食物的 x 坐标从窗口右侧不断减小实现“向左移动”的视觉效果。地面是一条水平线草地位于窗口底部。速度单位是“像素/帧”。比如障碍物每帧移动 5 像素在 60 帧每秒的情况下就是每秒移动 300 像素。速度的数值会影响游戏手感这是后续调整难度时需要重点关注的参数。3.3 跳跃、重力与碰撞检测跳跃是最容易做崩的部分。一个简单的实现方式是给猫咪一个垂直速度变量vy。每次更新时给vy加上一个重力加速度gravity然后让y坐标加上vy。当y到达地面高度时把y拉回地面并将vy归零。玩家按下空格键时如果猫咪在地面就给vy设置一个向上的初速度。用物理的语言说这就是每一帧做一次数值积分。代码里可以这样理解vy gravity # 速度受重力影响 y vy # 位置受速度影响跳跃初速度jump_power和重力gravity的比值决定了跳跃高度和手感。初速度绝对值越大跳得越高重力越大起跳和落地越快手感越“重”。碰撞检测使用pygame.Rect.colliderect()本质是判断两个矩形是否有交集。每次更新后把猫咪的矩形区域和所有障碍物的矩形区域做一次相交判断如果相交就触发游戏结束。3.4 随机生成与难度递增为了让每一局都不同障碍物和食物的出现位置需要随机。本文用random.random()控制生成概率有 70% 的概率生成障碍物30% 的概率生成奖励食物。难度递增通过“每隔一段时间加速”来实现。注意这里的速度不能无限增加否则游戏会很快变得不可玩所以我在代码里加了最大速度限制。4. 完整实战从零写一个可运行的猫咪狂奔小游戏4.1 创建项目文件新建一个文件夹cat_run在里面创建main.py。完整代码如下我会按功能模块拆分讲解。4.2 初始化窗口与常量这一部分负责导入依赖、定义窗口尺寸、颜色、字体等全局配置。# 文件路径cat_run/main.py import pygame import random import sys # 初始化 Pygame pygame.init() # 窗口尺寸 SCREEN_WIDTH 800 SCREEN_HEIGHT 400 GRASS_HEIGHT 80 # 颜色常量RGB SKY_BLUE (135, 206, 235) GRASS_GREEN (124, 252, 0) DARK_GREEN (34, 139, 34) BODY_WHITE (240, 240, 240) EAR_BLACK (30, 30, 30) EYE_BLACK (20, 20, 20) NOSE_PINK (255, 182, 193) OBSTACLE_RED (178, 34, 34) FOOD_GOLD (255, 215, 0) WHITE (255, 255, 255) # 游戏帧率 FPS 60 # 创建窗口和时钟 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(猫咪狂奔记 - Simons Cat 风格跑酷) clock pygame.time.Clock() # 字体先尝试使用中文字体找不到就退回默认字体 try: font pygame.font.SysFont(simhei, 28, boldTrue) except Exception: font pygame.font.Font(None, 28)这段代码里有一个细节pygame.font.SysFont(simhei, ...)在不同系统上可能找不到对应字体所以用try-except做了一个回退。这样代码在 Windows、macOS、Linux 上都能正常运行。4.3 实现猫咪角色类Cat类负责猫咪的绘制、跳跃和地面检测。class Cat: 猫咪角色负责绘制、跳跃和地面检测。 def __init__(self, x, y, width60, height50): self.x x self.y y self.width width self.height height self.vy 0 self.on_ground True self.jump_power -14 self.gravity 0.8 def jump(self): 在地面上时触发跳跃给一个向上的初速度。 if self.on_ground: self.vy self.jump_power self.on_ground False def update(self): 更新猫咪的垂直位置并处理落地逻辑。 self.vy self.gravity self.y self.vy ground_level SCREEN_HEIGHT - GRASS_HEIGHT - self.height if self.y ground_level: self.y ground_level self.vy 0 self.on_ground True def get_rect(self): 返回猫咪的矩形区域用于碰撞检测。 return pygame.Rect(self.x, self.y, self.width, self.height) def draw(self, surface): 在画面上绘制一只简化版猫咪。 head_x self.x 10 head_y self.y - 10 # 尾巴左右摆动 tail_sway pygame.time.get_ticks() // 100 % 2 if tail_sway 0: tail_points [ (self.x 55, self.y 5), (self.x 75, self.y - 5), (self.x 80, self.y 10), ] else: tail_points [ (self.x 55, self.y 5), (self.x 75, self.y 15), (self.x 80, self.y 2), ] pygame.draw.lines(surface, EAR_BLACK, False, tail_points, 4) # 身体 body_rect pygame.Rect(self.x, self.y, 52, 38) pygame.draw.rect(surface, BODY_WHITE, body_rect, border_radius12) # 头 pygame.draw.circle(surface, BODY_WHITE, (head_x, head_y), 18) # 耳朵两个黑色三角形 left_ear [ (head_x - 16, head_y - 6), (head_x - 10, head_y - 22), (head_x - 2, head_y - 12), ] right_ear [ (head_x 2, head_y - 12), (head_x 10, head_y - 22), (head_x 16, head_y - 6), ] pygame.draw.polygon(surface, EAR_BLACK, left_ear) pygame.draw.polygon(surface, EAR_BLACK, right_ear) # 眼睛和鼻子 pygame.draw.circle(surface, EYE_BLACK, (head_x - 7, head_y - 2), 3) pygame.draw.circle(surface, EYE_BLACK, (head_x 7, head_y - 2), 3) pygame.draw.circle(surface, NOSE_PINK, (head_x, head_y 5), 3) # 胡须 pygame.draw.line(surface, EAR_BLACK, (head_x - 10, head_y 4), (head_x - 22, head_y 1), 1) pygame.draw.line(surface, EAR_BLACK, (head_x - 10, head_y 6), (head_x - 22, head_y 8), 1) pygame.draw.line(surface, EAR_BLACK, (head_x 10, head_y 4), (head_x 22, head_y 1), 1) pygame.draw.line(surface, EAR_BLACK, (head_x 10, head_y 6), (head_x 22, head_y 8), 1)jump_power为负数是因为 Pygame 的 y 轴向下减小 y 坐标意味着向上移动。gravity为 0.8 表示每帧速度增加 0.8 像素/帧配合 -14 的初速度跳跃最高点大约为最高跳跃高度 jump_power² / (2 * gravity) ≈ 14² / 1.6 ≈ 122 像素这个高度足够跳过 50 像素高的障碍物同时也能吃到设置在高处的食物。4.4 实现障碍物与食物障碍物Obstacle类只有两个核心职责向左移动和绘制自身。class Obstacle: 障碍物从右侧向左移动形状类似栅栏或纸箱。 def __init__(self, x, y, width32, height40, speed5): self.rect pygame.Rect(x, y, width, height) self.speed speed self.max_speed 15 self.passed False def update(self): 向左移动障碍物。 self.rect.x - self.speed def draw(self, surface): 绘制障碍物并添加白色条纹装饰。 pygame.draw.rect(surface, OBSTACLE_RED, self.rect, border_radius4) stripe1 pygame.Rect(self.rect.x 6, self.rect.y 8, 5, self.rect.height - 16) stripe2 pygame.Rect(self.rect.x 20, self.rect.y 8, 5, self.rect.height - 16) pygame.draw.rect(surface, WHITE, stripe1, border_radius2) pygame.draw.rect(surface, WHITE, stripe2, border_radius2)食物Food类画成一条金色小鱼。鱼的 x 坐标同样向左移动玩家碰到后加分。class Food: 食物小鱼拾取后加分。 def __init__(self, x, y, radius10, speed5): self.radius radius self.rect pygame.Rect(x, y, radius * 2, radius * 2) self.speed speed self.max_speed 15 self.collected False def update(self): 向左移动食物。 self.rect.x - self.speed def draw(self, surface): 绘制一条简化版小鱼。 center_x self.rect.x self.radius center_y self.rect.y self.radius # 鱼身体 fish_body pygame.Rect(center_x - 8, center_y - 5, 12, 10) pygame.draw.ellipse(surface, FOOD_GOLD, fish_body) # 鱼尾巴 tail [ (center_x 4, center_y), (center_x 10, center_y - 6), (center_x 10, center_y 6), ] pygame.draw.polygon(surface, FOOD_GOLD, tail) # 鱼眼睛 pygame.draw.circle(surface, EYE_BLACK, (center_x - 4, center_y - 2), 2)这里需要注意两个类的更新逻辑都是rect.x - self.speed即向左移动。passed字段接下来会在主循环中用来标记“已经越过的障碍物”方便以后扩展得分逻辑。4.5 绘制背景为了让画面不那么单调我用简洁的几何图形绘制了蓝天、草地和云朵。def draw_background(surface): 绘制天空、草地和装饰性云朵。 surface.fill(SKY_BLUE) # 三朵云 clouds [(120, 60), (400, 100), (650, 50)] for cloud_x, cloud_y in clouds: pygame.draw.circle(surface, WHITE, (cloud_x, cloud_y), 20) pygame.draw.circle(surface, WHITE, (cloud_x 22, cloud_y - 8), 16) pygame.draw.circle(surface, WHITE, (cloud_x 44, cloud_y), 18) pygame.draw.rect(surface, WHITE, (cloud_x 8, cloud_y - 8, 36, 18)) # 草地 grass_rect pygame.Rect(0, SCREEN_HEIGHT - GRASS_HEIGHT, SCREEN_WIDTH, GRASS_HEIGHT) pygame.draw.rect(surface, GRASS_GREEN, grass_rect) # 小草装饰 for i in range(0, SCREEN_WIDTH, 40): blade_y SCREEN_HEIGHT - GRASS_HEIGHT pygame.draw.line(surface, DARK_GREEN, (i, blade_y), (i 4, blade_y - 10), 2)这段代码每次填充整个窗口再绘制对 800x400 的小窗口来说性能完全没问题。如果以后窗口做得很大再考虑只刷新发生变化的部分。4.6 主循环与游戏逻辑主函数把前面的模块串起来处理事件、生成物体、检测碰撞、绘制画面。def main(): cat Cat(80, SCREEN_HEIGHT - GRASS_HEIGHT - 50) obstacles [] foods [] score 0 game_over False spawn_timer 0 speedup_timer 0 running True while running: clock.tick(FPS) # 1. 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False if event.type pygame.KEYDOWN: if event.key pygame.K_SPACE: if game_over: # 重新开始 cat Cat(80, SCREEN_HEIGHT - GRASS_HEIGHT - 50) obstacles.clear() foods.clear() score 0 game_over False else: cat.jump() if not game_over: # 2. 更新状态 cat.update() # 按一定节奏生成障碍物或食物 spawn_timer 1 if spawn_timer 70: spawn_timer 0 if random.random() 0.7: obstacle_h random.choice([36, 44, 52]) obstacle_y SCREEN_HEIGHT - GRASS_HEIGHT - obstacle_h obstacles.append( Obstacle(SCREEN_WIDTH 20, obstacle_y, 32, obstacle_h) ) else: # 食物放在高处需要跳跃才能吃到 food_y SCREEN_HEIGHT - GRASS_HEIGHT - 100 foods.append(Food(SCREEN_WIDTH 20, food_y, 10)) # 更新障碍物并移出屏幕 for obstacle in obstacles[:]: obstacle.update() if obstacle.rect.right 0: obstacles.remove(obstacle) elif not obstacle.passed and obstacle.rect.right cat.x: obstacle.passed True # 更新食物并检测拾取 for food in foods[:]: food.update() if food.rect.right 0: foods.remove(food) elif cat.get_rect().colliderect(food.rect): foods.remove(food) score 10 # 碰撞检测 cat_rect cat.get_rect() for obstacle in obstacles: if cat_rect.colliderect(obstacle.rect): game_over True break # 难度递增限制最大速度 speedup_timer 1 if speedup_timer 120: speedup_timer 0 for obstacle in obstacles: if obstacle.speed obstacle.max_speed: obstacle.speed 1 for food in foods: if food.speed food.max_speed: food.speed 1 # 3. 绘制画面 draw_background(screen) for obstacle in obstacles: obstacle.draw(screen) for food in foods: food.draw(screen) cat.draw(screen) score_surface font.render(f得分: {score}, True, EYE_BLACK) screen.blit(score_surface, (10, 10)) if game_over: over_surface font.render(游戏结束按空格键重新开始, True, OBSTACLE_RED) over_rect over_surface.get_rect(center(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) screen.blit(over_surface, over_rect) pygame.display.flip() pygame.quit() sys.exit() if __name__ __main__: main()主循环中值得注意的几个点for obstacle in obstacles[:]这种写法是遍历列表的副本这样可以在循环过程中安全地执行remove。食物生成在SCREEN_HEIGHT - GRASS_HEIGHT - 100这个高度也就是猫咪跳跃后才能碰到避免玩家在地面上白捡分。碰撞检测是在所有物体更新完成之后进行的保证每一帧用的是最新位置。重新开始功能只重置角色、障碍物、食物和分数这几个关键变量窗口和字体不用重建。4.7 运行与验证运行完整代码python main.py预期效果窗口标题显示“猫咪狂奔记 - Simons Cat 风格跑酷”。背景是蓝天、白云、草地。一只白色身体、黑色耳朵和尾巴的小猫站在窗口左侧。障碍物从右侧不断出现并向左移动。按下空格键猫咪起跳落到草地上。碰到小鱼得分加 10 分碰到障碍物游戏结束并显示提示文字。等待几秒后障碍物移动速度明显变快。如果你能顺利看到这些表现说明整个游戏的核心逻辑已经跑通了。5. 常见问题与排查思路问题现象常见原因解决思路pip install pygame失败网络原因或 Python 版本不兼容使用国内镜像源安装确认 Python 版本为 3.8报错unexpected keyword argument border_radiusPygame 版本低于 2.0升级 Pygame 到 2.x或删除所有border_radius参数窗口闪退代码缩进错误或事件循环没有正确运行检查缩进确认在while running中处理了pygame.QUIT事件猫咪落到窗口底部以下地面检测逻辑没触发检查ground_level的计算公式确保y ground_level时重置位置跳跃没有反应键盘事件没有捕获或on_ground一直是 False在事件循环中确认打印event.key确认落地后on_ground被设为 True中文文字显示为方块系统找不到指定中文字体simhei改用pygame.font.Font(None, 28)或传入本地字体文件路径游戏速度在不同电脑上差别大没有控制帧率确保主循环第一行执行clock.tick(FPS)性能低画面卡顿每帧创建过多 Surface 对象把静态文字和背景提前渲染好避免每帧重建如果遇到“游戏结束后按空格无法重新开始”多半是事件处理里把game_over和普通跳跃判断写在了同一个分支之外。建议在事件处理里先用if event.key pygame.K_SPACE:统一捕获空格键再根据game_over决定是重置游戏还是触发跳跃。6. 最佳实践与工程建议6.1 代码组织与命名规范单文件示例适合学习但真实项目建议拆分模块。一个比较清晰的拆分方式如下config.py保存窗口尺寸、颜色、速度等常量。sprites.py统一管理所有游戏物体的类。main.py只负责游戏循环和事件分发。命名方面类名使用大驼峰如Cat、Obstacle函数和变量使用小写下划线如draw_background、game_over。颜色常量统一用大写字母加下划线方便复用。6.2 性能优化建议这个项目体量很小一般来说不会遇到性能瓶颈但以下几个习惯值得养成不要在draw()方法里重复调用pygame.font.Font(...)字体对象最好只创建一次。背景如果不是动态变化的可以提前渲染到一个Surface每帧直接blit上去。删除对象时优先使用remove而不是del避免遍历过程中修改列表导致的迭代异常。如果游戏物体数量很大可以把障碍物和食物分到两个列表避免每帧遍历所有对象做无用的碰撞检测。6.3 玩法与交互优化从“能玩”到“好玩”之间可以优化几个点跳跃手感。调整jump_power和gravity的数值让跳跃更轻盈或更沉重。蓄力跳跃。长按空格时跳得更高增加操作深度。双段跳。给猫咪额外一次空中跳跃的机会降低新手门槛。碰撞宽容度。把碰撞盒略微缩小不要用整个猫咪矩形做检测让玩家感觉更公平。6.4 后续扩展方向完成基础版本之后可以按下面的方向继续加功能引入图片素材。把几何绘制改为加载 PNG 序列帧提升画面表现力。添加音效。用pygame.mixer播放跳跃、得分、撞击音效。制作开始界面。增加标题、操作说明和最高分展示。增加多场景。随分数切换草地、夜晚、雨天等不同背景。数据持久化。把最高分写入本地文件或 SQLite方便后续做排行榜。控制方式扩展。支持鼠标点击跳跃、手机触屏虚拟按键等。每加一个功能之前先想清楚它到底是让游戏更有趣还是让玩家更容易疲惫。跑酷游戏的本质是节奏感新增功能最好服务于节奏而不是堆砌内容。7. 总结与学习路线到这里“猫咪狂奔记”小游戏已经完整落地了。回顾一下我们从 Simons Cat 这个题材出发梳理了 2D 跑酷游戏的核心机制然后用 Pygame 实现了窗口、绘制、键盘事件、重力跳跃、碰撞检测、随机生成和难度递增。整个流程虽然代码量不大但覆盖了游戏开发最常见的链路。建议你接下来不要急着继续加功能而是先做三件小事把jump_power改成 -16、-18感受跳跃高度变化对游戏难度的影响。把障碍物生成概率从 0.7 改成 0.5看看得分难度如何变化。把猫咪的颜色从白色改成灰色再自己加一副眼镜或领结锻炼绘制坐标的感觉。拿着这份代码多改几轮你对 Pygame 事件、碰撞和物理模拟的理解就会明显比“照着敲一遍”更深一层。跑酷游戏看似简单但想把跳跃手感打磨舒服里面可以研究的细节其实非常多。希望这篇教程能给你一个不错的起点。