news 2026/9/9 22:45:45

从零用Python和Pygame写俄罗斯方块:核心逻辑与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零用Python和Pygame写俄罗斯方块:核心逻辑与实战

简介:这是一份基于Python语言实现的俄罗斯方块小游戏源码包,主要面向Python初学者、游戏开发爱好者以及需要课程设计的同学。压缩包内共包含4个文件,以1个核心Python脚本为主,另附2个wav格式游戏音效和1个mp3背景音乐,整体大小仅6MB,轻量易用。代码采用模块化方式组织,覆盖棋盘布局、方块生成与旋转、自动下落、碰撞检测、消行得分等经典俄罗斯方块核心机制,并集成音效反馈与背景音乐,与网上零散代码不同,这份压缩包将程序与音效素材打包完整,下载即可运行体验。目前该资源已有322人学习下载,说明其在学习参考方面具备一定热度。读者不仅可以直接运行游玩,还可以在此基础上二次开发,增加等级加速、最高分记录、暂停与开始界面等个性化功能,是理解游戏循环和Python编程逻辑的实用案例。 俄罗斯方块这玩意儿,大概是游戏开发里最经典的入门项目了。逻辑不复杂,但麻雀虽小五脏俱全,从数据建模、碰撞检测到界面渲染,一个完整的小游戏该有的环节它全都有。用 Python 写一个能跑起来的版本,比想象中要费点心思,但收获也是实打实的——尤其适合刚学完语法、想找个项目练手的朋友。

这篇文我会从零开始,把整个实现思路、核心代码、还有我实际踩过的坑都捋一遍。不是那种丢一份完整代码让你跑完就完事的教程,而是把“为什么这么写”讲清楚,这样你拿到任何一块逻辑,都能自己改、自己扩展。

1. 整体方案与核心设计思路

先说我最终选定的技术方案:Python 3.10 + pygame 2.x。为什么用 pygame?因为它就是 Python 生态里做 2D 小游戏最省事的库,窗口创建、事件处理、图形绘制、帧率控制全都帮你封装好了,你不用去碰底层的窗口 API 或者图形渲染逻辑,能专注在游戏逻辑本身。

游戏的核心数据模型其实特别朴素,就三样东西:一个二维数组表示的网格(棋盘)、一个当前方块、一个下一个方块。网格负责记录已经固定的方块位置,当前方块负责移动和旋转,下一个方块只是用来预览,顺便让玩家有点盼头。

我建议把代码拆成三个模块,而不是全部塞进一个文件里:

  • tetris.py:定义方块形状、颜色、旋转逻辑
  • game.py:定义游戏类,负责网格、碰撞、消行、计分
  • main.py:主循环,处理输入、驱动游戏逻辑、渲染画面

初始版本可以把三者合并到一个文件里跑通,但拆开更有利于你后续加功能——比如加音效、加等级加速、加“幽灵方块”预览落点。我实测下来,拆开写之后的调试体验会舒服很多,因为每个模块的职责边界清晰,出问题能快速定位。

1.1 网格与坐标系的设定

俄罗斯方块的标准网格是 10 列 × 20 行,列数多了游戏太宽松,少了又挤得慌,10×20 是多年验证下来的黄金比例。我用一个二维列表来表达网格,每个元素要么是0(空),要么是颜色值(该位置已被方块占用)。

这里有个非常容易踩的坑:网格的行列索引和屏幕坐标的方向关系。我习惯把grid[row][col]定义成第row行、第col列,也就是先纵后横。但在 pygame 的坐标系里,画矩形用的是(x, y),先横后纵。如果不做转换,画出来的方块会“翻转”或者错位。所以我在写draw函数时,会明确做一次换算:x = col * BLOCK_SIZEy = row * BLOCK_SIZE,每次画之前都想清楚自己在操作哪个坐标系,否则很容易画出歪七扭八的东西。

1.2 方块形状的数据建模

七种经典方块(I、O、T、S、Z、J、L)的数据模型也值得琢磨。最直观的表示方式是用 4×4 或者 3×3 的布尔矩阵,包含方块形状的地方为1,其余为0。比如 T 型方块可以表示为:

[0, 1, 0], [1, 1, 1], [0, 0, 0]

