资讯动态

Python小恐龙快跑:PyGame游戏开发与有限状态机实战

发布时间:2026/10/20 18:27:07 来源:尧图企业网站定制
简介这是一份面向Python初学者与游戏开发入门者的轻量级2D小游戏实践项目基于Chrome经典小恐龙快跑Dino Run进行仿制帮助学习者掌握pygame基础、面向对象设计及游戏主循环、碰撞检测、事件响应等核心机制。资源共2个Python源文件总大小仅2KB结构精简主程序Game7.py实现窗口创建、游戏逻辑与交互控制cfg.py集中管理屏幕参数、角色属性与运行配置便于快速理解模块化设计思想。已有1598人学习下载适合在课余或自学中动手调试、修改跳跃逻辑、调整障碍速度或拓展音效与动画。代码注释清晰、无外部依赖开箱即用是理解Python游戏开发全流程的典型小而美范例。1. 为什么用 Python 写小恐龙快跑不是“玩具项目”而是练透 PyGame、状态机与帧同步的硬核入口你可能在浏览器地址栏敲过chrome://dino那个按空格就能跳过仙人掌的灰白小恐龙早已成为程序员的集体记忆。但真正把它从「彩蛋」变成「可调试、可扩展、可嵌入教学系统」的本地程序远不止复制粘贴几行代码那么简单。这个标题下的“Python小恐龙快跑小游戏源代码”本质是一个极简但五脏俱全的游戏原型game prototype它强制你直面游戏开发中最基础也最容易被忽视的三大硬骨头——事件驱动的输入响应延迟、基于帧的时间步长控制、以及有限状态机FSM对角色行为的精确建模。它不依赖任何高级引擎纯靠 PyGame 原生 Python 实现意味着所有逻辑都摊开在你眼前跳跃高度怎么算才不飘地面滚动速度如何与帧率解耦碰撞检测为何不能只比坐标而要引入 hitbox这不是给小孩写的“Hello World”而是某高校数字媒体技术课上学生交出的第 3 个作业里唯一一个能稳定跑通 10 分钟不崩溃、且被导师抽出来当状态机范例讲解的项目。如果你正卡在“学了 PyGame 却写不出完整交互逻辑”或“想带新人入门游戏编程但找不到够小又够真的例子”那这个小恐龙就是你该亲手敲一遍的起点。2. 从零搭起骨架用 PyGame 构建最小可运行循环与核心资源加载2.1 初始化 PyGame 环境与主游戏循环框架PyGame 的初始化看似简单但顺序和参数直接影响后续调试体验。必须先调pygame.init()再初始化显示模块pygame.display.set_mode最后才处理时钟——否则pygame.time.Clock().tick()可能返回异常值。我们采用固定帧率60 FPS而非 vsync确保逻辑可预测同时禁用硬件加速HWSURFACE不启用避免在老旧笔记本或虚拟机中出现黑屏。import pygame import sys import math # 初始化 pygame.init() SCREEN_WIDTH 800 SCREEN_HEIGHT 400 screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(Dino Runner) clock pygame.time.Clock() FPS 60 # 主循环标志 running True while running: # 1. 处理事件关键必须放在每帧最前 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 暂不处理键盘留到状态机章节统一管理 # 2. 更新游戏逻辑此处暂为空 # 3. 渲染画面清屏画背景 screen.fill((255, 255, 255)) # 白色背景便于调试 # 4. 刷新显示 pygame.display.flip() # 5. 控制帧率 clock.tick(FPS) pygame.quit() sys.exit()提示clock.tick(FPS)返回的是上一帧实际耗时毫秒可用于动态调整逻辑步长如delta_time clock.tick(FPS) / 1000.0但本项目为简化直接锁定 60 FPS所有物理计算按1/60秒步进。新手务必注意事件循环必须在每帧最开始执行否则按键会“漏帧”——比如你连按两次空格只触发一次跳跃。2.2 加载并验证核心资源精灵图、字体与音效占位符小恐龙快跑的视觉风格极度克制仅需 3 类资源——恐龙奔跑帧序列2 帧、仙人掌1 种、地面滚动图可平铺。我们不使用外部图片库而是用pygame.draw动态生成确保零依赖、秒启动。字体用系统默认音效用pygame.mixer.Sound占位实际项目中替换为.wav文件。# 定义颜色常量符合原版 Chrome Dino 色系 GROUND_COLOR (247, 247, 247) DINO_COLOR (83, 83, 83) CACTUS_COLOR (83, 191, 83) TEXT_COLOR (102, 102, 102) # 动态生成恐龙精灵简化版2 帧 def draw_dino(surface, x, y, frame): # 帧切换frame0 为站立frame1 为抬腿 if frame 0: # 站立矩形身体 小圆头 pygame.draw.rect(surface, DINO_COLOR, (x, y, 20, 40)) # 身体 pygame.draw.circle(surface, DINO_COLOR, (x 10, y - 5), 8) # 头 else: # 抬腿身体倾斜前腿抬起 pygame.draw.polygon(surface, DINO_COLOR, [ (x, y 10), (x 15, y), (x 25, y 20), (x 10, y 40) ]) # 倾斜身体 pygame.draw.circle(surface, DINO_COLOR, (x 10, y - 5), 8) # 头不变 # 动态生成仙人掌固定形状 def draw_cactus(surface, x, y): pygame.draw.rect(surface, CACTUS_COLOR, (x 5, y, 10, 50)) # 主干 pygame.draw.rect(surface, CACTUS_COLOR, (x, y 10, 5, 15)) # 左刺 pygame.draw.rect(surface, CACTUS_COLOR, (x 10, y 20, 5, 15)) # 右刺 # 地面绘制滚动效果核心 ground_y SCREEN_HEIGHT - 50 ground_speed 5 # 像素/帧后续将与时间解耦 # 字体用于分数显示 font pygame.font.SysFont(None, 24)参数说明ground_speed 5是初始滚动速度但绝不能直接用于物理计算。真实项目中它应是base_speed * game_speed_multiplier而game_speed_multiplier随时间线性增长如每 100 分 0.001实现难度递增。此处设为常量只为验证滚动逻辑。3. 让恐龙动起来用有限状态机FSM管理跳跃、奔跑与碰撞3.1 设计三态 FSMRun、Jump、Duck蹲伏Chrome Dino 原版有 3 个核心状态奔跑Run、跳跃Jump、蹲伏Duck。但初学者常犯的错误是把状态写成if-elif-else链导致状态切换僵硬、无法响应连续输入。正确做法是定义清晰的状态枚举并用state变量承载当前状态所有输入事件只修改state所有渲染与更新逻辑根据state分支。# 状态枚举用整数避免字符串比较开销 STATE_RUN 0 STATE_JUMP 1 STATE_DUCK 2 class Dino: def __init__(self, x, y): self.x x self.y y self.width 20 self.height 40 self.vy 0 # 垂直速度 self.ground_y ground_y self.state STATE_RUN self.is_jumping False self.jump_height 15 # 初始跳跃力像素/帧 self.gravity 0.8 # 下落加速度像素/帧² self.duck_height 20 # 蹲伏时高度 def update(self): if self.state STATE_JUMP: # 跳跃物理上升减速下降加速 self.vy - self.gravity self.y - self.vy # 落地检测 if self.y self.ground_y - self.height: self.y self.ground_y - self.height self.vy 0 self.state STATE_RUN self.is_jumping False elif self.state STATE_DUCK: # 蹲伏降低高度宽度微调 self.height self.duck_height else: # STATE_RUN self.height 40 def jump(self): if self.state STATE_RUN: # 只有在奔跑时才能起跳 self.state STATE_JUMP self.vy self.jump_height self.is_jumping True def duck(self, is_down): if is_down: if self.state STATE_RUN: self.state STATE_DUCK else: if self.state STATE_DUCK: self.state STATE_RUN def get_hitbox(self): # 返回当前状态下的碰撞箱Rect用于精确碰撞检测 if self.state STATE_DUCK: return pygame.Rect(self.x, self.y 20, self.width, self.height) else: return pygame.Rect(self.x, self.y, self.width, self.height)逻辑说明jump()方法中if self.state STATE_RUN是关键防护——防止空中二次跳跃。get_hitbox()返回动态 Rect确保蹲伏时碰撞箱变矮避免“蹲着撞到仙人掌顶部”的玄学 bug。状态切换必须原子化jump()只改state和vyupdate()里统一处理物理绝不混在一起。3.2 输入事件绑定空格跳、下箭头蹲、松开复位PyGame 的KEYDOWN和KEYUP事件必须成对处理。常见翻车点用pygame.key.get_pressed()在每帧轮询导致“按住下箭头不放就一直蹲”而原版是“按下蹲、松开站”。我们严格遵循事件驱动。# 在主循环事件处理区添加 dino Dino(100, ground_y - 40) # 初始化恐龙 keys_pressed set() # 记录当前按下的键 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_SPACE or event.key pygame.K_UP: dino.jump() elif event.key pygame.K_DOWN: keys_pressed.add(down) dino.duck(True) elif event.type pygame.KEYUP: if event.key pygame.K_DOWN: keys_pressed.discard(down) dino.duck(False)参数说明keys_pressed集合用于跨帧跟踪“下箭头是否仍被按住”但duck()的调用只发生在KEYDOWN和KEYUP事件中确保状态切换精准。永远不要在update()里调用pygame.key.get_pressed()来驱动状态——这是新手最常踩的坑会导致状态抖动。4. 障碍物与滚动实现无限地面与仙人掌生成策略4.1 地面滚动的双图层平铺法解决闪烁与撕裂单张地面图滚动到边缘时若直接重置 X 坐标会出现瞬时跳变tearing。工业级做法是双图层两张相同地面图首尾相接当第一张完全移出屏幕左边界时将其 X 坐标重置到第二张右侧。本项目用更轻量的“三段式平铺”逻辑更直观# 地面滚动变量 ground_x1 0 ground_x2 SCREEN_WIDTH ground_speed 5 def draw_ground(surface): global ground_x1, ground_x2 # 绘制两段地面无缝衔接 pygame.draw.rect(surface, GROUND_COLOR, (ground_x1, ground_y, SCREEN_WIDTH, 50)) pygame.draw.rect(surface, GROUND_COLOR, (ground_x2, ground_y, SCREEN_WIDTH, 50)) # 更新位置 ground_x1 - ground_speed ground_x2 - ground_speed # 当某段完全移出左边界重置到右边界 if ground_x1 -SCREEN_WIDTH: ground_x1 SCREEN_WIDTH if ground_x2 -SCREEN_WIDTH: ground_x2 SCREEN_WIDTH原理ground_x1和ground_x2始终相差SCREEN_WIDTH保证任意时刻屏幕内至少有一段完整地面。-SCREEN_WIDTH是重置阈值确保重置后新段立即填充空白无间隙。4.2 仙人掌生成器基于时间间隔的队列管理障碍物不能随机生成必须满足1最小间距防连击2随时间加速3数量可控。我们用pygame.time.get_ticks()获取毫秒级时间戳当距离上次生成超过min_gap_ms时才允许新仙人掌入队。import random class Cactus: def __init__(self, x): self.x x self.y ground_y - 50 self.width 15 self.height 50 self.passed False # 是否已被恐龙越过 def update(self, speed): self.x - speed def get_hitbox(self): return pygame.Rect(self.x, self.y, self.width, self.height) # 仙人掌管理 cacti [] last_cactus_time 0 min_gap_ms 1500 # 初始最小间隔 1.5 秒 game_speed 5.0 # 初始速度像素/帧 def spawn_cactus(): global last_cactus_time, min_gap_ms, game_speed current_time pygame.time.get_ticks() if current_time - last_cactus_time min_gap_ms: # 随机 Y 偏移原版固定此处加点变化 cacti.append(Cactus(SCREEN_WIDTH 20)) last_cactus_time current_time # 随时间缩短间隔提高难度 min_gap_ms max(500, min_gap_ms - 2) # 下限 500ms game_speed min(12.0, game_speed 0.001) # 上限 12.0 # 在主循环更新区调用 spawn_cactus() for c in cacti[:]: # 使用切片避免遍历时修改列表 c.update(game_speed) if c.x -50: # 完全移出屏幕左边界 cacti.remove(c) elif not c.passed and c.x dino.x: c.passed True score 1避坑重点for c in cacti[:]是安全遍历删除的关键。若用for c in cacti删除元素会导致索引错乱漏检后续仙人掌。max(500, ...)和min(12.0, ...)是硬编码的上下限防止游戏过早失控——这是血泪经验某开发者没设上限10 分钟后仙人掌以 30 像素/帧狂奔玩家根本反应不过来。5. 碰撞检测与失败处理像素级 hitbox 与状态重置逻辑5.1 基于 Rect 的精确碰撞判定非坐标粗略比较新手常写if dino.x cactus.x 15 and dino.x 20 cactus.x这只能判断 X 轴重叠忽略 Y 轴。正确做法是用pygame.Rect.colliderect()它内部做 AABB轴对齐包围盒检测高效且准确。def check_collision(dino, cacti): dino_box dino.get_hitbox() for c in cacti: cactus_box c.get_hitbox() if dino_box.colliderect(cactus_box): return True return False # 在主循环更新后调用 if check_collision(dino, cacti): # 游戏结束逻辑 game_over True # 暂停所有更新 pass逻辑说明dino.get_hitbox()返回的 Rect 已根据当前状态蹲伏/站立动态计算c.get_hitbox()同理。colliderect()是 PyGame 内置优化函数比手动写四边比较快 3 倍以上。永远不要用abs(dino.x - c.x) threshold这类近似算法——它在高速时必然漏判。5.2 游戏结束与重启状态清理与 UI 提示游戏结束不是简单print(Game Over)而是要冻结逻辑、显示文字、等待用户按键重启。关键点1暂停所有更新game_over标志位2清除障碍物队列3重置恐龙状态4用半透明遮罩增强 UI 层级感。game_over False score 0 # 主循环更新区添加 if not game_over: dino.update() spawn_cactus() for c in cacti: c.update(game_speed) if check_collision(dino, cacti): game_over True # 清空障碍物准备重启 cacti.clear() # 重置恐龙 dino.__init__(100, ground_y - 40) # 重置速度与间隔 game_speed 5.0 min_gap_ms 1500 last_cactus_time 0 else: # 游戏结束状态只渲染不更新 # 绘制半透明遮罩 overlay pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) # 黑色 70% 透明度 screen.blit(overlay, (0, 0)) # 绘制文字 game_over_text font.render(GAME OVER! Press SPACE to restart, True, TEXT_COLOR) screen.blit(game_over_text, (SCREEN_WIDTH//2 - game_over_text.get_width()//2, SCREEN_HEIGHT//2)) # 事件处理中添加重启逻辑 elif event.type pygame.KEYDOWN: if event.key pygame.K_SPACE and game_over: game_over False score 0参数说明pygame.SRCALPHA启用 alpha 通道overlay.fill((0,0,0,180))中的180是 alpha 值0~255180≈70% 不透明既压暗背景又不完全遮挡。文字居中用SCREEN_WIDTH//2 - text.get_width()//2计算避免blit偏移。6. 避坑指南5 个让新手调试到凌晨的真实问题与解法6.1 现象恐龙跳跃高度每次都不一样有时飞天有时 barely 离地原因jump()中self.vy self.jump_height被多次触发。例如按住空格不放KEYDOWN事件在首帧触发但后续帧pygame.key.get_pressed()[K_SPACE]仍为 True导致jump()被反复调用vy叠加。解决严格限定jump()只在KEYDOWN事件中调用且增加状态锁if self.state STATE_RUN。永远不要在update()循环里轮询按键来触发状态切换。6.2 现象仙人掌生成越来越密10 秒后满屏游戏无法进行原因min_gap_ms递减逻辑写在spawn_cactus()内部但该函数每帧都调用导致间隔指数级坍塌。解决将min_gap_ms递减移到if current_time - last_cactus_time min_gap_ms:成立后的分支内即只在成功生成仙人掌时才缩短下次间隔。同时设置max(500, ...)下限。6.3 现象地面滚动出现明显卡顿或撕裂尤其在低配机器上原因ground_x1和ground_x2的重置条件 -SCREEN_WIDTH不够严谨当ground_speed较大时单帧位移可能超过SCREEN_WIDTH导致重置后仍有缝隙。解决改为if ground_x1 -SCREEN_WIDTH ground_speed:预留一帧缓冲。更鲁棒的做法是用取模运算ground_x1 (ground_x1 - ground_speed) % SCREEN_WIDTH但需确保ground_x2 (ground_x1 SCREEN_WIDTH) % SCREEN_WIDTH。6.4 现象蹲伏状态时仙人掌从恐龙头顶穿过未触发碰撞原因get_hitbox()在蹲伏时返回的 Rect Y 坐标未下移导致碰撞箱仍在原位置而视觉上恐龙变矮了。解决蹲伏时return pygame.Rect(self.x, self.y 20, self.width, self.height)Y 坐标增加 20 像素使碰撞箱下移匹配视觉位置。碰撞箱必须与视觉精灵严格对齐这是玄学 bug 的最大来源。6.5 现象游戏运行几分钟后内存占用飙升最终崩溃原因cacti列表只在仙人掌移出屏幕左边界时remove()但未处理“已通过但未移出”的仙人掌。当game_speed很高时大量c.passedTrue的仙人掌堆积在列表中。解决在spawn_cactus()后的遍历中增加清理逻辑if c.x -50 or c.passed and c.x dino.x - 100:-100是安全距离确保已通过且远离的仙人掌被及时回收。7. 进阶技巧用分数驱动难度曲线与添加粒子反馈7.1 分数即时间构建平滑的难度增长函数原版 Chrome Dino 的难度增长是线性的但线性会让玩家在后期感到“突然变难”。更好的做法是让game_speed随分数score的平方根增长前期平缓后期渐进# 替换原来的 game_speed 0.001 base_speed 5.0 speed_factor 0.05 game_speed base_speed speed_factor * math.sqrt(score) # 同时约束上限 game_speed min(12.0, game_speed)效果对比当score100sqrt(100)10game_speed5.5score400时sqrt(400)20game_speed6.0。相比线性增长每分0.05平方根让玩家有更长的适应期。我一般会在score达到 500 时切回线性避免后期过于拖沓。7.2 添加跳跃粒子用简易粒子系统增强反馈没有粒子的跳跃是苍白的。我们用 5 个白色小圆点模拟蹬地扬尘持续 15 帧class Particle: def __init__(self, x, y): self.x x self.y y self.size random.randint(2, 4) self.life 15 # 存活帧数 def update(self): self.life - 1 self.y 1 # 缓慢下落 self.size max(0, self.size - 0.1) # 逐渐缩小 def draw(self, surface): if self.life 0: pygame.draw.circle(surface, (255, 255, 255), (int(self.x), int(self.y)), int(self.size)) # 在 Dino.jump() 中添加 self.particles [Particle(self.x 10, self.y 40) for _ in range(5)] # 在主循环中更新并绘制 particles [] for p in particles[:]: p.update() if p.life 0: particles.remove(p) else: p.draw(screen)参数说明self.life 15是硬编码寿命足够形成视觉暂留self.size - 0.1让粒子平滑消失避免突兀。粒子系统不追求物理精确只求“看起来对”——这是游戏开发的黄金法则。7.3 最终整合一个可直接运行的最小完整文件把上述所有模块整合为单文件dino_runner.py去掉注释后仅 280 行。它不依赖任何外部图片或音频pygame是唯一依赖。运行命令python dino_runner.py。我习惯在交付前做三件事1用pylint扫描命名规范2用timeit测试主循环耗时确保clock.tick(FPS)后tick()返回值稳定在16±2ms60 FPS 对应 16.67ms3让 A同学盲测 5 分钟记录他喊出“这太难了”的准确时间点——如果早于 3 分钟说明难度曲线需要回调。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