编译平台:VS2008 + .Net Framework 3.5
语言: C#
使用工具:Expression Design 4
Expression Blend
3、实现游戏用户界面
尽管程序员可以使用VS编写XAML代码的方式来构造用户界面,但是对于有设计爱好的用户来说,使用类似Photoshop一样的Expression套件能将
软件美工最大化。对于怪物与目标块的图形显示,示例使用了Expression Design来设计图形,然后将其导入到Expression Blend中进行布局处理。
3、1 使用Expression Design设计图案
Expression Design 是一个专业的图表和图形设计工具。该工具提供了矢量图形的绘制能力,强大之处可以像PhotoShop一样设计好用户界面或是所需要量的图形,
使用其导出功能导出为XAML资源或代码。Expression Design主界面如下:
Design 通常是与Blend紧密相关的。美工人员使用Design强大的设计功能来设计界面元素,导出给Blend进行编辑。最后通过VS设计程序代码。
在示例中,使用Design设计了一个Cell图案,在其中添加了多个图层,每一层放置各自不同的图案,比如箱子、怪物、墙体。
然后使用Design的导出功能将这些图案导出为资源字典,以便于程序引用这些图形。导出窗口如下:
然后在App.xaml中的资源字典的定义中使用<ResourceDictionary.MergedDictionaries>指定Cell.xaml作为整个应用程序级别的资源。
3、2 实现用户主界面
在开始布局游戏区域前,主界面上需要一个Banner来显示游戏名称,为游戏主界面添加背景图片以增加界面的效果。
游戏创建了两个用户控件来实现这样的特效,位于游戏项目的Controls文件夹下。
BackgroundControl.xaml用户控件实现非常简单,通过使用Expression Design来设计背景图片,然后导出为独立的Xaml文件。
导出设置如下:
图形将被置于一个Canvas画布中,在MainWindow.xaml中通过引用这个控件来设置背景色。
Banner.xmal的实现与BackgroundControl.xaml的实现类似。
主界面的布局非常简单,主要分为四行:
(1) 第一行
主要放了一个Border、一个Rectangle、一个Viewbox(里面放Banner)
(2) 第二行
在一个Grid中加入一个Rectangle、显示关卡代码、关数、按钮
(3) 第三行
只是一个间隔,没放东西
(4) 第四行
如上,位于Viewbox的Grid, x:Name为grid_Game在游戏启动时动态创建行和列定义,并在行列中放置多个Button按钮来实现游戏的方块。
在MainWindow.xaml的声明中,为窗口关联了Loaded事件,当该事件触发时,将执行Window_Load代码。这段代码在游戏窗口打开时,开始游戏第一关。
Window_Loaded将调用在资源中实例化的Game类,在Window.Resource资源区,首先定义了Game类,代码如下:
[csharp]
<!-- 用于整个游戏的Game实例. -->
lt;Sokoban:Game x:Key="sokobanGame"/>
<!-- 用于整个游戏的Game实例. -->
<Sokoban:Game x:Key="sokobanGame"/>在MainWindow.xaml.cs中相应地定义了一个Game属性,该属性使用在资源中指定的x:Key键值查找Game对象实例。Game属性的声明如下:
[csharp]
/// <summary>
/// 获取定义在Window资源中的Game对象的实例
/// </summary>
/// <value>游戏实例.</value>
Game Game
{
get
{
return (Game)TryFindResource("sokobanGame");
}
}
/// <summary>
/// 获取定义在Window资源中的Game对象的实例
/// </summary>
/// <value>游戏实例.</value>
Game Game
{
get
{
return (Game)TryFindResource("sokobanGame");
}
}Game属性的get获取器使用TryFindResource() 方法,传入指定的查找对象实例,并转换为Game对象。因为TryFindResource()方法返回object类型的实例。
[csharp]
//
// 摘要:
// 搜索具有指定键的资源,如果找到,则返回该资源。
//
// 参数:
// resourceKey:
// 要查找的资源的键标识符。
//
// 返回结果:
// 找到的资源;如果未找到具有所提供 key 的资源,则为 null。
public object TryFindResource(object resourceKey);
//
// 摘要:
// 搜索具有指定键的资源,如果找到,则返回该资源。
//
// 参数:
// resourceKey:
// 要查找的资源的键标识符。
//
// 返回结果:
// 找到的资源;如果未找到具有所提供 key 的资源,则为 null。
public object TryFindResource(object resourceKey);
3、3 程序启动时加载关卡
Window_Loaded 将使用Game对象加载游戏关卡,并初始化用户界面。
[csharp]
void Window_Loaded(object sender, RoutedEventArgs e)
{ //当Game的Level属性发生变更时,会触发PropertyChanged事件
Game.PropertyChanged += game_PropertyChanged;
try
{
/* 加载并开始第一级游戏,Level属性变更,
* 触发Game.PropertyChanged */
Game.Start();
}
catch (Exception ex)
{ //异常处理消息。
MessageBox.Show("加载游戏出现异常. " + ex.Message);
}
}
void Window_Loaded(object sender, RoutedEventArgs e)
{ //当Game的Level属性发生变更时,会触发PropertyChanged事件
Game.PropertyChanged += game_PropertyChanged;
try
{
/* 加载并开始第一级游戏,Level属性变更,
* 触发Game.PropertyChanged */
Game.Start();
}
catch (Exception ex)
{ //异常处理消息。
MessageBox.Show("加载游戏出现异常. " + ex.Message);
}
}Game.Start将开始游戏的每一关,Start() 方法调用的LoadLevel()方法内部触发了Game.PropertyChanged事件。
只要游戏的状态发生变化,game_PropertyChanged事件处理代码便会执行,该事件中的代码将开始游戏界面的更新工作。
[csharp]
/// <summary>
/// 通过加载第一关来开始游戏
/// </summary>
public void Start()
{
if (sokobanService != null)
{ //获取关卡数
LevelCount = sokobanService.LevelCount;
}
else
{ //得到关卡总数,通过获取关卡文件的个数来得到
string[] files = Directory.GetFiles(levelDirectory, "*.skbn");
LevelCount = files.Length;
}
LoadLevel(0);//加载关卡
}
/// <summary>
/// 通过加载第一关来开始游戏
/// </summary>
public void Start()
{
if (sokobanService != null)
{ //获取关卡数
LevelCount = sokobanService.LevelCount;
}
else
{ //得到关卡总数,通过获取关卡文件的个数来得到
string[] files = Directory.GetFiles(levelDirectory, "*.skbn");
LevelCount = files.Length;
}
LoadLevel(0);//加载关卡
}
[csharp]
void game_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{ //判断传入的属性名称
switch (e.PropertyName)
{
case "GameState"://如果为GameState变更
UpdateGameDisplay();//更新游戏的界面显示
break;
}
}
void game_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{ //判断传入的属性名称
switch (e.PropertyName)
{
case "GameState"://如果为GameState变更
UpdateGameDisplay();//更新游戏的界面显示
break;
}
}
在以上代码中,判断PropertyName是否为GameState,如果为GameState属性发生变更,则调用UpdateGameDisplay来更新游戏界面的显示。
3、4 更新游戏界面的显示
UpdateGameDisplay方法将根据游戏的状态显示不同的信息,使玩家理解游戏当前所在的状态。
如果游戏处于开始运行状态,将调用Initialiselevel() 方法来初始化游戏的关卡。UpdateGameDisplay的实现如下:
[csharp]
/// <summary>
/// 设置游戏进度状态显示,
/// </summary>
void UpdateGameDisplay()
{
switch (Game.GameState)
{
case GameState.Loading://如果游戏处于加载中
FeedbackControl1.Message = //在界面上显示响应消息
new FeedbackMessage { Message = "正在加载..." };
ContinuePromptVisible = false;
break;
case GameState.GameOver://如果游戏结束状态
FeedbackControl1.Message =//显示结束信息
new FeedbackMessage { Message = "游戏结束" };
ContinuePromptVisible = true;
break;
case GameState.Running: //如果游戏处于开始运行状态
ContinuePromptVisible = false;
FeedbackControl1.Message = new FeedbackMessage();
InitialiseLevel();//初始化游戏关卡界面
break;
case GameState.LevelCompleted: //如果玩家玩过关
FeedbackControl1.Message = //显示玩过关的消息
new FeedbackMessage { Message = "恭喜您,成功过关!" };
MediaElement_LevelComplete.Position = TimeSpan.MinValue;
MediaElement_LevelComplete.Play();//播放声音
ContinuePromptVisible = true;
break;
case GameState.GameCompleted://如果玩完了所有的关卡
FeedbackControl1.Message = new FeedbackMessage //显示最终信息
{ Message = "干得好. \n游戏完成! \n请在Codeproejct联系游戏作者" };
MediaElement_GameComplete.Position = TimeSpan.MinValue;
MediaElement_GameComplete.Play();//播放完成音乐
break;
}
}
/// <summary>
/// 设置游戏进度状态显示,
/// </summary>
void UpdateGameDisplay()
{
switch (Game.GameState)
{
case GameState.Loading://如果游戏处于加载中
FeedbackControl1.Message = //在界面上显示响应消息
new FeedbackMessage { Message = "正在加载..." };
ContinuePromptVisible = false;
break;
case GameState.GameOver://如果游戏结束状态
FeedbackControl1.Message =//显示结束信息
new FeedbackMessage { Message = "游戏结束" };
ContinuePromptVisible = true;
break;
case GameState.Running: //如果游戏处于开始运行状态
ContinuePromptVisible = false;
FeedbackControl1.Message = new FeedbackMessage();
InitialiseLevel();//初始化游戏关卡界面
break;
case GameState.LevelCompleted: //如果玩家玩过关
FeedbackControl1.Message = //显示玩过关的消息
new FeedbackMessage { Message = "恭喜您,成功过关!" };
MediaElement_LevelComplete.Position = TimeSpan.MinValue;
MediaElement_LevelComplete.Play();//播放声音
ContinuePromptVisible = true;
break;
case GameState.GameCompleted://如果玩完了所有的关卡
FeedbackControl1.Message = new FeedbackMessage //显示最终信息
{ Message = "干得好. \n游戏完成! \n请在Codeproejct联系游戏作者" };
MediaElement_GameComplete.Position = TimeSpan.MinValue;
MediaElement_GameComplete.Play();//播放完成音乐
break;
}
}
UpdateGameDisplay()方法通过判断Game类中定义的GameState来向UI界面显示游戏状态消息。
FeedbackControl1是一个自定义的用于游戏界面显示消息的用户控件,该控件主要用于在用户控件上显示一些消息。
[csharp]
<Controls:FeedbackControl Grid.Row="3" x:Name="FeedbackControl1" Margin="10,10,10,10" Click="FeedbackControl1_Click"/>
<Controls:FeedbackControl Grid.Row="3" x:Name="FeedbackControl1" Margin="10,10,10,10" Click="FeedbackControl1_Click"/>
UpdateGameDisplay初始化关卡界面是在Running状态,即游戏开始运行后,开始初始化关卡,
这是通过 InitialiseLevel();初始化游戏关卡界面。
InitialiseLevel();方法将初始化学Grid的行列定义,并在行列中放置Button控件作为方块的容器。
由于在调用InitialiseLevel();前,LoadLevel()方法已经加载了关卡数据到Level类中,因此可以将Cell对象与指定的Button控件相关联。
[csharp]
/// <summary>
/// 使用游戏级别初始化Grid
/// </summary>
void InitialiseLevel()
{
commandManager.Clear();//清除命令集合
//清除Grid子元素集合,以及行列定义集合
grid_Game.Children.Clear();
grid_Game.RowDefinitions.Clear();
grid_Game.ColumnDefinitions.Clear();
//根据关卡中的行数向Grid中添加行定义
for (int i = 0; i < Game.Level.RowCount; i++)
{
grid_Game.RowDefinitions.Add(new RowDefinition());
}
//根据关卡中的列数向Grid中添加列定义
for (int i = 0; i < Game.Level.ColumnCount; i++)
{
grid_Game.ColumnDefinitions.Add(new ColumnDefinition());
}
for (int row = 0; row < Game.Level.RowCount; row++)
{ //循环遍历行
for (int column = 0; column < Game.Level.ColumnCount; column++)
{ //循环遍历列
Cell cell = Game.Level[row, column];//得到行列中的Cell对象
cell.PropertyChanged += cell_PropertyChanged;//关联属性变更事件
Button button = new Button();//实例化Button控件
button.Focusable = false; //该控件不允许获取焦点
//将Button的DataContext指定为cell对象,以便在XAML中控制
button.DataContext = cell;
button.Padding = new Thickness(0, 0, 0, 0);//按钮无边框
button.Style = (Style)Resources["Cell"];//指定按钮样式
button.Click += Cell_Click;//关联按钮单击事件
//通过附加属性设置按锯位于Grid的行和列
Grid.SetColumn(button, column);
Grid.SetRow(button, row);
grid_Game.Children.Add(button);//将按钮添加到Grid控件列表中
}
}
textBox_LevelCode.Text = Game.LevelCode;//显示关卡号
label_LevelNumber.Content = //显示当前关卡和总关卡信息
Game.Level.LevelNumber + 1 + "/" + Game.LevelCount;
grid_Main.DataContext = Game.Level;//设置主界面的DataContext为关卡
mediaElement_Intro.Position = TimeSpan.MinValue;//播放介绍音乐
mediaElement_Intro.Play(); //播放音乐
grid_Game.Focus(); //游戏区域得到焦点
}
/// <summary>
/// 使用游戏级别初始化Grid
/// </summary>
void InitialiseLevel()
{
commandManager.Clear();//清除命令集合
//清除Grid子元素集合,以及行列定义集合
grid_Game.Children.Clear();
grid_Game.RowDefinitions.Clear();
grid_Game.ColumnDefinitions.Clear();
//根据关卡中的行数向Grid中添加行定义
for (int i = 0; i < Game.Level.RowCount; i++)
{
grid_Game.RowDefinitions.Add(new RowDefinition());
}
//根据关卡中的列数向Grid中添加列定义
for (int i = 0; i < Game.Level.ColumnCount; i++)
{
grid_Game.ColumnDefinitions.Add(new ColumnDefinition());
}
for (int row = 0; row < Game.Level.RowCount; row++)
{ //循环遍历行
for (int column = 0; column < Game.Level.ColumnCount; column++)
{ //循环遍历列
Cell cell = Game.Level[row, column];//得到行列中的Cell对象
cell.PropertyChanged += cell_PropertyChanged;//关联属性变更事件
Button button = new Button();//实例化Button控件
button.Focusable = false; //该控件不允许获取焦点
//将Button的DataContext指定为cell对象,以便在XAML中控制
button.DataContext = cell;
button.Padding = new Thickness(0, 0, 0, 0);//按钮无边框
button.Style = (Style)Resources["Cell"];//指定按钮样式
button.Click += Cell_Click;//关联按钮单击事件
//通过附加属性设置按锯位于Grid的行和列
Grid.SetColumn(button, column);
Grid.SetRow(button, row);
grid_Game.Children.Add(button);//将按钮添加到Grid控件列表中
}
}
textBox_LevelCode.Text = Game.LevelCode;//显示关卡号
label_LevelNumber.Content = //显示当前关卡和总关卡信息
Game.Level.LevelNumber + 1 + "/" + Game.LevelCount;
grid_Main.DataContext = Game.Level;//设置主界面的DataContext为关卡
mediaElement_Intro.Position = TimeSpan.MinValue;//播放介绍音乐
mediaElement_Intro.Play(); //播放音乐
grid_Game.Focus(); //游戏区域得到焦点
}
InitialiseLevel首先清除命令管理器中的命令列表,清除Grid中的行列定义及子内容。
然后根据游戏关卡的行数和列数创建行列定义,循环遍历行列。先获取在Game.Level 实例中加载的cell 对象。以便与稍后将要创建的Button控件相关联。
在获取了Cell并设置了Cell的PropertyChanged事件为cell_PropertyChanged事件处理代码后实例化一个Buttons控件。
该Button控件将作为一个游戏方块,指定其不能得焦点,不能具有边界,并关联其Click单击事件。
最后将该Button添加到Grid中。
cell_PropertyChanged事件主要用于判断当CellContents属性发生变更,将根据方块的内容是一个箱子还是角色来播放不同的音乐。
3、5 处理方块单击事件
当用户单击某个按钮时,会执行Cell_Click事件处理代码。
该事件处理代码将根据鼠标单击的位置,让怪物转到当前单击的位置点,如果玩家是按下键盘的确Shit键并单击鼠标,将执行PushCommand命令,
否则JumpCommand命令。命令的执行是通过CommandManager这个命令管理器来实现的。Cell_Click事件代码如下:
[csharp]
void Cell_Click(object sender, RoutedEventArgs e)
{
Button button = (Button)e.Source;//得到当前单击的Button实例
Cell cell = (Cell)button.DataContext;//获取DataContext
CommandBase command; //要执行的命令变量
if (Keyboard.IsKeyDown(Key.LeftShift) //如果按下左或右Shift
|| Keyboard.IsKeyDown(Key.RightShift))
{ //实例化一个PushCommand执行推动命令
command = new PushCommand(Game.Level, cell.Location);
}
else //如果没有按下Shift,则执行跳转命令
{ //实例化跳转命令
command = new JumpCommand(Game.Level, cell.Location);
}//使用CommandManager命令管理器执行命令
commandManager.Execute(command);
}
void Cell_Click(object sender, RoutedEventArgs e)
{
Button button = (Button)e.Source;//得到当前单击的Button实例
Cell cell = (Cell)button.DataContext;//获取DataContext
CommandBase command; //要执行的命令变量
if (Keyboard.IsKeyDown(Key.LeftShift) //如果按下左或右Shift
|| Keyboard.IsKeyDown(Key.RightShift))
{ //实例化一个PushCommand执行推动命令
command = new PushCommand(Game.Level, cell.Location);
}
else //如果没有按下Shift,则执行跳转命令
{ //实例化跳转命令
command = new JumpCommand(Game.Level, cell.Location);
}//使用CommandManager命令管理器执行命令
commandManager.Execute(command);
}
代码首先得到当前单击的Button对象,根据该Button对象获取到与其相关联的cell对象。
然后判断当前是否按下了左或右Shit键。
如理按下了Shit键,则实例化PushCommand命令,否则实例化JumpCommand命令,要求CommandManager命令
管理器来执行相应的推移或跳转操作。
3、6 使用Command模式发送命令请求
Command模式是为了让命令的请求与命令的执行者进行解耦。CommandManager只对抽象的CommandBase进行执行,而不与实际的
命令代码进行解耦。
在用户界面上,当用户执行命令时,传递实际的命令给CommandManager 执行,实际上这是利用了面向对象的多态技术。
当用户界面按下键盘上的按钮时,一系列具体的命令对象产生,然后交给CommandManager命令管理器进行执行。
在MainWindow.xaml的定义中,响应了主窗体的KeyDown事件,将执行Window_KeyDown事件处理代码。
[csharp]
/// <summary>
/// 处理Window控件的KeyDown事件
/// </summary>
void Window_KeyDown(object sender, KeyEventArgs e)
{
CommandBase command = null;//用于保存命令的变量
Level level = Game.Level; //得到当前关卡对象实例
if (Game != null) //如果己经初始化了Game
{ //判断当前游戏的状态是否在运行状态
if (Game.GameState == GameState.Running)
{
switch (e.Key) //获取按键
{
case Key.Up://如果是向上方向键,则向上移动
command = new MoveCommand(level, Direction.Up);
break;
case Key.Down://如果是向下方向键,则向下移动
command = new MoveCommand(level, Direction.Down);
break;
case Key.Left://如果是向左方向键,则向左移动
command = new MoveCommand(level, Direction.Left);
break;
case Key.Right://如果是向右方向键,则向右移动
command = new MoveCommand(level, Direction.Right);
break;
case Key.Z://如果是Ctrl+Z键
if (Keyboard.Modifiers == ModifierKeys.Control)
{ //执行Undo操作,将从撤消堆栈中取上一次执行的命令
commandManager.Undo();
}
break;
case Key.Y://如果是Ctrl+Y键
if (Keyboard.Modifiers == ModifierKeys.Control)
{ //执行Redo操作,将从命令堆栈中取上一次执行的命令
commandManager.Redo();
}
break;
}
}
else
{
switch (Game.GameState) //根据游戏的不同状态判断
{
case GameState.GameOver://如果游戏结束
Game.Start(); //按任意键重新开始
break;
case GameState.LevelCompleted://如果关卡玩过关
Game.GotoNextLevel();//按任意键开始下一关
break;
}
}
}
if (command != null)//根据己经赋好的命令对象
{ //使用命令管理器的Execute执行命令
commandManager.Execute(command);
}
}
/// <summary>
/// 处理Window控件的KeyDown事件
/// </summary>
void Window_KeyDown(object sender, KeyEventArgs e)
{
CommandBase command = null;//用于保存命令的变量
Level level = Game.Level; //得到当前关卡对象实例
if (Game != null) //如果己经初始化了Game
{ //判断当前游戏的状态是否在运行状态
if (Game.GameState == GameState.Running)
{
switch (e.Key) //获取按键
{
case Key.Up://如果是向上方向键,则向上移动
command = new MoveCommand(level, Direction.Up);
break;
case Key.Down://如果是向下方向键,则向下移动
command = new MoveCommand(level, Direction.Down);
break;
case Key.Left://如果是向左方向键,则向左移动
command = new MoveCommand(level, Direction.Left);
break;
case Key.Right://如果是向右方向键,则向右移动
command = new MoveCommand(level, Direction.Right);
break;
case Key.Z://如果是Ctrl+Z键
if (Keyboard.Modifiers == ModifierKeys.Control)
{ //执行Undo操作,将从撤消堆栈中取上一次执行的命令
commandManager.Undo();
}
break;
case Key.Y://如果是Ctrl+Y键
if (Keyboard.Modifiers == ModifierKeys.Control)
{ //执行Redo操作,将从命令堆栈中取上一次执行的命令
commandManager.Redo();
}
break;
}
}
else
{
switch (Game.GameState) //根据游戏的不同状态判断
{
case GameState.GameOver://如果游戏结束
Game.Start(); //按任意键重新开始
break;
case GameState.LevelCompleted://如果关卡玩过关
Game.GotoNextLevel();//按任意键开始下一关
break;
}
}
}
if (command != null)//根据己经赋好的命令对象
{ //使用命令管理器的Execute执行命令
commandManager.Execute(command);
}
}
代码中的CommandBase 抽象基类用来保存具体的Command命令,这是利用了多态的原理。
如果游戏全Game 不为null,将根据游戏是否在运行状态获取用户所按下的按钮。
3、7 使用MultiDataTrigger改变方块外观
尽管使用Grid和Button布好了游戏界面,但是默认情况下,所有的Button都使用Cell外观。
幸好WPF提供了强大的样式触发器,可以根据指定的条件变更其外观。
到目前为止,已经使用了Expression Design创建了用于不同方块的图案及角色形状,并且这些图形都以画刷的形式嵌入到了应用程序的资源中。
那么通过MultiDataTrigger就可以根据特定的Cell名称来应用不同的按钮填充,使得按钮可以显示不同的外观。
对于方块内容样式,比如方块中是否有一个箱子,或是当前角色移动到某个方块的位置,使用MultiDataTrigger多条件触发器定义以下XAML代码:
通过WPF提供的强大的样式触发器,当移动角色或推动箱子是地,UI端会自动变更显示内容,不需要编程实现设置图形的位置。
上面讲到Button的Style是Rectangle,这是在InitialiseLevel()方法中,为每一个单元格创建按钮时,指定了按钮的默认模式为Cell时指定的。
[csharp]
button.Style = (Style)Resources["Cell"];//指定按钮样式
button.Style = (Style)Resources["Cell"];//指定按钮样式
Cell样式指定了按钮的呈现外观,因为按钮是一个内容控件,可以通过指定其Template来改变按钮的默认呈现。Cell样式如下:
方块显示的Rectangle指定的Style为"CellStyle",它会根据Button所关联的不同的Cell类型来显示不同的方块样式。如下:
通过这些样式触发器的设置,游戏关卡内容一旦回去,就自动使用各自不同的方块外观来显示整个游戏的布局。
当鼠标或键盘移动时,会根据CellContents的Name属性自动设置移动,实现了游戏的运行显示效果。
作者:chenyujing1234