ARTICLE DETAIL

资讯详情

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

Pygame开发2D跑酷游戏:从碰撞检测到游戏循环完整实战

Pygame开发2D跑酷游戏:从碰撞检测到游戏循环完整实战 大家好我是你们的老朋友。之前在自己的小游戏项目里尝试做一个“跑酷 收集”玩法时发现网上关于 Pygame 游戏开发的资料虽然多但大部分只讲“如何让方块动起来”对碰撞检测、道具生成、得分机制、游戏状态管理这些真正决定游戏好坏的部分反而一笔带过。所以今天这篇文章我想以“汤姆猫跑酷捡水挑战”这个经典玩法为切入点完整拆解一个 2D 跑酷小游戏的开发全过程。你会看到从零开始如何设计游戏循环、如何用 Pygame 实现角色跳跃、如何实现“捡水”得分、如何控制障碍物生成以及最后如何打包成可执行文件。无论是刚开始学 Python 游戏开发的新手还是想快速搭建一个跑酷 Demo 的开发者这篇文章都能给你一套能直接跑通的方案。1. 项目背景与核心玩法拆解1.1 什么是“汤姆猫跑酷捡水挑战”在很多休闲跑酷游戏里玩家的核心目标只有两个跑得远吃得多。“汤姆猫跑酷捡水挑战”这类玩法把“吃东西”的维度进一步聚焦为“捡水”也就是玩家操控角色在奔跑过程中需要尽可能多地收集场景中的水滴道具同时躲避石头、树桩、仙人掌等障碍物。听起来逻辑很简单但如果要用代码实现你会发现它至少要拆出以下模块角色控制模块处理左右移动、跳跃、重力、下落以及碰撞体积。障碍物生成模块按一定间隔随机生成障碍物让玩家有危机感。水滴道具模块生成可收集的道具并支持判定“是否吃到”。计分与状态模块记录得分、游戏加速、失败结束、重新开始。渲染模块把游戏画面、文字、背景、动画按每帧刷新。掌握这些模块之后你完全可以把“捡水”换成“捡金币”“捡星星”只需要修改素材和道具命中效果即可。1.2 为什么用 Pygame 来实现Pygame 是 Python 生态中最常用的 2D 游戏开发库。它不要求你理解复杂的引擎底层也不会像大型游戏引擎那样带来额外的学习成本。它的设计思路更接近“用代码手写一套游戏循环”这反而非常适合理解游戏开发的核心逻辑。而且 Pygame 有几个特点对新手非常友好纯 Python API只要会基础语法就能上手。自带精灵类Sprite、碰撞检测方法、图形绘制接口。跨平台Windows / macOS / Linux 均可运行。打包工具生态成熟可以用 PyInstaller 方便地生成独立可执行文件。这篇文章里的项目实战我们选择的就是 Python Pygame 技术栈。1.3 项目最终效果预览我们先明确一下这次要做的游戏最终长什么样画面2D 横版跑酷背景是移动的公路/草地。角色一个可跳跃的猫咪角色或者矩形占位模型。障碍物从右侧向左侧移动的障碍物速度随时间加快。道具水滴随机出现在空中或地面碰到则得分。规则角色碰到障碍物游戏结束角色捡到水滴得分 1屏幕上方显示实时得分。2. 环境准备与项目初始化2.1 安装 Python 与 Pygame本文示例使用 Python 3.10 作为演示环境但 Python 3.7 以上基本都可以运行。如果你的电脑上还没有 Python可以先去 Python 官网下载并安装记得勾选“Add Python to PATH”。安装好 Python 之后打开命令行工具输入以下命令安装 Pygamepip install pygame如果你使用的是国内网络环境可以追加镜像源加速安装pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后验证是否成功python -c import pygame; print(pygame.ver)如果在终端输出了类似2.5.2的版本号说明环境已经就绪。2.2 IDE 与项目目录结构推荐使用 Visual Studio Code 或者 PyCharm 进行开发。两者都支持 Python 调试对查看变量和游戏循环运行状态很有帮助。我们定义如下项目结构tom-cat-run/ ├── main.py # 游戏入口 ├── game_settings.py # 全局配置尺寸、颜色、重力等 ├── player.py # 角色类 ├── obstacle.py # 障碍物类 ├── water_drop.py # 水滴道具类 ├── game_controller.py # 游戏循环和逻辑控制 └── assets/ # 可以放图片、音效素材本文用图形替代如果你只是为了跑通 Demo把全部代码放在一个 main.py 里也可以。但后续要扩展功能拆分模块的方式会省很多事。3. 跑酷游戏核心原理讲解在写代码之前我们先把跑酷游戏背后涉及的核心概念讲清楚这样你的每一步代码都有据可循。3.1 游戏主循环游戏开发的世界观本质上是一个“每一帧都在刷新画面”的循环处理用户输入按键、鼠标点击。更新所有对象的位置和状态。检查碰撞事件。绘制当前画面。控制帧率。在 Pygame 中这个循环用while实现配合时钟对象pygame.time.Clock()来决定每秒刷新多少帧标准取 60 帧每秒FPS 60。import pygame pygame.init() screen pygame.display.set_mode((800, 400)) clock pygame.time.Clock() running True while running: # 1. 处理事件 for event in pygame.event.get(): if event.type pygame.QUIT: running False # 2. 更新逻辑 # ... # 3. 绘制画面 pygame.display.flip() clock.tick(60) pygame.quit()clock.tick(60)的意思是把游戏固定运行在每秒 60 帧。帧率过高会导致角色运动速度过快帧率过低会导致画面发卡所以控制帧率是跑酷游戏最基本的一步。3.2 坐标系统与移动原理Pygame 的坐标系以窗口左上角为原点(0, 0)x 轴向右递增y 轴向下递增。也就是说角色往右移动是 x 增加往左移动是 x 减少角色跳跃上升是 y 减少下落是 y 增加。移动的核心就是每一帧都给对象的位置坐标加上一个速度值# 每帧更新 player.rect.x player.speed_x player.rect.y player.speed_y障碍物之所以看起来像在向玩家移动实际上是障碍物自身的 x 坐标每帧向左递减而玩家固定站在左侧。这种相对运动是横版跑酷最常用的实现方式。3.3 重力与跳跃如果没有重力角色跳起来后会直接飘走。为了让跳跃有抛物线效果我们给角色增加一个竖直方向的速度每帧把速度加到 y 坐标上同时给速度加上一个“重力加速度”gravity 0.8 jump_velocity -15 # 玩家按下跳跃键时 player.speed_y jump_velocity # 每帧 player.speed_y gravity player.rect.y player.speed_y当角色落地y 坐标到达地面高度时重置 speed_y 为 0并允许下一次跳跃。3.4 碰撞检测Pygame 提供了碰撞检测的便捷方法最常见的是rect.colliderect()用于检测两个矩形是否重叠。我们在游戏循环里让玩家矩形分别和障碍物列表、水滴列表进行碰撞判断即可。if player.rect.colliderect(obstacle.rect): # 游戏结束 if player.rect.colliderect(water.rect): # 得分并删除该水滴4. 完整实战实现“汤姆猫跑酷捡水挑战”接下来进入代码实现。为了让文章清晰我会把代码按模块拆开讲解每个模块都标明文件路径最后组合运行。4.1 创建全局配置文件文件game_settings.py全局配置集中管理的好处是当你想要调整游戏窗口大小、背景颜色、角色速度时不需要在多个文件里翻找。# 文件路径game_settings.py SCREEN_WIDTH 800 SCREEN_HEIGHT 400 FPS 60 # 颜色定义 BLACK (0, 0, 0) WHITE (255, 255, 255) BLUE (135, 206, 250) GREEN (124, 252, 0) GROUND_COLOR (139, 69, 19) # 地面高度以屏幕底部为参考 GROUND_Y 330 # 玩家参数 PLAYER_WIDTH 40 PLAYER_HEIGHT 50 PLAYER_X 100 PLAYER_COLOR (255, 165, 0) # 重力参数 GRAVITY 0.8 JUMP_POWER -15 # 障碍物参数 OBSTACLE_WIDTH 30 OBSTACLE_HEIGHT 40 OBSTACLE_COLOR (169, 169, 169) OBSTACLE_SPEED 6 # 水滴道具参数 WATER_RADIUS 12 WATER_COLOR (65, 105, 225) WATER_SCORE 1这一段配置是整个游戏的“总控制台”。实际项目中游戏窗口大小、角色移动速度、障碍物生成速度都会被单独配置以便后续做难度平衡。4.2 创建玩家类文件player.pyPlayer 类负责封装角色的外观、位置、跳跃状态和动画更新逻辑。我们先用矩形加颜色模拟外观你要是愿意可以把外部猫咪图片加载到rect所在位置。# 文件路径player.py import pygame from game_settings import ( PLAYER_WIDTH, PLAYER_HEIGHT, PLAYER_X, PLAYER_COLOR, GROUND_Y, GRAVITY, JUMP_POWER, ) class Player: def __init__(self): self.rect pygame.Rect(PLAYER_X, GROUND_Y - PLAYER_HEIGHT, PLAYER_WIDTH, PLAYER_HEIGHT) self.color PLAYER_COLOR self.speed_y 0 self.is_jumping False def jump(self): # 只有在地面上才能起跳 if not self.is_jumping: self.speed_y JUMP_POWER self.is_jumping True def update(self): # 重力作用 self.speed_y GRAVITY self.rect.y self.speed_y # 落地判断 if self.rect.y GROUND_Y - PLAYER_HEIGHT: self.rect.y GROUND_Y - PLAYER_HEIGHT self.speed_y 0 self.is_jumping False def draw(self, screen): pygame.draw.rect(screen, self.color, self.rect)为什么把is_jumping独立出来因为如果不限制空中不能二次跳跃玩家连续按空格会导致角色“飞”起来。处理跳跃时最重要的是“状态管理”而不是单纯改变 y 坐标。4.3 创建障碍物类文件obstacle.py障碍物会不断从右侧生成并向左侧移动。为了增加变化我们给障碍物设置一个移动速度属性这样后续可以随分数提高统一加速。# 文件路径obstacle.py import pygame import random from game_settings import ( SCREEN_WIDTH, OBSTACLE_WIDTH, OBSTACLE_HEIGHT, OBSTACLE_COLOR, OBSTACLE_SPEED, GROUND_Y, ) class Obstacle: def __init__(self): self.width OBSTACLE_WIDTH self.height OBSTACLE_HEIGHT self.rect pygame.Rect( SCREEN_WIDTH, GROUND_Y - OBSTACLE_HEIGHT, self.width, self.height, ) self.color OBSTACLE_COLOR # 随机速度增加一点难度变化 self.speed OBSTACLE_SPEED random.randint(-1, 2) def update(self): self.rect.x - self.speed def draw(self, screen): pygame.draw.rect(screen, self.color, self.rect) def is_offscreen(self): return self.rect.x self.rect.width 0is_offscreen方法用于判断障碍物是否已经完全移出屏幕左侧。如果不能及时删除离开屏幕的对象游戏运行几分钟后内存会持续增长导致越来越卡。4.4 创建水滴道具类文件water_drop.py水滴滴道具是得分核心。它和障碍物的区别在于障碍物碰到会出局水滴碰到会加分。为了增加趣味水滴出现的位置可以分布在地面和空中区域跳跃才能吃到空中的水滴。# 文件路径water_drop.py import pygame import random from game_settings import ( SCREEN_WIDTH, WATER_RADIUS, WATER_COLOR, GROUND_Y, ) class WaterDrop: def __init__(self): self.radius WATER_RADIUS self.color WATER_COLOR # y 坐标范围地面附近或空中 low_y GROUND_Y - WATER_RADIUS high_y GROUND_Y - 120 self.y random.randint(high_y, low_y) self.x SCREEN_WIDTH self.speed 6 def update(self): self.x - self.speed def draw(self, screen): pygame.draw.circle(screen, self.color, (int(self.x), int(self.y)), self.radius) def get_rect(self): # 圆形的碰撞矩形为了简单使用外接矩形 return pygame.Rect(self.x - self.radius, self.y - self.radius, self.radius * 2, self.radius * 2) def is_offscreen(self): return self.x self.radius 0这里用get_rect()把圆形水滴变成一个外接矩形主要是为了方便与玩家矩形做碰撞检测。4.5 创建游戏控制器文件game_controller.py游戏控制器负责整合玩家、障碍物、水滴和渲染逻辑。这部分是游戏真正的“大脑”。控制器需要维护一个障碍物列表和一个水滴列表并且在循环中不断生成新的对象。# 文件路径game_controller.py import pygame import random from game_settings import * from player import Player from obstacle import Obstacle from water_drop import WaterDrop class GameController: def __init__(self, screen): self.screen screen self.clock pygame.time.Clock() self.player Player() self.obstacles [] self.water_drops [] self.score 0 self.running True self.game_over False # 计时器控制生成间隔 self.obstacle_timer 0 self.water_timer 0 def handle_events(self): for event in pygame.event.get(): if event.type pygame.QUIT: self.running False if event.type pygame.KEYDOWN: if event.key pygame.K_SPACE and not self.game_over: self.player.jump() if event.key pygame.K_r and self.game_over: self.__init__(self.screen) # 重置游戏 def update(self): if self.game_over: return self.player.update() # 生成障碍物 self.obstacle_timer 1 if self.obstacle_timer random.randint(60, 110): self.obstacles.append(Obstacle()) self.obstacle_timer 0 # 生成水滴 self.water_timer 1 if self.water_timer random.randint(40, 90): self.water_drops.append(WaterDrop()) self.water_timer 0 # 更新障碍物 for obstacle in self.obstacles: obstacle.update() self.obstacles [obs for obs in self.obstacles if not obs.is_offscreen()] # 更新水滴 for drop in self.water_drops: drop.update() self.water_drops [drop for drop in self.water_drops if not drop.is_offscreen()] # 碰撞检测障碍物 for obstacle in self.obstacles: if self.player.rect.colliderect(obstacle.rect): self.game_over True # 碰撞检测水滴 for drop in self.water_drops: drop_rect drop.get_rect() if self.player.rect.colliderect(drop_rect): self.water_drops.remove(drop) self.score WATER_SCORE def draw(self): self.screen.fill(BLUE) pygame.draw.rect(self.screen, GROUND_COLOR, (0, GROUND_Y, SCREEN_WIDTH, SCREEN_HEIGHT - GROUND_Y)) # 绘制玩家 self.player.draw(self.screen) # 绘制障碍物 for obstacle in self.obstacles: obstacle.draw(self.screen) # 绘制水滴 for drop in self.water_drops: drop.draw(self.screen) # 显示分数 font pygame.font.Font(None, 36) score_text font.render(fScore: {self.score}, True, WHITE) self.screen.blit(score_text, (20, 20)) if self.game_over: over_text font.render(Game Over! Press R to Restart, True, (255, 0, 0)) self.screen.blit(over_text, (SCREEN_WIDTH // 2 - 220, SCREEN_HEIGHT // 2)) def run(self): while self.running: self.handle_events() self.update() self.draw() pygame.display.flip() self.clock.tick(FPS)这里的计时器方法很关键。它通过逐帧累加计数器并在达到随机阈值时生成新对象实现“随机间隔出现障碍物”的效果。生成逻辑中列表推导式[obs for obs in self.obstacles if not obs.is_offscreen()]会一次性过滤掉屏幕外的对象避免列表无限增长。4.6 游戏入口文件main.py入口文件做的事情很简单初始化 Pygame、创建窗口、创建控制器、启动游戏。# 文件路径main.py import pygame from game_settings import SCREEN_WIDTH, SCREEN_HEIGHT, BLUE from game_controller import GameController def main(): pygame.init() screen pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption(汤姆猫跑酷捡水挑战) controller GameController(screen) controller.run() pygame.quit() if __name__ __main__: main()4.7 运行与验证在项目根目录执行python main.py预期效果如下窗口大小为 800 × 400标题为“汤姆猫跑酷捡水挑战”。地面是棕色横条天空是浅蓝色玩家是一个橙色矩形。按键盘空格键角色向上跳跃。屏幕右侧会不断生成灰色障碍物和蓝色圆形水滴。吃到水滴左上角分数增加。碰到障碍物屏幕中央出现 “Game Over! Press R to Restart”。按 R 键游戏重新开始。关闭窗口程序退出。5. 常见问题与排查思路新手在运行跑酷游戏时比较容易遇到下面几类问题。不要慌按表格顺序排查基本都能解决。问题现象常见原因解决思路执行python main.py报ModuleNotFoundError: No module named pygamePygame 未安装或安装在其它 Python 环境执行pip install pygame检查 IDE 是否使用了虚拟环境角色按空格不跳跃事件监听里没有识别K_SPACE角色is_jumping状态未重置检查 handle_events 中 KEYDOWN 判断确认 update 里落地重置逻辑正确角色跳跃时出现“二连跳”或空中漂浮没有限制空中跳跃跳起后GROUND_Y判断出错在 jump() 开头加if not self.is_jumping判断游戏运行时间越长越卡障碍物或水滴离开屏幕后未清理确认使用了is_offscreen()过滤列表检查是否使用remove收集到不可见对象分数不增加但看起来水滴和角色重叠了水滴碰撞使用了圆形外接矩形视觉上有偏差或者碰撞检测里 remove 顺序问题先用get_rect()打印矩形范围调试并把水滴碰撞判断放到更新逻辑中且确保调用remove后不再遍历该对象按 R 无法重新开始重置方式只是调用__init__但可能存在局部引用问题推荐把重置逻辑抽成reset()方法手动初始化玩家、障碍物、分数等属性这里我最想强调的还是“删除屏幕外对象”。哪怕你写一个小 Demo这个习惯也必须养成否则后续切换照片、加大地图、生成大量粒子特效时性能会瞬间崩溃。6. 最佳实践与工程建议6.1 把常量与业务逻辑分离上面我们把窗口尺寸、颜色、重力等常量放在了game_settings.py不要觉得这是小题大做。实际项目里一旦你开始调优手感会发现不断在改数值跳跃力度太轻、障碍物太快、地面高度太低。如果每个数值散落在业务代码中最后只能全局搜索大海捞针。推荐做法# 游戏手感参数独立成一个配置 DIFFICULTY_CONFIG { normal: { obstacle_speed: 6, score_speed_up: 20, }, hard: { obstacle_speed: 10, score_speed_up: 15, }, }6.2 将重置逻辑单独封装游戏结束按 R 重置时不要习惯性地直接调用__init__。一方面重新创建对象开销大另一方面如果__init__里需要加载大图片或音频资源重置时可能卡顿。更合理的设计def reset(self): self.player Player() self.obstacles.clear() self.water_drops.clear() self.score 0 self.game_over False self.obstacle_timer 0 self.water_timer 06.3 碰撞检测的精度与性能在这个 Demo 里我们用矩形碰撞colliderect。但如果你把角色换成一张带透明背景的猫咪图片矩形碰撞会显得“明明没碰到却判定失败”。这时候可以考虑 Pygame 的mask碰撞即先对两张图片的透明区域做掩码再检测是否重叠。当然全屏 mask 碰撞性能会下降所以通常做法是先用矩形碰撞做粗检测。矩形命中了再用 mask 做精细检测。这就是游戏开发里常见的“宽阶段 窄阶段”碰撞检测思想。6.4 难度递增这款“捡水挑战”不能一直保持同一个速度否则玩到后面会无聊。最常见的做法是每获得一定分数障碍物初始速度 1障碍物生成间隔时间缩短。这里可以抽一个方法来处理def adjust_difficulty(self): speed_add self.score // 10 for obstacle in self.obstacles: obstacle.speed OBSTACLE_SPEED speed_add其中score // 10表示每得 10 分所有障碍物速度加 1。6.5 素材替换思路当你把橙色矩形换成真正的“汤姆猫”素材时只需要处理两点用pygame.image.load()加载图像然后通过pygame.transform.scale()缩放到合适尺寸。让图片的中心位置跟着player.rect.center走保证碰撞矩形不变。def draw(self, screen): screen.blit(self.image, self.rect.topleft)6.6 日志与调试游戏开发时不要只用print输出。建议在最开始就设计一个超简单的日志工具把障碍物生成、水滴收集、碰撞出局等事件记录成结构化日志。强烈建议记录的关键字段有帧号、事件类型、角色位置、对象位置。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(message)s) logging.info(Player at x%d y%d, obstacle count%d, player.rect.x, player.rect.y, len(obstacles))7. 总结与进一步学习建议这篇文章我们把“汤姆猫跑酷捡水挑战”这样一个游戏玩法拆成了可以落地的 Pygame 项目核心掌握的内容可以归纳为几条理解了 2D 跑酷游戏的基本循环事件处理、逻辑更新、碰撞检测、画面渲染。学会了用 Rect 管理对象位置用重力参数实现跳跃用速度差值实现障碍物移动。掌握了障碍物和水滴的批量生成、删除、复用以及简单的计分和重置流程。了解了从 Python 代码到独立窗口程序的运行流程。如果你接下来想把项目做成一个真正可分享的游戏我建议按下面的路线继续深入把矩形角色换成真正的猫咪图片素材加入跑步动画帧切换。增加障碍物种类例如高障碍必须跳跃、低障碍必须蹲下。加入音效比如喝水时的“咕噜”声、撞到障碍物时的失败音效。增加最高分存储把分数写入本地文件。使用 PyInstaller 把 main.py 打包成 .exe 文件。打包命令参考pip install pyinstaller pyinstaller -F -w main.py-w表示打包成窗口程序不显示命令行。这款小游戏虽然简单但它几乎包含了一个“完成品游戏”需要处理的全部基础问题状态管理、碰撞检测、随机生成、对象生命周期、用户输入响应。把这几块吃透再去看 Unity、Godot 或者其它游戏引擎时你会发现很多概念都是相通的。如果这篇文章对你有帮助可以先收藏备用。当你真的动手把代码跑起来再按自己的想法加功能就不会觉得游戏开发是一件难以落地的事了。
返回列表