65.9K
CodeProject 正在变化。 阅读更多。
Home

砖块球游戏:WPF C# 游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (12投票s)

2012年10月4日

CPOL

8分钟阅读

viewsIcon

105142

downloadIcon

5245

这不是一个简单的砖块球游戏,我们需要同时集中注意力在两个球上。

引言

这款游戏是一款砖块球游戏,需要使用两个球来打破顶部的砖块。 在这个游戏中,您需要用手指操作两个底部的砖块(红色和蓝色)。 如果游戏球碰到底部区域,游戏将结束。 您应该尽力完成所有关卡。 本游戏将为您提供中间文件,以便您可以创建任意数量的自定义游戏关卡。 您可以通过创建复杂的游戏关卡来挑战您的朋友。

关键信息

右箭头:蓝色砖块向右移动

左箭头:蓝色砖块向左移动

S 键:红色砖块向左移动

F 键:红色砖块向右移动

触摸屏

您可以使用金色板来控制红色/蓝色滑块。 您可以用手指在该区域滑动来移动滑块。 第一次触摸将被视为红色砖块的移动,第二次触摸被视为蓝色砖块的移动。 您需要使用两根手指来移动砖块。 只用一根手指,您只能移动红色滑块。 您可以切换滑块位置,如果先用右手手指触摸,左手手指(控制红色砖块)就可以移动蓝色滑块。

让我们看看屏幕

我创建了七个不同的关卡来玩这个游戏。

如您在上面的屏幕截图中所见。 按 F5 键游戏将开始。 您可以按 F1 获取有关如何玩游戏的更多信息。 在游戏过程中,如果您想暂停游戏,只需按空格键即可。

要恢复游戏,您需要再次按下空格键。 在玩游戏时,请确保蓝球击中蓝色滑块,红球击中红色滑块。 颜色相反的滑块将不起作用。 否则您将输掉游戏。

最初屏幕将是完全空白的。 您可以看到底部有两个球和两个可移动的砖块。 您可以同时使用触摸屏移动这些砖块。

让我们自定义游戏

您会在项目中找到一个文本文件,其中包含一些逗号分隔的值。

,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,1,,,,,

上面是可能的示例,展示了它可能的样子。 每一行都包含顶部砖块的外观信息。

可以创建许多可能的组合。 如果两个逗号之间没有提供值,则在游戏区域中将不会生成砖块。 逗号之间的值可以是 1、2 或 3。 每个值表示球需要碰撞一个特定的砖块多少次才能被打破。

让我们看看一些逻辑

[图 1:为每个方向分配一个整数值以供进一步计算。]

在黑色游戏区域内部,球可以顺时针或逆时针旋转。 根据旋转方向,我们为每次旋转创建一个值,以指示游戏中正在进行的旋转状态。 例如,当前旋转是逆时针的,如果球在当前位置 0 碰到底部砖块,那么下一个移动将是方向 3(请参见上图)。

那么这个数字表示什么?

画布的左上方是 X 和 Y 轴的起始位置。 该位置的 X 和 Y 值为零,当我们移动到第 0 个方向时,需要增加 X 和 Y。 这意味着球将移动到右下角。

0 - X 和 Y 都将增加。

1 - X 减小,Y 增大。

2 - X 和 Y 都将减小。

3 - X 增大,Y 减小。

XAML 代码

由于我们是在顶部动态生成砖块,因此我们不需要更多的 xaml 代码来生成砖块。

