ARTICLE DETAIL

资讯详情

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

拆解pygame四个小游戏:从贪吃蛇到五子棋,掌握游戏开发核心逻辑

拆解pygame四个小游戏:从贪吃蛇到五子棋,掌握游戏开发核心逻辑 简介这套基于pygame的Python小游戏合集收纳了贪吃蛇、五子棋、扫雷、俄罗斯方块四款经典项目为Python初学者和游戏开发爱好者提供了一套可直接运行的练手素材。压缩包共29个文件包含6个py源码、18个bmp位图、3个png图片、1个ttf字体和1个md说明整体体积仅182KB结构简洁、易于查看和改动。目前已有548人学习下载可见其作为入门pygame参考的实用价值。源码完整覆盖了键盘监听、蛇身移动与碰撞检测、棋盘落子与胜负判断、扫雷随机布雷与翻开逻辑、方块旋转与整行消除等核心实现每款游戏都配有独立素材解压后即可运行体验。读者可通过阅读和调试这些代码直观理解游戏循环与事件驱动模型并将经典玩法改造为更多变体进而提升Python编程能力无论是课程设计还是自学练手都值得一用。1. 这份 pygame 源码包比你想的更有拆解价值很多初学者拿到这份pygame-master压缩包第一反应是“哦四个小游戏跑起来玩玩就完事了”。但等你真正把GluttonousSnake/main.py、MineSweeping/main.py、Tetris/main.py和Gomoku/ManAndMachine.py逐个打开你会发现这四个项目其实覆盖了 pygame 开发中最核心的四个能力维度实时键盘事件驱动的蛇形运动、基于二维数组和鼠标事件的网格逻辑、方块旋转与碰撞检测的矩阵变换、以及棋盘类游戏的状态判定算法。换句话说这份资源是一个非常适合用来做“从会写 Python 到能写游戏”的跳板。对 3 年以内的开发者你可以从里面抄主循环结构和事件分发模式对 5 年以上的熟手这份代码里一些不够严谨的设计比如全局变量泛滥、游戏状态没有单独抽象反而是很好的重构练手素材。下面我按实现深度的顺序把这四个项目逐一拆开来讲。2. 四个游戏共用的 pygame 主循环骨架2.1 为什么四份代码都长一个样把四个项目的 main.py 并排看一遍你会发现它们的结构几乎是同一个模板初始化 pygame、创建窗口、进入 while 循环、处理事件、更新状态、绘制画面。这不是巧合而是 pygame 程序的基本运行模型决定的。pygame 基于 SDL 封装它的所有交互都围绕一个“事件队列 持续渲染”的循环机制展开。import pygame import sys pygame.init() screen pygame.display.set_mode((600, 400)) clock pygame.time.Clock() while True: for event in pygame.event.get(): if event.type pygame.QUIT: pygame.quit() sys.exit() elif event.type pygame.KEYDOWN: print(f按下按键: {event.key}) screen.fill((0, 0, 0)) pygame.display.flip() clock.tick(60)这段代码里pygame.event.get()的作用是每帧从事件队列里取出所有待处理事件pygame.display.flip()负责把绘制好的缓冲内容一次性推送到屏幕。注意Clock.tick(60)是帧率控制的灵魂它的参数代表每秒最多刷新 60 次游戏逻辑的移动速度如果写死成“每帧移动 N 像素”那帧率越高物体跑得越快用 tick 锁帧之后速度才稳定。2.2 在骨架上挂游戏状态直接在主循环里写死所有逻辑一旦游戏功能变多代码就失控了。常见做法是引入一个简单的状态机用一个整数或枚举值来表示当前处于哪个界面。class GameState: MENU 0 RUNNING 1 GAMEOVER 2 state GameState.MENU while True: for event in pygame.event.get(): if event.type pygame.KEYDOWN: if state GameState.MENU and event.key pygame.K_SPACE: state GameState.RUNNING elif state GameState.GAMEOVER and event.key pygame.K_r: state GameState.RUNNING if state GameState.MENU: pass # 绘制菜单 elif state GameState.RUNNING: pass # 更新游戏逻辑 elif state GameState.GAMEOVER: pass # 绘制结束画面用状态机的好处是把“界面切换”和“游戏逻辑”解耦后续给扫雷加“胜利结算”或者给俄罗斯方块加“暂停”功能时只需要新增状态值不需要改动主循环的事件分发逻辑。这份源码包里没有写成这么规范但它原本的逻辑最终都能归类到这个框架里这就是合格从业者常说的“代码能跑不代表结构清晰”。3. 贪吃蛇与扫雷两种截然不同的游戏逻辑建模3.1 贪吃蛇的“移动”其实是坐标点列表的增删贪吃蛇的核心数据结构不是一张地图而是一个 Python 列表列表里每个元素是蛇身的一个坐标元组。蛇头往前挪一格就把新坐标插到列表头部再把尾部弹出这就是完整的移动逻辑。snake [(5, 5), (5, 4), (5, 3)] direction (1, 0) # x 方向 1向右 # 计算新蛇头位置 head_x, head_y snake[0] new_head (head_x direction[0], head_y direction[1]) # 吃到食物不弹出尾部蛇变长 if new_head food_pos: snake.insert(0, new_head) generate_new_food() else: snake.insert(0, new_head) snake.pop()这段逻辑里最关键的是direction这个方向向量。使用方向向量的好处是后续加“加速”“反向”等技能系统时只需要改向量值不需要改移动代码。另一个容易踩的坑是“不能直接反向”——如果当前方向是向右1, 0玩家按左方向键时游戏不应该允许蛇掉头否则蛇头会直接撞到自己的身体。处理方案很简单opposite (-direction[0], -direction[1]) if pressed_dir ! opposite: direction pressed_dir注意这里有个经典 bug如果连续两帧内玩家快速按了“上”再按“左”而蛇原本在向右走某些实现会先转上再转左导致蛇实际走了个直角这在严格规则下属于作弊理论上蛇在一次移动周期内只能转向一次。很多开源贪吃蛇代码都有这个问题修复方式是“只在每帧第一次取到方向键时更新 direction”后续按的键缓存到下一帧。3.2 扫雷的二维数组建模与洪水填充算法扫雷和贪吃蛇不同它的核心是静态数据建模。整个棋盘可以理解成两个二维数组一个存每格是不是地雷mine_map另一个存每格当前显示状态未翻开、翻开、标记旗帜。游戏开始时要随机布雷但首击不能踩雷这是初学者最容易忽略的规则。import random def generate_mine_map(width, height, mine_count, safe_x, safe_y): # 初始化全空棋盘 mine_map [[0 for _ in range(width)] for _ in range(height)] # 排除首次点击的格子及其周围 8 格 excluded set() for dx in (-1, 0, 1): for dy in (-1, 0, 1): excluded.add((safe_x dx, safe_y dy)) positions [(x, y) for y in range(height) for x in range(width) if (x, y) not in excluded] random.shuffle(positions) for i in range(mine_count): x, y positions[i] mine_map[y][x] 9 # 9 表示地雷 # 计算数字格 for y in range(height): for x in range(width): if mine_map[y][x] 9: continue count sum(1 for dx in (-1, 0, 1) for dy in (-1, 0, 1) if 0 x dx width and 0 y dy height and mine_map[y dy][x dx] 9) mine_map[y][x] count return mine_map这里把地雷设成 9普通数字格设为 0-8是扫雷实现里非常通用的编码方案。数字格的计算用了双重循环遍历每个格子的周围 8 邻域复杂度是 O(9 * width * height)对于标准 9x9 或 16x16 棋盘完全够用。mineblock.py这个文件就是负责把mine_map里的数值转换成对应图片或绘制逻辑的模块。点开空白格时要把周围所有连通的空白格一起翻开这个行为用递归的洪泛填充Flood Fill实现最直观但要注意递归深度——标准 Windows 扫雷最大棋盘 30x24720 格递归深度不会超过这个数Python 默认递归上限 1000 是安全的。用栈实现可以避免递归深度问题def reveal_empty(x, y, mine_map, revealed): stack [(x, y)] while stack: cx, cy stack.pop() if revealed[cy][cx]: continue revealed[cy][cx] True # 如果当前格是数字 0空白继续展开邻居 if mine_map[cy][cx] 0: for dx in (-1, 0, 1): for dy in (-1, 0, 1): nx, ny cx dx, cy dy if 0 nx len(mine_map[0]) and 0 ny len(mine_map): if not revealed[ny][nx]: stack.append((nx, ny))和递归版本相比栈实现不会出现“递归过深导致 RecursionError”的潜在问题在后续把棋盘扩展到自定义大尺寸时更稳妥。扫雷的难点不在 pygame 绘制而在这些纯粹的数据结构和算法逻辑——所以说学游戏开发本质是在学怎么把现实规则翻译成数据模型。3.3 两个游戏用到的 pygame 技术对照技术点贪吃蛇实现方式扫雷实现方式事件处理KEYDOWN监听方向键MOUSEBUTTONDOWN区分左右键坐标体系网格坐标转像素坐标像素坐标反推网格坐标碰撞检测判断蛇头是否与蛇身坐标重复判断点击格是否等于9游戏结束条件撞墙或撞自己踩雷或标记全部地雷像素坐标和网格坐标的相互转换是这类格子游戏绕不开的细节。贪吃蛇里是“逻辑坐标乘以格子尺寸得到绘制位置”扫雷里是“鼠标像素坐标除以格子尺寸取整得到格子编号”。建议在代码里统一用一个函数封装避免逻辑层和绘制层混用坐标系。MineSweeping目录下的main.py里如果看到类似(mouse_x - board_x) // cell_size的语句就是这个转换的具体体现。4. 俄罗斯方块与五子棋从矩阵旋转到胜负判定4.1 方块旋转的本质是矩阵转置加行反转俄罗斯方块的难度在七个方块形状的表示和旋转。常见做法是用嵌套元组或列表表示每个方块的血腥形状以一个 4x4 或更小的矩阵为载体。旋转操作不涉及三角函数而是线性代数里的矩阵变换。def rotate_clockwise(matrix): 顺时针旋转 90 度 1. 转置 2. 每行反转 transposed list(zip(*matrix)) return [list(reversed(row)) for row in transposed] # 示例I 型方块的一种旋转状态 shape_i [ [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0] ] rotated rotate_clockwise(shape_i) for row in rotated: print(row)list(zip(*matrix))这一步是矩阵转置把行变成列再对每一行做reversed就完成了顺时针 90 度旋转。逆时针旋转则是“转置后反转每一列”或者直接对顺时针旋转函数调用三次。理解这个变换后你会发现读blocks.py会容易很多——它里面定义的每个方块形状本质上就是一组可以做旋转运算的矩阵。旋转后的碰撞检测逻辑也必须同步考虑方块旋转后可能会超出边界或嵌入已有方块中解决方式是踢墙Wall Kick即旋转后如果发生碰撞就往左或右尝试平移 1-2 格看是否能找到一个合法落点。4.2 方块落定与消行的实现细节方块下落过程中要区分“活动方块”正在下落、跟随玩家操作和“固定方块”已经落定在底部或叠在其他方块上。很多初级实现把固定方块写在一个二维数组board里活动方块单独用一个坐标和形状表示。方块的 Y 轴下移步骤是先尝试把活动方块整体下移一行如果目标位置与board中的固定方块冲突或已触底就把活动方块“熔入”board。def can_move(matrix, offset_x, offset_y, board): for y, row in enumerate(matrix): for x, cell in enumerate(row): if not cell: continue board_x offset_x x board_y offset_y y if board_y len(board): return False if board_x 0 or board_x len(board[0]): return False if board[board_y][board_x]: return False return True消行逻辑是遍历board的每一行如果整行都是非零值删除该行并在顶部补一行全零行。注意这里顺序要从下往上扫因为消掉一行后上面的行会整体下移从上往下扫容易漏判。4.3 五子棋的胜负判定一个高效的区间扫描算法五子棋的逻辑分两块棋盘建模和三子算法。棋盘用一个 15x15 的二维数组0表示空位1表示黑子2表示白子。胜利判定的常见做法是遍历棋盘上每个棋子位置检查四个方向水平、垂直、正斜、反斜是否有连续五个同色棋子。def check_five(board, x, y, player): directions [(1, 0), (0, 1), (1, 1), (1, -1)] board_size len(board) for dx, dy in directions: count 1 # 正向延伸 for step in range(1, 5): nx, ny x dx * step, y dy * step if 0 nx board_size and 0 ny board_size and board[ny][nx] player: count 1 else: break # 反向延伸 for step in range(1, 5): nx, ny x - dx * step, y - dy * step if 0 nx board_size and 0 ny board_size and board[ny][nx] player: count 1 else: break if count 5: return True return False这段代码是 O(5 × 4) 的常数级检查。把“当前棋子”作为起点向两个方向延伸计数比每次全盘扫描高效得多。十五子棋还有一个进阶规则是“只能空手走不能跳”判定时还需要检查是否出现六个以上连子防止“长连禁手”争议。源码包里的Gomoku/ManAndMachine.py额外包含了一个人机对战模块电脑落子的核心是评分函数——对每个空位分别计算己方四个方向的连子潜力以及对手在同一个位置落子后的威胁分值两者加权求和后取最高分落子。评分函数里有一个值得注意的细节不仅要评价“当前空位连成 5 的可能”还要给“活三”“冲四”这类后续威胁打不同权重。一个简单的参考权重方案如下表局面特征防守权重进攻权重连二活二1010连三活三100150冲四一端被封150200连五已获胜1000010000实际调试时你会发现权重配比决定了电脑是激进型还是防守型。把进攻权重大幅调高后电脑更容易“莽赢”但也更容易漏掉对手的必杀招。4.4 一个容易忽略的细节胜负判定放在人机每次落子后都要执行不是只有玩家落子才需要判定电脑落子之后同样要立刻检查check_five否则会出现电脑已经连成五子但游戏不结束的体验问题。正确做法是把胜负判定封装成一个函数在玩家落子后调用一次再在电脑落子后调用一次。ManAndMachine.py里的实现顺序应该是玩家在棋盘上点击 → 判断该位置是否合法 → 落子 → 判定玩家胜负 → 如果未结束则进入电脑回合 → 电脑选点 → 落子 → 判定电脑胜负。5. 环境配置与三个高频报错的一次性解决办法5.1 pygame 安装失败的常见原因如果你在这份源码包的 README 指引下执行pip install pygame在部分 Python 3.12 环境或苹果 M 系列芯片的机器上有可能看到error: failed to build pygame when getting requirements to build wheel的报错。这个报错通常不是你的问题而是当前环境的 Python 版本需要从源码编译 pygame但缺少编译依赖。Windows 上的解决方案通常是直接安装预编译 wheel 包或者把 Python 降到 3.8/3.9 这种 pygame 官方长期支持的版本。macOS 上缺的往往是SDL2相关依赖执行下面两步通常能解决# 先升级 pip再尝试安装二进制 wheel python -m pip install --upgrade pip pip install pygame --only-binary :all:--only-binary :all:的意思是强制 pip 只使用预编译的二进制包不要走源码编译流程。如果这一步还不行说明你的平台没有对应 wheel这时候检查 Python 版本是否是官方 python.org 下载的而不是其他渠道的定制版。5.2 运行源码包时的目录坑zip 解压后不要直接把main.py拖到 IDE 里运行而是要先进入对应的子目录。比如运行贪吃蛇应该先cd GluttonousSnake再python main.py因为这些源码里如果用了相对路径加载图片或字体在错误的工作目录下会直接抛出FileNotFoundError。5.3 从这份源码包里提炼可复用组件四个项目里最有复用价值的代码片段我个人的排序是五子棋的check_five判定函数可以扩展到任意连子数、扫雷的“排除首击位置再布雷”算法可以用于任何需要保证首次操作不失败的场景、俄罗斯方块的旋转函数加踢墙逻辑天然适用于任何网格类游戏最后才是贪吃蛇的主体循环。如果你想做一个自己的小游戏不需要从零开始把这几个函数单独抽出来粘到新项目里可以省掉大约 200 行的调试时间。抽代码时有个小技巧用 git 初始化仓库每次抽一个游戏的模块做一次 commit标注“从 pygame-master 移植 rotate_clockwise额外增加顺时针逆时针两种模式”这样后续出了问题可以随时回溯对比差异。这种做法比直接 CV 整份main.py更利于维护。本文还有配套的精品资源点击获取
返回列表