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项目:

  1. 点击启动页的“创建新项目”,进入模板选择界面。在搜索框输入“WPF”,筛选出“WPF应用(.NET Framework)”模板(注意区分“WPF应用(.NET)”,后者是较新的.NET Core版本模板),选中后点击“下一步”。
  2. 进入“配置新项目”界面,设置项目名称为“TetrisGame”(避免使用中文或特殊符号,防止路径错误),选择项目保存路径(建议放在非系统盘,如“D:\C#Projects\”),解决方案名称可默认与项目名称一致,勾选“将解决方案和项目放在同一目录中”(方便后续管理文件)。
  3. 在“框架”下拉菜单中选择“.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 核心功能测试

  1. 游戏启动测试:点击“开始游戏”按钮,检查是否能正常生成第一个方块,定时器是否启动(方块是否自动下落)。
  2. 键盘控制测试:按左、右方向键,检查方块是否能正常左右移动,是否会“穿墙”(碰撞检测是否生效);按上方向键,检查方块是否能正常旋转,旋转后是否会碰撞;按下方向键,检查方块是否能快速下落。
  3. 行消除测试:故意将方块堆成完整的行,检查是否能正常消除,消除后上方的行是否会向下移动,分数是否正确增加。
  4. 等级提升测试:通过消除行积累分数,当分数达到500、1000等整数倍时,检查等级是否提升,方块下落速度是否加快。
  5. 游戏结束测试:让方块堆到网格顶部,检查是否会弹出游戏结束提示,开始按钮是否重新启用。

4.2 常见问题修复

在测试过程中,可能会遇到以下问题,需针对性修复:

  • 方块旋转后位置异常:若旋转后方块超出边界,可能是旋转矩阵计算错误,需重新检查RotateShape()方法中的转置公式(确保rotatedShape[j, currentShape.GetLength(0) - 1 - i] = currentShape[i, j])。
  • 分数计算错误:若消除行后分数未正确增加,需检查ClearCompletedLines()中的得分公式(确保currentScore += linesCleared * ScorePerLine * currentLevel)。
  • 定时器重复启动:若多次点击“开始游戏”按钮导致定时器重复启动(方块下落速度变快),需在StartButton_Click中先停止定时器(gameTimer.Stop()),再重新启动。

4.3 体验优化建议

除了核心功能,还可以通过以下优化提升游戏体验:

  1. 添加音效:在方块下落、行消除、游戏结束时添加音效,增强沉浸感。可使用WPF的MediaPlayer类播放音频文件(如.wav格式)。
  2. 预览下一个方块:在右侧信息区添加“下一个方块”预览区,让玩家提前规划放置位置,提升游戏策略性。
  3. 暂停功能:添加暂停按钮,点击后停止定时器,再次点击恢复,方便玩家中途休息。
  4. 最高分记录:使用Settings或本地文件(如.txt)保存最高分,每次游戏结束后更新并显示,增加玩家的挑战性。
  5. 自定义皮肤:允许玩家选择方块颜色、画布背景色等,提升个性化体验。

五、总结与扩展:从俄罗斯方块到更多WPF小游戏

通过本文的步骤,我们成功用C# WPF实现了一款功能完整的俄罗斯方块游戏,涵盖了项目搭建、界面设计、核心逻辑编码、测试优化等全流程。在这个过程中,我们掌握了WPF的关键技术:

  • XAML布局:Grid、StackPanel等布局控件的使用,以及控件样式的自定义。
  • 后台逻辑:DispatcherTimer定时器、键盘事件处理、二维数组操作(网格与方块)。
  • 图形绘制:通过Canvas和Rectangle控件动态绘制游戏元素,实现UI更新。

这款俄罗斯方块的代码结构清晰,具有良好的扩展性。基于此框架,你可以尝试开发更多WPF小游戏,如:

  • 五子棋:用Grid布局棋盘,鼠标点击落子,实现胜负判定逻辑。
  • 贪吃蛇:类似俄罗斯方块的网格逻辑,控制蛇的移动、吃食物、碰撞检测。
  • 纸牌游戏:利用WPF的Image控件显示纸牌,实现发牌、出牌等交互逻辑。

WPF的学习需要理论与实践结合,多动手编写小游戏不仅能巩固知识点,还能培
养解决问题的能力。希望本文能成为你WPF游戏开发的起点,开启更多有趣的开发之旅!

本文标签: 俄罗斯方块 逻辑 核心 完整 指南