
1. 项目概述与核心价值最近在整理硬盘里的老项目翻出来一个大学时期用C#写的俄罗斯方块。当时写它纯粹是为了练手没想到后来在面试和带新人的时候这个“古老”的小游戏成了我讲解面向对象设计、游戏循环和C# WinForms/GDI绘图的最佳案例。很多人觉得俄罗斯方块太简单没什么技术含量但恰恰是这种规则明确、逻辑闭环的小项目最能考验一个开发者对基础架构的理解和代码的组织能力。今天我就把这个项目的核心实现思路、代码结构以及那些年踩过的坑重新梳理一遍分享给大家。无论你是刚接触C#想找个练手项目还是有一定经验想看看如何优雅地设计一个经典游戏相信都能从中获得启发。这个项目基于C# WinForms开发使用GDI进行图形绘制。它完整实现了俄罗斯方块的经典玩法七种不同形状的方块Tetromino随机下落、玩家控制移动旋转、行满消除、速度随等级递增等。麻雀虽小五脏俱全里面涉及了游戏状态管理、碰撞检测、矩阵运算、事件驱动、双缓冲绘图等多个核心编程概念。我会重点拆解游戏主循环的设计、方块数据的矩阵化表示以及如何实现流畅的碰撞检测与响应这三个最关键的模块并提供可直接运行的代码片段。2. 游戏整体架构与设计思路拆解在动手写代码之前我们先得想清楚整个游戏怎么运转。一个典型的游戏程序核心是一个不断运行的“循环”在这个循环里我们按固定的时间间隔做三件事处理用户输入、更新游戏逻辑、渲染画面。对于俄罗斯方块这个模型依然适用但我们需要用事件驱动的方式将其融入到WinForms的消息循环中。2.1 核心类的职责划分我采用了经典的面向对象设计将游戏的不同职责拆分到不同的类中这样代码更清晰也更容易维护和扩展。主要的核心类有以下几个GameBoard (游戏面板)这是游戏的核心大脑。它内部维护一个二维整型数组比如int[20, 10]用来表示游戏区域中每个格子当前的状态0表示空非0表示被某种方块占据。它负责所有与“场地”相关的逻辑方块的放置、消除满行、判断方块移动是否合法等。Tetromino (方块)代表一个正在下落的俄罗斯方块。它需要知道自己的形状七种之一、当前旋转状态、在游戏面板上的坐标。它的核心方法是旋转、移动左、右、下以及一个关键方法检查自己如果执行某个动作如左移是否会与GameBoard中已固定的方块发生碰撞。GameEngine (游戏引擎)这是驱动游戏运转的控制器。它持有一个GameBoard实例和一个当前活动的Tetromino实例。它的主要职责是驱动游戏主循环通常用一个Timer控件实现在每个时钟周期内驱动当前方块下落一格并检查是否需要固定方块、消除行、生成新方块以及判断游戏是否结束。MainForm (主窗体)这是WinForms的窗体类。它负责创建和持有GameEngine处理键盘事件将上下左右、空格键映射为对当前Tetromino的操作指令并在窗体的Paint事件中使用GDI将GameBoard和Tetromino的状态绘制出来。这种分离的架构好处很明显GameBoard只关心场地状态Tetromino只关心自己GameEngine负责协调调度MainForm负责交互和展示。任何一方的修改只要接口不变就不会轻易影响到其他部分。2.2 游戏主循环的实现选择在WinForms中实现游戏主循环主要有两种方式使用System.Windows.Forms.Timer控件或者使用一个独立的线程配合Thread.Sleep。对于俄罗斯方块这种节奏固定、不需要极高帧率的游戏使用Timer是更简单、更安全的选择因为它的事件回调是在UI线程上执行的我们直接在里面更新游戏状态和调用Invalidate()重绘画面不会引发跨线程访问控件的问题。我设置了一个Timer其Interval间隔属性决定了游戏的速度。例如初始等级可以设为500毫秒下落一格。随着消除行数的增加我们可以逐步减少这个间隔值让游戏加速。在Timer的Tick事件处理函数中我们调用GameEngine的Step方法驱动游戏逻辑前进一步。注意Forms.Timer的精度不高且如果UI线程繁忙比如进行大量计算它的触发可能会延迟。但对于俄罗斯方块来说这完全在可接受范围内。如果你要做的是需要高精度定时如60FPS的动作游戏就需要考虑使用System.Timers.Timer或System.Threading.Timer并处理好与UI线程的同步。3. 核心数据结构与算法解析俄罗斯方块的本质是对一个二维网格游戏板和其中一组特定形状方块的状态管理。如何高效地表示和操作这些数据是代码是否优雅的关键。3.1 方块的矩阵化表示七种俄罗斯方块I, J, L, O, S, T, Z每种有1到4种旋转形态。最直观的表示方法是为每种形态定义一个小的二维数组矩阵。例如一个“T”形方块在0度旋转时可以表示为一个3x3的矩阵// T形方块旋转状态0 int[,] shape new int[,] { {0, 1, 0}, {1, 1, 1}, {0, 0, 0} };这里用1表示方块实体0表示空白。Tetromino类内部可以存储一个这样的矩阵以及一个表示当前旋转状态的索引。当玩家按下旋转键时我们不是去计算复杂的几何变换而是简单地用下一个预定义的矩阵替换当前的矩阵。我通常会把一种方块的所有旋转形态预先定义在一个三维数组或列表中。public class Tetromino { // 所有方块的形状数据 [方块类型索引, 旋转状态索引, 行, 列] private static readonly int[,,,] Shapes ... // 初始化所有形状 private int _typeIndex; // 当前方块类型 private int _rotationIndex; // 当前旋转状态 public int[,] CurrentShape GetShape(_typeIndex, _rotationIndex); // 旋转方法本质上是改变_rotationIndex并确保不越界0-3循环 public void Rotate(bool clockwise) { int newRotation clockwise ? (_rotationIndex 1) % 4 : (_rotationIndex 3) % 4; // 在真正应用前需要先进行碰撞检测墙踢Wall Kick _rotationIndex newRotation; } }这种“查表法”比实时计算旋转要高效和准确得多也更容易处理俄罗斯方块经典的“墙踢”Wall Kick规则——即当旋转因碰撞而无法直接进行时系统会尝试将方块向旁边移动一格再旋转。3.2 碰撞检测的逻辑实现碰撞检测是游戏逻辑的核心它发生在每一次尝试移动或旋转方块之前。其逻辑非常直接我们虚拟地执行这次操作然后检查操作后的方块所占用的每一个网格坐标是否满足以下两个条件该坐标在游戏面板的边界内X在0到宽度-1之间Y大于等于0且小于高度。该坐标在游戏面板的底层数组GameBoard.Grid中对应的值为0即空位。只要有一个坐标不满足上述任一条件就判定为碰撞此次操作被拒绝。public class GameBoard { private int[,] _grid; // 游戏底板矩阵 public bool IsValidPosition(Tetromino tetromino, int testX, int testY) { int[,] shape tetromino.CurrentShape; for (int row 0; row shape.GetLength(0); row) { for (int col 0; col shape.GetLength(1); col) { // 只检查形状矩阵中为1的格子 if (shape[row, col] ! 0) { int boardX testX col; int boardY testY row; // 检查是否出界顶部出界是允许的因为新方块从顶部外生成 if (boardX 0 || boardX Width || boardY Height) { return false; } // 检查是否与已固定的方块重叠Y0时才检查 if (boardY 0 _grid[boardY, boardX] ! 0) { return false; } } } } return true; } }在GameEngine中每次Timer.Tick自动下落或接收到键盘指令手动移动/旋转时都会先调用这个IsValidPosition方法进行预测。只有预测通过才会真正更新方块的位置或旋转状态。3.3 消除行的算法与矩阵更新当方块下落到底部固定后我们需要扫描游戏底板_grid找出所有行都被填满的行。一个高效的扫描方法是从最底部索引最大的行开始向上扫描。public int ClearFullLines() { int linesCleared 0; // 从最底行开始向上扫描 for (int row Height - 1; row 0; row--) { bool isFull true; for (int col 0; col Width; col) { if (_grid[row, col] 0) { isFull false; break; } } if (isFull) { // 将该行以上的所有行向下移动一行 for (int rowToMove row; rowToMove 0; rowToMove--) { for (int col 0; col Width; col) { _grid[rowToMove, col] _grid[rowToMove - 1, col]; } } // 最顶行清空 for (int col 0; col Width; col) { _grid[0, col] 0; } linesCleared; // 因为当前行被移下来的新行覆盖需要再次检查同一行索引不变 row; } } return linesCleared; }这个算法是“原地”操作的它直接将满行上方的数据整体下移避免了使用额外的临时列表效率较高。返回消除的行数用于计算分数和判断是否升级。4. 关键模块的代码实现与详解理论讲完了我们来看具体代码。我会把最核心的几个类的骨架代码列出来并附上关键注释。4.1 GameBoard 类的实现GameBoard是游戏状态的存储器它的健壮性直接决定了游戏的稳定性。public class GameBoard { public int Width { get; } public int Height { get; } private int[,] _grid; // 游戏底板0为空非0为方块颜色/类型标识 public GameBoard(int width, int height) { Width width; Height height; _grid new int[height, width]; // 默认所有元素为0 } // 核心方法判断方块在指定位置是否合法 public bool IsValidPosition(Tetromino tetromino, int x, int y) { // 实现见上一节 } // 将当前活动的方块“固化”到底板上 public void PlaceTetromino(Tetromino tetromino, int x, int y, int value) { int[,] shape tetromino.CurrentShape; for (int row 0; row shape.GetLength(0); row) { for (int col 0; col shape.GetLength(1); col) { if (shape[row, col] ! 0) { int boardY y row; int boardX x col; // 只固化在面板内的部分新方块生成时部分可能在面板顶部之外 if (boardY 0 boardY Height boardX 0 boardX Width) { _grid[boardY, boardX] value; // value可以是颜色索引或方块类型 } } } } } // 消除满行并返回消除行数 public int ClearFullLines() { // 实现见上一节 } // 提供一个只读的网格视图用于绘制 public int[,] GetGridSnapshot() { return (int[,])_grid.Clone(); } // 重置游戏板 public void Clear() { Array.Clear(_grid, 0, _grid.Length); } }实操心得在PlaceTetromino方法中判断boardY 0非常重要。因为新生成的方块其初始位置的一部分通常是顶部几行可能位于游戏面板的可见区域之上Y坐标为负。我们只固化那些已经落入可见区域Y0的部分否则在访问_grid数组时会引发“索引超出数组边界”的异常。这是新手常踩的一个坑。4.2 Tetromino 类的实现Tetromino需要管理自己的状态并提供查询形状和尝试变换的方法。public class Tetromino { // 预定义所有方块的所有旋转形态 [7种方块4种旋转4x4矩阵] private static readonly int[,,,] Shapes new int[7, 4, 4, 4] { // I { { {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} }, { {0,0,1,0}, {0,0,1,0}, {0,0,1,0}, {0,0,1,0} }, { {0,0,0,0}, {0,0,0,0}, {1,1,1,1}, {0,0,0,0} }, { {0,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,0,0} } }, // J, L, O, S, T, Z 的定义类似... }; public int TypeIndex { get; private set; } public int RotationIndex { get; private set; } public int X { get; set; } public int Y { get; set; } // 获取当前形状的4x4矩阵 public int[,] CurrentShape { get { int[,] shape new int[4, 4]; for (int i 0; i 4; i) for (int j 0; j 4; j) shape[i, j] Shapes[TypeIndex, RotationIndex, i, j]; return shape; } } public Tetromino(Random random) { TypeIndex random.Next(0, 7); // 随机选择一种方块 RotationIndex 0; // 初始位置通常设置在顶部中央略偏上的位置例如 (Width/2 - 2, -1) X 3; // 假设游戏板宽104x4方块的中心在(1.5, 1.5)取整后X3能让方块大致居中 Y -1; // -1让方块的一部分如长条I从面板外开始下落 } // 尝试旋转返回旋转后的新实例用于碰撞检测 public Tetromino GetRotated(bool clockwise) { var newTetromino this.Clone(); // 需要实现Clone方法 newTetromino.RotationIndex clockwise ? (RotationIndex 1) % 4 : (RotationIndex 3) % 4; return newTetromino; } // 克隆当前方块状态 public Tetromino Clone() { return new Tetromino(new Random()) // Random参数这里用不上可以优化 { TypeIndex this.TypeIndex, RotationIndex this.RotationIndex, X this.X, Y this.Y }; } }注意事项我在这里使用了4x4的矩阵来统一表示所有方块即使像O方块2x2或I方块4x1也用4x4表示只是周围用0填充。这样做的好处是旋转计算和碰撞检测的代码可以统一处理非常方便。初始位置(X, Y)的设置需要根据你的游戏板坐标系统和方块矩阵大小仔细调整确保方块生成在正确的位置。4.3 GameEngine 类的实现GameEngine是粘合剂它把各个部分串联起来并控制游戏流程。public class GameEngine { private GameBoard _board; private Tetromino _currentTetromino; private Random _random; private System.Windows.Forms.Timer _gameTimer; public int Score { get; private set; } public int Level { get; private set; } public bool IsGameOver { get; private set; } // 事件用于通知UI更新 public event Action OnStateChanged; public event Action OnGameOver; public GameEngine(int width, int height) { _board new GameBoard(width, height); _random new Random(); _gameTimer new System.Windows.Forms.Timer(); _gameTimer.Tick GameTimer_Tick; Level 1; SetTimerInterval(); SpawnNewTetromino(); } public void Start() { if (IsGameOver) { Reset(); } _gameTimer.Start(); } public void Pause() _gameTimer.Stop(); public void Resume() _gameTimer.Start(); private void SetTimerInterval() { // 速度随等级提高而加快例如初始500ms每升一级减少50ms _gameTimer.Interval Math.Max(50, 500 - (Level - 1) * 50); } private void GameTimer_Tick(object sender, EventArgs e) { // 游戏主循环尝试让当前方块下落一格 if (TryMoveCurrentTetromino(0, 1)) // 尝试向下移动 { // 移动成功通知UI重绘 OnStateChanged?.Invoke(); } else { // 无法下落固定当前方块 LockCurrentTetromino(); // 检查并消除满行 int lines _board.ClearFullLines(); if (lines 0) { UpdateScore(lines); // 检查是否升级 // ... OnStateChanged?.Invoke(); } // 生成新方块 if (!SpawnNewTetromino()) { // 生成即碰撞游戏结束 GameOver(); return; } OnStateChanged?.Invoke(); } } // 尝试移动当前方块成功返回true public bool TryMoveCurrentTetromino(int deltaX, int deltaY) { int newX _currentTetromino.X deltaX; int newY _currentTetromino.Y deltaY; if (_board.IsValidPosition(_currentTetromino, newX, newY)) { _currentTetromino.X newX; _currentTetromino.Y newY; return true; } return false; } // 尝试旋转当前方块 public bool TryRotateCurrentTetromino(bool clockwise) { var rotated _currentTetromino.GetRotated(clockwise); // 简单旋转检测先尝试在原位旋转 if (_board.IsValidPosition(rotated, rotated.X, rotated.Y)) { _currentTetromino rotated; return true; } // 可在此处实现墙踢Wall Kick尝试将旋转后的方块向左/右微调一格 // ... return false; } private void LockCurrentTetromino() { // 将当前方块固化到底板上value可以用TypeIndex1来区分颜色 _board.PlaceTetromino(_currentTetromino, _currentTetromino.X, _currentTetromino.Y, _currentTetromino.TypeIndex 1); } private bool SpawnNewTetromino() { _currentTetromino new Tetromino(_random); // 检查生成位置是否合法与已固定的方块重叠 return _board.IsValidPosition(_currentTetromino, _currentTetromino.X, _currentTetromino.Y); } private void UpdateScore(int linesCleared) { // 经典计分规则消除1行100分2行300分3行500分4行800分 int[] scoreTable { 0, 100, 300, 500, 800 }; Score scoreTable[linesCleared] * Level; // 分数乘以当前等级 // 可以在此处检查分数是否达到升级阈值并更新Level和SetTimerInterval } private void GameOver() { _gameTimer.Stop(); IsGameOver true; OnGameOver?.Invoke(); } private void Reset() { _board.Clear(); Score 0; Level 1; IsGameOver false; SetTimerInterval(); SpawnNewTetromino(); OnStateChanged?.Invoke(); } // 提供数据给UI绘制 public int[,] GetBoardData() _board.GetGridSnapshot(); public Tetromino GetCurrentTetromino() _currentTetromino?.Clone(); // 返回克隆避免外部修改 }核心技巧GameEngine通过事件OnStateChanged,OnGameOver与UI层MainForm解耦。当游戏状态方块移动、消除行、游戏结束发生变化时引擎触发事件窗体在事件处理程序中调用Invalidate()方法触发重绘。这是一种非常清晰的前后端分离模式。另外GetCurrentTetromino方法返回的是克隆体这是为了防止UI层或其他地方意外修改了引擎内部正在使用的方块对象这是一种保护性编程。4.4 MainForm 的绘制与事件处理最后我们需要一个WinForms窗体来承载这一切。窗体的主要工作是创建GameEngine实例响应其事件处理键盘输入并在Paint事件中绘制游戏画面。public partial class MainForm : Form { private GameEngine _engine; private const int CellSize 25; // 每个格子的像素大小 public MainForm() { InitializeComponent(); DoubleBuffered true; // 启用双缓冲消除绘制闪烁 _engine new GameEngine(width: 10, height: 20); _engine.OnStateChanged Engine_OnStateChanged; _engine.OnGameOver Engine_OnGameOver; this.KeyDown MainForm_KeyDown; this.Paint MainForm_Paint; } private void Engine_OnStateChanged() { this.Invalidate(); // 请求重绘整个窗体 } private void Engine_OnGameOver() { _gameTimer.Stop(); MessageBox.Show($游戏结束最终得分{_engine.Score}); } private void MainForm_KeyDown(object sender, KeyEventArgs e) { if (_engine.IsGameOver) return; switch (e.KeyCode) { case Keys.Left: _engine.TryMoveCurrentTetromino(-1, 0); break; case Keys.Right: _engine.TryMoveCurrentTetromino(1, 0); break; case Keys.Down: _engine.TryMoveCurrentTetromino(0, 1); break; case Keys.Up: // 旋转 _engine.TryRotateCurrentTetromino(clockwise: true); break; case Keys.Space: // 硬降 HardDrop(); break; case Keys.P: // 暂停/继续 TogglePause(); break; } this.Invalidate(); // 每次按键操作后立即重绘响应更及时 } private void HardDrop() { // 硬降一直向下移动直到碰到底部 while (_engine.TryMoveCurrentTetromino(0, 1)) { } // 触发一次Timer_Tick的逻辑立即固定方块 _gameTimer_Tick(null, EventArgs.Empty); } private void MainForm_Paint(object sender, PaintEventArgs e) { Graphics g e.Graphics; g.Clear(Color.Black); // 黑色背景 // 1. 绘制已固定的方块 int[,] board _engine.GetBoardData(); for (int row 0; row board.GetLength(0); row) { for (int col 0; col board.GetLength(1); col) { if (board[row, col] ! 0) { DrawCell(g, col, row, GetColor(board[row, col])); } } } // 2. 绘制当前正在下落的方块 var current _engine.GetCurrentTetromino(); if (current ! null) { int[,] shape current.CurrentShape; for (int row 0; row 4; row) { for (int col 0; col 4; col) { if (shape[row, col] ! 0) { // 只绘制在面板内的部分 int boardX current.X col; int boardY current.Y row; if (boardY 0) // 只绘制已经进入可视区域的部分 { DrawCell(g, boardX, boardY, GetColor(current.TypeIndex 1), isCurrent: true); } } } } } // 3. 绘制网格线可选 using (Pen gridPen new Pen(Color.DarkGray, 1)) { for (int x 0; x 10; x) g.DrawLine(gridPen, x * CellSize, 0, x * CellSize, 20 * CellSize); for (int y 0; y 20; y) g.DrawLine(gridPen, 0, y * CellSize, 10 * CellSize, y * CellSize); } // 4. 在窗体标题或某个Label上显示分数和等级 this.Text $俄罗斯方块 - 分数: {_engine.Score} 等级: {_engine.Level}; } private void DrawCell(Graphics g, int x, int y, Color color, bool isCurrent false) { Rectangle rect new Rectangle(x * CellSize, y * CellSize, CellSize, CellSize); // 绘制填充矩形 using (Brush brush new SolidBrush(color)) { g.FillRectangle(brush, rect); } // 绘制边框当前方块用白色边框已固定的用深色边框 using (Pen pen new Pen(isCurrent ? Color.White : Color.FromArgb(100, color))) { g.DrawRectangle(pen, rect); } // 可选绘制内部高光增加立体感 // ... } private Color GetColor(int value) { // 根据value1-7返回对应的颜色 Color[] colors { Color.Cyan, Color.Blue, Color.Orange, Color.Yellow, Color.Green, Color.Purple, Color.Red }; return colors[(value - 1) % colors.Length]; } // 开始按钮点击事件 private void btnStart_Click(object sender, EventArgs e) { _engine.Start(); } }绘图优化心得DoubleBuffered true;这行代码至关重要。WinForms默认在Paint事件中直接绘图到屏幕如果绘图区域复杂或频繁刷新会出现严重的闪烁。启用双缓冲后所有绘图操作先在内存中的位图上完成然后一次性绘制到屏幕上画面会变得非常平滑。另外在DrawCell方法中我为当前活动的方块绘制了白色边框为已固定的方块绘制了半透明的深色边框这样在视觉上能很好地区分两者用户体验更佳。5. 常见问题、调试技巧与扩展思路即使按照上面的代码实现了在实际运行中你很可能还会遇到一些“诡异”的问题。下面是我在开发和调试过程中总结的一些常见坑点和解决思路。5.1 碰撞检测的边界情况处理问题方块在靠近边界旋转时有时会“嵌”进墙里或者直接消失。分析与解决这通常是因为旋转后的方块矩阵超出了游戏板的边界。我们的IsValidPosition方法已经处理了边界检查但问题可能出在方块的“原点”上。我们使用4x4矩阵表示方块其旋转中心默认在矩阵的几何中心附近。但有些方块如I、S、Z的官方旋转规则如SRS并不是简单的几何旋转还包含“墙踢”偏移。一个简单的增强方法是在TryRotateCurrentTetromino中如果原位旋转失败不是直接返回false而是尝试将旋转后的方块向左、右、上、下各移动一格或根据官方墙踢表看看哪个位置是合法的。这是一个进阶话题但能极大提升游戏手感的专业性。5.2 游戏循环与UI响应的冲突问题按下方向键移动方块时感觉有延迟或者快速连续按键会被吞掉。分析与解决这涉及到WinForms的消息循环和键盘事件处理机制。KeyDown事件不是连续触发的它有一个“重复延迟”。对于需要快速响应的游戏可以设置窗体的KeyPreview属性为true并重写ProcessCmdKey方法这样可以更早地捕获按键消息。更高级的做法是在GameEngine中维护一个当前按键状态的字典DictionaryKeys, bool在KeyDown事件中将其设为true在KeyUp事件中设为false。然后在游戏主循环Timer_Tick中根据这个状态字典来连续移动方块例如如果左键被按住每过几帧就尝试左移一次。这能实现“长按连续移动”的效果体验更好。5.3 内存与性能问题问题游戏运行一段时间后感觉变卡。分析与解决对于俄罗斯方块性能瓶颈通常不在计算而在绘图。避免在Paint事件中创建大量Pen和Brush就像上面的示例代码我使用了using语句确保Graphics对象使用的Pen和Brush被及时释放。更好的做法是将常用的画笔和画刷作为窗体的成员变量一次性创建在Paint事件中重复使用。只绘制脏区域我们的示例简单地在状态变化时调用this.Invalidate()重绘整个窗体。对于更大的游戏区域可以只重绘发生变化的那一小块区域使用Invalidate(Rectangle)方法。检查事件绑定确保OnStateChanged等事件没有重复绑定导致同一个方法被多次调用。可以在绑定前先解绑。5.4 游戏体验的打磨与扩展一个基础版本完成后可以考虑添加更多功能来提升可玩性和代码的健壮性下一个方块预览在GameEngine中再维护一个Tetromino _nextTetromino在固定当前方块后将_next赋给_current再生成一个新的_next。在UI上开辟一个小区域绘制_nextTetromino。阴影Ghost Piece在绘制当前方块时同时计算其“硬降”最终位置并在那里用半透明的颜色绘制一个轮廓方便玩家预判。暂停与继续我们已经实现了Pause()和Resume()方法只需要在UI上关联一个按钮或按键即可。游戏状态持久化保存最高分记录到本地文件或注册表。音效与动画在消除行、固定方块、游戏结束时播放简单的系统提示音。消除行时可以增加一个短暂的高亮动画。改为WPF或Unity实现用WinFormsGDI是实现原理学习的最佳选择。如果你想获得更炫酷的视觉效果或跨平台可以将核心逻辑GameBoard,Tetromino,GameEngine移植到WPF使用Canvas和DrawingVisual绘制或Unity使用GameObject和Sprite中UI层代码会变但核心算法几乎不用改。最后调试这类游戏程序最有效的工具就是“输出状态”。你可以在GameEngine的关键步骤如TryMove成功/失败、Lock、ClearLines中将当前的游戏板状态以文本形式输出到Debug.WriteLine或者绘制一个简单的字符界面到控制台。这能帮你直观地看到逻辑是否正确远比在图形界面里猜测要高效。