ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C#游戏开发实战:从零构建百变方块游戏,掌握核心算法与架构设计

C#游戏开发实战:从零构建百变方块游戏,掌握核心算法与架构设计

1. 项目概述:从零到一,用C#打造你的第一款方块游戏

如果你对C#编程感兴趣,并且一直想亲手做一个能玩、能分享的完整项目,那么这个“百变方块游戏”实战项目可能就是为你量身定做的。这不仅仅是一个简单的“俄罗斯方块”克隆,而是一个融合了经典玩法、自定义方块形态、积分系统和UI交互的完整游戏开发过程。对于初学者来说,它能让你系统性地掌握C#在游戏开发中的核心应用,从变量、类、方法到事件、委托和数据结构;对于有一定基础的开发者,它则是一个绝佳的框架,你可以基于此扩展出更复杂的游戏逻辑、特效,甚至联网对战功能。整个项目我们将使用最纯粹的C#和Windows Forms(或WPF)来完成,不依赖Unity等重型引擎,目的是让你透彻理解游戏循环、碰撞检测、图形绘制这些底层原理,而不是被引擎的封装所迷惑。当你亲手看到自己编写的代码让一个个方块落下、旋转、消行,那种成就感是看多少教程都无法比拟的。接下来,我们就从最核心的设计思路开始,一步步拆解这个百变方块游戏的构建过程。

2. 游戏核心设计与架构拆解

在动手写第一行代码之前,我们必须把游戏的骨架搭好。一个看似简单的方块游戏,其内部逻辑环环相扣,设计上的疏忽会导致后期代码难以维护甚至推倒重来。

2.1 核心需求与功能模块解析

百变方块游戏的核心玩法虽然经典,但“百变”二字意味着我们需要在基础之上增加灵活性和扩展性。经过分析,我们可以将整个游戏拆解为以下几个核心模块:

  1. 游戏核心逻辑模块:这是游戏的大脑。它负责维护游戏的状态(进行中、暂停、结束),管理当前正在下落的方块(我们称之为“活动方块”)以及已经落到底部静止的方块(我们称之为“地图”或“网格”)。它还需要驱动游戏的主循环,每隔一定时间就让活动方块自动下落一格。
  2. 方块数据与行为模块:这是游戏的血肉。我们需要定义所有不同形状的方块(如长条、方块、L形、Z形等)。每个方块需要包含其自身的形态数据(一个二维数组或坐标列表来表示其占据的格子)、颜色以及最关键的行为:旋转和移动。考虑到“百变”,我们的设计应该易于添加新的方块类型。
  3. 用户输入处理模块:这是游戏的神经。玩家通过键盘(通常是方向键和空格键)控制方块的移动、旋转和加速下落。这个模块需要实时监听键盘事件,并将操作指令安全、及时地传递给核心逻辑模块。
  4. 渲染与UI模块:这是游戏的脸面。它负责将核心逻辑模块中的抽象数据(如网格状态、方块位置)绘制到屏幕上,形成玩家看到的彩色方块。同时,它还需要显示游戏分数、等级、下一个预览方块等UI信息。
  5. 碰撞检测与消行逻辑模块:这是游戏的规则裁判。它需要判断活动方块的下一个移动或旋转位置是否合法(是否超出边界或与已静止的方块重叠)。当一行被填满时,它需要触发消行逻辑,并计算得分。

这五个模块相互协作,构成了游戏的基本运行框架。一个好的架构应该让它们之间的依赖关系清晰、耦合度低。例如,UI模块只负责“显示”,它不应该包含游戏规则逻辑;输入模块只负责“传达指令”,而不决定指令是否有效。

2.2 技术选型:为什么选择Windows Forms/WPF与纯C#

看到“游戏开发”,很多人第一反应是Unity。但对于学习C#语言特性和理解游戏底层原理而言,从轻量级的框架开始更为合适。

  • 选择Windows Forms或WPF的原因:它们都是.NET框架内成熟的桌面应用开发技术,能快速创建窗口、控件和处理用户输入。对于我们的2D方块游戏,其自带的绘图(Graphics)功能和定时器(Timer)控件完全够用。Windows Forms更简单直接,上手快;WPF则更现代,支持更丰富的UI和数据绑定,但学习曲线稍陡。本项目为了聚焦于游戏逻辑,建议初学者从Windows Forms开始。
  • 摒弃Unity等引擎的考量:Unity固然强大,但它封装了太多细节(如游戏循环、物理更新)。对于一个教学性质的、旨在深入理解原理的项目,使用Unity就像开着自动挡学车,虽然快,但你可能永远不知道离合器是如何工作的。用纯C#和基础框架,你需要自己实现游戏循环(用Timer模拟)、自己处理每一帧的绘制、自己写碰撞检测算法。这个过程能极大地锻炼你的编程思维和解决问题的能力。
  • “百变”的扩展性基础:基于这种轻量级架构,你可以轻松地修改方块数据源(比如从配置文件读取新的方块形状)、改变游戏规则(比如增加特殊道具方块)、甚至替换渲染方式(比如用更酷炫的图形库重绘)。这种灵活性是项目被称为“百变”的底气所在。