<Window x:Class="WPFGame1.Gammer" Name="myWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        WindowStyle="None"
        AllowsTransparency="True"
        ResizeMode="CanMinimize"
        Title="Duo Brick Breaker" Height="650" Width="700" 
        KeyDown="Window_KeyDown" 
        WindowStartupLocation="CenterScreen" 
        Icon="/DuoBrickBreaker;component/Images/icon.ico">
    <Border BorderBrush="Black" BorderThickness="10" >
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="529"></RowDefinition>
                <RowDefinition Height="40"></RowDefinition>
                <RowDefinition Height="40"></RowDefinition>
            </Grid.RowDefinitions>
            <Canvas Name="MyGameCanvas" Width="650" 
                        Height="500" Background="LightPink" Opacity="0.6">
                <Rectangle Name="rectangleRed" Width="100" 
                    Height="15" RadiusX="3" RadiusY="3" 
                    Fill="Red" Canvas.Left="0" 
                    Canvas.Top="500" Opacity="1"></Rectangle>
                <Rectangle Name="rectangleBlue" Width="100" 
                   Height="15" Fill="Blue" RadiusX="3" 
                   RadiusY="3" Canvas.Left="550" 
                   Canvas.Top="500" Opacity="1" ></Rectangle>
                <Ellipse Name="GameBallRed" Fill="{DynamicResource MyRadialGradientRed}" 
                   Width="30" Height="30" Canvas.Left="30" 
                   Canvas.Top="470" Opacity="1" />
                <Ellipse Name="GameBallBlue" Fill="{DynamicResource MyRadialGradientBlue}" 
                   Width="30" Height="30" Canvas.Left="589" 
                   Canvas.Top="470" Opacity="1"></Ellipse>
            </Canvas>
            <Canvas Grid.Row="1" Height="40" 
               Background="Gold" Margin="10" Name="canvas1">
            </Canvas>
            <TextBlock Grid.Row="2" Text="F1:Help F5:Play 
               Space:Pause" FontSize="24" Margin="5" 
               HorizontalAlignment="Center" />
        </Grid>
    </Border>
    <Window.Resources>
        <RadialGradientBrush x:Key="MyRadialGradientRed" GradientOrigin="0.5,0.5">
            <GradientStop Color="Transparent" Offset="1"></GradientStop>
            <GradientStop Color="Red" Offset="0.8"></GradientStop>
            <GradientStop Color="Red" Offset="0.5"></GradientStop>
        </RadialGradientBrush>
        <RadialGradientBrush x:Key="MyRadialGradientBlue" GradientOrigin="0.5,0.5">
            <GradientStop Color="Transparent" Offset="1"></GradientStop>
            <GradientStop Color="Blue" Offset="0.8"></GradientStop>
            <GradientStop Color="Blue" Offset="0.5"></GradientStop>
        </RadialGradientBrush>
    </Window.Resources>
</Window> 

我们使用了 WPF 功能来增强用户界面。 但我不会深入讨论每一个。 您可以从 MSDN 中学习。 即使如此,关于每一个点都有许多文章被撰写。

我们重要的控件是 Canvas 和 Grid 控件。 这两个控件将帮助我们创建出色的用户界面。

让我们看看移动游戏球的代码。

private void moveGameBall(int currentDirection, 
      ref double gameBallTop, ref double gameBallLeft, ref Ellipse gameBall)
{
    switch (currentDirection)
    {
        case 0:
            gameBallTop += motionRatio;
            gameBallLeft += motionRatio;
            break;

        case 1:
            gameBallTop += motionRatio;
            gameBallLeft -= motionRatio;
            break;

        case 2:
            gameBallTop -= motionRatio;
            gameBallLeft -= motionRatio;
            break;

        case 3:
            gameBallTop -= motionRatio;
            gameBallLeft += motionRatio;
            break;
        default:
            MessageBox.Show("Ehhh Error occur!!!");
            break;
    }

    Canvas.SetTop(gameBall, gameBallTop);
    Canvas.SetLeft(gameBall, gameBallLeft);
}

如您在上面的函数中所见,球将以定义的 motionRatio 的比例移动。 如果我们增加它的值,游戏速度将加快。 我们可以通过为每个关卡更改 motionRatio 来制作更高级的游戏。

让我们从文本文件中创建我们的顶部砖块图案

