admin 管理员组文章数量: 1184232
从零搭建C# WPF俄罗斯方块:完整开发指南与核心逻辑解析
在桌面应用开发领域,C# WPF凭借其强大的UI渲染能力和灵活的XAML布局语法,成为制作交互类小游戏的理想选择。俄罗斯方块作为经典益智游戏,不仅承载着无数人的童年回忆,其核心的方块移动、碰撞检测、行消除等逻辑,更是学习游戏开发的绝佳案例。本文将带您从零开始,一步步用C# WPF实现一款功能完整的俄罗斯方块游戏,涵盖项目搭建、界面设计、核心逻辑编码、功能优化等全流程,即使是WPF新手也能跟随操作,最终掌握小游戏开发的关键技术。
一、开发环境准备与项目创建
在开始编码前,我们需要先搭建合适的开发环境,并完成项目的基础配置。合适的环境配置能避免后续开发中出现兼容性问题,确保代码顺利运行。
1.1 开发工具与框架选择
本次开发选用Visual Studio 2022作为开发工具,这款IDE对.NET框架有着完善的支持,内置了WPF项目模板、可视化设计器和调试工具,能极大提升开发效率。框架方面,选择**.NET Framework 4.7.2**,该版本兼顾了稳定性和兼容性,既支持WPF的核心特性,又能在大多数Windows系统上顺畅运行,无需担心用户因框架版本问题无法启动游戏。
为什么不选择更新的.NET 6或.NET 7?一方面,对于简单的桌面小游戏,.NET Framework 4.7.2的功能已完全够用,且无需用户额外安装.NET运行时(多数Windows系统默认预装);另一方面,Visual Studio 2022对.NET Framework项目的模板支持更成熟,新手更容易上手。
1.2 项目创建步骤详解
打开Visual Studio 2022后,按照以下步骤创建WPF项目:
- 点击启动页的“创建新项目”,进入模板选择界面。在搜索框输入“WPF”,筛选出“WPF应用(.NET Framework)”模板(注意区分“WPF应用(.NET)”,后者是较新的.NET Core版本模板),选中后点击“下一步”。
- 进入“配置新项目”界面,设置项目名称为“TetrisGame”(避免使用中文或特殊符号,防止路径错误),选择项目保存路径(建议放在非系统盘,如“D:\C#Projects\”),解决方案名称可默认与项目名称一致,勾选“将解决方案和项目放在同一目录中”(方便后续管理文件)。
- 在“框架”下拉菜单中选择“.NET Framework 4.7.2”,确认所有配置无误后点击“创建”。等待Visual Studio自动生成项目结构,生成完成后,在“解决方案资源管理器”中可看到项目包含的核心文件:App.config(配置文件)、App.xaml(应用程序入口配置)、MainWindow.xaml(主窗口界面文件)、MainWindow.xaml.cs(主窗口逻辑代码文件)。
1.3 项目结构解析
生成的项目结构虽简单,但每个文件都有明确的作用,理解其功能有助于后续开发:
- App.xaml:用于配置应用程序的全局属性,如启动窗口(默认指定MainWindow.xaml为启动窗口)、资源字典(可定义全局样式、颜色等)。本次开发中,我们无需修改此文件,保持默认配置即可。
- MainWindow.xaml:采用XAML语法描述主窗口的UI布局,包括窗口大小、背景色、控件位置等。所有可视化元素(如游戏画布、分数显示、按钮)都将在此文件中定义。
- MainWindow.xaml.cs:MainWindow.xaml的后台代码文件,采用C#编写,负责处理UI交互逻辑(如按钮点击、键盘输入)、游戏核心逻辑(方块生成、移动、碰撞检测)等,是整个游戏的“大脑”。
- Properties文件夹:包含项目的属性配置,如程序集信息(软件名称、版本号)、引用的程序集等,无需手动修改。
二、游戏界面设计:用XAML构建可视化交互区
WPF的核心优势之一是“界面与逻辑分离”,通过XAML可以直观地设计UI,无需编写大量C#代码。本次俄罗斯方块的界面主要分为三个部分:游戏画布(用于显示方块和网格)、信息显示区(分数、等级)、控制按钮(开始游戏),下面将详细讲解每个部分的设计思路和代码实现。
2.1 界面设计原则与布局选择
俄罗斯方块的界面需要简洁、清晰,避免过多元素干扰玩家操作。考虑到界面元素的布局需求,我们选择Grid(网格布局) 作为根容器,Grid可以将窗口划分为不同的区域,方便控制各个元素的位置。在Grid中,我们将左侧区域用于放置游戏画布(Canvas控件),右侧区域用StackPanel(栈式布局)垂直排列分数、等级显示文本和开始按钮——StackPanel适合需要垂直或水平排列的元素,无需手动计算每个控件的位置,简化布局代码。
另外,需要注意窗口的尺寸设计:游戏画布的大小由网格尺寸决定(我们设计的网格为20行、10列,每个格子30px,因此画布宽度为10×30=300px,高度为20×30=600px);窗口整体宽度设为600px(左侧300px画布+右侧200px信息区+100px边距),高度设为700px(600px画布+100px顶部边距),确保界面在大多数显示器上显示完整,且比例协调。
2.2 XAML界面代码实现
打开MainWindow.xaml文件,删除默认生成的代码,替换为以下完整界面代码,代码中已添加详细注释,方便理解每个控件的作用:
<Window x:Class="TetrisGame.MainWindow"
xmlns="http://schemas.microsoft/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft/winfx/2006/xaml"
Title="WPF俄罗斯方块" <!-- 窗口标题 -->
Height="700" Width="600" <!-- 窗口大小 -->
ResizeMode="NoResize" <!-- 禁止窗口缩放(避免画布变形) -->
WindowStartupLocation="CenterScreen"> <!-- 窗口启动时居中显示 -->
<!-- 根容器:Grid布局,将窗口分为左右两部分 -->
<Grid Background="#F5F5F5"> <!-- 窗口背景色设为浅灰色,提升视觉体验 -->
<!-- 左侧:游戏画布,用于绘制方块和网格 -->
<Canvas x:Name="GameCanvas" <!-- 命名为GameCanvas,方便后台代码调用 -->
Background="Black" <!-- 画布背景为黑色,突出方块颜色 -->
Width="300" Height="600" <!-- 画布大小:20行×10列,每格30px -->
Margin="50,50,0,50" <!-- 边距:上50px、左50px、下50px -->
SnapsToDevicePixels="True"> <!-- 启用像素对齐,避免方块边缘模糊 -->
</Canvas>
<!-- 右侧:信息显示与控制区,用StackPanel垂直排列 -->
<StackPanel Orientation="Vertical" <!-- 垂直排列子元素 -->
HorizontalAlignment="Right" <!-- 右对齐 -->
Width="150" <!-- 宽度150px,避免过宽 -->
Margin="0,50,50,0"> <!-- 边距:上50px、右50px -->
<!-- 分数显示区 -->
<TextBlock Text="当前分数:" <!-- 文本内容 -->
Foreground="#2C3E50" <!-- 文本颜色:深蓝色 -->
FontSize="22" <!-- 字体大小 -->
FontWeight="Bold" <!-- 字体加粗 -->
Margin="0,0,0,5"/> <!-- 下 margin 5px,与分数值区分 -->
<TextBlock x:Name="ScoreText" <!-- 命名为ScoreText,后台代码更新分数 -->
Text="0" <!-- 默认分数为0 -->
Foreground="#E74C3C" <!-- 文本颜色:红色,突出分数 -->
FontSize="24" <!-- 字体大小比标签大,更醒目 -->
FontWeight="Bold"
Margin="0,0,0,20"/> <!-- 下 margin 20px,与等级区区分 -->
<!-- 等级显示区 -->
<TextBlock Text="当前等级:"
Foreground="#2C3E50"
FontSize="22"
FontWeight="Bold"
Margin="0,0,0,5"/>
<TextBlock x:Name="LevelText" <!-- 命名为LevelText,后台代码更新等级 -->
Text="1" <!-- 默认等级为1 -->
Foreground="#E74C3C"
FontSize="24"
FontWeight="Bold"
Margin="0,0,0,40"/> <!-- 下 margin 40px,与按钮区分 -->
<!-- 开始游戏按钮 -->
<Button x:Name="StartButton" <!-- 命名为StartButton,后台绑定点击事件 -->
Content="开始游戏" <!-- 按钮文本 -->
Background="#3498DB" <!-- 按钮背景色:天蓝色 -->
Foreground="White" <!-- 按钮文本颜色:白色 -->
FontSize="20" <!-- 字体大小 -->
FontWeight="Bold"
Height="50" <!-- 按钮高度 -->
BorderThickness="0" <!-- 取消边框 -->
CornerRadius="5" <!-- 圆角设计,提升美观度 -->
Cursor="Hand" <!-- 鼠标悬浮时显示手型,提示可点击 -->
Click="StartButton_Click"> <!-- 绑定点击事件:StartButton_Click -->
<!-- 按钮hover效果:背景色变深 -->
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2980B9"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</Grid>
</Window>
2.3 界面设计亮点与优化
上述XAML代码不仅实现了基础的界面布局,还加入了多处细节优化,提升用户体验:
- 禁止窗口缩放:通过
ResizeMode="NoResize"避免玩家缩放窗口导致游戏画布变形,保证网格和方块的比例始终一致。 - 像素对齐:
SnapsToDevicePixels="True"确保方块边缘清晰,避免WPF默认抗锯齿导致的模糊问题,提升游戏视觉效果。 - 交互反馈:按钮添加了hover效果(鼠标悬浮时背景色变深),并设置手型光标,让玩家明确知道“这是可点击的按钮”,增强交互感。
- 色彩搭配:采用“浅灰窗口背景+黑色画布+深蓝标签+红色分数”的配色方案,既避免了视觉疲劳,又让关键信息(分数、等级)突出,符合游戏界面的设计逻辑。
三、游戏核心逻辑编码:用C#实现方块的“生命旅程”
界面设计完成后,接下来是游戏的核心——用C#编写方块生成、移动、碰撞检测、行消除等逻辑。这部分代码将全部放在MainWindow.xaml.cs中,我们将按照“数据定义→初始化→核心功能→交互处理”的顺序逐步实现,确保逻辑清晰、易于理解。
3.1 核心数据定义:用变量描述游戏状态
在编写功能前,首先需要定义一系列变量,用于存储游戏的核心数据(如网格状态、当前方块、分数等级等)。这些变量是连接界面和逻辑的桥梁,也是后续所有功能的基础。在MainWindow类中添加以下私有变量:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace TetrisGame
{
public partial class MainWindow : Window
{
#region 核心数据定义
// 1. 网格配置
private const int GridSize = 30; // 每个格子的像素大小(与画布尺寸对应)
private const int Rows = 20; // 网格行数
private const int Cols = 10; // 网格列数
private int[,] gameGrid; // 二维数组:存储网格状态,1表示有方块,0表示空
// 2. 定时器:控制方块自动下落
private DispatcherTimer gameTimer; // WPF专用定时器,用于UI线程更新
private const int InitialInterval = 500; // 初始下落间隔(毫秒):500ms/次
// 3. 游戏状态:分数、等级
private int currentScore; // 当前分数
private int currentLevel; // 当前等级
private const int ScorePerLine = 100; // 每消除一行的分数
private const int LevelUpScore = 500; // 升级所需分数(每得500分升1级)
// 4. 方块数据:形状、颜色、位置
// 7种方块形状(I、O、T、L、J、S、Z),用二维数组表示,1表示方块格子
private int[][,] shapes = new int[7][,]
{
// I型:4×4网格,垂直排列
new int[4,4] { {0,0,1,0}, {0,0,1,0}, {0,0,1,0}, {0,0,1,0} },
// O型:3×3网格,2×2方块
new int[3,3] { {0,0,0}, {0,1,1}, {0,1,1} },
// T型:3×3网格,中间一行全满,中间下方有一个
new int[3,3] { {0,0,0}, {1,1,1}, {0,1,0} },
// L型:3×3网格,中间一行全满,左下角有一个
new int[3,3] { {0,0,0}, {1,1,1}, {1,0,0} },
// J型:3×3网格,中间一行全满,右下角有一个
new int[3,3] { {0,0,0}, {1,1,1}, {0,0,1} },
// S型:3×3网格,右上两个+左下两个
new int[3,3] { {0,0,0}, {0,1,1}, {1,1,0} },
// Z型:3×3网格,左上两个+右下两个
new int[3,3] { {0,0,0}, {1,1,0}, {0,1,1} }
};
// 方块颜色:与7种形状一一对应,颜色鲜艳,易于区分
private Brush[] shapeColors = new Brush[]
{
Brushes.Cyan, // I型:青色
Brushes.Yellow, // O型:黄色
Brushes.Purple, // T型:紫色
Brushes.Orange, // L型:橙色
Brushes.Blue, // J型:蓝色
Brushes.Green, // S型:绿色
Brushes.Red // Z型:红色
};
// 当前方块状态
private int[,] currentShape; // 当前下落的方块形状
private int currentShapeIndex; // 当前方块的索引(用于匹配颜色)
private int currentX; // 当前方块的X坐标(左上角在网格中的列号)
private int currentY; // 当前方块的Y坐标(左上角在网格中的行号)
#endregion
// 构造函数:窗口初始化时调用
public MainWindow()
{
InitializeComponent(); // 初始化UI(必须调用,由WPF自动生成)
InitGame(); // 初始化游戏数据
}
// 后续功能代码将在这里添加...
}
}
3.2 游戏初始化:为开始游戏做准备
InitGame()方法用于初始化游戏的基础状态,包括定时器配置、分数等级重置等,在窗口构造函数中调用,确保游戏启动时处于“就绪”状态。实现代码如下:
/// <summary>
/// 初始化游戏数据
/// </summary>
private void InitGame()
{
// 1. 初始化网格:20行×10列,全部设为0(空)
gameGrid = new int[Rows, Cols];
// 2. 初始化分数和等级
currentScore = 0;
currentLevel = 1;
ScoreText.Text = currentScore.ToString(); // 更新UI上的分数显示
LevelText.Text = currentLevel.ToString(); // 更新UI上的等级显示
// 3. 初始化定时器:控制方块自动下落
gameTimer = new DispatcherTimer();
gameTimer.Tick += GameLoop; // 定时器触发时调用GameLoop方法
gameTimer.Interval = TimeSpan.FromMilliseconds(InitialInterval); // 初始间隔500ms
// 注意:此时定时器未启动,需点击“开始游戏”按钮后启动
}
3.3 开始游戏:重置状态并生成第一个方块
当玩家点击“开始游戏”按钮时,需要重置游戏状态(如清空之前的网格、重置分数),并生成第一个下落的方块,同时启动定时器。实现StartButton_Click事件处理方法:
/// <summary>
/// 开始游戏按钮点击事件
/// </summary>
private void StartButton_Click(object sender, RoutedEventArgs e)
{
// 1. 重置游戏状态(防止重复点击导致的状态混乱)
gameGrid = new int[Rows, Cols]; // 清空网格
currentScore = 0; // 重置分数
currentLevel = 1; // 重置等级
ScoreText.Text = currentScore.ToString(); // 更新UI
LevelText.Text = currentLevel.ToString();
GameCanvas.Children.Clear(); // 清空画布上的所有元素(避免残留之前的方块)
// 2. 重置定时器间隔(如果之前升级过,恢复到初始速度)
gameTimer.Interval = TimeSpan.FromMilliseconds(InitialInterval);
// 3. 生成第一个方块
GenerateNewShape();
// 4. 启动定时器:方块开始自动下落
gameTimer.Start();
// 5. 禁用按钮(避免游戏中重复点击)
StartButton.IsEnabled = false;
}
3.4 方块生成:随机选择形状并设置初始位置
GenerateNewShape()方法用于随机选择一种方块形状,并设置其初始位置(默认在网格顶部中间,X坐标为4,Y坐标为0,确保方块居中下落)。同时,该方法还会检查新生成的方块是否与已有方块重叠(若重叠,说明游戏结束)。实现代码如下:
/// <summary>
/// 生成新的方块
/// </summary>
private void GenerateNewShape()
{
// 1. 随机选择一种方块(0-6,共7种)
Random random = new Random();
currentShapeIndex = random.Next(0, 7); // 记录当前方块的索引(用于匹配颜色)
currentShape = shapes[currentShapeIndex]; // 获取对应的形状
// 2. 设置初始位置:X=4(列号),Y=0(行号),确保方块在网格顶部中间
currentX = Cols / 2 - currentShape.GetLength(1) / 2; // 水平居中:总列数/2 - 方块列数/2
currentY = 0; // 从顶部开始下落
// 3. 检查新方块是否与已有方块重叠(若重叠,游戏结束)
if (CheckCollision())
{
gameTimer.Stop(); // 停止定时器
MessageBox.Show($"游戏结束!最终分数:{currentScore}\n最终等级:{currentLevel}",
"游戏结束",
MessageBoxButton.OK,
MessageBoxImage.Information); // 弹出游戏结束提示
StartButton.IsEnabled = true; // 重新启用开始按钮,允许玩家重新开始
return;
}
// 4. 绘制新生成的方块
DrawCurrentShape();
}
3.5 碰撞检测:确保方块不“穿墙”、不“叠穿”
碰撞检测是游戏的核心逻辑之一,用于判断方块移动或旋转后是否超出网格边界,或与已固定的方块重叠。CheckCollision()方法通过遍历当前方块的每个格子,检查其在网格中的位置是否合法,返回true表示碰撞,false表示无碰撞。实现代码如下:
/// <summary>
/// 碰撞检测:判断当前方块是否与边界或已固定的方块碰撞
/// </summary>
/// <returns>true=碰撞,false=无碰撞</returns>
private bool CheckCollision()
{
// 遍历当前方块的每一行(i:行索引)
for (int i = 0; i < currentShape.GetLength(0); i++)
{
// 遍历当前方块的每一列(j:列索引)
for (int j = 0; j < currentShape.GetLength(1); j++)
{
// 只检查方块的有效格子(值为1的格子)
if (currentShape[i, j] == 1)
{
// 计算该格子在游戏网格中的实际坐标
int gridRow = currentY + i; // 实际行号 = 当前方块Y坐标 + 方块内 row 索引
int gridCol = currentX + j; // 实际列号 = 当前方块X坐标 + 方块内 column 索引
// 检查1:是否超出网格下边界(行号 >= 总行数)
if (gridRow >= Rows)
return true;
// 检查2:是否超出网格左右边界(列号 < 0 或 >= 总列数)
if (gridCol < 0 || gridCol >= Cols)
return true;
// 检查3:是否与已固定的方块重叠(行号 >=0 且网格中该位置为1)
// 注意:gridRow可能为负数(方块刚开始下落时在网格顶部外),需排除
if (gridRow >= 0 && gameGrid[gridRow, gridCol] == 1)
return true;
}
}
}
// 所有格子都合法,无碰撞
return false;
}
3.6 方块绘制:在画布上显示方块与网格
DrawCurrentShape()方法用于在Canvas控件上绘制当前下落的方块和已固定的方块(网格)。WPF中,我们通过创建Rectangle(矩形)控件来表示每个方块格子,并设置其位置、大小和颜色,然后添加到Canvas的Children集合中。实现代码如下:
/// <summary>
/// 绘制当前方块和已固定的网格
/// </summary>
private void DrawCurrentShape()
{
// 1. 清空画布:避免残留之前的方块图像
GameCanvas.Children.Clear();
// 2. 绘制已固定的方块(网格中值为1的位置)
for (int i = 0; i < Rows; i++)
{
for (int j = 0; j < Cols; j++)
{
if (gameGrid[i, j] == 1)
{
// 创建矩形:表示已固定的方块格子
Rectangle fixedRect = new Rectangle
{
Width = GridSize - 2, // 宽度:格子大小 - 2px(留1px间隙,避免粘连)
Height = GridSize - 2,
Fill = Brushes.Gray, // 已固定的方块用灰色表示,与当前方块区分
Stroke = Brushes.DarkGray, // 边框颜色:深灰色
StrokeThickness = 1 // 边框厚度:1px
};
// 设置矩形在Canvas中的位置:左上角坐标 = 列号×格子大小 + 1px(与间隙对应)
Canvas.SetLeft(fixedRect, j * GridSize + 1);
Canvas.SetTop(fixedRect, i * GridSize + 1);
// 将矩形添加到画布
GameCanvas.Children.Add(fixedRect);
}
}
}
// 3. 绘制当前下落的方块
for (int i = 0; i < currentShape.GetLength(0); i++)
{
for (int j = 0; j < currentShape.GetLength(1); j++)
{
if (currentShape[i, j] == 1)
{
// 创建矩形:表示当前方块的格子
Rectangle shapeRect = new Rectangle
{
Width = GridSize - 2,
Height = GridSize - 2,
Fill = shapeColors[currentShapeIndex], // 使用当前方块对应的颜色
Stroke = Brushes.White, // 边框颜色:白色,更醒目
StrokeThickness = 1
};
// 计算矩形在Canvas中的位置:当前方块坐标 + 方块内索引
int canvasX = (currentX + j) * GridSize + 1;
int canvasY = (currentY + i) * GridSize + 1;
Canvas.SetLeft(shapeRect, canvasX);
Canvas.SetTop(shapeRect, canvasY);
GameCanvas.Children.Add(shapeRect);
}
}
}
}
3.7 方块移动:响应键盘输入与自动下落
方块的移动分为两种:一是玩家通过键盘控制的左右移动和快速下落,二是定时器控制的自动下落。下面分别实现这两种移动逻辑。
3.7.1 自动下落:定时器触发的GameLoop
GameLoop()方法是定时器的触发事件处理方法,每隔固定时间(如500ms)调用一次,让方块向下移动一格。实现代码如下:
/// <summary>
/// 游戏主循环:定时器触发,控制方块自动下落
/// </summary>
private void GameLoop(object sender, EventArgs e)
{
MoveDown(); // 方块向下移动一格
}
3.7.2 向下移动:MoveDown()方法
MoveDown()方法用于让方块向下移动一格,若移动后发生碰撞,则将当前方块固定到网格中,然后检查是否有可消除的行,最后生成新的方块。实现代码如下:
/// <summary>
/// 方块向下移动一格
/// </summary>
private void MoveDown()
{
currentY++; // Y坐标+1,向下移动一格
// 检查移动后是否碰撞
if (CheckCollision())
{
currentY--; // 碰撞,回退到上一位置
FixShapeToGrid(); // 将当前方块固定到网格中
ClearCompletedLines(); // 检查并消除完整的行
GenerateNewShape(); // 生成新的方块
}
// 重新绘制方块(更新UI)
DrawCurrentShape();
}
3.7.3 左右移动:通过键盘事件处理
玩家通过左、右方向键控制方块左右移动,我们需要重写WPF窗口的OnKeyDown方法,监听键盘输入。实现代码如下:
/// <summary>
/// 监听键盘输入:控制方块左右移动、旋转、快速下落
/// </summary>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
// 若定时器未启动(游戏未开始),不响应键盘输入
if (!gameTimer.IsEnabled)
return;
// 根据按下的键执行不同操作
switch (e.Key)
{
case Key.Left: // 左方向键:向左移动
currentX--;
if (CheckCollision()) // 碰撞则回退
currentX++;
break;
case Key.Right: // 右方向键:向右移动
currentX++;
if (CheckCollision())
currentX--;
break;
case Key.Down: // 下方向键:快速下落(一次移动一格)
MoveDown();
break;
case Key.Up: // 上方向键:旋转方块
RotateShape();
break;
}
// 重新绘制方块(更新UI)
DrawCurrentShape();
}
3.8 方块旋转:实现形状的90度旋转
方块旋转是俄罗斯方块的核心玩法之一,我们通过矩阵转置的方式实现方块的90度顺时针旋转。旋转后需要检查是否碰撞,若碰撞则取消旋转(回退到原形状)。实现RotateShape()方法:
/// <summary>
/// 方块旋转:90度顺时针旋转
/// </summary>
private void RotateShape()
{
// 1. 保存当前形状(用于旋转后碰撞时回退)
int[,] originalShape = currentShape;
// 2. 计算旋转后形状的尺寸:原行数变为列数,原列数变为行数
int rotatedRows = currentShape.GetLength(1);
int rotatedCols = currentShape.GetLength(0);
int[,] rotatedShape = new int[rotatedRows, rotatedCols];
// 3. 矩阵转置:实现90度顺时针旋转
// 公式:rotatedShape[j, 原行数-1 -i] = 原形状[i,j]
for (int i = 0; i < currentShape.GetLength(0); i++)
{
for (int j = 0; j < currentShape.GetLength(1); j++)
{
rotatedShape[j, currentShape.GetLength(0) - 1 - i] = currentShape[i, j];
}
}
// 4. 应用旋转后的形状
currentShape = rotatedShape;
// 5. 检查旋转后是否碰撞,若碰撞则回退到原形状
if (CheckCollision())
{
currentShape = originalShape;
}
}
3.9 行消除与分数等级更新:实现游戏难度递进
当某一行的所有格子都被方块填满时,需要消除该行,并将上方的行向下移动一格,同时根据消除的行数增加分数,分数达到一定值后提升等级,并加快方块下落速度(减少定时器间隔)。实现FixShapeToGrid()和ClearCompletedLines()方法:
3.9.1 固定方块到网格:FixShapeToGrid()
当方块无法继续下落时(碰撞),需要将其固定到游戏网格中(即把网格中对应位置设为1),以便后续绘制和碰撞检测。实现代码如下:
/// <summary>
/// 将当前方块固定到游戏网格中
/// </summary>
private void FixShapeToGrid()
{
for (int i = 0; i < currentShape.GetLength(0); i++)
{
for (int j = 0; j < currentShape.GetLength(1); j++)
{
if (currentShape[i, j] == 1)
{
// 计算方块格子在网格中的实际位置
int gridRow = currentY + i;
int gridCol = currentX + j;
// 确保位置合法(避免超出网格顶部)
if (gridRow >= 0 && gridCol >= 0 && gridCol < Cols)
{
gameGrid[gridRow, gridCol] = 1; // 设为1,表示有固定方块
}
}
}
}
}
3.9.2 消除完整行与更新分数等级:ClearCompletedLines()
该方法遍历所有行,检查是否有完整的行(所有列都为1),若有则消除该行,并计算消除的行数,更新分数和等级,同时调整定时器间隔(提升难度)。实现代码如下:
/// <summary>
/// 检查并消除完整的行,更新分数和等级
/// </summary>
private void ClearCompletedLines()
{
int linesCleared = 0; // 记录消除的行数
// 从最下方的行向上遍历(避免行移动后判断错误)
for (int i = Rows - 1; i >= 0; i--)
{
bool isLineComplete = true; // 标记当前行是否完整
// 检查当前行的所有列是否都为1
for (int j = 0; j < Cols; j++)
{
if (gameGrid[i, j] == 0)
{
isLineComplete = false;
break; // 有空格,该行不完整,跳出循环
}
}
// 若当前行完整,消除该行
if (isLineComplete)
{
linesCleared++; // 消除行数+1
// 将当前行上方的所有行向下移动一格
for (int k = i; k > 0; k--)
{
for (int j = 0; j < Cols; j++)
{
gameGrid[k, j] = gameGrid[k - 1, j]; // 下一行 = 上一行
}
}
// 将最顶部的行设为0(空行)
for (int j = 0; j < Cols; j++)
{
gameGrid[0, j] = 0;
}
// 由于当前行已被上方行覆盖,需要重新检查当前行(i不变)
i++;
}
}
// 若消除了行,更新分数和等级
if (linesCleared > 0)
{
// 计算得分:消除行数 × 每行分数 × 当前等级(等级越高,得分越高)
currentScore += linesCleared * ScorePerLine * currentLevel;
ScoreText.Text = currentScore.ToString(); // 更新UI分数显示
// 计算新等级:每得500分升1级(整数除法)
int newLevel = currentScore / LevelUpScore + 1;
if (newLevel > currentLevel)
{
currentLevel = newLevel;
LevelText.Text = currentLevel.ToString(); // 更新UI等级显示
// 提升难度:加快方块下落速度(间隔减少50ms,最低100ms)
int newInterval = InitialInterval - (currentLevel - 1) * 50;
gameTimer.Interval = TimeSpan.FromMilliseconds(Math.Max(newInterval, 100));
}
}
}
四、功能测试与优化:让游戏更稳定、更好玩
代码编写完成后,需要进行充分的测试,修复潜在问题,并进行细节优化,确保游戏体验流畅。以下是常见的测试点和优化方向:
4.1 核心功能测试
- 游戏启动测试:点击“开始游戏”按钮,检查是否能正常生成第一个方块,定时器是否启动(方块是否自动下落)。
- 键盘控制测试:按左、右方向键,检查方块是否能正常左右移动,是否会“穿墙”(碰撞检测是否生效);按上方向键,检查方块是否能正常旋转,旋转后是否会碰撞;按下方向键,检查方块是否能快速下落。
- 行消除测试:故意将方块堆成完整的行,检查是否能正常消除,消除后上方的行是否会向下移动,分数是否正确增加。
- 等级提升测试:通过消除行积累分数,当分数达到500、1000等整数倍时,检查等级是否提升,方块下落速度是否加快。
- 游戏结束测试:让方块堆到网格顶部,检查是否会弹出游戏结束提示,开始按钮是否重新启用。
4.2 常见问题修复
在测试过程中,可能会遇到以下问题,需针对性修复:
- 方块旋转后位置异常:若旋转后方块超出边界,可能是旋转矩阵计算错误,需重新检查
RotateShape()方法中的转置公式(确保rotatedShape[j, currentShape.GetLength(0) - 1 - i] = currentShape[i, j])。 - 分数计算错误:若消除行后分数未正确增加,需检查
ClearCompletedLines()中的得分公式(确保currentScore += linesCleared * ScorePerLine * currentLevel)。 - 定时器重复启动:若多次点击“开始游戏”按钮导致定时器重复启动(方块下落速度变快),需在
StartButton_Click中先停止定时器(gameTimer.Stop()),再重新启动。
4.3 体验优化建议
除了核心功能,还可以通过以下优化提升游戏体验:
- 添加音效:在方块下落、行消除、游戏结束时添加音效,增强沉浸感。可使用WPF的
MediaPlayer类播放音频文件(如.wav格式)。 - 预览下一个方块:在右侧信息区添加“下一个方块”预览区,让玩家提前规划放置位置,提升游戏策略性。
- 暂停功能:添加暂停按钮,点击后停止定时器,再次点击恢复,方便玩家中途休息。
- 最高分记录:使用
Settings或本地文件(如.txt)保存最高分,每次游戏结束后更新并显示,增加玩家的挑战性。 - 自定义皮肤:允许玩家选择方块颜色、画布背景色等,提升个性化体验。
五、总结与扩展:从俄罗斯方块到更多WPF小游戏
通过本文的步骤,我们成功用C# WPF实现了一款功能完整的俄罗斯方块游戏,涵盖了项目搭建、界面设计、核心逻辑编码、测试优化等全流程。在这个过程中,我们掌握了WPF的关键技术:
- XAML布局:Grid、StackPanel等布局控件的使用,以及控件样式的自定义。
- 后台逻辑:DispatcherTimer定时器、键盘事件处理、二维数组操作(网格与方块)。
- 图形绘制:通过Canvas和Rectangle控件动态绘制游戏元素,实现UI更新。
这款俄罗斯方块的代码结构清晰,具有良好的扩展性。基于此框架,你可以尝试开发更多WPF小游戏,如:
- 五子棋:用Grid布局棋盘,鼠标点击落子,实现胜负判定逻辑。
- 贪吃蛇:类似俄罗斯方块的网格逻辑,控制蛇的移动、吃食物、碰撞检测。
- 纸牌游戏:利用WPF的
Image控件显示纸牌,实现发牌、出牌等交互逻辑。
WPF的学习需要理论与实践结合,多动手编写小游戏不仅能巩固知识点,还能培
养解决问题的能力。希望本文能成为你WPF游戏开发的起点,开启更多有趣的开发之旅!
版权声明:本文标题:从零搭建 C# WPF 俄罗斯方块:完整开发指南与核心逻辑解析 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1766219207a3445094.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论