ARTICLE DETAIL

资讯详情

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

用Python和Pygame实现俄罗斯方块:游戏开发核心机制全解析

用Python和Pygame实现俄罗斯方块:游戏开发核心机制全解析 1. 为什么拿Python写俄罗斯方块一个过时项目的三大价值先说个反直觉的结论俄罗斯方块可能是你人生第一款游戏的最佳选择但它绝不是练手用的玩具项目。我用Python实现过俄罗斯方块、贪吃蛇、2048、Flappy Bird回过头来看俄罗斯方块的技术含量最扎实踩坑最多做完之后对编程的理解也最通透。你可能觉得这游戏1984年就有了网上源码一大堆再写一遍有什么意义这个想法我特别理解但实际动手之后你会发现照抄源码和独立实现完全是两码事。自己写一遍俄罗斯方块至少有三层价值是其他项目替代不了的第一层是游戏开发核心机制的系统训练。俄罗斯方块体量小但五脏俱全坐标系、碰撞检测、随机算法、实时输入响应、状态机切换、计分规则——这些是任何游戏开发都绕不开的底层逻辑。别小看这几百行代码它把游戏引擎的基础概念全部覆盖了一遍。第二层是对状态管理的深刻理解。这是我最看重的。俄罗斯方块的核心难点不在算法复杂度而在于游戏状态特别多当前方块在动、下一个方块在候场、方块落地后要固定、固定后要消行、消行后上面的行要整体下移……每一帧你都要同时维护这些状态而且任何一处处理不当游戏就会崩。这种一堆状态互相影响的场景正是工程开发里最常见也最头疼的问题。第三层是即时反馈带来的正向循环。写网页后端你敲半天代码可能看不到任何效果但写俄罗斯方块每完成一个模块游戏立刻就有肉眼可见的变化。这种反馈速度非常有利于坚持学下去特别适合刚入门Python想找项目练手的朋友。我这次用的是pygame一个非常经典的2D游戏开发库。Pygame没有上手难度装个库就能跑API设计也很符合直觉。它不负责帮你封装游戏逻辑只提供基础的窗口、绘制、事件处理能力所有游戏逻辑都要你自己动脑实现——这正是我们要的效果不然什么都让框架干完了学不到东西。这篇文章我会从游戏设计思路开始讲起核心代码逐块拆解最后聊几个我实测遇到的坑和应对方案难度循序渐进不管你是刚学完Python语法还是已经能独立写脚本都能跟得上。2. 动手前的设计把俄罗斯方块翻译成数据结构很多人写游戏有个坏习惯一上来就写代码结果写到一半发现逻辑乱成一团。我建议你学学写游戏的人的思路先在草稿纸上把游戏机制理清楚转化为精确的数学模型再动手。2.1 先明确游戏规则俄罗斯方块的基础规则大家都很熟从代码角度重新梳理一遍核心是这几点游戏区域是一块固定宽高的矩形区域方块逐行堆叠。方块有7种形状每种由4个小方格组成这个叫四格骨牌Tetromino。方块从顶部出现自动下落下落过程中玩家可以左右移动、旋转、加速下坠。方块触底或被已有方块挡住后固定在该位置然后生成下一个方块。一行格子被完全填满时整行消除上方所有方块整体下移一行。方块堆叠到区域顶部、新方块无法生成时游戏结束。看起来简单但翻译成代码的时候就产生了几个关键问题如何用数据表示方块和游戏区域如何判断移动/旋转是否合法落地判定怎么做消行时数组怎么操作2.2 游戏区域的数学建模游戏区域我定义为一个10 x 20的网格。为什么是10x20这是经典俄罗斯方块的尺寸宽度10列、高度20行保证方块在水平方向有足够的移动空间同时画面比例舒适。这个尺寸不需要特别精确你完全可以按自己喜好调整但建议保持列数 x 行数的形式方便后续的矩阵操作。我用一个二维列表来表示网格初始化时全填0表示空格COLS 10 ROWS 20 grid [[0 for _ in range(COLS)] for _ in range(ROWS)]当一个格子被方块占据时把0改成该方块的形状编号或者直接改成一个非零值我习惯用形状编号后面绘制颜色时可以直接查表。这块网格就是整个游戏的大地图所有碰撞检测、消行判断都基于它来完成 —— 我称之为静态网格因为方块一旦固定进去就不再移动只参与消行计算。2.3 定义7种方块的形状数据7种标准方块分别是 I、O、T、S、Z、J、L。严格来说 I 方块由4个格子排成一条直线O 方块是2x2的正方形其余的都是3x2或2x3的L、T、S、Z形。数据怎么存最直观的方式是矩阵法用嵌套列表描述每种形状的占位图1代表有格子0代表空位。比如SHAPES { I: [ [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0] ], O: [ [1, 1], [1, 1] ], T: [ [0, 1, 0], [1, 1, 1], [0, 0, 0] ], # S, Z, J, L 类似 }这种表示方式的最大好处是旋转操作可以通过矩阵转置加行反转来实现后面会细说碰撞检测也只需要检查形状矩阵里非零位置是否越界或和网格冲突。缺点是形状矩阵的长宽不一致处理时要小心。我实际项目里用的是另一种坐标法不是说矩阵法不好而是坐标法在旋转计算上更直观虽然代码逻辑更绕。坐标法把每个方块看成一组相对坐标SHAPES { I: [(0, 0), (1, 0), (2, 0), (3, 0)], O: [(0, 0), (1, 0), (0, 1), (1, 1)], T: [(1, 0), (0, 1), (1, 1), (2, 1)], S: [(1, 0), (2, 0), (0, 1), (1, 1)], Z: [(0, 0), (1, 0), (1, 1), (2, 1)], J: [(0, 0), (0, 1), (1, 1), (2, 1)], L: [(2, 0), (0, 1), (1, 1), (2, 1)], }这里的坐标是(x, y)其中 x 是列偏移y 是行偏移。每个方块生成时都有一个基准点anchor假设基准点位于游戏区域顶部的某个位置那么方块实际占用的格子坐标 基准点坐标 每个格子的偏移量。两种方案我最终选了坐标法原因是在写旋转逻辑时可以平移到原点做旋转矩阵乘法分类讨论的情况更少。不过对刚开始的同学我建议你从矩阵法入手理解起来更自然等项目跑通了再尝试改成坐标法对比一下差异——两种都试一次你对数据如何组织影响代码复杂度这件事会有很深的体感。2.4 颜色映射表每个方块形状对应一个颜色没太多技术含量纯粹是为了视觉效果也顺便把你的代码和图形对应起来COLORS { I: (0, 255, 255), # 青色 O: (255, 255, 0), # 黄色 T: (160, 32, 240), # 紫色 S: (0, 255, 0), # 绿色 Z: (255, 0, 0), # 红色 J: (0, 0, 255), # 蓝色 L: (255, 165, 0), # 橙色 }颜色值采用RGB元组pygame绘制时直接用。这个映射表在网格数据存了形状编号之后绘制阶段就能从编号反查颜色省事很多。3. 核心逻辑逐个拆解移动、旋转、碰撞、消行这一节是整个项目的核心。我建议你在看代码前先想清楚什么是俄罗斯方块每一帧要做的事再对照代码看你猜对了多少。3.1 方块类和它的生命周期我先封装一个Block类每个方块实例代表一个正在活动、还未落地的方块class Block: def __init__(self, shape, x, y): self.shape shape self.x x # 基准点列坐标 self.y y # 基准点行坐标 self.cells SHAPES[shape] # 相对坐标列表 self.color COLORS[shape] def get_positions(self): 返回当前方块实际占用的所有格子坐标行, 列 positions [] for dx, dy in self.cells: positions.append((self.y dy, self.x dx)) return positions注意这里我把get_positions里坐标写成(行, 列)对应网格的(row, col)这样后面操作grid[row][col]时不需要转换不容易搞混。基准点的初始位置我一般定为(x4, y0)左右让方块出现在区域顶部中央。为什么不直接把实际坐标存在方块里而是要基准点相对偏移再用函数算一次因为旋转操作本质上只改变相对偏移强行把它合在一起存每次移动、旋转都要同步改一大堆坐标容易出bug。分离基准点和相对坐标之后左右移动只需要改self.x下落只需要改self.y旋转只需要改self.cells互不干扰。3.2 碰撞检测不认真写这里游戏就各种穿墙碰撞检测是俄罗斯方块的命门。每一帧方块移动前都要问一句我想去的那个位置会不会越界会不会和静态网格里已有的方块重叠写个独立的判断函数def valid_move(block, grid, dx0, dy0): for row, col in block.get_positions(): new_row row dy new_col col dx if new_col 0 or new_col COLS: return False if new_row ROWS: return False if new_row 0 and grid[new_row][new_col] ! 0: return False return True这个函数做了三件事左右边界检查、底部越界检查、和已有静态方块冲突检查。顶部越界我特意没查因为方块生成时有可能有一小部分在区域外经典俄罗斯方块允许方块部分隐藏在顶部之外只要不访问负索引列表就不会崩。这里有个很关键的设计决策碰撞检测永远用假设移动后的坐标来判断而不是先移动再回滚。前者叫试算后者叫移动-回滚。试算的好处是不会污染当前状态逻辑分支少不容易出bug。我见过不少初学者写成先移动发现碰撞了再移回去这种思路在帧率不稳定的时候会出现肉眼可见的闪烁和鬼畜移动实测体验非常差——因为你无法保证移回去的时机和你检测到碰撞的时机完全同步。调用方式就非常简洁# 向左移动一格 if valid_move(block, grid, dx-1): block.x - 1 # 向下移动一格 if valid_move(block, grid, dy1): block.y 1 else: # 不能下落了方块固定 fix_block(block, grid)注意valid_move的grid参数是静态网格不包含当前活动方块自身。所以判断当前方块和当前方块之间不会产生自己和自己碰撞的误判。3.3 旋转矩阵运算帮你省掉八种情况旋转是最容易写崩的部分。7种方块里O方块转了等于没转I、T、S、Z、J、L各有各的转法。如果每种情况都手动写一套数据代码会又长又容易出错。我用的方案是通用的旋转算法把方块每个格子的相对坐标绕基准点旋转90度。前提是方块定义用坐标法旋转90度对应数学公式新x 原y 新y -原x这是顺时针旋转如果方向不对就反过来用def rotate(block): new_cells [] for dx, dy in block.cells: new_dx -dy new_dy dx new_cells.append((new_dx, new_dy)) rotated Block(block.shape, block.x, block.y) rotated.cells new_cells return rotated但这么写出来实际运行会有问题旋转后的方块可能超出左右边界或者撞到静态网格。这就是经典的墙踢Wall Kick问题。简单说旋转发生在靠近墙壁的位置时光靠换坐标还不够需要尝试把旋转后的方块往左/右平移一点看能不能挤进去。我的处理方式很朴素但足够用——依次尝试5个候选位置原地不动、左移1格、右移1格、左移2格、右移2格只要有一个位置合法就采用def try_rotate(block, grid): for dx in [0, -1, 1, -2, 2]: test_block Block(block.shape, block.x dx, block.y) test_block.cells rotate_cells(block.cells) if valid_move(test_block, grid): return test_block return block # 所有位置都冲突放弃旋转这个方案不算完美但实际游戏里手感正常足以覆盖绝大多数旋转撞墙的场景。如果你想要更细腻的手感可以研究现代俄罗斯方块标准的SRSSuper Rotation System规范它会定义每个形状专用的踢墙修正表——但那个复杂度对入门项目来说有点杀鸡用牛刀。还有一个点值得提醒I方块长度是4旋转后横向占用4列所以它撞边界的概率比其他方块高。实测中I方块用[0, -1, 1, -2, 2]这组偏移可能不够稳你如果发现I方块贴边旋转不好用可以加一个dx3或dx-3的候选。3.4 消行从数组里删行比你想的更讲究方块固定到网格后就要检查是否有完整的行。逻辑不复杂遍历网格每一行如果该行全为非零值就说明满行需要消除。但消除这个操作有讲究。新手最容易写错的方式是# 错误示范 for row in grid: if all(cell ! 0 for cell in row): grid.remove(row) grid.insert(0, [0 for _ in range(COLS)])这行代码看着没问题实际会出bug——grid.remove(row)在多个满行同时出现时可能会因为索引错位导致漏删或误删而且remove操作本身是 O(n) 的列表查找效率并不好。推荐用收集满行索引统一重建网格的方式def clear_rows(grid): full_rows [i for i in range(ROWS) if all(cell ! 0 for cell in grid[i])] if not full_rows: return 0 # 保留未满的行 new_grid [row for i, row in enumerate(grid) if i not in full_rows] # 顶部补空行 while len(new_grid) ROWS: new_grid.insert(0, [0 for _ in range(COLS)]) for i in range(ROWS): grid[i] new_grid[i] return len(full_rows)思路是先把满行过滤掉剩下的行往上移然后在顶部补空行。这样即使同时消多行也不会出错而且代码语义很清楚消行就是扔掉满行顶部插入空行。计分规则经典版是这样的消1行100分消2行300分消3行500分消4行800分这个曲线是故意拉大的——一次消4行拿Tetris的收益远大于分4次各消1行鼓励玩家堆方块追求连消。我代码里就按这个标准写。3.5 方块下落和游戏主循环到这里游戏的基本模块都有了。怎么把它们串起来答案是游戏主循环。Pygame的标准循环结构是处理用户输入事件更新游戏状态方块下落、消行判定等绘制画面控制帧率重复FPS 60 clock pygame.time.Clock() while running: dt clock.tick(FPS) / 1000.0 # 转换为秒 for event in pygame.event.get(): if event.type pygame.QUIT: running False elif event.type pygame.KEYDOWN: if event.key pygame.K_LEFT: # 左移逻辑 elif event.key pygame.K_RIGHT: # 右移逻辑 elif event.key pygame.K_DOWN: # 加速下落 elif event.key pygame.K_UP: # 旋转 elif event.key pygame.K_SPACE: # 硬降到底 ... # 自动下落按固定时间间隔触发 if time.time() - last_fall_time fall_interval: if valid_move(current_block, grid, dy1): current_block.y 1 else: fix_block(current_block, grid) rows_cleared clear_rows(grid) score SCORE_TABLE.get(rows_cleared, 0) spawn_new_block() last_fall_time time.time() # 绘制 draw_grid(grid) draw_block(current_block) draw_next_block(next_block) draw_score(score) pygame.display.flip()这里fall_interval是方块自动下落的时间间隔初始值建议设为1.0秒左右这个速度对新手刚好。后面可以随着消行数增加而缩短下落间隔游戏越来越快难度自然上升。我用的公式是fall_interval max(0.1, 1.0 - (level - 1) * 0.05)每升1级减少0.05秒最低到0.1秒不再降低不然到后面物理上就没有操作时间了。3.6 硬降直接落底按空格键让方块瞬间落到底部这个功能作为硬降Hard Drop在俄罗斯方块里是标配。实现起来也简单不断dy1尝试下落直到撞到为止。def hard_drop(block, grid): while valid_move(block, grid, dy1): block.y 1 fix_block(block, grid)做完这一下游戏马上进入固定方块的流程。注意硬降之后要立刻pygame.display.flip()刷新一次画面否则玩家会感觉方块瞬移后界面没跟上。4. 画界面pygame里不再抽象的绘制逻辑游戏逻辑写完了但你现在翻开代码屏幕上还是一片黑。接下来处理渲染这部分对小白来说反而是最容易有成就感的——因为你写下去的每一行代码都立刻变成了屏幕上的画面。4.1 初始化pygame窗口import pygame import sys pygame.init() CELL_SIZE 30 # 每个格子边长像素 WINDOW_WIDTH COLS * CELL_SIZE 200 # 右侧留200px放预览区 WINDOW_HEIGHT ROWS * CELL_SIZE screen pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(Python 俄罗斯方块)每个格子30像素10列宽就是300px20行高就是600px。右侧预留200px的区域用来放下一个方块预览和分数显示这个布局比较经典玩家一目了然。4.2 绘制静态网格中的方块网格数据里存的是数字形状编号渲染时要把数字映射为颜色并画成方块def draw_grid(screen, grid): for row in range(ROWS): for col in range(COLS): cell_value grid[row][col] if cell_value ! 0: color SHAPE_COLORS[cell_value] # cell_value是形状字符串如I rect pygame.Rect(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE) pygame.draw.rect(screen, color, rect) pygame.draw.rect(screen, (40, 40, 40), rect, 1) # 边框线这个循环本质上就是遍历二维数组、逐个画格子。既然网格是二维数组绘制就用双重循环每个非零位置画一个矩形。边框线用深色画1像素宽会让方块看起来有立体感视觉上更舒服。4.3 绘制当前活动方块活动方块不在grid里所以要单独绘制。逻辑一样只是用get_positions()获取坐标def draw_block(screen, block): for row, col in block.get_positions(): if row 0: continue # 顶部外的部分不画 rect pygame.Rect(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE) pygame.draw.rect(screen, block.color, rect) pygame.draw.rect(screen, (40, 40, 40), rect, 1)注意row 0要跳过因为方块刚生成时可能在顶边界之外如果你直接画会画到窗口外面pygame不会报错但画面会闪烁一下。4.4 绘制下一个方块和分数右侧预览区绘制下一个方块思路就是把下一个方块的格子画在偏移后的坐标上def draw_next_block(screen, next_block): preview_x COLS * CELL_SIZE 40 preview_y 100 for dx, dy in next_block.cells: rect pygame.Rect(preview_x dx * CELL_SIZE, preview_y dy * CELL_SIZE, CELL_SIZE, CELL_SIZE) pygame.draw.rect(screen, next_block.color, rect) pygame.draw.rect(screen, (40, 40, 40), rect, 1)分数就直接用pygame.font.Font渲染成文本画到右侧区域。4.5 背景和网格线的绘制细节为了让游戏界面更清晰我习惯在每次刷新时先画背景色、再画细网格线、再画方块。细网格线用浅灰色每格一个小方框不然玩的时候视觉上不好定位def draw_grid_lines(screen): for x in range(0, COLS * CELL_SIZE 1, CELL_SIZE): pygame.draw.line(screen, (60, 60, 60), (x, 0), (x, ROWS * CELL_SIZE)) for y in range(0, ROWS * CELL_SIZE 1, CELL_SIZE): pygame.draw.line(screen, (60, 60, 60), (0, y), (COLS * CELL_SIZE, y))这个不是必须的但实测加上网格线之后玩家更容易判断方块的落点游戏体验提升非常明显。作为对比我见过不少新手项目不画网格线玩起来总是差那么点意思就是少了这个细节。5. 边写边踩的坑我在编码和运行中遇到的4个经典问题到这里你的俄罗斯方块应该已经能玩起来了。但我要提前说说这4个坑它们是我写这个项目时真实遇到过的而且每一个都卡了我超过半小时。知道它们的存在至少能让你少走一半弯路。5.1 深拷贝陷阱改了一个方块所有方块都变了这是Python新手最容易踩的坑。我在定义Block类时方块初始化的代码这么写class Block: def __init__(self, shape, x, y): self.shape shape self.x x self.y y self.cells SHAPES[shape]问题在于SHAPES[shape]返回的是列表对象的引用。所有I方块共享同一个列表。当你对一个方块执行旋转时你修改了self.cells实际上是在修改所有I方块共享的那份数据——等下一个I方块生成时它的初始形状就已经是被旋转过的了。解决办法是每次创建方块时拷贝一份新列表self.cells [list(pair) for pair in SHAPES[shape]]或者用copy.deepcopy(SHAPES[shape])。这个坑报错很奇怪——游戏逻辑看起来没问题但方块每次生成后形状越来越离谱最后游戏直接没法玩。如果你遇到这个现象优先检查是不是共享数据被修改了。5.2 消行时的列表索引错乱前面我提过remove方法的问题。如果你用了边遍历边删除的写法for i in range(len(grid)): if all(cell ! 0 for cell in grid[i]): grid.pop(i)这里有个致命bug当你pop掉第 i 行后原本的 i1 行变成了新的 i 行但循环会继续从 i1 开始导致原本应该检查的行被跳过了。如果连续两行都满你只会消掉其中一行另一行永远消不掉。所以我才反复强调用先收集满行索引、再统一重建网格的方案。这个方案彻底绕开了在循环中修改被遍历对象这个经典陷阱。5.3 帧率与下落速度你的方块有时会跳格我最初实现自动下落时用了pygame.time.delay()来控制速度结果在电脑负载变高的时候方块下落会出现一顿一顿的现象严重时甚至会跳过一行直接落到底部。后来我改用time.time()记录上次下落时刻每帧检查现在是否已经过了下落间隔current_time time.time() if current_time - last_fall_time fall_interval: # 执行下落 last_fall_time current_time这样下落速度不再受帧率波动影响——即使某一帧卡了一下下一帧也会补上应有的下落动作而不是直接跳过多帧。游戏运行速度由time.time()决定而不是帧数这是游戏开发里很基础也很重要的一课。5.4 硬降后界面卡住的假象按空格硬降后如果方块落地生成新方块有时候界面会看起来像卡住了一样等了半天才有反应。这个问题的根源是硬降后新方块马上又触发了自动下落但新方块刚生成时last_fall_time没有重置导致新方块瞬间又下落了一格视觉上像游戏抖了一下。解决办法是硬降后强制重置一下last_fall_time time.time()让新方块有完整的初始等待时间。还有一个类似的问题是玩家在方块落地瞬间按了移动键结果按键事件堆积新方块一生成就立刻被移动了给人感觉方块瞬移。处理方式是按下键时先判断当前方块是否已经固定固定了就忽略后续按键事件。6. 完整源码逻辑汇总与运行效果实测整个项目的源码结构大概是这样的完整源码后面可以单独存成.py文件直接跑- 常量定义COLS、ROWS、CELL_SIZE、SHAPES、COLORS - Block类方块数据 当前位置 相对坐标 - valid_move(grid, block, dx, dy)碰撞检测 - rotate_cells(cells)旋转坐标计算 - try_rotate(grid, block)带墙踢的旋转 - fix_block(block, grid)把方块固定进静态网格 - clear_rows(grid)消行并返回消行数 - spawn_block()生成新方块从7种形状中随机选 - draw_grid / draw_block / draw_next_block / draw_score界面绘制 - main()游戏主循环整合所有逻辑运行效果我实测过窗口打开后方块从顶部匀速下落左右方向键控制移动、上方向键旋转、下方向键加速、空格硬降。消行时整行消失、上方网格下滑分数刷新下一个方块在右侧预览区显示。整体目测帧率稳定在60 FPS手感流畅。这里给一个关键的建议每次改完代码先跑一轮人工测试再继续往下写。比如你刚写完碰撞检测就手动控制方块撞左右墙、落地确认不会穿墙刚写完消行逻辑就故意堆一行满行确认能消掉。别等整个游戏写完了再一口气测试到时候一堆bug缠在一起排错会非常痛苦。7. 三个很加分的进阶功能按需扩展如果你基础版跑通了下面这几个功能可以按兴趣选做它们每个都能锻炼一个不同的编程方向。7.1 7-bag随机算法让方块分布更均匀我之前直接用random.choice(SHAPES.keys())随机生成方块玩起来偶尔会出现连续抽到3个I方块的极端情况体验不太好。经典俄罗斯方块用的是7-bag算法把7种方块随机打乱放到一个袋子里每次从袋子里取一个袋子空了再重新洗牌。这样能保证每7个方块中每种形状至少出现一次分布最均匀。import random bag [] def next_shape(): global bag if not bag: bag list(SHAPES.keys()) random.shuffle(bag) return bag.pop()这个算法非常简单但带来的游戏体验提升非常明显强烈建议加上。7.2 软降得分和连消奖励很多俄罗斯方块变体规则里玩家主动加速下落按方向键↓会获得每格1分的小奖励同时硬降每格2分。这个小机制鼓励玩家积极操作而不是等着方块自己落下。连消奖励也可以做成累进式如果消行发生在一次固定后的短时间内比如2秒内分数按第几次连消成倍增加。实现上需要一个combo计数器每次消行时combo 1消行失败或超时重置。这个机制能让游戏的高分感飙升玩起来有很强的正反馈。7.3 阴影块Ghost Piece在经典俄罗斯方块里当前方块的底部会显示一个半透明的阴影表示这个方块如果硬降会落在哪个位置。实现思路是用hard_drop的逻辑算出一个落到最低点的临时方块但只用来画半透明图形不参与实际逻辑。这个功能对新手提升操作精度特别有效实现成本不高但看起来很专业。画阴影的代码大致长这样ghost Block(block.shape, block.x, block.y) while valid_move(ghost, grid, dy1): ghost.y 1 # 半透明绘制pygame.draw.rect时用带透明度的颜色或者把颜色RGB值乘0.5用pygame.Surface加set_alpha()实现透明度效果代码大约多写十行视觉档次立刻上来。8. 运行环境与快速启动最后把运行环境说一下方便你照着搭Python版本3.6以上都可以我用的是3.10。依赖库pygame。安装命令pip install pygame。运行方式把完整源码保存为tetris.py在终端执行python tetris.py。如果安装pygame时速度太慢可以先换国内源比如pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simpleWindows、macOS、Linux都能跑没有平台差异性的坑。macOS如果遇到pygame显示窗口黑屏的问题通常是因为macOS的OpenGL驱动和pygame版本不匹配升级pygame版本一般能解决。源码我建议你一定要自己敲一遍尽量不要复制粘贴。就算觉得理解了敲的过程中还是会发现很多以为自己懂、真写起来却不对的细节——比如某个变量名拼错了、某个缩进级别写错了、某个方法返回值和预期不符。这些错误恰恰是学习编程最宝贵的部分。敲完一遍再对照别人的源码看差异你会发现每个人都对同一套规则有不同的理解这个对比过程本身就是高级学习。
返回列表