3. 核心模块的详细实现与编码实战

理论说得再多,不如一行代码。我们现在就进入具体的实现环节。我会以Windows Forms为例,但核心逻辑完全适用于WPF。

3.1 游戏世界的基石:网格(Grid)与方块(Block)类的设计

首先,我们需要定义游戏进行的主战场——网格。

public class GameGrid { // 网格的宽和高(以方块为单位) public int Rows { get; } public int Columns { get; } // 用一个二维数组表示网格状态,0表示空,大于0的数字表示不同颜色的方块 private readonly int[,] grid; public GameGrid(int rows, int columns) { Rows = rows; Columns = columns; grid = new int[rows, columns]; } // 索引器,方便访问和设置特定位置的值 public int this[int row, int column] { get => grid[row, column]; set => grid[row, column] = value; } // 判断一个位置是否在网格内 public bool IsInside(int row, int column) { return row >= 0 && row < Rows && column >= 0 && column < Columns; } // 判断一个位置是否为空 public bool IsEmpty(int row, int column) { return IsInside(row, column) && grid[row, column] == 0; } // 判断一整行是否被填满 public bool IsRowFull(int row) { for (int col = 0; col < Columns; col++) { if (IsEmpty(row, col)) { return false; } } return true; } // 判断一整行是否完全为空(用于优化) public bool IsRowEmpty(int row) { for (int col = 0; col < Columns; col++) { if (!IsEmpty(row, col)) { return false; } } return true; } // 清除某一行的数据,并将其上方的所有行下移 private void ClearRow(int row) { for (int col = 0; col < Columns; col++) { grid[row, col] = 0; } } private void MoveRowDown(int row, int numRows) { for (int col = 0; col < Columns; col++) { grid[row + numRows, col] = grid[row, col]; grid[row, col] = 0; } } // 清除所有已填满的行,并返回清除的行数(用于计分) public int ClearFullRows() { int cleared = 0; // 从底部开始向上检查,这样下移行时不会影响未检查的行 for (int row = Rows - 1; row >= 0; row--) { if (IsRowFull(row)) { ClearRow(row); cleared++; } else if (cleared > 0) { // 如果上面已经清除了行,当前行需要下移 MoveRowDown(row, cleared); } } return cleared; } }

注意:这里使用int来表示格子状态是一个简单有效的选择。0代表空,1-7可以代表7种经典方块的颜色。你也可以设计一个Cell类来存储更丰富的信息,但对于第一个版本,int数组在性能和简洁性上是最优的。

接下来是方块(Block)类,它是“百变”的核心。

public abstract class Block { // 方块的形态由其在4x4局部网格中的位置决定。 // Tiles数组存储了方块所占格子的相对坐标。 protected abstract Position[][] Tiles { get; } // 方块的起始偏移位置,确保新方块出现在网格顶部中央 protected abstract Position StartOffset { get; } // 方块的颜色标识,对应GameGrid中的数值 public abstract int Id { get; } // 当前旋转状态(0-3) private int rotationState; // 当前方块在游戏网格中的偏移位置 private Position offset; public Block() { offset = new Position(StartOffset.Row, StartOffset.Column); } // 获取当前旋转状态下,方块所有格子在游戏网格中的实际位置 public IEnumerable<Position> TilePositions() { foreach (Position p in Tiles[rotationState]) { yield return new Position(p.Row + offset.Row, p.Column + offset.Column); } } // 顺时针旋转方块 public void RotateCW() { rotationState = (rotationState + 1) % Tiles.Length; } // 逆时针旋转方块 public void RotateCCW() { if (rotationState == 0) rotationState = Tiles.Length - 1; else rotationState--; } // 移动方块 public void Move(int rows, int columns) { offset.Row += rows; offset.Column += columns; } // 重置方块状态(用于生成新方块或预览) public void Reset() { rotationState = 0; offset.Row = StartOffset.Row; offset.Column = StartOffset.Column; } } // 一个简单的坐标结构体 public struct Position { public int Row { get; set; } public int Column { get; set; } public Position(int row, int column) { Row = row; Column = column; } }

有了抽象的Block类,我们就可以轻松创建具体的方块类型。例如,最经典的I型方块(长条):

public class IBlock : Block { // 定义I型方块的四种旋转形态 protected override Position[][] Tiles => new Position[][] { new Position[] { new(1,0), new(1,1), new(1,2), new(1,3) }, new Position[] { new(0,2), new(1,2), new(2,2), new(3,2) }, new Position[] { new(2,0), new(2,1), new(2,2), new(2,3) }, new Position[] { new(0,1), new(1,1), new(2,1), new(3,1) } }; // 起始位置稍微靠上,给旋转留出空间 protected override Position StartOffset => new Position(-1, 3); public override int Id => 1; // 对应颜色1 }

实操心得:定义方块形态的坐标时,我强烈建议你在纸上画一个4x4的格子,标出每个旋转状态下方块的位置。这样能直观地检查坐标是否正确,避免运行时出现奇怪的旋转bug。StartOffsetRow为-1,是为了让长条方块生成时,有一部分在网格顶部“之上”,这样旋转时不会卡在顶部边界。

3.2 游戏状态机与主循环的实现

游戏状态(Game State)是控制游戏流程的关键。我们定义一个GameState类来统领全局。

public class GameState { public GameGrid Grid { get; } public Block CurrentBlock { get; private set; } public Block NextBlock { get; private set; } public bool GameOver { get; private set; } public int Score { get; private set; } // 方块池,用于随机生成方块 private readonly Block[] blocks; private readonly Random random = new Random(); public GameState() { Grid = new GameGrid(22, 10); // 常见规格,22行(顶部2行隐藏),10列 // 初始化方块池 blocks = new Block[] { new IBlock(), new JBlock(), new LBlock(), new OBlock(), new SBlock(), new TBlock(), new ZBlock() }; // 生成当前和下一个方块 NextBlock = RandomBlock(); CurrentBlock = RandomBlock(); // 将当前方块放入网格(逻辑上),并检查是否游戏结束(新方块无法放置) if (!BlockFits()) { GameOver = true; } } private Block RandomBlock() { // 从方块池中随机选择一个,并重置其状态 Block block = blocks[random.Next(blocks.Length)]; block.Reset(); return block; } // 核心判断:当前方块的所有格子是否都在网格内且为空 private bool BlockFits() { foreach (Position p in CurrentBlock.TilePositions()) { if (!Grid.IsEmpty(p.Row, p.Column)) { return false; } } return true; } // 旋转当前方块,如果旋转后位置非法则回滚 public void RotateBlockCW() { CurrentBlock.RotateCW(); if (!BlockFits()) { CurrentBlock.RotateCCW(); // 回滚 } } public void RotateBlockCCW() { CurrentBlock.RotateCCW(); if (!BlockFits()) { CurrentBlock.RotateCW(); } } // 移动当前方块,如果移动后位置非法则回滚 public void MoveBlockLeft() { CurrentBlock.Move(0, -1); if (!BlockFits()) { CurrentBlock.Move(0, 1); } } public void MoveBlockRight() { CurrentBlock.Move(0, 1); if (!BlockFits()) { CurrentBlock.Move(0, -1); } } public void MoveBlockDown() { CurrentBlock.Move(1, 0); if (!BlockFits()) { CurrentBlock.Move(-1, 0); // 回滚,意味着方块触底 PlaceBlock(); // 放置方块 } } // 判断方块是否已经“锁定”(即无法再向下移动) private bool IsBlockLocked() { // 尝试向下移动一格 CurrentBlock.Move(1, 0); bool canMoveDown = BlockFits(); CurrentBlock.Move(-1, 0); // 移回原位 return !canMoveDown; // 如果不能向下移动,则已锁定 } // 放置方块:将当前方块的格子写入网格,并生成新方块 private void PlaceBlock() { foreach (Position p in CurrentBlock.TilePositions()) { Grid[p.Row, p.Column] = CurrentBlock.Id; } // 检查并清除满行 int linesCleared = Grid.ClearFullRows(); // 计分规则:一次消1行得100分,2行得300分,3行得500分,4行得800分(经典计分) Score += linesCleared switch { 1 => 100, 2 => 300, 3 => 500, 4 => 800, _ => 0 }; // 生成新方块 CurrentBlock = NextBlock; NextBlock = RandomBlock(); // 检查新方块是否能放下,不能则游戏结束 if (!BlockFits()) { GameOver = true; } } // 硬降落:直接将方块移动到底部 public void DropBlock() { while (!IsBlockLocked()) { CurrentBlock.Move(1, 0); } PlaceBlock(); } }

主循环的驱动,我们使用Windows Forms的Timer控件。在窗体的设计器中拖入一个Timer,设置Interval属性(如300毫秒控制下落速度),并在Tick事件中调用GameState.MoveBlockDown()。键盘事件则通过重写窗体的OnKeyDown方法处理。

public partial class MainForm : Form { private GameState gameState; private System.Windows.Forms.Timer gameTimer; public MainForm() { InitializeComponent(); gameState = new GameState(); gameTimer = new System.Windows.Forms.Timer(); gameTimer.Interval = 300; // 下落间隔,可随等级加快 gameTimer.Tick += GameTimer_Tick; gameTimer.Start(); this.DoubleBuffered = true; // 启用双缓冲,减少绘制闪烁 } private void GameTimer_Tick(object sender, EventArgs e) { if (!gameState.GameOver) { gameState.MoveBlockDown(); this.Invalidate(); // 触发重绘 } else { gameTimer.Stop(); MessageBox.Show("游戏结束!最终得分:" + gameState.Score); } } protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (gameState.GameOver) return; switch (e.KeyCode) { case Keys.Left: gameState.MoveBlockLeft(); break; case Keys.Right: gameState.MoveBlockRight(); break; case Keys.Down: gameState.MoveBlockDown(); break; case Keys.Up: gameState.RotateBlockCW(); break; case Keys.Z: gameState.RotateBlockCCW(); break; case Keys.Space: gameState.DropBlock(); break; case Keys.P: gameTimer.Enabled = !gameTimer.Enabled; // 暂停/继续 break; } this.Invalidate(); // 每次操作后重绘 } private void MainForm_Paint(object sender, PaintEventArgs e) { // 在这里调用绘制方法,将gameState中的网格和方块画出来 DrawGrid(e.Graphics); DrawBlock(e.Graphics, gameState.CurrentBlock); DrawNextBlock(e.Graphics, gameState.NextBlock); DrawScore(e.Graphics, gameState.Score); } }

3.3 图形渲染与用户界面绘制

绘制部分是让游戏“活”起来的关键。我们需要将GameGrid中的数字和Block的位置转换为屏幕上的彩色矩形。

private void DrawGrid(Graphics g) { // 定义每个方块的大小(像素) int cellSize = 30; // 定义网格绘制的起始位置 int offsetX = 20; int offsetY = 20; // 绘制已固定的方块 for (int r = 0; r < gameState.Grid.Rows; r++) { for (int c = 0; c < gameState.Grid.Columns; c++) { int cellValue = gameState.Grid[r, c]; if (cellValue > 0) { // 根据方块ID选择颜色 Brush brush = GetBrushByBlockId(cellValue); // 绘制填充矩形 g.FillRectangle(brush, offsetX + c * cellSize, offsetY + r * cellSize, cellSize, cellSize); // 绘制边框 g.DrawRectangle(Pens.Black, offsetX + c * cellSize, offsetY + r * cellSize, cellSize, cellSize); } else { // 绘制空白格子的边框 g.DrawRectangle(Pens.LightGray, offsetX + c * cellSize, offsetY + r * cellSize, cellSize, cellSize); } } } } private void DrawBlock(Graphics g, Block block) { int cellSize = 30; int offsetX = 20; int offsetY = 20; Brush brush = GetBrushByBlockId(block.Id); foreach (Position p in block.TilePositions()) { // 只绘制在可视网格内的部分(顶部隐藏行不绘制) if (p.Row >= 0) { g.FillRectangle(brush, offsetX + p.Column * cellSize, offsetY + p.Row * cellSize, cellSize, cellSize); g.DrawRectangle(Pens.Black, offsetX + p.Column * cellSize, offsetY + p.Row * cellSize, cellSize, cellSize); } } } private Brush GetBrushByBlockId(int id) { // 定义经典俄罗斯方块的七种颜色 return id switch { 1 => Brushes.Cyan, // I 2 => Brushes.Blue, // J 3 => Brushes.Orange, // L 4 => Brushes.Yellow, // O 5 => Brushes.Green, // S 6 => Brushes.Purple, // T 7 => Brushes.Red, // Z _ => Brushes.White }; } private void DrawNextBlock(Graphics g, Block nextBlock) { // 在游戏区域右侧绘制下一个预览方块 int previewX = 20 + 10 * 30 + 50; // 主网格右侧 int previewY = 20; int previewCellSize = 20; // 预览方块可以小一点 // 为了预览美观,通常将预览方块绘制在其局部坐标的中心 // 这里简化处理,直接绘制 Brush brush = GetBrushByBlockId(nextBlock.Id); // 注意:预览时需要使用方块的原始形态(即旋转状态0),并计算一个合适的偏移使其居中显示 // 这里需要根据nextBlock.Tiles[0]来计算包围盒并居中,代码略复杂,此处省略详细计算,仅示意 g.DrawString("下一个:", this.Font, Brushes.Black, previewX, previewY); // ... 具体绘制nextBlock.Tiles[0]中格子的代码 ... } private void DrawScore(Graphics g, int score) { // 在合适位置绘制分数 g.DrawString($"分数:{score}", new Font("Arial", 14), Brushes.Black, 20, 20 + 22 * 30 + 20); }

注意事项:在DrawBlock时,我们判断if (p.Row >= 0),这是因为我们的游戏网格有22行,但顶部2行可能是“隐藏”的,用于生成新方块。绘制时跳过这些行,可以让玩家看不到顶部边界外的方块部分,体验更好。另外,启用窗体的DoubleBuffered属性至关重要,它能有效解决绘制时的画面闪烁问题。

4. 项目进阶与“百变”扩展思路

完成基础版本后,这个项目就成为了一个绝佳的试验场。你可以从以下几个方向进行扩展,真正体现“百变”的魅力。

4.1 游戏性增强:等级、速度与音效

一个完整的游戏离不开难度曲线和感官反馈。

  • 动态速度:将gameTimer.Interval与游戏等级(Level)或已消除行数(Lines)挂钩。例如,每消除10行,等级提升,下落间隔减少一定毫秒数。
    private int level = 1; private int linesClearedTotal = 0; // 在PlaceBlock方法中更新 linesClearedTotal += linesCleared; level = 1 + linesClearedTotal / 10; // 每10行升一级 gameTimer.Interval = Math.Max(50, 300 - (level - 1) * 25); // 速度有下限
  • 音效系统:使用System.Media.SoundPlayer播放简单的WAV音效。在方块移动、旋转、锁定、消行、游戏结束时触发对应的音效文件,能极大提升游戏体验。
  • 积分系统多样化:除了基础的消行得分,可以加入“连击”(Combo)奖励,即连续多次消行有额外加分。还可以加入“背靠背”(Back-to-Back)奖励,即连续使用硬降落或连续消除多行。

4.2 架构优化:实现真正的“百变”方块

要让方块“百变”,关键在于将方块数据与游戏逻辑解耦。我们可以将方块的形状定义(Tiles数组)和颜色(Id)放到外部配置文件中(如JSON、XML)。

  1. 定义数据模型
    [ { "Name": "IBlock", "Id": 1, "Color": "#00FFFF", "Tiles": [ [[1,0],[1,1],[1,2],[1,3]], [[0,2],[1,2],[2,2],[3,2]], [[2,0],[2,1],[2,2],[2,3]], [[0,1],[1,1],[2,1],[3,1]] ] }, // ... 其他方块定义 ]
  2. 创建方块工厂:修改GameState,不再硬编码blocks数组,而是从配置文件加载,并动态创建方块对象。这样,你甚至可以在游戏运行时,让玩家自己设计并导入新的方块形状,实现真正的“百变”。

4.3 性能与体验调优

  • 输入延迟与重复:Windows Forms的KeyDown事件在按键按住时,会先触发一次,然后有短暂停顿,再开始快速重复触发。这对于需要快速左右移动的方块游戏体验不佳。可以考虑使用GetAsyncKeyStateAPI或更精细的输入管理来获得更即时的响应。
  • 绘制优化:目前我们每次Invalidate()都会重绘整个游戏区域。可以尝试只重绘发生变化的部分(脏矩形),但对于这个规模的项目,全屏重绘在性能上完全可接受。更重要的优化是确保所有BrushPen对象被复用,而不是在每次绘制时创建。
  • 游戏状态保存/加载:实现一个简单的存档功能,将GameGrid的数组、当前分数、等级、下一个方块序列化保存到文件。这涉及到对象序列化(如使用System.Text.Json)的知识。

5. 开发中常见问题与调试技巧实录

即使设计得再完美,编码过程中也一定会遇到各种问题。下面是我在开发类似项目时踩过的一些坑和解决方法。

5.1 方块旋转与边界“卡住”问题

问题描述:方块在靠近左右边界或底部时旋转,可能会有一部分格子旋转到网格外部,导致旋转失败或被错误地判定为碰撞。

根本原因:我们的旋转算法只考虑了方块自身的形态变化,没有实施“踢墙”(Wall Kick)机制。这是官方俄罗斯方块中的一个重要规则:当方块旋转后与墙壁或其他方块重叠时,系统会尝试将方块向左、右或上轻微移动一格,如果移动后能放下,则旋转成功。

解决方案:为每个方块类型定义一套“踢墙”偏移数据。在RotateBlockCW等方法中,旋转后如果BlockFits()返回false,则遍历预定义的偏移量,尝试将方块移动相应的位置,直到找到一个合法位置为止。I型方块的踢墙表通常与其他方块不同,需要单独定义。

5.2 游戏循环与绘制不同步导致的“鬼影”

问题描述:方块看起来有残影,或者移动不跟手。

排查思路

  1. 检查双缓冲:确保窗体DoubleBuffered属性已设置为true
  2. 检查绘制时机:确保只在游戏状态发生改变(如方块移动、旋转、下落)或定时器触发时调用this.Invalidate()。避免在无关事件中频繁触发重绘。
  3. 检查绘制逻辑:在Paint事件中,确保每次都是先清除背景(e.Graphics.Clear(this.BackColor)),再绘制所有元素。绘制顺序应为:背景 -> 网格 -> 已固定方块 -> 活动方块 -> UI文字。
  4. 检查计时器精度System.Windows.Forms.Timer精度较低,且其Tick事件是在UI线程上执行的。如果Tick事件处理函数(即MoveBlockDown和重绘)耗时过长,会导致游戏卡顿。如果对精度要求高,可以考虑使用System.Threading.TimerSystem.Diagnostics.Stopwatch配合多线程,但要注意跨线程访问UI控件的问题。

5.3 内存泄漏与资源管理

问题描述:游戏运行一段时间后,内存占用缓慢增长。

常见原因

  1. 事件未注销:如果动态创建了Timer或订阅了其他事件,在窗体关闭时务必取消订阅(timer.Tick -= GameTimer_Tick)。
  2. GDI+对象未释放:如果你创建了自定义的BrushPenFont,务必在使用后调用.Dispose()方法,或者使用using语句块。使用系统预定义的BrushesPens则无需担心,它们是共享资源。
  3. 大型对象长期持有:检查是否有全局集合(如List)在不断添加数据而从未清理。在我们的游戏中,GameGrid的大小是固定的,一般不会有问题。

5.4 方块随机生成的不公平性

问题描述:使用简单的Random.Next()可能导致连续多次出现同一种方块,或者某种方块长期不出现,影响游戏体验。

解决方案:使用“7-Bag”随机生成器。这是现代俄罗斯方块游戏的标配算法。它预先准备好一个包含所有7种方块各一个的“袋子”,然后随机打乱这个袋子,按顺序从袋子里取出方块。当一个袋子取空后,再生成下一个新的、打乱的袋子。这样可以保证在短期内(7个方块内)每种方块都会出现一次,长期来看分布均匀,避免了极端情况。

private List<Block> blockBag = new List<Block>(); private void RefillBag() { blockBag.Clear(); // 假设allBlockTypes是包含所有方块类型的列表 var shuffled = allBlockTypes.OrderBy(x => random.Next()).ToList(); blockBag.AddRange(shuffled); } public Block GetNextBlockFromBag() { if (blockBag.Count == 0) RefillBag(); Block block = blockBag[0]; blockBag.RemoveAt(0); block.Reset(); return block; }

走到这里,你已经完成了一个功能完整、代码结构清晰的C#百变方块游戏。从最基础的数据结构设计,到游戏循环和输入处理,再到图形绘制和高级特性扩展,这个项目几乎涵盖了小型桌面游戏开发的所有核心知识点。最重要的是,你拥有了一个完全由自己掌控的代码库,你可以随心所欲地修改规则、添加特效、改变外观,甚至将其作为学习更复杂游戏架构的起点。编程的乐趣,就在于将想法通过代码变为现实,并看着它按照你的规则运行起来。希望这个实战项目能成为你C#游戏开发之路上一块坚实的基石。

返回列表