每个方块我还会配一个颜色元组(r, g, b),比如 T 型方块用紫色(128, 0, 128),I 型用青色(0, 255, 255)。这个颜色不只是好看,调试时也能靠颜色快速判断当前操作的是哪个方块。

所有方块统一存在一个字典或者列表里,每种方块附带它的 spawn 位置(生成位置)。我实测下来,I 型方块因为长度是 4,它的生成位置需要单独处理,否则一出生就顶到左边界或者右边界,这种情况是新手最容易忽略的。

2. 七种方块的表示与旋转算法

这节是俄罗斯方块实现里最值得花时间琢磨的部分。旋转看似简单,但牵涉到坐标系变换和边界条件,写不好就会出现“方块旋转后位置偏移”或者“旋转后穿墙”的诡异现象。

2.1 旋转矩阵与坐标变换

先聊数学基础。二维平面上把一个点绕原点顺时针旋转 90 度,变换公式是:

新x = 原y 新y = -原x

但方块矩阵的旋转不能直接套这个公式,因为矩阵的“原点”在左上角,行号向下增长,跟数学坐标系的 y 轴方向是反的。我实际用的是“转置 + 行反转”的组合拳来实现顺时针旋转:

  1. 先对矩阵做转置(行列互换)
  2. 再把每一行反转

这个操作的直观理解是:把矩阵竖过来,再左右翻一下,形状就会顺时针转动 90 度。代码实现很简洁:

def rotate_clockwise(matrix): return [list(row) for row in zip(*matrix)][::-1]

zip(*matrix)做转置,[::-1]做行反转。实测对七种方块都适用,无需单独处理。

2.2 旋转后位置校正

旋转时最容易出的 bug 是:方块靠墙时一旋转,旋转后的形状有一部分超出了网格边界,或者叠到了已经固定的方块上面。最简单的处理方式是“尝试旋转,失败就不转”,但这会让玩家在靠墙时完全无法旋转,操作手感很差。

业界标准的做法是“墙踢”(Wall Kick)。核心思路是:旋转后如果发生碰撞,就把方块分别向左右平移 1 格、2 格再尝试,直到找到一个合法位置。如果所有偏移都失败,才放弃这次旋转。这个机制只加了几行代码,但手感会天差地别:

def try_rotate(grid, block): rotated = rotate_clockwise(block.shape) for dx in (0, -1, 1, -2, 2): if not collides(grid, block.x + dx, block.y, rotated): block.shape = rotated block.x += dx break

我加了从 0 到正负 1、2 的偏移序列,基本覆盖了靠墙旋转的常见场景。注意偏移顺序有讲究:先试原位,再试左右各 1 格,最后才试 2 格。因为大多数情况下原位就能放下,这样可以减少不必要的计算。

2.3 碰撞检测的判定条件

碰撞检测的规则其实很简单:方块矩阵里的每个1对应的网格位置,要么出了网格边界(左、右、下),要么网格里已经有固定方块。任何一条满足就算碰撞。

这里有个性能细节:没必要检查方块 4×4 矩阵里的所有 16 个位置,只需要检查矩阵里值为1的位置。你可以预先做一个坐标列表,比如方块在矩阵里的那些非零格子的相对偏移,移动时只遍历这个列表,把(x + col, y + row)换算成网格坐标去查,效率更高,而且逻辑更清晰。网格 200 格,方块最多 4 格,这种规模下性能差异不大,但代码可读性会好很多。

我实际写代码时用了一个更简单的方案:先判断边界条件(x 是否小于 0、x 是否超过 10、y 是否超过 20),再判断是否和已固定的方块重叠。因为网格本身很小,这种顺序判断在最坏情况下也只有 4 次网格查询,完全够用。

3. 完整实现步骤与关键代码解析

先说环境准备。确保你本机装好了 Python 3.10 或更高版本,然后用 pip 把 pygame 装进来:

pip install pygame

如果你用的编辑器是 VS Code,建议在项目根目录建一个.venv虚拟环境再装依赖,不然全局环境装多了容易跟其他项目冲突。这个习惯我建议从一开始就养成,别等出了依赖冲突再后悔。

3.1 游戏类的骨架设计

我用一个Game类来封装所有游戏状态。初始化时做这几件事:创建 20×10 的全零网格、随机选一个当前方块和下一个方块、初始化分数和游戏结束标记。核心属性就五个:gridcurrent_blocknext_blockscoregame_over

