拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用pygame开发俄罗斯方块:从核心逻辑到渲染的完整指南

简介Python游戏编程源码-pygame俄罗斯方块是一份完整的经典游戏项目面向Python与pygame游戏开发初学者及进阶者可用于课程设计、项目实战或兴趣学习。资源覆盖游戏窗口创建、事件循环、方块生成与旋转、二维数组碰撞检测、消行计分、键盘操作、音效与视觉表现等核心内容并附带一份详细的俄罗斯方块游戏报告系统记录了需求分析、架构设计、编码实现及调试优化全过程。压缩包共29个文件包含8个py源码含主游戏、计分、菜单等模块、10个pyc编译文件、4个wav音效消除、升级、游戏结束等以及md/docx文档、txt说明、png示例图等总大小仅2.1MB。已有83人学习下载。通过研读源码与报告读者可以掌握pygame开发的典型流程理解模块拆分与复用、碰撞检测算法、绘制与动画优化等实用技巧并可从测试脚本与存档功能中借鉴经验是一份高性价比的入门与实战参考。1. 用 pygame 写俄罗斯方块先想清楚逻辑再碰绘图“用 pygame 写俄罗斯方块”这句话通常意味着你已能写基础 Python 脚本但还没真正把事件循环、坐标映射、逐帧刷新这三件事串起来。俄罗斯方块是个恰到好处的练手项目游戏规则简单——七个形状、一个下落、一行消去但它覆盖了游戏开发最核心的逻辑闭环输入如何影响状态、状态如何推动画面、计时事件如何触发变化。对 pygame 新手而言最大的误区是一上来就画界面逻辑还没立住结果方块堆满屏幕后只能重启进程。下面按“形状定义 → 碰撞逻辑 → 主循环 → 渲染 → 打磨”的顺序展开适合想拿到一份能改、能跑、能加功能的俄罗斯方块源码的读者也顺带覆盖 pygame 安装和环境配置里最容易翻车的几个点。2. 俄罗斯方块核心逻辑形状坐标、旋转与碰撞判定2.1 用二维小矩阵定义 7 种方块俄罗斯方块的 7 种标准形状Tetromino都由 4 个方格组成差别只在相对位置。常见的源码实现会把这 7 种形状写成预置数据而不是在运行期用算法拼出来。我最常用的表示方式是二维小矩阵每种形状是一个只含 0 和 1 的嵌套列表1 表示该坐标有方块。SHAPES [ [[1, 1, 1, 1]], # I [[1, 1], [1, 1]], # O [[0, 1, 0], [1, 1, 1]], # T [[0, 1, 1], [1, 1, 0]], # S [[1, 1, 0], [0, 1, 1]], # Z [[1, 0, 0], [1, 1, 1]], # J [[0, 0, 1], [1, 1, 1]], # L ]I 是 1×4 矩阵O 是 2×2其余是 2×3。矩阵宽度不统一没有关系后续所有位置计算都依赖len(line)和len(matrix)动态推导不需要补齐成同样尺寸。这个写法比把每个形状都硬编码成 4×4 位图更省事也更好读每一行里的 0 和 1 对应渲染时要不要画格子。后面的碰撞检测里for r, line in enumerate(shape)天然把行号和行内容同时取出来不必维护额外索引。2.1.1 用一个打印语句验证形状定义新手最容易犯的错误是 0、1 位置写反。SHAPES 定义完之后先打印一次再往下写比跑起游戏发现方块形状不对再回头查快得多for line in SHAPES[2]: print(.join(# if c else . for c in line))T 形状的轮廓会显示成“.#.”和“###”。这一步放在常量定义后、主循环前属于“先立数据、再谈绘制”的落地习惯。俄罗斯方块源码里相当一部分莫名 bug 出在形状数据或旋转矩阵写错提前验证能省下不少调试时间。2.2 旋转转置加翻转一个函数搞定 90 度旋转是所有 pygame 俄罗斯方块实现里最容易绕弯的地方。常见做法是写一个极短函数把矩阵顺时针转 90 度def rotate_cw(shape): return [list(row) for row in zip(*shape[::-1])]shape[::-1]把矩阵上下翻转zip(*...)按列重新组合两个操作叠起来就是顺时针旋转。以 T 为例[[0,1,0],[1,1,1]]旋转后得到[[1,0],[1,1],[1,0]]正好从“朝上”变成“朝右”。zip返回元组外层用list(row)转回可变列表避免后续修改行时报“tuple 不支持赋值”。不同项目对旋转规则要求不同。做简化版时转置即可想更接近标准俄罗斯方块的 SRSSuper Rotation System就要给每种方块预置 4 个旋转帧再加一组踢墙偏移表。两种方案的取舍如下方案代码量主要坑适用阶段转置法一个函数靠墙旋转会被碰撞挡住入门源码、原型验证预置 4 帧 踢墙表约 60 行数据帧顺序易写错追求标准手感对大多数 pygame 练手项目转置法已经足够。只要在旋转失败时做几次左右平移补偿手感就不会太差def try_rotate(shape, row, col): rotated rotate_cw(shape) for offset in (0, -1, 1, -2, 2): if not collides(board, rotated, row, col offset): return rotated, row, col offset return shape, row, coloffset序列就是最简单的踢墙策略先试原位再试左移一格、右移一格、左移两格、右移两格全部失败就保持原样。顺序有讲究越靠后的偏移对视觉位置的改变越大因此要优先尝试近处。2.3 碰撞检测先算候选位置再统一检查碰撞检测只有一种统一姿势把“移动后”的小矩阵放到候选坐标上逐格检查是否越界或压到已落定方块。不要先移动再检测也不要在移动过程中逐步回退那会引入一帧的视觉跳变。def collides(board, shape, row, col): for r, line in enumerate(shape): for c, cell in enumerate(line): if not cell: continue if row r BOARD_H: return True if col c 0 or col c BOARD_W: return True if board[row r][col c]: return True return False这里 board 是 20 行 × 10 列row、col 是当前形状左上角在 board 中的位置。检查顺序有讲究先查下边界row r BOARD_H再查左右边界最后查叠块。下边界放最前面是因为下落逻辑每帧都会触发检查先排除越界可以避免后面访问board时出现下标越界。左右边界检查覆盖“矩阵有一部分超出屏幕”的情况比如方块贴近左壁旋转时某些格子的列号会变成负数。2.4 消行与锁定过滤重建比逐行下移更稳方块落定后要先把数据合并进 board再检查整行是否已满。合并时顺带记录颜色索引否则渲染阶段会丢失每个方块的颜色def merge(board, shape, row, col, shape_idx): for r, line in enumerate(shape): for c, cell in enumerate(line): if cell: board[row r][col c] shape_idx 1 def clear_rows(board): keep [row for row in board if not all(row)] cleared BOARD_H - len(keep) return cleared, [[0] * BOARD_W for _ in range(cleared)] keepmerge里存的是shape_idx 1因为 board 用 0 表示空1 到 7 表示七种颜色的已落定方块。clear_rows用“过滤重建”替代逐行 pop 再 insert。逐行 pop 需要处理行号偏移连续消两行时极易删错行过滤重建的做法是保留所有不满的行在顶部补上被清掉行数的空行逻辑直观且不会因索引移动出错。返回的cleared行数直接交给计分逻辑使用。3. pygame 主循环与输入处理事件、计时与重力3.1 初始化、主循环骨架与 dt 累加pygame 主循环固定是“事件 → 更新 → 绘制”三段式。先初始化屏幕、时钟和字体再进入循环。一个最简但能跑通的最小骨架import pygame import sys CELL 30 BOARD_W, BOARD_H 10, 20 GAME_W, GAME_H BOARD_W * CELL, BOARD_H * CELL SIDEBAR_W 140 pygame.init() screen pygame.display.set_mode((GAME_W SIDEBAR_W, GAME_H)) pygame.display.set_caption(pygame Tetris) clock pygame.time.Clock() font pygame.font.SysFont(simhei, 24) running True while running: dt clock.tick(60) for event in pygame.event.get(): if event.type pygame.QUIT: running False # 更新逻辑 # 绘制逻辑 pygame.display.flip() pygame.quit() sys.exit()clock.tick(60)返回上一帧到这一帧经过的毫秒数一般叫 dt。整个游戏的时间基准都由 dt 决定。绘制部分放在pygame.display.flip()之前flip()会把后台缓冲真正交换到屏幕上绘制顺序出错通常只影响观感但刷新顺序写错会造成下一帧内容覆盖当前帧。3.1.1 tick、sleep、set_timer 三种计时方式的取舍方式用法典型问题time.sleep(0.01)每帧阻塞 10ms键盘响应延迟窗口拖拽时卡顿clock.tick(FPS) dt 累加精确随时可改要手动维护累加器pygame.time.set_timer定时向事件队列塞事件改间隔需重新调用一次下落速度频繁变化时dt 累加要维护一个fall_timer每帧追加 dt超过fall_speed就触发下移并把计时归零。无论选哪种都不要用固定帧数做下落计时因为窗口被拖动或后台运行时会掉帧按帧数走会让方块突然加速。3.2 键盘输入左右移动、旋转、硬降、软降按键处理放在 KEYDOWN 分支里。左右移动和旋转都遵循同一个模式先算候选新位置再让碰撞函数做决定。for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_LEFT: if not collides(board, cur_shape, cur_row, cur_col - 1): cur_col - 1 elif event.key pygame.K_RIGHT: if not collides(board, cur_shape, cur_row, cur_col 1): cur_col 1 elif event.key pygame.K_UP or event.key pygame.K_x: cur_shape, cur_row, cur_col try_rotate(cur_shape, cur_row, cur_col) elif event.key pygame.K_DOWN: soft_drop() elif event.key pygame.K_SPACE: hard_drop()K_UP和K_x同时映射到旋转是为了覆盖不同操作习惯。soft_drop 和 hard_drop 最容易写混软降一次只走一格硬降直接落底并锁定。def soft_drop(): global cur_row if collides(board, cur_shape, cur_row 1, cur_col): lock_piece() else: cur_row 1 def hard_drop(): global cur_row while not collides(board, cur_shape, cur_row 1, cur_col): cur_row 1 lock_piece()两个函数都要调用 lock_piece避免同一段锁定逻辑写两遍。lock_piece 内部按顺序做合并、消行、计分、生成新方块、判断新方块生成位置是否立刻碰撞。若立刻碰撞游戏结束。所有全局状态必须在锁定后一次性更新完毕否则会出现方块穿模或者消行后分数没更新的怪问题。3.3 用 set_timer 接管重力下落把下落也交给事件队列是 pygame 里常见且干净的做法。下落速度用毫秒间隔表示等级变化时改间隔即可FALL_EVENT pygame.USEREVENT 1 fall_speed 500 pygame.time.set_timer(FALL_EVENT, fall_speed) while running: for event in pygame.event.get(): if event.type FALL_EVENT: soft_drop() # 其余 QUIT、KEYDOWN 照旧set_timer 的好处是逻辑入口单一定时事件到达时调 soft_drop和按键触发的 soft_drop 共用同一套碰撞与锁定逻辑不会出现“按键下落一次、定时又下落一次”的重复计数问题。等级提升需要提速时再调用一次pygame.time.set_timer(FALL_EVENT, new_fall_speed)旧定时器会被新参数覆盖。如果改用 dt 累加方案切换速度时要记得重置fall_timer否则积压的时间差会一次触发多次下落。3.4 连续按键的 DAS 处理稳定手感要自己算延迟pygame 的 KEYDOWN 事件自带系统级重复按住方向键会按系统设置持续触发。但这个重复延迟在 Windows、macOS、Linux 上各不相同手感飘忽。想要稳定的侧移手感要么用pygame.key.set_repeat统一重复间隔要么在更新阶段自己累积时间move_timer 0 MOVE_INTERVAL 80 # 毫秒 move_timer dt keys pygame.key.get_pressed() if move_timer MOVE_INTERVAL and (keys[pygame.K_LEFT] or keys[pygame.K_RIGHT]): move_timer 0 # 按左或右执行一次移动pygame.key.set_repeat(delay, interval)是更省事的方案调用一次即可delay 是按住多久开始重复interval 是之后每次重复的毫秒间隔。入门源码里可以直接用 150/50追求手感时再单独调这两个值。4. pygame 渲染把游戏逻辑映射到屏幕像素4.1 布局常量与坐标换算pygame 绘图的基本单位是矩形。棋盘 10×20 格选定CELL 30后游戏区是 300×600 像素右侧留 140 像素放预览和分数。先把布局定成常量而不是在绘制代码里写死数字CELL 30 BOARD_W, BOARD_H 10, 20 GAME_W, GAME_H BOARD_W * CELL, BOARD_H * CELL SIDEBAR_W 140 X0, Y0 10, 10 # 棋盘左上角留白X0、Y0 是棋盘相对窗口的偏移量。绘制任一格子时用网格坐标乘上 CELL 再加偏移得到屏幕坐标def draw_cell(screen, color, row, col): rect pygame.Rect(X0 col * CELL, Y0 row * CELL, CELL - 1, CELL - 1) pygame.draw.rect(screen, color, rect)矩形宽高刻意设为CELL - 1让格子之间露出 1 像素背景色省掉单独画网格线的代码。若想画网格线需要额外遍历range(BOARD_W 1)画竖线、range(BOARD_H 1)画横线两种效果接近但CELL - 1方案迭代更少。4.1.1 与布局相关的几个常量的作用常量典型值作用CELL30主棋盘单格边长像素单位X0 / Y010棋盘左上角的窗口偏移SIDEBAR_W140右侧信息栏宽度用于预览区定位后续调整单元格大小只需要改 CELL所有坐标换算自动跟随。这也是编码习惯里“配置与逻辑分离”的小体现。4.2 落定方块、当前方块与颜色索引的绘制渲染时把 board 和当前活动方块分开画。board 里存的是颜色索引加一取出后要先减一再查颜色表COLORS [ (0, 255, 255), # I (255, 255, 0), # O (160, 0, 255), # T (0, 255, 0), # S (255, 0, 0), # Z (0, 0, 255), # J (255, 160, 0), # L ] def draw_board(screen, board): for y in range(BOARD_H): for x in range(BOARD_W): v board[y][x] if v: draw_cell(screen, COLORS[v - 1], y, x) def draw_active(screen, shape, row, col, shape_idx): for r, line in enumerate(shape): for c, cell in enumerate(line): if cell: draw_cell(screen, COLORS[shape_idx], row r, col c)COLORS 的下标和 SHAPES 一一对应。board 存颜色索引的目的是让已落定方块保留原始颜色否则全部用同一种灰渲染消行时视觉反馈会弱很多。当前方块的颜色直接用 shape_idx 取不需要加一偏移。4.3 下一个方块预览与分数文本侧栏预览不需要复用主棋盘的 30 像素格子用 20 像素小格即可。绘制前先算出当前形状的列数让预览水平居中def draw_next(screen, next_idx): shape SHAPES[next_idx] shape_w len(shape[0]) ox GAME_W 40 (96 - shape_w * 20) // 2 oy 140 for r, line in enumerate(shape): for c, cell in enumerate(line): if cell: rect pygame.Rect(ox c * 20, oy r * 20, 20, 20) pygame.draw.rect(screen, COLORS[next_idx], rect)96 - shape_w * 20是侧栏内剩余空白// 2让不同宽度的方块居中显示。分数文本用pygame.font.SysFont渲染。如果系统找不到 simhei会自动回退默认字体中文可能出现方框常见做法是只渲染数字和英文标签直接避开字体问题。分数渲染放在pygame.display.flip()之前即可文字叠在侧栏不会和棋盘冲突。4.4 游戏结束与再开一局当新方块生成后立刻碰撞判定游戏结束。最简单的 UI 实现是game_over 置 True主循环里只画结束文本等玩家按 R 重置if game_over: text font.render(GAME OVER [R] restart, True, (255, 80, 80)) screen.blit(text, (GAME_W // 2 - 100, GAME_H // 2 - 20))按 R 重置时需要恢复三个状态棋盘清空、分数归零、重新生成当前方块和下一个方块。容易漏掉的是要同步调用pygame.time.set_timer恢复初始下落间隔否则升级过的等级还会残留在定时器里新开局直接以极快速度下落。5. 计分、速度表和 pygame 环境问题快速排雷5.1 计分规则与等级速度表推荐用经典计分单行 100、双行 300、三行 500、四行 800乘以当前等级。每消 10 行升一级。下落间隔按等级递减常用参考表等级下落间隔 (ms)1800270036004500540063007200等级变化时重新调用一次pygame.time.set_timer(FALL_EVENT, new_speed)即可。这个速度表也适合直接作为初始配置写在文件顶部方便以后把等级参数抽到 JSON 或配置类里。5.2 幽灵方块一个低成本高收益的小特性在当前位置下方画一个暗淡的落点能显著降低预判难度。实现只有一次循环检查def ghost_row(shape, row, col): g row while not collides(board, shape, g 1, col): g 1 return g绘制时用灰色按 ghost_row 的位置画一遍当前形状再正常画当前方块。幽灵方块只影响渲染不触碰任何状态变量所以不会引入额外 bug。5.3 pygame 安装报错与解释器不一致pip install pygame出现error: failed to build pygame when getting requirements to build wheel时多半是环境缺少编译依赖或 pip 尝试从源码构建。优先装官方预编译的 wheel 包python -m pip install --pre pygame python -m pip install pygame -UVSCode 里import pygame报“No module named pygame”最常见原因不是安装失败而是 pip 装进了另一个 Python 解释器。先执行python -m pip install pygame再在 VSCode 右下角确认当前解释器是否和命令行用的是同一个版本两个环境错位会让安装结果看起来像没生效。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门