string[] stagesInfo = File.ReadAllLines("GameStages.txt"); 

这将读取 CSV 的所有行并将其存储在字符串数组中。 现在您可以通过 `stageInfo` 数组中的每个字符串值获得每个游戏关卡。

当用户点击“开始新游戏”按钮时,我们将 0 索引传递给函数,表示从第一关开始全新游戏。

private void brickGenerator(int currentStage)
{
    changeBackgroundImage();
    Rectangle rct;

    try
    {
        bricks.Clear();

        if (stagesInfo.Length <= currentStage)
        {
            movingTimer.Stop();
            MessageBox.Show("You have completed all the Stage. Congratulation!!!");
        }
        else
        {
            brickInfo = stagesInfo[currentStage].Split(',');

            for (int i = 1; i <= 10; i++)
            {
                for (int j = 1; j <= 10; j++)
                {
                    rct = new Rectangle();
                    rct.Opacity = 1;
                    if (!string.IsNullOrWhiteSpace(brickInfo[(j + ((i - 1) * 10)) - 1]))
                    {
                        int brickType = Convert.ToInt16(brickInfo[(j + ((i - 1) * 10)) - 1]);

                        switch (brickType)
                        {
                            case 0:
                                break;

                            case 1:
                                rct.Fill = Brushes.YellowGreen;
                                break;

                            case 2:
                                rct.Fill = Brushes.DarkOrange;
                                break;

                            case 3:
                                rct.Fill = Brushes.Khaki;
                                break;
                        }

                        rct.Height = 25;
                        rct.Width = 60;
                        rct.Stroke = Brushes.Black;
                        rct.RadiusX = 1;
                        rct.RadiusY = 1;
                        rct.StrokeThickness = 1;
                        Canvas.SetLeft(rct, (j * 60) - 30);
                        Canvas.SetTop(rct, (i * 25));
                        bricks.Insert((j + ((i - 1) * 10)) - 1, rct);
                        MyGameCanvas.Children.Insert(0, rct);
                    }
                    else
                    {
                        rct.Visibility = System.Windows.Visibility.Collapsed;
                        bricks.Add(rct);
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

上面的代码中有许多需要注意的地方。 如您所见,我们创建了一个具有不同填充颜色的动态矩形。 我们选择了三种不同的颜色来显示砖块。 这将有助于识别砖块的类型。 正如我之前讨论过的,我们有三种需要打破的砖块。 因此,我们为每块砖块指定了颜色。

我们正在文本文件中维护游戏关卡信息。 因此,一旦游戏中的所有行都完成并且您成功完成了关卡,这意味着您赢得了游戏。

我们将这个矩形添加到 `Canvas` 以及 `Rectangle` 的 `List` 中,以供进一步计算。 在下一节中,您将了解存储的目的。

计算碰撞的砖块

现在我们需要检查球何时会与动态生成的砖块碰撞。 为此,我们使用 LINQ Lambda 查询来识别需要打破的砖块。

要检查球是否与砖块碰撞,我们必须计算球相对于画布上的不同点。 如果我们在球上计算 20 个点,并且其中任何一个点位于任何砖块内部,则意味着球与该砖块发生了碰撞。

private List<coordinates> getCircularPoints(Ellipse gameBall)
{
    int distance = (int)gameBall.Width / 2;
    double originX = Canvas.GetLeft(gameBall) + distance;
    double originY = Canvas.GetTop(gameBall) - distance;

    List<coordinates> pointLists = new List<coordinates>();
    coordinates point;
    for (int i = 0; i < 360; i = i + 24)
    {
        point = new coordinates();

        point.X = (int)Math.Round(originX + distance * Math.Sin(i));
        point.Y = (int)(gameBall.Width + Math.Round(originY - distance * Math.Cos(i)));
        pointLists.Add(point);
    }

    return pointLists;
}

上面的代码将计算游戏球上的坐标,并返回相对于画布的 X 和 Y 信息列表。 我们在球的周长上计算了总共 15 个不同的点。

现在,在每次球移动后,我们需要检查球是否与任何砖块碰撞。 我们使用了 Linq 查询进行计算。

var conflictedBrick = bricks.Where(s => ballCoordinate.Any(p =>
p.X >= Canvas.GetLeft(s) &&
p.X <= Canvas.GetLeft(s) + s.Width &&
p.Y <= Canvas.GetTop(s) + s.Height &&
p.Y >= Canvas.GetTop(s)));

如果 `conflictedBrick` 计数大于零,则意味着球与砖块碰撞,我们需要对其执行某些步骤。 如果球碰撞,我们需要将其从用户画布中删除,并且还需要改变球的移动方向。 如果画布上不存在任何砖块,我们还应该将用户移至下一关或显示“您赢得了游戏!!!”。

实际上,我们并没有删除砖块,只是将砖块隐藏起来并从私有列表中移除。 因此,在将来的计算中它不会再次出现。

我们在上面的段落中讨论过的所有步骤都可以从附加的代码中进行检查。

在计算时需要检查许多条件。 因此,您可能会对代码中的不同逻辑感到惊讶。 但每一段代码都有其自身的用途。

我们有三种不同类型的砖块,所以我们还需要检查是否碰撞了 3 型砖块,然后更改砖块的颜色并继续。 如果砖块是 1 型,我们只需将其从用户画布中隐藏。

改变球的方向

代码的另一个重要部分是,我们如何改变球在画布内的移动。 实际上,我们正在增加/减少画布中的 X 和 Y 坐标。 我们确保球不会超出画布的边界。

private void changeBallDirection(ref int _currentDirection, 
          Ellipse _gameBall, Rectangle _crashBrick, coordinates nearCoordinate)
{
    int hitAt;
    int left = (int)(nearCoordinate.X - Canvas.GetLeft(_crashBrick));
    int right = (int)(nearCoordinate.X - (Canvas.GetLeft(_crashBrick) + _crashBrick.Width));
    int top = (int)(nearCoordinate.Y - Canvas.GetTop(_crashBrick));
    int bottom = (int)(nearCoordinate.Y - (Canvas.GetTop(_crashBrick) + _crashBrick.Height));

    int[] values = { Math.Abs(left), Math.Abs(right), Math.Abs(top), Math.Abs(bottom) };
    Array.Sort(values);

    if (values[0] == left)
        hitAt = 3;
    else if (values[0] == right)
        hitAt = 1;
    else if (values[0] == top)
        hitAt = 0;
    else
        hitAt = 2;
    // more code is in this function to do other calculation
    // but i have display only important code here.

}

如果我们得到的坐标表明球与砖块碰撞,那么我们的下一步是球在砖块的哪个表面上碰撞。 以下是我们当球在表面上碰撞时所需的可能旋转。

查看图 1 以计算下一个旋转。

当前方向 砖块表面 新方向
0 顶部 3
左侧 1
右侧 不适用
底部 不适用
1 顶部 2
左侧 不适用
右侧 0
底部 不适用
2 顶部 不适用
左侧 不适用
右侧 3
底部 1
3 顶部 不适用
左侧 2
右侧 不适用
底部 0

让我们看看,如果球的当前方向是 0(见图 1),那么球可能在顶部/左侧与砖块碰撞。 当方向为 0 时,球永远不会碰到右侧或底部砖块。 如果球碰到顶部表面,那么下一个方向应该是 3(参见图 1:1),如果球碰到左侧表面,则下一个方向应该是 1(参见图 1:2)。

希望逻辑清晰,如果您对逻辑或任何其他问题有任何疑问,请随时提问。

历史

首次发布

2012/10/10:鼠标无法移动砖块,只能使用键盘移动两个球。

2012/11/15:游戏初始版本,现在可以称为“Duo Brick Breaker”

© . All rights reserved.