C#俄罗斯方块游戏开发:从核心逻辑到图形界面的完整实现 1. 项目概述与核心价值最近在整理硬盘时翻出了一个多年前用C#写的俄罗斯方块游戏源码。这个项目虽然不大但麻雀虽小五脏俱全它几乎涵盖了桌面应用开发、游戏逻辑设计、面向对象编程和事件驱动模型等核心知识点。对于正在学习C#尤其是想从“控制台程序”迈向“图形界面应用”的开发者来说这是一个绝佳的练手项目。它不像大型游戏引擎那样复杂但又足够让你理解一个完整可交互程序是如何从零搭建起来的。这个项目最吸引我的地方在于它的架构清晰度。它没有把所有代码都堆在一个窗体文件里而是采用了经典的分层设计将纯粹的游戏逻辑比如方块旋转、消行判断、分数计算封装在一个独立的类库中而将图形绘制和用户交互放在窗体应用程序里。这种设计模式在C#开发中被称为“关注点分离”它让代码的可读性、可维护性和可测试性都大大提升。无论你是想学习如何设计一个健壮的游戏核心还是想掌握Windows Forms或WPF的绘图与事件处理这个源码都能给你带来直接的启发。2. 架构设计与核心模块拆解2.1 分层架构为什么要把逻辑和界面分开很多新手在写第一个小游戏时习惯把所有代码都写在窗体的后台文件里。按钮点击事件里直接修改游戏状态Paint事件里直接根据状态画图。这样做虽然快但代码很快就会变得难以维护。想象一下如果你想把这个俄罗斯方块游戏从Windows Forms移植到WPF或者甚至做成一个控制台版本用字符画方块你会发现几乎要重写所有代码因为游戏逻辑和界面渲染死死地耦合在一起。这个源码采用的核心-界面分离架构就完美解决了这个问题。它创建了两个项目Tetris.Core (类库项目)这是游戏的心脏。它只关心游戏规则不关心任何图形。它定义了Block方块、GameBoard游戏板等核心类以及MoveLeft、Rotate、CheckLines等核心方法。这个DLL可以被任何类型的客户端WinForms, WPF, Console, 甚至Web API调用。Tetris.Windows (Windows窗体应用项目)这是游戏的脸和手脚。它负责创建窗口、处理键盘事件、定时触发游戏更新并调用Tetris.Core提供的方法来获取游戏状态最后用GDI将状态绘制到屏幕上。这种设计的优势显而易见高内聚、低耦合。核心逻辑的单元测试可以独立进行界面更换成本极低。这也是企业级应用开发中非常推崇的设计思想。2.2 核心数据模型如何用代码描述俄罗斯方块游戏的核心是数据。在Tetris.Core中我们需要用几个关键的类来构建整个游戏世界。2.2.1 Block类方块的灵魂每个下落的俄罗斯方块我们用一个Block类来表示。这个类需要包含哪些信息形状 (Shape)俄罗斯方块有7种基本形状I, J, L, O, S, T, Z。我们可以用一个二维布尔数组bool[,]来表示。例如T型方块在3x3网格中的表示可能是{{false, true, false}, {true, true, true}, {false, false, false}}其中true代表有方块单元。颜色 (Color)每种形状对应一种颜色用于界面绘制。位置 (Position)一个Point结构体表示当前方块左上角在游戏板网格中的坐标。这里的一个关键技巧是预定义所有形状。我们可以在一个静态类BlockShapes中预先定义好这7种形状的二维数组以及对应的颜色。当需要生成一个新方块时随机选择一个形状模板进行深拷贝并赋予其初始位置通常是板顶中部。2.2.2 GameBoard类游戏的舞台GameBoard类代表那个有固定宽度和高度的网格舞台。它内部维护一个二维数组grid用于记录每个格子当前的状态是空的还是已经被固定的某种颜色的方块。它的核心职责包括碰撞检测判断一个Block在某个位置是否与已固定的方块或边界发生碰撞。这是游戏逻辑中最关键的函数之一。固定方块当一个方块无法再下落时将其形状“烙印”到grid数组中。消行检测与计分遍历每一行如果该行所有格子都被填满则消除该行并将上面的所有行下移。消除的行数直接影响得分通常消除行数越多单次得分系数越高。游戏状态管理是否游戏结束新生成的方块无法放置、当前分数、等级随分数提高而增加用于控制下落速度等。2.3 游戏主循环与事件驱动游戏是如何“动”起来的这涉及到两个核心机制定时器和事件。在窗体项目中我们会使用一个Timer控件。将这个Timer的Interval间隔属性设置为一个值比如500毫秒。每隔这么多毫秒Timer的Tick事件就会触发。在这个事件的处理函数里我们执行一次“游戏帧更新”调用GameBoard的CurrentBlock.MoveDown()尝试让当前方块下落一格。如果发生碰撞则固定方块并检查消行。如果固定后游戏结束则停止定时器否则生成一个新的随机方块作为CurrentBlock。最后调用this.Invalidate()方法通知窗体需要重绘。与此同时我们需要监听窗体的KeyDown事件。当玩家按下方向键时左/右键调用CurrentBlock.MoveLeft()或MoveRight()并在移动前进行碰撞检测。上键调用CurrentBlock.Rotate()同样需要检测旋转后是否合法经典的“踢墙”算法就在这里体现即旋转后如果卡墙可以尝试向左/右微调一格。下键可以加速下落通常直接执行一次“硬降”或连续调用MoveDown()直到碰撞。这样定时器驱动自动下落键盘事件驱动玩家操控两者结合就构成了游戏的基本循环。3. 核心算法与难点实现详解3.1 旋转算法的实现与“踢墙”处理方块的旋转听起来简单实现起来却有几个坑。最直观的方法是对于一个以某个点为中心的方块形状二维数组应用一个90度的旋转变换矩阵。但俄罗斯方块的旋转并非绕形状的中心而是绕一个“旋转中心点”通常定义在形状数组的某个索引上比如(1,1)对于4x4的I型方块。3.1.1 基础旋转算法我们可以预先计算好每个形状的4个旋转状态0°, 90°, 180°, 270°存储起来。旋转时只需切换到下一个状态即可。这是最常用且高效的方法。在Block类中可以有一个RotationState属性0-3和一个Shapes数组来存储这4个状态下的二维布尔数组。旋转操作就是(RotationState 1) % 4。3.1.2 “踢墙” (Wall Kick) 机制这是实现中的一大难点。想象一下一个长条I型方块紧贴右侧墙壁你按下上键旋转。按照规则旋转后的形状可能会超出右边界。在纯数学旋转下这个操作是非法的游戏会禁止旋转这会让玩家感到非常别扭。“踢墙”机制就是为了解决这个问题。它的原理是当旋转后发生碰撞时系统不会直接拒绝而是尝试将方块向几个特定的方向左、右、下微移一个单位看看在这些新位置上旋转是否合法。如果合法就允许旋转并移动到这个位置。这个“尝试列表”是官方规则定义的对于不同形状、不同旋转状态尝试的偏移量顺序都不同。在代码中我们需要为每个形状的每个旋转状态变化定义一个ListPoint的“踢墙测试向量”。在旋转时先计算旋转后的形状然后进行碰撞检测。如果碰撞就遍历这个测试向量列表将旋转后的形状加上偏移量再次检测直到找到一个合法位置或者遍历完所有向量后仍不合法则旋转失败。3.2 消行与网格更新的高效算法当一行被填满时我们需要消除它并将上面的所有行下移。一个朴素的算法是遍历每一行如果满行则将该行以上的所有行逐行向下复制。这个算法是O(n²)的对于10x20的网格虽然没问题但不够优雅。一个更高效、更“程序员”的做法是使用双指针思想设置一个writeRow指针从最底行开始比如第19行。设置一个readRow指针也从最底行开始从下往上遍历。如果readRow指向的行是满行则跳过它readRow上移一行writeRow不动。如果readRow指向的行不是满行则将这一行的数据复制到writeRow指向的行。然后writeRow和readRow都上移一行。遍历结束后writeRow指针以上的所有行即0到writeRow-1行都应该被清空。这个过程只需要遍历网格一次O(n)就同时完成了消行和行下移的操作。在GameBoard的ClearLines方法中实现这个算法会让代码看起来非常清爽。3.3 随机方块生成与“7-Bag”算法如何保证游戏的公平性和可玩性如果你用纯粹的随机函数Random.Next(0,7)来生成下一个方块可能会连续出现5个S形方块让玩家直接崩溃。这种纯粹的随机在概率上称为“独立随机”体验很差。俄罗斯方块官方使用了一种叫做“7-Bag”随机生成器的算法。它的原理很简单准备一个“袋子”里面装有I, J, L, O, S, T, Z这7种形状各一个。随机打乱这个袋子里的7个方块。按顺序从袋子里取出方块提供给游戏。当袋子取空后重新装入7个方块再次打乱。这样就保证了在任意连续7个方块中每种形状都会出现且仅出现一次完全杜绝了长期缺失某种方块或连续出现同种方块的情况。在代码中我们可以用一个ListBlockType来表示这个袋子和一个int索引来记录取到了第几个。当索引7时重新初始化并打乱列表。4. 图形界面绘制与性能优化4.1 使用GDI进行双缓冲绘制在Windows Forms中绘图是在OnPaint方法或Paint事件中进行的。如果你直接在Paint事件中绘图在画面频繁更新时比如方块快速下落会出现严重的闪烁。这是因为窗体在擦除旧背景和绘制新内容之间屏幕会短暂地显示空白。双缓冲技术是解决闪烁的标准方案。其原理是在内存中创建一个与屏幕绘图区域一样的“画布”Bitmap先在这个内存画布上完成所有绘制操作然后再一次性将这个内存画布的内容绘制到屏幕控件上。这样屏幕更新从多次变为一次彻底消除了闪烁。在C# WinForms中实现双缓冲非常简单在窗体构造函数中设置this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);。在Paint事件处理程序中所有的绘图操作都针对传入的PaintEventArgs e中的Graphics对象进行系统会自动为你处理双缓冲逻辑。4.2 网格与方块的绘制细节绘制部分的核心是将抽象的游戏数据模型转换为屏幕上的像素。4.2.1 坐标转换游戏板GameBoard使用网格坐标列行例如(5, 10)。而绘图使用的是像素坐标X, Y。我们需要一个转换函数pixelX gridX * CELL_SIZE BOARD_OFFSET_XpixelY gridY * CELL_SIZE BOARD_OFFSET_Y其中CELL_SIZE是每个格子方块的像素大小如30pxOFFSET是游戏板在窗体上的起始位置。4.2.2 绘制当前下落方块和已固定方块绘制分为两部分已固定方块遍历GameBoard.grid数组对于每个非空单元格根据其存储的颜色值在对应像素位置绘制一个填充的矩形并通常加上一个高光边框和阴影来营造立体感。当前下落方块获取CurrentBlock的形状数组和位置遍历其中为true的单元格用当前方块的颜色绘制一个矩形。为了让下落方块更醒目可以绘制得稍微透明一些使用带Alpha值的颜色或者只绘制边框。4.2.3 绘制“幽灵方块”一个提升玩家体验的细节是绘制“幽灵方块”Ghost Piece即当前方块如果直接落到底部时的预览位置。这能帮助玩家更好地判断放置点。 实现方法从当前方块位置开始循环执行MoveDown()直到发生碰撞记录下这个最终位置。然后在绘制时在这个最终位置的每个单元格绘制一个只有空心边框或半透明填充的方块。4.3 游戏状态信息的显示除了主游戏区域我们还需要在窗体侧边或底部显示游戏状态信息分数 (Score)直接从GameBoard.Score属性读取。等级 (Level)根据分数区间计算得出等级决定了方块下落的速度Timer.Interval BaseInterval - Level * SpeedIncrease。下一个方块 (Next Block)绘制一个预览区域将GameBoard.NextBlock的形状绘制在那里。控制说明用Label控件简单标注按键功能。绘制这些信息时使用Graphics.DrawString方法并选择合适的字体和刷子Brush。为了美观可以为这些信息区域添加一个背景面板。5. 代码结构解析与关键类实现5.1 Tetris.Core 类库项目详解这个项目是游戏的大脑不引用任何图形界面相关的库如System.Windows.Forms。我们创建几个核心类。5.1.1 BlockType 枚举与 BlockShape 静态类首先定义一个枚举清晰表示7种方块类型。namespace Tetris.Core { public enum BlockType { I, J, L, O, S, T, Z } }然后创建一个静态类BlockShapes它包含一个只读字典将BlockType映射到其四个旋转状态下的形状数据和一个颜色。public static class BlockShapes { public static readonly IReadOnlyDictionaryBlockType, (Color Color, bool[][,] Rotations) Definitions; static BlockShapes() { var dict new DictionaryBlockType, (Color, bool[][,])(); // 以I型方块为例 bool[,] iRotation0 new bool[4, 4] { ... }; bool[,] iRotation1 new bool[4, 4] { ... }; // ... 定义所有旋转 dict[BlockType.I] (Color.Cyan, new bool[][,] { iRotation0, iRotation1, iRotation2, iRotation3 }); // ... 定义J, L, O, S, T, Z Definitions dict; } }5.1.2 Block 类的实现Block类需要封装方块的类型、当前旋转状态、位置并提供移动和旋转方法。public class Block { public BlockType Type { get; } public Point Position { get; private set; } // 网格坐标 public int RotationState { get; private set; } public Color Color BlockShapes.Definitions[Type].Color; public bool[,] Shape BlockShapes.Definitions[Type].Rotations[RotationState]; public Block(BlockType type, Point startPosition) { Type type; Position startPosition; RotationState 0; } public Block Clone() { return new Block(this.Type, this.Position) { RotationState this.RotationState }; } public void Move(int deltaX, int deltaY) { Position new Point(Position.X deltaX, Position.Y deltaY); } public void Rotate(bool clockwise) { RotationState (RotationState (clockwise ? 1 : 3)) % 4; // 顺时针或逆时针 } // 注意实际的旋转需要结合GameBoard进行碰撞检测和踢墙这里只是改变状态。 }5.1.3 GameBoard 类的实现这是最核心的类它管理游戏状态。public class GameBoard { private readonly int _width; private readonly int _height; private readonly Color?[,] _grid; // 存储每个格子固定的方块颜色null表示空 private readonly Random _random new Random(); private readonly ListBlockType _bag new ListBlockType(); private int _bagIndex 0; public Block CurrentBlock { get; private set; } public Block NextBlock { get; private set; } public int Score { get; private set; } public int Level Score / 1000 1; // 简单的等级计算 public bool IsGameOver { get; private set; } public GameBoard(int width, int height) { _width width; _height height; _grid new Color?[width, height]; InitializeBag(); NextBlock GenerateRandomBlock(); SpawnNewBlock(); } private void InitializeBag() { _bag.Clear(); _bag.AddRange((BlockType[])Enum.GetValues(typeof(BlockType))); ShuffleBag(); _bagIndex 0; } private void ShuffleBag() { /* 使用Fisher-Yates洗牌算法 */ } private Block GenerateRandomBlock() { if (_bagIndex _bag.Count) { InitializeBag(); } var type _bag[_bagIndex]; // 初始位置通常在顶部中间例如 (Width/2 - 2, 0)根据方块形状调整 return new Block(type, new Point(_width / 2 - 2, 0)); } public void SpawnNewBlock() { CurrentBlock NextBlock; NextBlock GenerateRandomBlock(); // 生成即碰撞游戏结束 if (CheckCollision(CurrentBlock)) { IsGameOver true; } } private bool CheckCollision(Block block) { // 遍历block.Shape中所有为true的单元格加上block.Position判断是否超出边界或与_grid冲突 // ... } public bool MoveCurrentBlock(int deltaX, int deltaY) { var testBlock CurrentBlock.Clone(); testBlock.Move(deltaX, deltaY); if (CheckCollision(testBlock)) return false; CurrentBlock.Move(deltaX, deltaY); return true; } public bool RotateCurrentBlock(bool clockwise) { var testBlock CurrentBlock.Clone(); testBlock.Rotate(clockwise); // 这里应加入踢墙检测逻辑 if (!CheckCollision(testBlock)) { CurrentBlock.Rotate(clockwise); return true; } // 踢墙逻辑尝试... return false; } public bool DropCurrentBlock() { if (MoveCurrentBlock(0, 1)) return true; // 无法下落固定方块 LockCurrentBlock(); ClearLines(); SpawnNewBlock(); return false; } private void LockCurrentBlock() { // 将CurrentBlock的形状“印”到_grid上 } private void ClearLines() { // 使用双指针算法消除满行并更新分数 int linesCleared 0; // ... 消行逻辑 Score CalculateScore(linesCleared, Level); } }5.2 Tetris.Windows 窗体项目详解这个项目引用Tetris.Core并负责创建用户界面。5.2.1 主窗体设计创建一个Windows窗体主要包含一个Panel控件作为游戏绘制区域pbGameBoard。几个Label控件用于显示分数、等级。另一个小的Panel或PictureBox用于预览下一个方块pbNextBlock。一个Timer控件gameTimer。一个Button用于开始/暂停游戏。在窗体构造函数中初始化GameBoard对象设置gameTimer的间隔并启用双缓冲。5.2.2 游戏循环与绘制gameTimer_Tick事件处理器是游戏的心脏。private void gameTimer_Tick(object sender, EventArgs e) { // 1. 游戏逻辑更新 _gameBoard.DropCurrentBlock(); // 尝试下落一格 if (_gameBoard.IsGameOver) { gameTimer.Stop(); MessageBox.Show(游戏结束); return; } // 2. 触发重绘 pbGameBoard.Invalidate(); pbNextBlock.Invalidate(); // 3. 更新UI显示 lblScore.Text _gameBoard.Score.ToString(); lblLevel.Text _gameBoard.Level.ToString(); }pbGameBoard_Paint事件处理器负责绘制。private void pbGameBoard_Paint(object sender, PaintEventArgs e) { var g e.Graphics; g.Clear(Color.Black); // 清空背景 // 1. 绘制已固定的方块网格 for (int x 0; x _gameBoard.Width; x) { for (int y 0; y _gameBoard.Height; y) { var cellColor _gameBoard.GetCell(x, y); // 需要在GameBoard中暴露此方法 if (cellColor.HasValue) { DrawBlockCell(g, x, y, cellColor.Value); } } } // 2. 绘制当前下落方块 var current _gameBoard.CurrentBlock; DrawBlock(g, current); // 3. 绘制网格线可选 // ... } private void DrawBlockCell(Graphics g, int gridX, int gridY, Color color) { int pixelX gridX * CELL_SIZE; int pixelY gridY * CELL_SIZE; using (var brush new SolidBrush(color)) { g.FillRectangle(brush, pixelX, pixelY, CELL_SIZE, CELL_SIZE); } // 绘制边框增加立体感 using (var pen new Pen(Color.White, 2)) { g.DrawRectangle(pen, pixelX, pixelY, CELL_SIZE, CELL_SIZE); } }5.2.3 键盘控制重写窗体的OnKeyDown方法或处理KeyDown事件。protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (_gameBoard.IsGameOver) return; switch (e.KeyCode) { case Keys.Left: _gameBoard.MoveCurrentBlock(-1, 0); break; case Keys.Right: _gameBoard.MoveCurrentBlock(1, 0); break; case Keys.Up: _gameBoard.RotateCurrentBlock(true); break; case Keys.Down: _gameBoard.DropCurrentBlock(); // 按一次下落一格 break; case Keys.Space: // 空格键硬降 while (_gameBoard.DropCurrentBlock()) { } break; } pbGameBoard.Invalidate(); // 操作后立即重绘 }6. 进阶功能扩展与优化思路一个基础版本完成后你可以尝试添加更多功能让游戏更完整、更专业。6.1 游戏状态的持久化保存/读取实现一个简单的存档功能。你需要序列化GameBoard的关键状态。可以定义一个GameState类包含Score,Level,Grid数据CurrentBlock,NextBlock等信息。使用System.Text.Json或Newtonsoft.Json库将其序列化为JSON字符串然后保存到文件。加载时反序列化并恢复游戏状态。注意Block对象可能包含对静态形状数据的引用序列化时需要特殊处理比如只保存BlockType和Position、RotationState。6.2 音效与背景音乐使用System.Media.SoundPlayer播放简单的.wav格式音效如移动、旋转、消行、游戏结束的声音。对于背景音乐可以考虑使用NAudio等更强大的第三方库来播放MP3等格式。将音效播放封装在GameBoard的相关方法中如LockCurrentBlock时播放锁定音效实现逻辑与表现的进一步分离。6.3 难度曲线与速度控制让游戏随等级提高而变难。最简单的方式是让gameTimer.Interval随着Level提高而减小。可以设计一个公式如Interval Math.Max(50, 500 - (Level * 30))保证有一个最小间隔如50ms防止速度过快。更精细的控制可以为每个等级单独设定下落速度、消行得分系数等。6.4 实现“暂停”与“重置”功能暂停功能很简单停止gameTimer即可。但要注意暂停时最好能有一个视觉提示如半透明覆盖层。重置功能需要重新初始化GameBoard对象并重置所有UI显示。6.5 代码重构与设计模式应用当功能越来越多时代码可能变得混乱。可以考虑应用一些设计模式观察者模式GameBoard作为被观察者Subject当分数、等级、网格数据发生变化时通知所有观察者如主窗体的UI控件。这样可以将状态更新逻辑解耦。状态模式将游戏的不同状态进行中、已暂停、已结束封装成独立的类避免在窗体代码中使用大量的if-else判断。策略模式将不同的方块生成算法如纯随机、7-Bag抽象成接口方便动态切换。7. 常见问题排查与调试技巧在开发过程中你肯定会遇到各种问题。这里记录几个典型问题及其解决方法。7.1 方块旋转时位置异常或穿透问题描述方块旋转后部分单元跑到了游戏板外面或者穿过了已固定的方块。排查步骤检查旋转中心确认你的形状数组定义是否正确旋转是否是绕着你预期的中心点进行的。打印出旋转前后的形状数组用文本或图形可视化对比。验证碰撞检测在RotateCurrentBlock方法中在调用CheckCollision前后打印出测试方块的位置和形状信息。确保碰撞检测函数遍历了形状数组中的所有true单元格。实现并检查“踢墙”逻辑如果没有实现踢墙紧贴边界的旋转必然失败。如果实现了但仍有问题检查你的“踢墙测试向量”列表是否正确以及尝试的顺序是否符合官方规则。7.2 游戏画面闪烁严重问题描述游戏运行时方块和网格闪烁不停视觉体验极差。解决方案确保双缓冲已开启如前所述在窗体构造函数中设置双缓冲样式是最根本的。避免在Paint事件外绘图所有绘图代码都应放在Paint事件处理程序或重写的OnPaint方法中。不要在Timer_Tick或KeyDown事件里直接调用Graphics绘图。使用Invalidate()而非Refresh()Invalidate()标记控件区域为需要重绘由系统在合适的时机触发Paint事件。Refresh()会强制立即同步重绘可能引起闪烁。通常使用Invalidate()即可。减少不必要的重绘区域可以使用Invalidate(Rectangle)只重绘发生变化的区域但这在俄罗斯方块中优化收益不大因为变化区域通常占比较大。7.3 键盘响应不灵敏或重复触发问题描述按下方向键没反应或者按一次键方块连续移动好几格。排查步骤检查窗体KeyPreview属性确保窗体KeyPreview属性设置为true这样窗体才能先于内部控件接收到键盘事件。处理KeyDown而非KeyPressKeyDown事件更适合游戏控制它能区分左右Shift、Ctrl等键且响应更直接。处理重复触发Windows系统在按住一个键时会先触发一次KeyDown稍作停顿后开始快速连续触发。这可能导致玩家只想移动一格结果移动了多格。解决方法有两种使用键盘状态轮询在Timer_Tick事件中检查Control.ModifierKeys或使用[DllImport(user32.dll)]导入GetAsyncKeyState函数来查询某个键的当前物理状态。根据状态持续更新而不是依赖事件。在KeyDown事件中处理但在KeyUp事件中重置一个标志设置一个bool isKeyHeld标志在KeyDown时如果标志为false则执行动作并设标志为true在KeyUp时重置标志为false。这样可以确保一次按键只触发一次动作但需要为每个动作键维护状态稍显复杂。对于俄罗斯方块第一种方法在定时器里轮询更常见。7.4 游戏逻辑与绘制不同步问题描述有时看到方块已经移动但绘制的位置还是老的或者反过来。解决方案确保数据更新在重绘之前在Timer_Tick中必须先调用_gameBoard.Update()或类似方法更新所有游戏逻辑状态然后再调用Invalidate()。绘制只依赖数据状态在Paint事件中所有的绘制代码都应基于_gameBoard的当前状态如CurrentBlock.Position,_grid内容。绝对不要在绘制时再次计算或修改游戏状态。考虑线程安全虽然WinForms的Timer默认在UI线程触发但如果你使用了其他线程如后台线程计算AI在更新游戏状态和调用Invalidate()时必须通过Control.Invoke或BeginInvoke方法切换到UI线程否则会导致跨线程访问控件错误和状态不一致。7.5 性能问题排查对于俄罗斯方块性能通常不是问题。但如果发现游戏在低配置电脑上卡顿检查定时器间隔Timer.Interval不要设置得过小如小于50ms这会给UI线程带来不必要的负担。优化绘制代码避免在Paint事件中创建大量Pen,Brush,Font等GDI对象。应在窗体加载时创建并复用它们或在using语句中创建确保销毁。对于静态背景如网格线可以绘制到一个Bitmap上缓存起来每次重绘时直接绘制这个位图而不是重新画线。使用性能分析工具Visual Studio自带的性能分析器Performance Profiler可以帮助你找到代码中的热点消耗CPU最多的函数。