class Game: def __init__(self): self.grid = [[0] * COLS for _ in range(ROWS)] self.current_block = random_block() self.next_block = random_block() self.score = 0 self.game_over = False

注意一个 Python 的老坑:[[0] * COLS] * ROWS这样写是不对的,因为乘法创建的是同一个列表的引用,改一行会带动所有行变化。必须用列表推导式[[0] * COLS for _ in range(ROWS)]创建一个全新的二维列表。这个坑我当年踩得莫名其妙,排查了半天才发现是引用共享的问题,这里先帮你排掉。

3.2 游戏主循环的三段式结构

pygame 的主循环其实是非常固定的三段式:处理事件、更新状态、渲染画面。对应到俄罗斯方块,就是:读取键盘输入、尝试移动或旋转方块、让方块自动下落、检查消行、再画出所有东西。

while not game.game_over: for event in pygame.event.get(): if event.type == pygame.QUIT: run = False if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT: game.move(-1, 0) elif event.key == pygame.K_RIGHT: game.move(1, 0) elif event.key == pygame.K_DOWN: game.move(0, 1) elif event.key == pygame.K_UP: game.try_rotate() elif event.key == pygame.K_SPACE: game.hard_drop() game.update() game.draw(screen) pygame.display.flip() clock.tick(60)

pygame.display.flip()负责把画好的内容真正显示到屏幕上,而clock.tick(60)保证帧率稳定在 60 FPS,别小看这两行,少了它们游戏画面会撕裂或者跑得飞快。

3.3 方块的固定与消行逻辑

方块落到不能继续下落时,要做两件事:把方块写进网格,然后检查有没有满行需要消除。写进网格就是把方块矩阵里的1对应的坐标填上颜色值。

消行逻辑很有意思。很多人的第一反应是“从上往下判断哪些行满了,然后一行一行删掉”。但 Python 的列表提供了更优雅的做法:遍历所有行,把不满的行保留下来,然后在列表头部补充相应数量的空行。这样一次遍历就能搞定消行,而且不会出现删除中间行导致索引错乱的问题:

def clear_lines(self): new_grid = [row for row in self.grid if any(cell == 0 for cell in row)] lines_cleared = ROWS - len(new_grid) for _ in range(lines_cleared): new_grid.insert(0, [0] * COLS) self.grid = new_grid self.score += lines_cleared * 100

这里any(cell == 0 for cell in row)是判断“这行是不是还有空位”,如果有空位就保留,满行就被自然过滤掉。insert(0, ...)在头部插入新行,模拟上方的新方块掉下来。计分规则我用了最简单的方式:消除 n 行得 n × 100 分,实际游戏可以再细化成 100、300、500、800 的阶梯计分,看你喜好。

顺带一提,硬降(hard drop)的实现:当玩家按下空格键时,让方块一路下落到底。用 while 循环反复执行“尝试下移,失败就固定”,然后立即固定方块并切换到下一个方块。手感上要保留一次 soft drop 的加分(一般加 1 分),让追求极限分数的玩家有得玩。

3.4 渲染与画面绘制

绘制的逻辑分三层:背景网格线、已固定的方块、当前下落的方块。背景网格用灰色细线画出 10×20 的格子,让玩家对位置有参考。已固定方块遍历网格,值不为 0 的位置画对应颜色的实心矩形。当前下落方块则根据它的坐标和形状矩阵,画在网格上方,通常再加一个边框让它的轮廓更清晰。

这里有个体验优化:我建议把“下一个方块”的预览画在游戏区域右侧,这样玩家可以提前规划放哪。实现起来就是在右侧固定位置,根据next_block的形状画一个小号的方块预览。别小看这个小功能,实际体验的提升非常明显。

字体渲染有个小坑:pygame 默认字体不支持中文,如果你想把“分数”“游戏结束”这些文字显示成中文,需要加载系统字体。否则屏幕上会出现一排小方框乱码。最简单的做法是用pygame.font.SysFont("simsun", 36)或者"microsoftyahei"。如果你在 Linux 或 macOS 上跑,字体名要换成你系统里有的中文字体,比如"notosanscjk"

4. 常见问题与排查技巧实录

