ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python+pygame实现消消乐游戏:从二维数组到打包发布

Python+pygame实现消消乐游戏:从二维数组到打包发布 简介一份基于PythonPygame的桌面端消消乐游戏项目定位为期末大作业/课程设计面向计算机专业学生、初学者以及需要项目实战练习的开发者可解决选题难、源码不完整、文档缺失等常见问题。压缩包共120个文件包含4个Python主程序、70个PNG图片素材、20个MP3与20个OGG音频以及YAML配置、README说明等包体约9.57MB目录区隔清楚便于按资源类型查找。目前已有140人学习下载。项目不仅能运行还提供了完整文档说明覆盖Pygame游戏循环、事件响应、消除判定、音效播放等关键环节适合用来理解游戏开发的基本架构也可作为课程设计答辩的演示素材。读者可在此基础上扩展计分逻辑、特效动画或关卡编辑器通过实际修改与调试提升独立开发能力。1. 基于pythonpygame的桌面端消消乐游戏项目复盘很多人在期末拿到这个题目时第一反应是去找现成的源码直接交差。但真正用 pygame 写完一个能跑、能玩、能演示的消消乐之后你会发现这个项目最值钱的不是「消消乐」三个字而是从二维数组建模、到事件驱动渲染、再到交互动画这一整条 Python 游戏开发链路。作为一个在 git 仓库里见过几十个消消乐作业的人我可以告诉你一个反直觉的事实90% 的同类项目不是死于算法难度而是死于「数据结构混乱 动画阻塞主循环 打包后资源路径丢失」这三个坑。对于打算拿它应付期末答辩的读者或者是想拿 Python 做第一个图形界面项目的初学者这篇博文会从棋盘建模讲起一直讲到 PyInstaller 打包成单文件 exe覆盖这个题目最常见的需求和选型思路。2. 消消乐游戏核心逻辑设计与数据结构选型2.1 用二维数组表示棋盘而不是面向对象硬套消消乐的本质是「二维网格上的状态机」。棋盘上一共有多少种颜色、多少行多少列用一张二维表就能表达清楚。绝大多数期末项目的棋盘规模是 8×8 或 9×9颜色类型 5 到 6 种。这个规模下最合适的数据结构就是 Python 内置的 list of list也就是嵌套列表。import random ROWS, COLS 8, 8 # 8x8 棋盘 NUM_TYPES 5 # 5 种方块类型编号 0-4 def create_board(rowsROWS, colsCOLS, num_typesNUM_TYPES): board [[random.randint(0, num_types - 1) for _ in range(cols)] for _ in range(rows)] # 初始化时避免直接出现可消除组合 for r in range(rows): for c in range(cols): while has_match_at(board, r, c): board[r][c] random.randint(0, num_types - 1) return board代码逻辑并不复杂第一行用列表推导式生成随机方块的棋盘随后遍历每一个格子检查当前位置在第一张棋盘上是否已经构成三连如果构成了就重新随机一个值。这里的has_match_at是核心检测函数在后面马上实现。注意不要用 numpy 的二维数组尽管它索引方便但会引入额外依赖在课程设计答辩时反而会被老师追问「为什么用 numpy 而不用标准库」徒增解释成本。2.2 消除检测暴力的横向纵向三连判断就够了消除检测是所有匹配消除游戏的地基。不少初学者一上来就想着用 Flood Fill 或者 BFS 做连通域检测但实际上消消乐的规则通常只是「横向或纵向连续 3 个及以上相同方块」并不要求 L 形或 T 形等复杂组合。因此最简单的实现方式是逐行检查横向连续段逐列检查纵向连续段记录所有满足条件的坐标放入一个待消除集合。def find_matches(board): matched set() rows, cols len(board), len(board[0]) # 横向扫描 for r in range(rows): c 0 while c cols - 2: if board[r][c] board[r][c 1] board[r][c 2]: start c while c cols and board[r][c] board[r][start]: matched.add((r, c)) c 1 else: c 1 # 纵向扫描逻辑与横向完全对称 for c in range(cols): r 0 while r rows - 2: if board[r][c] board[r 1][c] board[r 2][c]: start r while r rows and board[r][c] board[r][start]: matched.add((r, c)) r 1 else: r 1 return matched这段代码的核心是用 while 循环找出连续段而不是用 for 加 if因为 for 循环在跳过重复项时索引不好控制。匹配算法的时间复杂度是 O(R × C)对于 9×9 棋盘是 81 次操作视觉上不会有任何卡顿。获得matched集合之后消除只需要把对应坐标的方块标记为 -1 或者直接删除进入下一步「掉落补位」。消消乐里还有一个关键约束是「死局检测」当整个棋盘没有任何可交换后形成的匹配时要洗牌重开。常见做法是把棋盘上所有相邻交换全部模拟一遍每模拟一个位置就调用一次find_matches如果全部失败说明当前是死局。这个暴力检测的复杂度是 O(R × C × 4)对于作业规模完全够用不需要引入任何剪枝优化。def has_legal_move(board): rows, cols len(board), len(board[0]) for r in range(rows): for c in range(cols): # 向右交换 if c 1 cols: board[r][c], board[r][c 1] board[r][c 1], board[r][c] if find_matches(board): board[r][c], board[r][c 1] board[r][c 1], board[r][c] return True board[r][c], board[r][c 1] board[r][c 1], board[r][c] # 向下交换 if r 1 rows: board[r][c], board[r 1][c] board[r 1][c], board[r][c] if find_matches(board): board[r][c], board[r 1][c] board[r 1][c], board[r][c] return True board[r][c], board[r 1][c] board[r 1][c], board[r][c] return False注意交换后检测完必须立即换回来否则棋盘状态就被污染了。这是期末作业里最常见的数据完整性错误之一。2.3 掉落补位重力模型与空位压缩消除后的方块从上方掉下来是消消乐视觉体验的核心也是一个容易写崩的地方。逻辑上要分两步做第一步把每一列的方块按「从下往上遍历、非空方块下沉」的方式压缩第二步在列顶生成新的随机方块。下面给出一个在二维列表上原地操作的例子。def apply_gravity(board): rows, cols len(board), len(board[0]) for c in range(cols): # 从下往上收集非空值 column_vals [] for r in range(rows - 1, -1, -1): if board[r][c] ! -1: column_vals.append(board[r][c]) # 从下往上重新填回 for r in range(rows - 1, -1, -1): if column_vals: board[r][c] column_vals.pop(0) else: board[r][c] random.randint(0, NUM_TYPES - 1) return board-1在这里代表空气或空位是前面消除时写入的标记。上述代码的处理逻辑是逐列重建从上往下读从下往上写空位由新随机块补齐。如果方块需要按「行」掉落而不是按列即横向掉落模式做法类似循环方向换一下即可。它解决的问题是消除后留下洞洞上方的方块需要下落到底部这是所有三消游戏的公共底盘。如果处理不好后续动画都会基于错误的位置导致闪烁和穿模。3. pygame 桌面端窗口、事件循环与鼠标交互3.1 初始化窗口与主循环骨架pygame 的窗口管理很直白pygame.display.set_mode创建窗口pygame.event.get()捕获输入pygame.display.flip()刷新画面。一个健壮的桌面端游戏主循环必须包含固定帧率控制和退出事件处理。import pygame CELL_SIZE 64 GRID_X_OFFSET 40 GRID_Y_OFFSET 40 WINDOW_WIDTH COLS * CELL_SIZE GRID_X_OFFSET * 2 WINDOW_HEIGHT ROWS * CELL_SIZE GRID_Y_OFFSET * 2 def run_game(): pygame.init() screen pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(消消乐 Desktop Match-3) clock pygame.time.Clock() board create_board() running True while running: # 事件处理关闭窗口、鼠标点击 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.MOUSEBUTTONDOWN: handle_click(event.pos, board) # 绘制棋盘与方块 draw_board(screen, board) pygame.display.flip() clock.tick(60) pygame.quit()需要特别注意clock.tick(60)这一行它把主循环的帧率限制在 60 FPS同时天然充当了 time.sleep 的角色。很多教程会误导新手用pygame.time.delay做动画延迟这会在 UI 线程里阻塞事件处理造成窗口拖拽时无响应。正确做法是永远用主循环的 tick 控制节奏。窗口尺寸的计算公式是棋盘格数量 × 格子边长 两倍的偏移量这个公式是桌面端布局的基础改成其他数值会直接导致绘制越界或者窗口留白过多。3.2 从像素坐标到棋盘坐标的映射鼠标点击的event.pos给出的是像素坐标而棋盘逻辑操作的是行列坐标所以需要一个转换函数。这个函数在整个游戏里被频繁调用必须写得短且严谨。def pos_to_cell(mouse_pos): mx, my mouse_pos col (mx - GRID_X_OFFSET) // CELL_SIZE row (my - GRID_Y_OFFSET) // CELL_SIZE if 0 row ROWS and 0 col COLS: return row, col return None代码做了两件事先通过差值除以格子边长得到棋盘坐标再用边界检查确认点击没有落在棋盘外侧。这里有个隐蔽的坑如果不加上后三行的边界判断用户点击窗口边缘时计算出的 row 或 col 可能是负数或超出范围后续访问board[row][col]会直接触发 IndexError。在期末演示时老师多半会故意点边缘来测试健壮性这个防护是必须的。3.3 选中、交换与非法操作的视觉反馈一个完整的交互序列是第一次点击选中方块第二次点击相邻方块则交换交换后检测匹配如果匹配则进入消除如果不匹配则交换回去。为了实现选中状态的可视化需要一个selected变量保存当前被选中的格子坐标。selected None def handle_click(mouse_pos, board): global selected cell pos_to_cell(mouse_pos) if cell is None: return if selected is None: selected cell else: row1, col1 selected row2, col2 cell # 检查是否相邻不含斜对角 if abs(row1 - row2) abs(col1 - col2) 1: board[row1][col1], board[row2][col2] board[row2][col2], board[row1][col1] if find_matches(board): selected None else: # 交换后无匹配立即换回 board[row1][col1], board[row2][col2] board[row2][col2], board[row1][col1] selected None else: # 点击非相邻格子重新选中新格子 selected cell相邻判断用了曼哈顿距离公式abs(r1-r2) abs(c1-c2) 1它天然排除了斜对角的情况。交换后的处理逻辑必须遵循「先模拟后提交」原则先在棋盘上交换调用find_matches做检测有匹配就保留交换结果没有就换回来。这里如果把检测和交换的顺序搞反会出现用户明明交换了却毫无反应的 bug排查起来非常费时。绘制选中状态时最简单的做法是在目标格子周围画一个高亮边框用 pygame 的pygame.draw.rect即可不需要额外加载资源。4. 动画实现、像素级渲染与性能优化4.1 消除动画与掉落动画不要用 while 循环很多初学者写动画时会陷入一个误区在检测到匹配后立刻用一个 while 循环把所有格子一步步移动到目标位置每步调用pygame.display.flip()。这会导致整个程序在动画播放期间无法响应任何鼠标点击甚至窗口关闭按钮都点不动。正确思路是把动画状态机放入主循环用「帧计数或时间差」来驱动位置插值。animating False animation_progress 0.0 def update_animation(dt): global animation_progress, animating if animating: animation_progress dt * 8 # 速度控制8 秒内完成整个动画可调 if animation_progress 1.0: animation_progress 0.0 animating False在run_game的主循环里把dt设置为clock.tick(60) / 1000.0也就是上一帧到这一帧的真实秒数。这样动画速度在不同性能的机器上表现一致不会出现高端机上快如闪电、低端机上慢如卡死的情况。动画期间的事件处理仍然执行但可以选择忽略鼠标点击请求也可以把点击事件排队等待动画结束。对于消除特效更轻量的做法是「闪烁 缩放消失」方块在被消除前先闪白 2 帧然后缩小到 0。pygame 的pygame.transform.smoothscale可以完成缩放但要注意它每次都创建新 surface频繁调用会产生大量垃圾对象。优化手段是预先创建好各尺寸的图像缓存用字典保存scaled_cache {} def get_scaled_image(img, size): if size not in scaled_cache: scaled_cache[size] pygame.transform.smoothscale(img, size) return scaled_cache[size]4.2 绘制函数中的性能细节draw_board是每帧都执行的函数它内部的效率直接决定游戏的流畅度。最容易踩的坑是在 for 循环里反复加载图片文件这会让磁盘 I/O 成为瓶颈帧率直接掉到个位数。正确做法是把所有方块图片在初始化阶段加载到内存中绘制时只做 blit 操作。def load_images(): images [] for i in range(NUM_TYPES): img pygame.Surface((CELL_SIZE - 4, CELL_SIZE - 4)) # 这里可以用 pygames 绘制不同颜色方块也可以用真实图片替代 img.fill((40, 80 i * 30, 160)) images.append(img) return images def draw_board(screen, board, images): screen.fill((30, 30, 30)) for r in range(ROWS): for c in range(COLS): if board[r][c] 0: x GRID_X_OFFSET c * CELL_SIZE 2 y GRID_Y_OFFSET r * CELL_SIZE 2 screen.blit(images[board[r][c]], (x, y))加载单据的方案里注释提到也可以用真实图片替代。如果你使用自己的 png 图片需要确保图片原始尺寸接近格子大小否则放大缩小都会产生性能问题。这里的绘制函数内部没有做任何条件判断分支每帧固定遍历 64 个格子绘制顺序与棋盘索引一一对应稳定性最高。pygame.Surface.fill和screen.blit的比例是 1:1在低配机器上如果出现卡顿优先检查是否在循环中创建了新 Surface。pygame 的文档里明确提到 Surface 的创建是重量级操作应该被移到初始化阶段。4.3 提升画面质感的几个渲染技巧如果不满足于纯色方块可以在格子交接处绘制浅色分隔线增强棋盘的结构感。另一个常用技巧是给选中的方块增加一个呼吸光晕效果即边框的亮度随时间变化。这个效果可以通过math.sin(pygame.time.get_ticks() * 0.005)生成一个 -1 到 1 之间的振荡值再映射到 RGB 上。import math def draw_selected_square(screen, selected): if selected is None: return r, c selected x GRID_X_OFFSET c * CELL_SIZE y GRID_Y_OFFSET r * CELL_SIZE glow int(128 127 * math.sin(pygame.time.get_ticks() * 0.005)) pygame.draw.rect(screen, (glow, glow, 0), (x, y, CELL_SIZE, CELL_SIZE), 3)光晕取的是正弦函数的绝对值或原值颜色通道会随时间在 1 到 255 之间循环视觉上像是呼吸。这种做法不需要任何额外素材代码量也极小但对于答辩演示的观感提升明显属于性价比极高的渲染增强手段。音频方面pygame 的pygame.mixer.Sound适合播放消除音效但加载音频文件同样要注意只初始化一次。消除时调用sound.play()不会阻塞主循环因为它是在后台混音线程播放的。如果不需要音频可以在初始化时跳过pygame.mixer.init()避免某些没有音频设备的机器上报错。5. 打包发布与课程设计文档写作要点5.1 用 PyInstaller 打包成免 Python 环境的桌面端 exe期末大作业通常需要现场演示而演示机器上未必安装了 Python 和 pygame 环境。用 PyInstaller 把项目打包成独立的 Windows 可执行文件是一个成熟的方案。首先确保安装了 PyInstallerpip install pyinstaller如果pip install pygame在安装时卡住通常是因为默认的 PyPI 源在国外。这时候把 pip 源换到清华镜像或者阿里云镜像是最快的解决方案代价只是修改一个配置项本质上是为 Python 环境管理省时间。打包命令如下pyinstaller --onefile --windowed --name match3 main.py--onefile表示生成单个 exe 文件--windowed表示不显示命令行黑窗口--name指定生成文件名。如果你在代码里用了外部图片或音频资源必须用--add-data参数把它们打包进去否则 exe 会报找不到文件pyinstaller --onefile --windowed --add-data assets;assets --name match3 main.py这里的assets;assets是 Windows 下的源路径与目标路径的对应关系中间用分号分隔。注意在main.py代码里资源路径最好用sys._MEIPASS适配这是 PyInstaller 运行时解压临时目录的环境变量。不处理这个问题的话打包后的 exe 在双击运行时可能会因为相对路径变化而加载不到资源这是最常见的打包失败原因。5.2 课程设计文档的结构与关键图表期末大作业的评分标准通常包括需求分析、系统设计、核心算法说明、测试和总结。文档不需要面面俱到但要突出你做了哪些决策。例如在「核心算法」一节里重点解释find_matches的时间复杂度是 O(R×C)与棋盘规模是线性关系说明消消乐在高分辨率棋盘上的表现趋于常量开销。画一个简单的流程图比写一百字更能说明问题但考虑到文档里的流程图通常是用文字描述的建议描述为「消除检测分为横向连续段扫描与纵向连续段扫描两个阶段」。在「测试分析」部分列出几组实际运行的边界场景比如「所有方块均相同」「棋盘已无可用交换」「用户连续快速点击多个格子」等并写出预期结果与实际结果是否一致。这些场景不需要自动化测试框架手动记录即可。如果在文档中明确划分模块例如board.py负责棋盘逻辑、gui.py负责渲染与交互、main.py负责装配与启动那么答辩时老师一眼就能看出你对项目结构的把握。代码文件数量不用多三到四个模块拆分就足够支撑起整个项目的清晰度了。5.3 答辩演示时容易加分的操控细节一个可交互的桌面应用在演示时操作节奏与视觉流畅度会影响印象分。建议在代码里保留一个隐藏的调试快捷键例如按下 D 键时在窗口标题栏显示当前帧率 FPS 和棋盘匹配数。这样老师提问「性能怎么样」时你当场就能展示一个可量化的指标而不是回答「我觉得挺流畅的」。if event.type pygame.KEYDOWN and event.key pygame.K_d: fps clock.get_fps() pygame.display.set_caption(f消消乐 - FPS: {fps:.1f})另外如果你的消消乐支持「提示」功能即高亮显示任一可用的合法交换这是很加分的设计。实现方法是在has_legal_move函数的暴力遍历过程中把第一个满足交换条件的位置缓存下来然后绘制时把这个位置描边即可。这个功能实现成本极低但演示效果极佳尤其是当棋盘进入死局边缘时它能直观展示你的程序「有智商」。最后打包后的 exe 文件体积在 30MB 到 80MB 之间是正常的因为 Python 运行时和 pygame 库都被塞进了单文件里。如果要求交付源码记得把dist/match3.exe与src源码目录分别放好在文档中写清楚运行环境为 Python 3.10 及以上版本、pygame 2.x 系列这能省去评分老师配置环境的精力也避免因为环境版本不一致导致运行报错。本文还有配套的精品资源点击获取
返回列表