WPF 入门教程贪吃蛇游戏(一)
我们将在 WPF 中实现经典的贪吃蛇游戏,最终结果将如下所示:
创建游戏通常是许多人(尤其是年轻人)被吸引学习编程的原因。但问题通常是:我如何开始以及我需要什么?嗯,你需要一种编程语言,比如 C#、C++ 或任何其他流行的语言,如果你能熟练使用你喜欢的语言,你真的不需要其他任何东西:只需从底部开始,向底部添加像素即可屏幕,在某些时候,你可能有一个有效的游戏。
然而,大多数人更愿意在低级的东西上得到一些帮助。如果有一个库或框架可以为您添加像素,为什么要手动将像素添加到屏幕上,这样您就可以专注于构建一个有趣的游戏?有许多框架可以帮助您做到这一点,实际上, 其中之一就是 WPF 框架 。
现在承认,当您想要创建游戏时,WPF 并不是最明显的选择——它绝对是一个主要专注于为面向业务的应用程序创建用户界面的框架。但是,WPF 框架中有许多元素可用于创建游戏,而且可能同样重要:您可以获得在 Windows 中绘制和控制窗口的所有机制。
因此,如果您希望创建一个简单的游戏,WPF 实际上可能是一个不错的选择。至少它对所有最基本的方面都有很大的帮助,比如创建一个窗口,为游戏绘制一个简单的区域等。如果你想添加高级 3D 图形和快速移动对象之类的东西,可能需要其他人的更多帮助库/框架,但它对于一个简单的游戏就足够了。
作为概念证明,我决定创建一个非常经典的贪吃蛇游戏的基于 WPF 的版本。它将使用常规 WPF 窗口作为其游戏区域,以及常规 WPF 控件/形状来创建实际游戏玩法。我选择 Snake 的原因是因为它相当容易实现(没有那么多的代码逻辑)并且因为它可以使用简单的几何图形(如正方形和圆形)来实现,这些图形可以很容易地与 WPF 框架一起使用。但也因为它仍然是一个非常有趣的游戏,尽管它的本质很简单!
如果您不了解贪吃蛇游戏,我只能假设您在 90 年代末/ 2000 年代初从未拥有过诺基亚手机。Snake 的第一个版本是在多年前编写和演示的,但当诺基亚决定在他们所有的手机中包含他们自己的版本时,它成为了一个大热门。
游戏玩法既简单又有趣: 在寻找食物(有时是苹果)的过程中,您将一条虚拟蛇向一个方向(左、右、上或下)移动。当你的蛇撞到苹果时,它被吃掉了,你的蛇长大了,一个新的苹果出现在屏幕上。如果您撞到墙壁或自己的蛇尾,游戏就会结束,您必须重新开始。你吃的苹果越多,你得到的分数就越高,但不撞到自己的尾巴就越难。
游戏玩法有很多变化 - 例如,每次吃一个苹果时,蛇移动的速度通常会增加,使其变得越来越难,但并非所有的 Snake 实现都会这样做。另一个变化是墙壁 - 一些实现将允许您穿过墙壁并从对面出去,而其他实现将在您撞到墙壁时立即结束游戏。
在我们的 SnakeWPF 中,墙壁很硬(如果碰到它们,蛇就会死),并且您每吃一个苹果,速度就会呈指数增加,直到某个点。
创建游戏区域
为了创建我们的 SnakeWPF 游戏,我们将从创建地图开始。这将是一个封闭的区域,蛇必须在其中移动——如果你愿意的话,这是一个蛇坑。我决定我的蛇坑应该看起来像一个棋盘,由同样大小的方块组成,它们的尺寸与蛇的身体相同。我们将在两次迭代中创建地图:其中一些将在 XAML 中布局,因为它很容易,而我们将在代码隐藏中绘制背景方块,因为它是重复和动态的。
游戏区 XAML
因此,让我们从 XAML 开始 - 一个在 Border 控件内带有 Canvas 面板的简单窗口,以创建受限区域:
<Window x:Class="WpfTutorialSamples.Games.SnakeWPFSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfTutorialSamples.Games"
mc:Ignorable="d"
Title="SnakeWPF - Score: 0" SizeToContent="WidthAndHeight">
<Border BorderBrush="Black" BorderThickness="5">
<Canvas Name="GameArea" ClipToBounds="True" Width="400" Height="400">
</Canvas>
</Border>
</Window>
我们的游戏现在看起来像这样:
我们使用 Canvas 作为实际的游戏区域,因为它允许我们向其中添加控件,我们可以完全控制位置。我们稍后会使用它,但现在,请注意以下事项:
- 没有为窗口定义宽度/高度 - 相反,我们为画布定义了它,因为这是我们需要完全控制的部分。然后,我们通过将 SizeToContent 属性设置为 WidthAndHeight 来确保 Window 将相应地调整其大小。如果我们改为定义窗口的宽度/高度,则其中的可用空间将取决于操作系统用于 Windows 的边框大小,这可能取决于主题等。
- 我们将Canvas的 ClipToBounds 属性设置为 True - 这很重要,否则我们添加的控件将能够扩展到 Canvas 面板的边界之外
从代码隐藏中绘制背景
如前所述,我想要游戏区域的棋盘背景。它由许多正方形组成,因此从代码隐藏中添加它会更容易(或使用图像,但这不是那么动态!)。我们需要在 Window 内的所有控件都初始化/渲染后立即执行此操作,对我们来说幸运的是,Window 有一个事件: ContentRendered 事件。我们将在 Window 声明中订阅它:
Title="SnakeWPF - Score: 0" SizeToContent="WidthAndHeight" ContentRendered="Window_ContentRendered"
现在转到代码隐藏,让我们开始吧。首先,我们需要定义在绘制蛇、背景方块等时使用的大小。它可以在 Window 类的顶部完成:
public partial class SnakeWPFSample : Window
const int SnakeSquareSize = 20;
.....
现在,在我们的 ContentRendered 事件中,我们将调用 DrawGameArea() 方法,它将完成所有艰苦的工作。它看起来像这样:
private void Window_ContentRendered(object sender, EventArgs e)
DrawGameArea();
private void DrawGameArea()
bool doneDrawingBackground = false;
int nextX = 0, nextY = 0;
int rowCounter = 0;
bool nextIsOdd = false;
while(doneDrawingBackground == false)
Rectangle rect = new Rectangle
Width = SnakeSquareSize,
Height = SnakeSquareSize,
Fill = nextIsOdd ? Brushes.White : Brushes.Black
GameArea.Children.Add(rect);
Canvas.SetTop(rect, nextY);
Canvas.SetLeft(rect, nextX);
nextIsOdd = !nextIsOdd;
nextX += SnakeSquareSize;
if(nextX >= GameArea.ActualWidth)
nextX = 0;
nextY += SnakeSquareSize;
rowCounter++;
nextIsOdd = (rowCounter % 2 != 0);
if(nextY >= GameArea.ActualHeight)
doneDrawingBackground = true;
如前所述,这些文章比本教程中的其他文章需要更多的 C# 知识,因此我不会逐行介绍,但这里是我们所做工作的一般描述: 在 while 循环中,我们不断地创建Rectangle 控件的实例并将其添加到 Canvas ( GameArea )。我们用白色或黑色画笔填充它,它使用我们的 SnakeSquareSize 常量作为宽度和高度,因为我们希望它是一个正方形。在每次迭代中,我们使用 nextX 和 nextY 来控制何时移动到下一行(当我们到达右边界时)和何时停止(当我们同时到达底部和右边界时)。
结果如下:
创建和移动蛇
我们为 SnakeWPF 实现中的蛇创建了一个很好的区域来移动。有了这个,现在是时候创建实际的蛇,然后让它在该区域周围移动。再一次,我们将使用 WPF Rectangle 类来形成一条特定长度的蛇,每个元素的宽度和高度与背景方块相同,或者我们称之为:SnakeSquareSize 常量!
Demo下载:
推荐一款WPF MVVM框架开源项目:Newbeecoder.UI
https://www.zhihu.com/video/1520747754208391168创造蛇
我们将使用名为 DrawSnake() 的方法绘制蛇——该方法实际上非常简单,但它确实需要很多额外的东西,包括一个名为 SnakePart 的新类,以及 Window 类上的一些额外字段。让我们从 SnakePart 类开始,您通常会在新文件(例如 SnakePart.cs )中定义它:
using System.Windows;
namespace WpfTutorialSamples.Games
public class SnakePart
public UIElement UiElement { get; set; }
public Point Position { get; set; }
public bool IsHead { get; set; }
这个简单的类将包含有关蛇的每个部分的信息:元素在我们的游戏区域中的位置,UIElement(在我们的例子中是矩形)代表该部分,这是否是蛇的头部?我们稍后将使用所有这些,但首先,在我们的 Window 类中,我们需要定义几个字段以在我们的 DrawSnake() 方法中使用(以及稍后在其他方法中):
public partial class SnakeWPFSample : Window
const int SnakeSquareSize = 20;
private SolidColorBrush snakeBodyBrush = Brushes.Green;
private SolidColorBrush snakeHeadBrush = Brushes.YellowGreen;
private List<SnakePart> snakeParts = new List<SnakePart>();
......
我们定义了两个 SolidColorBrush 'es,一个用于身体,一个用于头部。我们还定义了一个 List<SnakePart>,它将保留对蛇所有部分的引用。有了它,我们现在可以实现我们的 DrawSnake() 方法:
private void DrawSnake()
foreach(SnakePart snakePart in snakeParts)
if(snakePart.UiElement == null)
snakePart.UiElement = new Rectangle()
Width = SnakeSquareSize,
Height = SnakeSquareSize,
Fill = (snakePart.IsHead ? snakeHeadBrush : snakeBodyBrush)
GameArea.Children.Add(snakePart.UiElement);
Canvas.SetTop(snakePart.UiElement, snakePart.Position.Y);
Canvas.SetLeft(snakePart.UiElement, snakePart.Position.X);
如您所见,此方法并不是特别复杂:我们遍历 蛇形部件 列表,对于每个部分,我们检查是否已为此部分指定了 UIElement - 如果没有,我们创建它(由 Rectangle 表示)并添加将其添加到游戏区域,同时在 SnakePart 实例的 UiElement 属性上保存对它的引用。请注意我们如何使用 SnakePart 实例的 Position 属性来定位 GameArea Canvas 内的实际元素。
这里的技巧当然是蛇的实际部分将在别处定义,允许我们向蛇添加一个或几个部分,给它们所需的位置,然后让 DrawSnake() 方法为我们做实际的工作. 我们将把它作为用于移动蛇的相同过程的一部分。
移动蛇
为了给 DrawSnake() 方法提供一些东西,我们需要填充 snakeParts 列表。这个列表一直作为绘制蛇的每个元素的基础,所以我们也将使用它来为蛇创建运动。移动蛇的过程基本上包括在蛇当前移动的方向上添加一个新元素,然后删除蛇的最后一部分。这将使看起来我们实际上是在移动每个元素,但实际上,我们只是在删除旧元素的同时添加新元素。
因此,我们将需要一个 MoveSnake() 方法,我将在一分钟内向您展示,但首先,我们需要在 Window 类定义的顶部添加更多内容:
public partial class SnakeWPFSample : Window
const int SnakeSquareSize = 20;
private SolidColorBrush snakeBodyBrush = Brushes.Green;
private SolidColorBrush snakeHeadBrush = Brushes.YellowGreen;
private List<SnakePart> snakeParts = new List<SnakePart>();
public enum SnakeDirection { Left, Right, Up, Down };
private SnakeDirection snakeDirection = SnakeDirection.Right;
private int snakeLength;
......
我们添加了一个新的枚举,称为 SnakeDirection ,它应该是不言自明的。为此,我们有一个私有字段来保存实际的当前方向( snakeDirection ),然后我们有一个整数变量来保存蛇的所需长度( snakeLength )。有了这些,我们就可以实现 MoveSnake() 方法了。它有点长,所以我为它的每个重要部分添加了内联注释:
private void MoveSnake()
// Remove the last part of the snake, in preparation of the new part added below
while(snakeParts.Count >= snakeLength)
GameArea.Children.Remove(snakeParts[0].UiElement);
snakeParts.RemoveAt(0);
// Next up, we'll add a new element to the snake, which will be the (new) head
// Therefore, we mark all existing parts as non-head (body) elements and then
// we make sure that they use the body brush
foreach(SnakePart snakePart in snakeParts)
(snakePart.UiElement as Rectangle).Fill = snakeBodyBrush;
snakePart.IsHead = false;
// Determine in which direction to expand the snake, based on the current direction
SnakePart snakeHead = snakeParts[snakeParts.Count - 1];
double nextX = snakeHead.Position.X;
double nextY = snakeHead.Position.Y;
switch(snakeDirection)
case SnakeDirection.Left:
nextX -= SnakeSquareSize;
break;
case SnakeDirection.Right:
nextX += SnakeSquareSize;
break;
case SnakeDirection.Up:
nextY -= SnakeSquareSize;
break;
case SnakeDirection.Down:
nextY += SnakeSquareSize;
break;
// Now add the new head part to our list of snake parts...
snakeParts.Add(new SnakePart()
Position = new Point(nextX, nextY),
IsHead = true