写这个项目的时候,有几个问题几乎每个初学者都会碰到,我把它们和对应的解决思路整理成一张速查表,方便你对着排查。

问题原因解决方法
方块叠到网格里还能继续移动固定方块后没有把值写进网格检查固定逻辑是否调用了merge_to_grid()
旋转时方块突然跳到老远的地方旋转后的矩阵没有做边界偏移处理加上壁踢逻辑,尝试 0、±1、±2 的偏移量
消行后整个网格都在闪烁消行时直接删除中间行导致索引错乱用“过滤 + 头部补空行”的方式重写
按方向键没反应或反应迟钝事件类型用错,KEYDOWNKEYUP混淆确认用的是KEYDOWN,且逻辑写在事件循环里
游戏速度越来越快自动下落计时没有随分数调整根据分数调整下落间隔fall_speed = max(0.1, 1 - score / 5000)
程序一打开就闪退缺少字体或图片资源检查字体初始化代码,确认路径存在

4.1 下落速度怎么控制才自然

自动下落最原始的实现是每帧都让方块下移一格,但这样游戏速度快到没法玩。实际标准做法是用计时器:每次下落之间间隔一定时间,间隔到了才让方块下移。这个时间初始设 1 秒,随分数增加逐步缩短,最短不要低于 0.1 秒,否则几乎没法操作。

我是用pygame.time.get_ticks()获取当前毫秒数,和上次下落时间做差,大于间隔时间就触发一次下落。这个方法比在事件循环里用pygame.time.set_timer()更灵活,因为你可以随时调整下落间隔,而且不会因为事件队列堆积导致多个下落事件同时触发。实测后期速度快了,set_timer模式下偶尔会出现方块一次性掉两格的抖动问题,get_ticks方案则没有这个问题。

4.2 按键手感与输入延迟

俄罗斯方块的操作响应速度直接影响游戏体验。如果你把移动逻辑写在KEYDOWN事件里,按一次只会移动一格,按住不动不会连续移动。想要“按住连续移动”,需要额外处理。

一个简单可用的方案是:在游戏循环里检测pygame.key.get_pressed(),它返回一个布尔数组表示当前所有按键的状态。你可以在每帧更新时判断“左键被按住”就执行移动。但直接这么做会移动得过快,所以需要加一个延迟计时器:按键被按住超过 100ms 后,每 50ms 移动一次。前 100ms 是“起始延迟”,防止玩家轻轻碰一下方向键方块就飞出去。

这两个参数(100ms 起始延迟、50ms 重复间隔)参考了经典红白机手柄的操作手感。我实际试玩对比过,不加延迟的版本操作非常飘,加了之后手感扎实很多。

4.3 性能与画面闪烁

俄罗斯方块对性能要求不高,但如果你的绘制代码存在穿回,还是会出现微妙的闪烁。核心原则:所有绘制只放在渲染阶段,游戏逻辑和输入处理都不要去动屏幕内容。统一用screen.fill(BG_COLOR)清屏,再从头绘制所有元素。

另外一个减少闪烁和卡顿的技巧是:把不变的元素(网格线、预览框)提前绘制到一张静态的Surface上,主循环里直接blit()这张图,再画动态的方块。实测下来帧率从 60 FPS 提升到 120 FPS 以上,但对这个项目来说其实感觉不出差别。不过这是用 pygame 做任何 2D 游戏都实用的优化技巧,值得养成习惯。

4.4 游戏结束判定

游戏结束的条件是:新方块生成时立刻与已有方块发生碰撞。因为新方块出生在网格顶部,如果顶部已经堆满了方块,说明玩家已经玩不下去了。判定逻辑写在spawn_block()里,生成新方块后立即检查碰撞,如果碰撞就把game_over置为True

这里有一个体验细节我踩过坑:不要把“游戏结束”直接写在由玩家操作触发的固定函数里,而是统一在生成新方块时判断。否则会出现一种情况:某次方块固定后消行,消掉了几行,但生成新方块时还是判定游戏结束,就会误判。正确做法是先生成新方块、再检测碰撞,不要先检测碰撞再生成。

5. 运行效果与后续扩展建议

跑通基本功能之后,你会在右下角看到一个 10×20 的网格区域,方块下落、左右移、旋转、消行都正常。游戏界面加上右侧的“下一个方块”预览和得分显示,整体观感已经接近一个正经的成品游戏了。

