WPF贪吃蛇游戏开发
目录
源码可以在github上获取https://github.com/ZombieAllen/WPF.git
源码可以在github上获取https://github.com/ZombieAllen/WPF.git
基于WPF的贪吃蛇游戏设计
摘要:针对现有的网上关于WPF以及C#开发缺乏系统化的学习资料,现在设计一款大家耳熟能详的贪吃蛇的小游戏,来比较好的把XMAL界面设计,XML文件的信息写入与读取,C#面向对象的程序设计整合到一个游戏里。
关键字:WPF;C#;XML;界面设计;Visual Studio 2019
1 引言
随着工厂信息化以及数字化的飞速发展,对于工厂的自动化工程师,掌握一门高级的开发语言,来开发界面美观的windows客户端,实现数据的采集,存储,展示已经变得越来越必须。跨学科的人才需求也在日渐加大。
C#语言是现如今比较流行的一门面向对象的编程语言,在微软新一代界面设计技术WPF的加持下,设计出一款界面美观,功能强大的windows客户端应用变得非常方便。
这款基于WPF的贪吃蛇游戏,会利用Canvas来绘制界面的背景以及蛇和果实,并利用障眼法来实现蛇的动画效果,并利用事件来实现方向控制,最后会利用xml技术来存储高分榜,游戏开始的时候可以列出玩家排名。
2 功能设计
2.1游戏区域
首先我们打开Visual Studio2019 , 新建WPF项目
短短的几行xmal代码,就实现了游戏窗口
接下来我们在这个Canvas区域画出网格,来规范蛇以及果实出现的位置
技巧:通过isOdd变量的值通过嵌套For循环来在画布上画出一个个颜色相间的方块。
最终实现的代码以及效果
private void Window_ContentRendered(object sender, EventArgs e)
{
DrawGameArea();
}
private void DrawGameArea()
{
for (int y = 0; y < GameArea.ActualHeight; y+=squareSize)
{
for (int x = 0; x < GameArea.ActualWidth; x += squareSize)
{
Rectangle rectangle = new Rectangle() { Width = squareSize, Height = squareSize };
rectangle.Fill = isOdd ? Brushes.White : Brushes.WhiteSmoke;
GameArea.Children.Add(rectangle);
Canvas.SetLeft(rectangle, x);
Canvas.SetTop(rectangle, y);
isOdd = !isOdd;
}
isOdd = !isOdd;
}
}
2.2 贪吃蛇动画
这里我们会接触第一个面向对象设计的套路:类-à实例
对于一条蛇来说,C#自身并没有这个实例。 这时候我们需要自己创建一个SnakePart的类,然后进行实例化出一条蛇。
实现方法:
在项目里添加一个SnakePart的类,定义其主要属性:UI元素,位置,是否是蛇头
代码如下
namespace WPF贪吃蛇游戏设计
{
public class SnakePart
{
public UIElement uIElement { get; set; }
public Point point { get; set; }
public Boolean isHead { get; set; }
}
}
然后我们用泛型集合List把一个个蛇的部分串起来,就成了一条蛇了。
List<SnakePart> snakeParts = new List<SnakePart>();
那么我们先画一条静态的蛇
定义蛇的初始位置以及长度
//初始位置
Point startPos = new Point(120, 120);
//初始长度
int snakeLength = 3;
然后判断是否是蛇头来画不同颜色的方框,并把方框添加到蛇的uIElement上,然后在Canvas里画出来
具体实现代码:
private void DrawSnake(Point startPos, int snakelength)
{
Boolean isHead = false;
double nextX = startPos.X;
double nextY = startPos.Y;
for (int i = 0; i < snakelength; i++)
{
if (i < snakelength - 1)
{
isHead = false;
DrawSnake_sub(isHead, nextX, nextY);
nextX += squareSize;
}
else
{
isHead = true;
DrawSnake_sub(isHead, nextX, nextY);
}
}
}
private void DrawSnake_sub(Boolean isHead , double nextX,double nextY)
{
Rectangle rectangle = new Rectangle() { Width = squareSize, Height = squareSize };
rectangle.Fill = isHead ? snakeHeadColor : snakeBodyColor;
SnakePart snakepart = new SnakePart();
snakepart.uIElement = rectangle;
snakeParts.Add(snakepart);
GameArea.Children.Add(rectangle);
Canvas.SetLeft(rectangle, nextX);
Canvas.SetTop(rectangle, nextY);
}
然后在页面渲染的时候调用
DrawSnake(startPos, snakeLength);
这样我们就初始化了一条蛇,具体实现结果如下(红色为蛇头,绿色为蛇身)
接下来我们的任务就是让蛇动起来:控制蛇的方向和蛇的速度
蛇的方向:
首先我们定义一个枚举类型SnakeDirection,来表示蛇移动的四个方向。
namespace WPF贪吃蛇游戏设计
{
public enum SnakeDirection
{
left,
top,
right,
down
}
}
然后我们设定蛇的初始行进方向为向右
接下来我们定义蛇的行进速度,这时候就得用到定时器以及事件了
首先我们实例化一个定时器dispatch
//定时器
DispatcherTimer dispatcher = new DispatcherTimer();
为其绑定一个事件MoveSnake(),定时器的间隔时间设定以及开始计时
dispatcher.Interval = TimeSpan.FromMilliseconds(1000);
dispatcher.Tick += Dispatcher_Tick;
dispatcher.Start();
接下来就是MoveSnake函数的编写了。
对于蛇的移动来说,其实用到的就是障眼法,分为4步
- 移去蛇尾
- 在原先蛇头的位置画蛇身
- 移去原先的蛇头
- 根据蛇头原先的位置,在其行进方向一个单元格的位置重新画一个蛇头
具体实现代码
private void MoveSnake()
{
//移去最后一节身子
GameArea.Children.Remove(snakeParts[0].uIElement);
snakeParts.Remove(snakeParts[0]);
//获取原先蛇头的位置,并重新画一个身子
double exHeadPosX = snakeParts[snakeParts.Count-1].point.X;
double exHeadPosY = snakeParts[snakeParts.Count - 1].point.Y;
DrawSnake_sub(false, exHeadPosX, exHeadPosY);
//移去原先的蛇头
GameArea.Children.Remove(snakeParts[snakeParts.Count - 2].uIElement);
snakeParts.Remove(snakeParts[snakeParts.Count - 2]);
//重新画一个蛇头
DrawSnake_sub(true, exHeadPosX+squareSize, exHeadPosY);
}
经过测试,蛇就可以沿着向右的方向每隔1s移动一格了
2.3 键盘控制方向
首先在窗体上定义Key_Up事件
KeyUp="Window_KeyUp"
然后在事件处理函数里定义不同的key值对应的操作
提示:对于贪吃蛇游戏来说,不存在直接回头的操作。
代码如下:
private void Window_KeyUp(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Left:
if (snakeDirection!=SnakeDirection.right && snakeDirection != SnakeDirection.left)
{
snakeDirection = SnakeDirection.left;
MoveSnake();
}
break;
case Key.Up:
if (snakeDirection!=SnakeDirection.down && snakeDirection != SnakeDirection.up)
{
snakeDirection = SnakeDirection.up;
MoveSnake();
}
break;
case Key.Right:
if (snakeDirection != SnakeDirection.right && snakeDirection != SnakeDirection.left)
{
snakeDirection = SnakeDirection.right;
MoveSnake();
}
break;
case Key.Down:
if (snakeDirection != SnakeDirection.down && snakeDirection != SnakeDirection.up)
{
snakeDirection = SnakeDirection.down;
MoveSnake();
}
break;
default:
break;
}
}
根据在游戏过程中获取到的snakeDirection的值,我们把之前添加蛇头的逻辑做了些修改,增加了方向的判断
代码如下:
//重新画一个蛇头
switch (snakeDirection)
{
case SnakeDirection.left:
DrawSnake_sub(true, exHeadPosX - squareSize, exHeadPosY);
break;
case SnakeDirection.up:
DrawSnake_sub(true, exHeadPosX , exHeadPosY - squareSize);
break;
case SnakeDirection.right:
DrawSnake_sub(true, exHeadPosX + squareSize, exHeadPosY);
break;
case SnakeDirection.down:
DrawSnake_sub(true, exHeadPosX , exHeadPosY + squareSize);
break;
default:
break;
}
这样就实现了蛇根据键盘的输入来进行移动的程序
实现效果如下:
2.4游戏区随机出现蛇果(不能出现在蛇身上)
原理:生成一个squaresize整数倍的数来定位蛇果的位置,然后通过canvas画出来,当然蛇果的位置不能出现在蛇身上以及出游戏边界
实现代码:
private void GenerateFood()
{
start:
Random random = new Random();
double foodPosX = random.Next(0, (int)GameArea.ActualWidth / squareSize)*squareSize;
double foodPosY = random.Next(0, (int)GameArea.ActualHeight / squareSize) * squareSize;
foreach (var snakePart in snakeParts)
{
if (snakePart.point.X==foodPosX && snakePart.point.Y==foodPosY)
{
goto start;
}
}
Ellipse food = new Ellipse() { Width = squareSize, Height = squareSize };
food.Fill = Brushes.Chocolate;
GameArea.Children.Add(food);
Canvas.SetLeft(food, foodPosX);
Canvas.SetTop(food, foodPosY);
}
实现效果如下
2.5碰撞监控
碰撞监控的效果:蛇与果实碰撞则果实消失,蛇身长度+1。如果蛇与边界碰撞,游戏结束
2.5.1 与果实碰撞
先来实现蛇与果实碰撞,蛇身长度+1的逻辑
思路:判断蛇头与蛇果的位置重合,则清除果实,重新生成果实,返回一个值,根据这个值来判断是否需要去掉蛇尾
代码实现:
private int CollisionCheck()
{
if (snakeParts[snakeParts.Count-1].point.X==foodPosX && snakeParts[snakeParts.Count - 1].point.Y==foodPosY)
{
GameArea.Children.Remove(food);
GenerateFood();
return 1;
}
else
{
return 0;
}
}
原来的MoveSnake函数里增加一个判断条件,来决定是否需要砍掉蛇尾
代码如下:
private void MoveSnake()
{
if (CollisionCheck()!=1)
{
//移去最后一节身子
GameArea.Children.Remove(snakeParts[0].uIElement);
snakeParts.Remove(snakeParts[0]);
}
实现效果如下:
2.5.2 与边界或者蛇身碰撞
如果蛇头与蛇身碰撞,或者蛇头超出边界,则游戏结束。
在CollisionCheck方法里添加如下代码:
else
{
foreach (var snakeBody in snakeParts.Take(snakeParts.Count - 1))
{
if (snakeBody.point.X == snakeParts[snakeParts.Count - 1].point.X && snakeBody.point.Y == snakeParts[snakeParts.Count - 1].point.Y)
{
EndGame();
}
}
if (snakeParts[snakeParts.Count - 1].point.X >= GameArea.ActualWidth ||
snakeParts[snakeParts.Count - 1].point.X < 0 ||
snakeParts[snakeParts.Count - 1].point.Y >= GameArea.ActualHeight ||
snakeParts[snakeParts.Count - 1].point.Y < 0)
{
EndGame();
}
return 0;
}
并为EndGame方法添加内容,当游戏结束的时候,停止定时器,并且弹出提示框
private void EndGame()
{
//dispatcher.Stop();
dispatcher.IsEnabled = false;
dispatcher.Stop();
MessageBox.Show("游戏结束","WPF贪吃蛇游戏",MessageBoxButton.OK,MessageBoxImage.Warning);
}
最终的效果
2.6 界面优化1
基本功能实现后,我们来对界面来美化一下。
首先我们重新设计下游戏结束的时候要弹出的窗体样式
在Canvas里添加如下代码:
<Border x:Name="bdr_EndGame" Visibility="Collapsed" Panel.ZIndex="1" BorderThickness="3" BorderBrush="AliceBlue" Width="400" Height="400" Margin="200" Background="Azure" Padding="100">
<StackPanel>
<TextBlock FontSize="30" Foreground="Red" TextAlignment="Center" Margin="0,20">游戏结束</TextBlock>
<TextBlock FontSize="30" Foreground="Green" TextAlignment="Center" Margin="0,10">您的得分是:</TextBlock>
<TextBlock x:Name="txt_score" FontSize="30" Foreground="Black" TextAlignment="Center" Width="90"></TextBlock>
</StackPanel>
</Border>
然后我们得到如下效果
然后我们在后端替换下原来的messagebox.设定该border的属性为visual
bdr_EndGame.Visibility = Visibility.Visible;
为了显示最终的得分,我们需要在程序里定义一个变量score,然后每次蛇头与食物重合的时候,score+1,然后把这个值转换成string类型,传给txt_score.Text
txt_score.Text = Score.ToString();
最终当游戏结束的时候,显示的效果入下图所示
接下来我们实现的功能是让游戏具有开始画面,当用户按下“S”键,游戏开始。
当游戏结束的时候,用户按下”S”键,游戏又重复开始。
首先我们设计下开始的画面
对应的XMAL代码如下:
<Border x:Name="bdr_Welcome" Visibility="Visible" Panel.ZIndex="1" BorderThickness="3" BorderBrush="AliceBlue" Width="600" Height="600" Margin="100" Background="Azure" Padding="100">
<StackPanel>
<TextBlock FontSize="50" Foreground="Red" TextAlignment="Center" Margin="0,20">WPF贪吃蛇</TextBlock>
<TextBlock FontSize="20" Foreground="Red" TextAlignment="Center" Margin="0,30" TextWrapping="Wrap">大多数游戏都会通关,但贪吃蛇不一样,像极了人的一生,不停奔波,不停索取,但,最终只会死亡</TextBlock>
<TextBlock FontSize="20" Foreground="Green" TextAlignment="Center" Margin="0,40">“S”键开始这段结局已注定的游戏</TextBlock>
</StackPanel>
</Border>
在程序后台,我们定义一个变量isGameRunning来作为游戏的运行状态。
我们首先来让程序接受键盘S的输入
case Key.S:
if (isGameRuning==false)
{
StartGame();
}
break;
当程序接收到键盘的key_up事件,并且是又S键触发的
我们就执行StartGame方法
大体思路:如果蛇和果实都还有残余在画布上,我们首先会清除这部分Ui元素,初始化游戏,然后开始计时。
实现代码如下:
private void StartGame()
{
//欢迎画面消失
bdr_Welcome.Visibility = Visibility.Collapsed;
//清除上一局游戏里的蛇身
if (snakeParts.Count!=0)
{
int snakeLength = snakeParts.Count;
for (int i = 0; i < snakeLength; i++)
{
GameArea.Children.Remove(snakeParts[0].uIElement);
snakeParts.Remove(snakeParts[0]);
}
}
//清除上一局游戏里的食物
if (food!=null)
{
GameArea.Children.Remove(food);
food = null;
}
bdr_EndGame.Visibility = Visibility.Collapsed;
//初始方向l
snakeDirection = SnakeDirection.right;
snakeDirectionPre = SnakeDirection.right;
//重新生成蛇身以及食物
DrawSnake(startPos, snakeLength);
GenerateFood();
//开始计时
dispatcher.Interval = TimeSpan.FromMilliseconds(500);
dispatcher.Start();
isGameRuning = true;
}
到现在为止,这已经是一个完整的WPF贪吃蛇程序了。
2.7 高分榜
对于一个游戏来说,游戏分数的记录,以及高分榜的排序也是吸引玩家的关键,可以让玩家有成就感,当他的名字出现在高分榜上。
现在我们就来实现这个功能
首先我们创建一个HighScore的类,代码如下
public class HighScore
{
public string Name { get; set; }
public int Score { get; set; }
}
在后台的定义下一个泛型集合highScoreList以及xmlName
//高分榜
List<HighScore> highScoreList = new List<HighScore>();
//xml文件路径
string xmlName = "HighScoreList.xml";
接下来的思路是:当程序载入的时候,判断在程序的路径下是否存在这个名为xml的文件,如果没有就创建一个,如果有就读取文件内容到highScoreList这个列表里。
对应的代码:
if (File.Exists(xmlName) == false)
{
CreateXMLFile();
}
else
{
StreamReader read = new StreamReader(xmlName);
ReadXML(read);
}
对应的CreateXMLFile方法和ReadXML的方法代码:
CreateXMLFile方法
private void CreateXMLFile()
{
//创建名为xmlName的文件流
Stream write = new FileStream(xmlName, FileMode.Create);
//设定xml的内部格式
GenerateXML(write);
}
private void GenerateXML(Stream write)
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof(List<HighScore>));
xmlSerializer.Serialize(write, highScoreList);
write.Close();
}
ReadXMLFile方法
private void ReadXML(StreamReader readStream)
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof(List<HighScore>));
highScoreList = (List<HighScore>)xmlSerializer.Deserialize(readStream);
highScoreList = new List<HighScore>(SortByScore(highScoreList));
Item_TopList.ItemsSource = highScoreList.Take(5);
readStream.Close();
}
当游戏结束的时候,需要判断下这次的成绩score是否在highScoreList这个集合的top5,如果是,需要弹出一个让玩家输入名字的对话框。不是则直接弹出成绩的对话框。
实现代码:
private IOrderedEnumerable<HighScore> SortByScore(List<HighScore> highscorelist)
{
var highScoreOrderList= highscorelist.OrderByDescending(x => x.Score);
return highScoreOrderList;
}
在我们程序载入并读取XML文件的时候,我们会把highScoreList的元素按照score降序排列,并重新赋给highScoreList,当游戏结束的时候,我们只需要比较这次的成绩score和highScoreList[4].score的大小,如果大于则弹出输入名字的对话框,不是则显示成绩
对应代码:
if (isGameRuning==false)
{
if (highScoreList.Count!=0 && score > highScoreList[4].Score)
{
bdr_AddHighScore.Visibility = Visibility.Visible;
}
}
else
{
bdr_TopList.Visibility = Visibility.Collapsed;
bdr_AddHighScore.Visibility = Visibility.Collapsed;
}
}
现在我们设计下两个界面,输入姓名的对话框以及高分榜的排名
输入姓名的对话框:
XMAL代码:
<Border x:Name="bdr_AddHighScore" Visibility="Visible" Panel.ZIndex="1" BorderThickness="3" BorderBrush="AliceBlue" Width="600" Height="600" Margin="100" Background="Azure" Padding="100">
<StackPanel>
<TextBlock FontSize="50" Foreground="Red" TextAlignment="Center" Margin="0,20">恭喜进入高分榜</TextBlock>
<TextBlock FontSize="20" Foreground="Red" TextAlignment="Center" Margin="0,30" TextWrapping="Wrap">请输入您的名字</TextBlock>
<TextBox x:Name="txt_PlayName" FontSize="20" Foreground="Green" TextAlignment="Center" Margin="0,40" Width="200"></TextBox>
<Button FontSize="20" Foreground="Green" Margin="0,10" Width="200" Background="Transparent" Click="btn_submit" >提交</Button>
</StackPanel>
</Border>
高分榜对话框:
对应的XMAL代码:
<Border x:Name="bdr_TopList" Visibility="Visible" Panel.ZIndex="1" BorderThickness="3" BorderBrush="AliceBlue" Width="600" Height="600" Margin="100" Background="Azure" Padding="100">
<StackPanel>
<TextBlock FontSize="50" Foreground="Red" TextAlignment="Center" Margin="0,20">高分榜</TextBlock>
<ItemsControl x:Name="Item_TopList" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel>
<TextBlock DockPanel.Dock="Left" TextAlignment="Left" FontSize="30" Text="{Binding Name}"></TextBlock>
<TextBlock DockPanel.Dock="Right" TextAlignment="Right" FontSize="30" Text="{Binding Score}"></TextBlock>
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
接下来就是输入姓名对话框弹出后,我们需要把输入的姓名以及对应的结果写入highScoreList里,输入完成后,当按下提交按钮,则会跳转到高分榜的界面。再次按下“S”键,则游戏重新开始
对应代码:
private void btn_submit(object sender, RoutedEventArgs e)
{
AddInfoToXML(txt_PlayName.Text);
StreamReader read = new StreamReader(xmlName);
ReadXML(read);
bdr_TopList.Visibility = Visibility.Visible;
bdr_AddHighScore.Visibility = Visibility.Collapsed;
}
private void AddInfoToXML(string name)
{
HighScore highScore = new HighScore() {Name = name,Score = score };
highScoreList.Add(highScore);
highScoreList = new List<HighScore>(SortByScore(highScoreList));
Stream write= new FileStream(xmlName, FileMode.Open);
GenerateXML(write);
}
private void GenerateXML(Stream write)
{
XmlSerializer xmlSerializer = new XmlSerializer(typeof(List<HighScore>));
xmlSerializer.Serialize(write, highScoreList);
write.Close();
}
这样我们利用XML的数据存储属性就完成了高分榜功能的制作。像这样数据量不大的情况下,没有必要用专业的数据库来做,XML轻量易操作,用在小型的程序里特别适合
2.8 界面优化2
最后一点工作:我们美化下窗体。
隐去windows默认的窗体外框,重新在Canvas上Dock一个stackPanel
XMAL代码如下:
<StackPanel DockPanel.Dock="Top" Background="Black">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40"></RowDefinition>
<RowDefinition Height="40"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="30" Foreground="White" FontWeight="Bold">贪吃蛇游戏</TextBlock>
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="30" Foreground="White" FontWeight="Bold">作者:Zombie</TextBlock>
<Button Grid.Row="0" Grid.Column="2" FontSize="30" Background="Transparent" Foreground="White" Click="btn_Close">X</Button>
<TextBlock Grid.Row="1" Grid.Column="0" FontSize="30" Foreground="Yellow" FontWeight="Bold" x:Name="txt_current_score"></TextBlock>
<TextBlock Grid.Row="1" Grid.Column="1" FontSize="30" Foreground="Yellow" FontWeight="Bold" x:Name="txt_current_speed"></TextBlock>
</Grid>
</StackPanel>
界面效果如下:
并增加了游戏难度,没吃一个果实,速度提高50,最快是时间间隔100ms
实现代码:
speed = speed > 100 ? speed - 50 : 100;
dispatcher.Interval = TimeSpan.FromMilliseconds(speed);
并最终在标题栏的第二行显示出分数以及速度
效果如下:
高分榜:
3 总结
到这里,我们就实现了一个比较完整的贪吃蛇游戏。贪吃蛇游戏在各个计算机语言设计中都是一个综合性比较强的项目。其中涉及了泛型,集合,Lambda表达式,Canvas这些比较难的概念,也有动画实现的小技巧。还涉及到利用XML进行数据的存储,调用来实现高分榜的部分。WPF特有的 XMAL语言,可以很好的实现非常精致的画面,以及和后端代码的分离。