首先,要制作贪吃蛇游戏,就需要先了解它的游戏规则还有特点:
通过键盘上下左右控制蛇的方向,寻找吃的东西,每吃一口就能得到一定的积分,而且蛇的身子会越吃越长,身子越长玩的难度就越大,而且蛇的速度越来越快,不能碰墙,不能咬到自己的身体,更不能咬自己的尾巴。
需求分析:游戏的主界面,游戏开始的界面,显示得分的界面,蛇和食物的设计。
游戏界面需要合理使用各种控件布局,蛇移动的区域应该是两种颜色交替排布的方格组成,这样才能清晰的看见蛇的移动,也是为了增加游戏体验。
蛇的设计:根据面向对象的设计思想,可以以一个方格为单位,然后多个方格就可以组成一条蛇了,为了区分蛇头和蛇身,还需要加一个bool类型的字段判断这方格是不是蛇头。
源文件及说明
<Window x:Class="WpfApp1.MainWindow"
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:WpfApp1"
mc:Ignorable="d"
Title="WPF 贪吃蛇:0"
SizeToContent="WidthAndHeight"
Loaded="Window_Loaded"
ContentRendered="Window_ContentRendered" KeyUp="Window_KeyUp" WindowStyle="None" ResizeMode="NoResize" MouseLeftButtonDown="Window_MouseLeftButtonDown"
>
<DockPanel Background="Black">
<Grid DockPanel.Dock="Top" Name="pnlTitleBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<Style TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10,0"/>
</Style>
</Grid.Resources>
<WrapPanel Margin="10,0,0,0">
<TextBlock>Score:</TextBlock>
<TextBlock x:Name="tbStatusScore">0</TextBlock>
</WrapPanel>
<WrapPanel Grid.Column="1">
<TextBlock>Speed:</TextBlock>
<TextBlock x:Name="tbStatusSpeed">0</TextBlock>
</WrapPanel>
<Button Focusable="False" Visibility="Hidden" x:Name="btPause" Grid.Column="2" Content="II" Background="Transparent" Click="BtPause_Click"/>
<Button Grid.Column="3" Content="X" Background="Transparent" Click="Button_Click"/>
</Grid>
<Border BorderThickness="5" BorderBrush="Black">
<Canvas x:Name="GameArea" ClipToBounds="True" Width="400" Height="400">
<Border Visibility="Collapsed" Panel.ZIndex="1" x:Name="bdrWelcomeMessage" Width="300" Height="300" BorderThickness="2" BorderBrush="Silver" Canvas.Left="50" Canvas.Top="50">
<StackPanel Background="AliceBlue">
<TextBlock Text="WPF贪吃蛇" FontSize="50" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,20,0,0"/>
<TextBlock FontWeight="Bold" TextWrapping="Wrap" FontSize="16" Margin="20" Text="用方向键来控制绿色蛇去吃红色的苹果。请注意,不要撞到墙壁,也不要撞到自己的身体,一旦撞到,则游戏结束!"/>
<TextBlock Text="按空格键开始游戏" FontSize="24" Foreground="Maroon" HorizontalAlignment="Center" FontWeight="Bold"/>
</StackPanel>
</Border>
<Border Visibility="Visible" Panel.ZIndex="1" x:Name="bdrEndOfGame" Width="300" Height="300" BorderThickness="2" BorderBrush="Silver" Canvas.Left="50" Canvas.Top="50">
<StackPanel Background="AliceBlue">
<TextBlock Text="OH NO!" FontSize="40" FontWeight="Bold" HorizontalAlignment="Center" Margin="0,20,0,0"/>
<TextBlock FontWeight="Bold" TextWrapping="Wrap" FontSize="26" Margin="20" Text="...你的蛇,死了!" HorizontalAlignment="Center"/>
<TextBlock x:Name="tbFinalScore" HorizontalAlignment="Center" Margin="0,0,0,20" FontSize="48">0</TextBlock>
<TextBlock Text="按空格键开始游戏" FontSize="24" Foreground="Maroon" HorizontalAlignment="Center" FontWeight="Bold"/>
</StackPanel>
</Border>
</Canvas>
</Border>
</DockPanel>
</Window>
1.最上方设计的状态面板,有分数和蛇的移动速度,利用Grid可以更好地调整各个板块所占地比例,然后通过<Grid.Resourse>分别对TextBlock和Button控件定义样式。
2.“暂停“按钮默认一开始不显示,即Visibility=”Hidden“,分数和速度是需要实时改变的,可以通过x:name, x:Name 用来在XAML中表示一个指定对象的名称,然后就可以在逻辑代码里来改变分数和速度的显示。例如:tbStatusScore.Text = currentScore.ToString();
3.Button控件里,属性click来绑定源程序里的两个函数,分别实现暂停和关闭窗口功能,而暂停和关闭按钮直接用“Ⅱ”和“X”来展示了,其实也可以用path绘图功能,能简单表示就不设计得那么麻烦了。
4.还需要画出地图和蛇,我选择的是Canvas控件,因为它可以完全自由地对控件的位置进行安排
<Canvas x:Name="GameArea" ClipToBounds="True" Width="400" Height="400">
这是Canvas控件的定义,同样的,用x:name方便对游戏区域进行操作,这里的Width和Height定义了游戏区域大小,而且还需要用它和方格大小来计算出行和列的格子数量。
是这样计算的:
private void Window_ContentRendered(object sender, EventArgs e)//窗口一启动即 执行
{
DrawGameArea();
maxX = (int)(GameArea.ActualWidth / SnakeSquareSize);
maxY = (int)(GameArea.ActualHeight / SnakeSquareSize);
bdrEndOfGame.Visibility = Visibility.Collapsed;//结束界面隐藏
bdrWelcomeMessage.Visibility = Visibility.Visible;//展示游戏开始界面
}
其中画游戏地图DrawGameArea函数如下:
private void DrawGameArea()
{
int nextX = 0;//下一个方格相对顶部的距离
int nextY = 0;//下一个方格相对左边界的距离
Boolean netxtIsOdd = false;//用来判断下一个方格决定画哪种颜色
while (true)
{
Rectangle rectangle = new Rectangle//用Rectangle来当格子
{
Width = SnakeSquareSize,
Height = SnakeSquareSize,//宽高初始化是20,const int SnakeSquareSize = 20;
Fill = netxtIsOdd ? Brushes.White : Brushes.LightGray//白色和亮灰色
};
GameArea.Children.Add(rectangle);//先加进Canvas
Canvas.SetTop(rectangle, nextY);
Canvas.SetLeft(rectangle, nextX);//再调整位置
netxtIsOdd = !netxtIsOdd;
nextX += SnakeSquareSize;
if (nextX >= GameArea.ActualWidth)//这里画的顺序是:由上到下,由左到右
{
nextX = 0;
nextY += SnakeSquareSize;
netxtIsOdd = !netxtIsOdd;
}
if (nextY >= GameArea.ActualHeight)
{
break;
}
}
}
现在游戏区域有了,接下来就是画“蛇“,这里我另外建了一个类定义蛇的身体:
class SnakePart//蛇的身体,但不是整条蛇,即一个小方格
{
public UIElement UiElement { set; get; }//UIElement 是 WPF 核心级实现的基类
public Point Position { set; get; }
public Boolean IsHead { set; get; }//表示这个方格是不是蛇头
}
要变成蛇,所以需要由许多SnakePart组成,即List snakeParts = new List();
蛇也有运动方向,定义枚举:
public enum SnakeDirection
{
Left,
Right,
Up,
Down
}
还定义了蛇身和蛇头颜色,来进行区分
//蛇身颜色
SolidColorBrush snakeBodyBrush = Brushes.Green;
//蛇头颜色
SolidColorBrush snakeHeadBrush = Brushes.Blue;
//蛇身
List<SnakePart> snakeParts = new List<SnakePart>();
//爬行方向
SnakeDirection snakeDirection = SnakeDirection.Right;
画蛇代码:
private void DrawSnake()
{
foreach (var snakePart in snakeParts)//遍历list,画出蛇的每一部分
{
if (snakePart.UiElement == null)
{
snakePart.UiElement = new Rectangle//还是用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 snakeFood = null;
//食物颜色为红色,食物的位置利用随机数来决定:
SolidColorBrush foodBrush = Brushes.Red;
//随机数
Random random = new Random();
private Point GetNextFoodPosition()//随机出现一个食物
{
int foodX = random.Next(0, maxX) * SnakeSquareSize;
int foodY = random.Next(0, maxY) * SnakeSquareSize;//食物的坐标
foreach (var snakePart in snakeParts)
{
if ((snakePart.Position.X == foodX) && (snakePart.Position.Y == foodY))//不能定位到蛇身上
{
return GetNextFoodPosition();//递归,如果下一个食物的位置在蛇的身上,重新返回一个食物位置
}
}
return new Point(foodX, foodY);
}
private void DrawSnakeFood()
{
Point foodPosition = GetNextFoodPosition();//调用上面的函数得到食物位置
snakeFood = new Ellipse//用圆形
{
Width = SnakeSquareSize,
Height = SnakeSquareSize,
Fill = foodBrush//红色的食物//食物颜色SolidColorBrush foodBrush = Brushes.Red;
};
GameArea.Children.Add(snakeFood);//先加进去
Canvas.SetLeft(snakeFood, foodPosition.X);
Canvas.SetTop(snakeFood, foodPosition.Y);//再调整位置
}
蛇的移动速度和语言播报:
//游戏是否在运行,游戏结束或者暂停后,即为false
Boolean isGameRuning = true;
//语音合成
private SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer();
//定时器
DispatcherTimer gameTickTimer = new DispatcherTimer();
//蛇具有一定的初始长度
const int SnakeStartLength = 3;
//蛇初始速度
const int SnakeStartSpeed = 500;
//蛇的最大速速
const int SnakeSpeedThreshold = 100;
其实蛇每次都只移动一个空格的,那他是怎么可以跑的快呢?
原因就是:gameTickTimer.Interval,它决定了计时器事件执行的间隔,计时器事件如下:
private void GameTickTimer_Tick(object sender, EventArgs e) //计时器事件
{
if (isGameRuning == false) return;
MoveSnake();//游戏正在运行。就一直移动蛇
}
接下来看“蛇”是如何移动的:
private void MoveSnake()
{
while (snakeParts.Count >= snakeLength)//这里的循环是针对一开始蛇还没完全出来的时候,就不需要移除蛇尾巴
{//蛇尾巴为是list的第一个元素
GameArea.Children.Remove(snakeParts[0].UiElement);//在Canvas里移除掉尾巴
snakeParts.RemoveAt(0);//
}
foreach (var snakePart in snakeParts)
{
(snakePart.UiElement as Rectangle).Fill = snakeBodyBrush;//换上身体的颜色
snakePart.IsHead = false;//蛇头变成身体
}
SnakePart snakeHead = snakeParts[snakeParts.Count - 1];//蛇头是list最后一个元素
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;
default:
break;
}
snakeParts.Add(new SnakePart //添个新蛇头
{
Position = new Point(nextX, nextY),
IsHead = true
}
);
DrawSnake();//重新画出来
DoCollisionCheck();//每次移动完都做判断,是否撞墙,是否撞到身体,是否吃到食物
}
所以蛇移动的本质其实是删掉蛇尾,蛇头变蛇身,然后再加一个蛇头,最后重新画出来,就形成蛇移动的画面了。其中的while循环,针对蛇没有完全出来和吃食物的情况,吃到食物或者蛇没有完全出来都不需要把尾巴去掉。
蛇每次移动都要判断蛇有没有死掉,或者吃到食物,蛇撞到身体和墙壁都结束游戏:
private void DoCollisionCheck()
{
SnakePart snakeHead = snakeParts[snakeParts.Count - 1];
if ((snakeHead.Position.X == Canvas.GetLeft(snakeFood)) &&//蛇吃食物
(snakeHead.Position.Y == Canvas.GetTop(snakeFood))//判断,蛇头的位置和食物的位置重合
)
{
EatSnakeFood();
return;
}
if ((snakeHead.Position.Y < 0) ||
(snakeHead.Position.X < 0) ||
(snakeHead.Position.Y >= GameArea.ActualHeight) ||
(snakeHead.Position.X >= GameArea.ActualWidth)
)//蛇有没有越界
{
EndGame();
return;
}
foreach (var snakeBodyPart in snakeParts.Take(snakeParts.Count - 1))
{ //蛇有没有撞到自己的身体
if ((snakeHead.Position.X == snakeBodyPart.Position.X) &&
(snakeHead.Position.Y == snakeBodyPart.Position.Y)
)
{
EndGame();
return;
}
}
}
如果蛇吃到食物:
private void EatSnakeFood()
{
snakeLength++;//蛇长增加一个
currentScore++;//分数增加一个
//增加游戏难度,即降低执行计时器事件的间隔
int timerInterval = Math.Max(SnakeSpeedThreshold, (int)(gameTickTimer.Interval.TotalMilliseconds) - (currentScore * 2));
gameTickTimer.Interval = TimeSpan.FromMilliseconds(timerInterval);//每次吃食物都会刷新速度,利用max让速度不会超过最大速
GameArea.Children.Remove(snakeFood);//去掉当前食物
DrawSnakeFood();//重新画一个食物
UpdateGameStatus();//更新游戏界面的分数和速度
}
如果结束游戏:
private void EndGame()
{
tbFinalScore.Text = currentScore.ToString();//最终分数传给界面
bdrEndOfGame.Visibility = Visibility.Visible;//游戏结束界面展示,同时也展示分数
PromptBuilder promptBuilder = new PromptBuilder();
promptBuilder.AppendText("你的得分:");
promptBuilder.AppendTextWithHint(currentScore.ToString(), SayAs.NumberCardinal);
promptBuilder.AppendText("分");//编辑播报内容
speechSynthesizer.SpeakAsync(promptBuilder);//语言播报分数
gameTickTimer.IsEnabled = false;//计时器暂停
}
游戏结束会弹出结束界面,会提示按空格开始新游戏,空格可开始新的游戏,这是因为在Windows里加了KeyUp=“Window_KeyUp”,可以监听键盘事件,而且蛇方向的改变也是在这个Window_KeyUp函数里实现的:
private void Window_KeyUp(object sender, KeyEventArgs e)
{
if (isGameRuning == false) return;//游戏没有在运行,直接结束
SnakeDirection originalSnakeDirection = snakeDirection;//得到蛇当前移动方向
switch (e.Key)
{
case Key.Up:
if (snakeDirection != SnakeDirection.Down)//蛇在往下走就不能往上
{
snakeDirection = SnakeDirection.Up;
}
break;
case Key.Down:
if (snakeDirection != SnakeDirection.Up)//蛇在往上走就不能往下
{
snakeDirection = SnakeDirection.Down;
}
break;
case Key.Left:
if (snakeDirection != SnakeDirection.Right)//蛇在往右走就不能往左
{
snakeDirection = SnakeDirection.Left;
}
break;
case Key.Right:
if (snakeDirection != SnakeDirection.Left)//蛇在往左走就不能往右
{
snakeDirection = SnakeDirection.Right;
}
break;
case Key.Space://空格,开始新的游戏
StartNewGame();
break;
default:
return;
}
if (snakeDirection != originalSnakeDirection)
{
MoveSnake();
}
}
按空格开始新的游戏:
private void StartNewGame()
{
foreach (var snakeBodyPart in snakeParts)
{
if (snakeBodyPart.UiElement != null)
{
GameArea.Children.Remove(snakeBodyPart.UiElement);//把蛇从Canvas上移除
}
}
snakeParts.Clear();//蛇的list也清空
if (snakeFood != null)
{
GameArea.Children.Remove(snakeFood);//清除残存的食物
}
snakeFood = null;
currentScore = 0;//分数清零
snakeLength = SnakeStartLength;//长度初始化
snakeDirection = SnakeDirection.Right;
snakeParts.Add(new SnakePart { Position = new Point(SnakeSquareSize * 5, SnakeSquareSize * 5) });
//蛇出现的位置
gameTickTimer.Interval = TimeSpan.FromMilliseconds(SnakeStartSpeed);
DrawSnake();//画蛇
DrawSnakeFood();//画蛇的食物
UpdateGameStatus();//更新界面分数和速度
gameTickTimer.IsEnabled = true;
gameTickTimer.Start();//开始游戏,开始计时
btPause.Visibility = Visibility.Visible;//暂停按钮可见
bdrWelcomeMessage.Visibility = Visibility.Collapsed;//欢迎页面隐藏
bdrEndOfGame.Visibility = Visibility.Collapsed;//游戏结束界面隐藏
}
其他一些功能(暂停,继续,关闭,托移):
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
this.DragMove();//允许通过鼠标进行拖移窗口
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Close();//关闭窗口
}
private void BtPause_Click(object sender, RoutedEventArgs e)
{
//游戏暂停
if (isGameRuning == false)
{
isGameRuning = true;
btPause.Content = "II";//即‘继续’变成‘暂停’
}
else
{
isGameRuning = false;
btPause.Content = "▶";//改变按钮的内容,即’暂停‘变成‘继续’
}
}
总结:
通过这次大作业,提高了我的图形界面编程的技巧还有动手能力,学到了许多新东西,比如改变控件的属性和内容,增加删除控件的子元素,还有监听键盘的操作等等。