如果想让这个项目从“作业”变成“作品”,我推荐这几个扩展方向,按难度从低到高排列:

  • 幽灵方块:在当前下落方块的同一列底部,用半透明方式绘制一个落点预览,帮助玩家精确摆放。实现方式很简单,复制当前位置,不断向下移动直到碰撞,然后画一个轮廓。
  • 音效与背景音乐:pygame 自带的mixer模块就能播放 WAV 格式音效,消行、旋转、硬降各配一个音效,游戏沉浸感立刻上一个档次。素材可以从免费音效网站下载并注明来源。
  • 等级加速:每消除十行升一级,每级下落速度提升一档,这也是经典俄罗斯方块的难度曲线。
  • 最高分存档:把最高分写到本地 JSON 或 SQLite 文件里,每次游戏结束读取并更新。这个功能可以顺便练习下文件操作和简单的数据持久化。
  • 暂停功能:按 P 键暂停游戏,再按继续。实现就是给主循环加一个状态变量,暂停时跳过更新逻辑但继续渲染画面。

我个人实际做下来,从零开始写完整个项目大概花了三个晚上,每天两小时左右。第一天搭骨架和方块渲染,第二天写移动和旋转逻辑、消行判定,第三天处理手感优化和界面完善。期间遇到的最大坑就是旋转后的碰撞检测边界问题,调试了差不多一晚上,最后还是通过加打印日志、把每一步的网格状态打印出来才定位到问题。所以强烈建议你在调试时多使用打印输出,把gridblock的状态打出来看,比对着黑屏幕猜要高效得多。

这个项目你的收获不会只停留在“我会写俄罗斯方块了”这件事上。从数据结构(二维数组与矩阵变换)、算法(碰撞检测)到事件驱动编程(主循环与输入处理),它把 Python 基础到进阶的很多知识点全部串起来了。以后不管你是想往游戏开发方向走,还是转去做数据处理、自动化脚本,这套“拆问题—设计模型—实现—调试”的方法论都通用。拿去玩吧,祝你写出来的俄罗斯方块能陪你度过不少摸鱼的快乐时光。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:45:33

隐私政策页面URL设计与故障排查完整指南

很多人做网站,功能页和落地页都打磨得不错,唯独隐私政策页面是被敷衍过去的重灾区。我这两年接手过几个因为隐私政策网站URL翻车的项目,有的是链接写成了动态参数,一换登录态就失效;有的是被WebView拦住死活打不开&…

作者头像 李华
网站建设 2026/9/9 22:43:59

生成的DLL多了个d?揭秘调试版与发布版的命名机制与处理方案

做 Windows 开发的朋友,肯定都遇到过这种让人摸不着头脑的情况:明明项目名是 MyLibrary ,编译完一看输出目录,躺着的是 MyLibraryd.dll 。你要是不留心直接拿去用,要么是程序启动就报找不到 DLL,要么是…

作者头像 李华
网站建设 2026/9/9 22:42:55

Java对接微信商家转账到零钱:接口选型、签名与回调避坑指南

简介:面向Java开发者的微信企业转账到零钱功能实现资料,聚焦企业付款、工资奖金发放、退款等典型业务场景。资源包内含两个核心Java文件,一个用于生成请求签名,另一个封装转账接口调用与参数组装,可直接借鉴到Spring等…

作者头像 李华
网站建设 2026/9/9 22:41:46

STM32 PWM呼吸灯实战:TIM3配置与引脚重映射详解

简介:这是一份基于STM32F1系列HAL库的双极性SPWM波形生成工程代码包,面向嵌入式开发、电力电子与电机控制领域的学习者和工程师,可用在逆变器、电机驱动等场景中产生逼近正弦波的调制信号,并支持通过修改滤波器参数改变输出频率。…

作者头像 李华
网站建设 2026/9/9 22:39:16

ImageNet按需下载:构建自定义数据集的轻量方案

简介:面向图像分类与计算机视觉研究者,提供一套基于 Python 3 的 ImageNet 子集自助下载方案。核心脚本可指定类别数量和每类图片张数,自动从 ImageNet 图像 URL 中随机筛选并抓取样本,用于快速搭建训练集、验证集或进行小规模实验…

作者头像 李华