目錄
源碼可以在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語言,可以很好的實現非常精緻的畫面,以及和後端代碼的分離。