C#入门学习-推箱子游戏(WPF技术兑现)

C#入门学习-----推箱子游戏(WPF技术实现)

欢迎大家提出意见,一起讨论!

转载请标明是引用于 http://blog.csdn.net/chenyujing1234

需要源码请与我联系。

 

 编译平台:VS2008 + .Net Framework 3.5

        语言: C#

使用工具:Expression Design 4

                    Expression Blend  (它们的获得请参考我的另一文章 http://write.blog.csdn.net/postedit/7659256)

 

3、实现游戏用户界面

尽管程序员可以使用VS编写XAML代码的方式来构造用户界面,但是对于有设计爱好的用户来说,使用类似Photoshop一样的Expression套件能将

软件美工最大化。对于怪物与目标块的图形显示,示例使用了Expression Design来设计图形,然后将其导入到Expression Blend中进行布局处理。

 

3、1 使用Expression Design设计图案

Expression Design 是一个专业的图表和图形设计工具。该工具提供了矢量图形的绘制能力,强大之处可以像PhotoShop一样设计好用户界面或是所需要量的图形,

使用其导出功能导出为XAML资源或代码。Expression Design主界面如下:

C#入门学习-推箱子游戏(WPF技术兑现)

Design 通常是与Blend紧密相关的。美工人员使用Design强大的设计功能来设计界面元素,导出给Blend进行编辑。最后通过VS设计程序代码。

在示例中,使用Design设计了一个Cell图案,在其中添加了多个图层,每一层放置各自不同的图案,比如箱子、怪物、墙体。

然后使用Design的导出功能将这些图案导出为资源字典,以便于程序引用这些图形。导出窗口如下:

C#入门学习-推箱子游戏(WPF技术兑现)

然后在App.xaml中的资源字典的定义中使用<ResourceDictionary.MergedDictionaries>指定Cell.xaml作为整个应用程序级别的资源。

C#入门学习-推箱子游戏(WPF技术兑现)

3、2 实现用户主界面

在开始布局游戏区域前,主界面上需要一个Banner来显示游戏名称,为游戏主界面添加背景图片以增加界面的效果。

游戏创建了两个用户控件来实现这样的特效,位于游戏项目的Controls文件夹下。

BackgroundControl.xaml用户控件实现非常简单,通过使用Expression Design来设计背景图片,然后导出为独立的Xaml文件 

导出设置如下:

 

C#入门学习-推箱子游戏(WPF技术兑现)

 

图形将被置于一个Canvas画布中,在MainWindow.xaml中通过引用这个控件来设置背景色。

C#入门学习-推箱子游戏(WPF技术兑现)

Banner.xmal的实现与BackgroundControl.xaml的实现类似。

主界面的布局非常简单,主要分为四行:

C#入门学习-推箱子游戏(WPF技术兑现)

(1) 第一行

主要放了一个Border、一个Rectangle、一个Viewbox(里面放Banner)

C#入门学习-推箱子游戏(WPF技术兑现)

 

(2) 第二行

在一个Grid中加入一个Rectangle、显示关卡代码、关数、按钮

C#入门学习-推箱子游戏(WPF技术兑现)

(3) 第三行

           只是一个间隔,没放东西

(4)  第四行

C#入门学习-推箱子游戏(WPF技术兑现)

 

如上,位于Viewbox的Grid, x:Name为grid_Game在游戏启动时动态创建行和列定义,并在行列中放置多个Button按钮来实现游戏的方块

在MainWindow.xaml的声明中,为窗口关联了Loaded事件,当该事件触发时,将执行Window_Load代码。这段代码在游戏窗口打开时,开始游戏第一关。

C#入门学习-推箱子游戏(WPF技术兑现)

Window_Loaded将调用在资源中实例化的Game类,在Window.Resource资源区,首先定义了Game类,代码如下:

   <!-- 用于整个游戏的Game实例. -->
		<Sokoban:Game x:Key="sokobanGame"/>

在MainWindow.xaml.cs中相应地定义了一个Game属性,该属性使用在资源中指定的x:Key键值查找Game对象实例。Game属性的声明如下:

/// <summary>
        /// 获取定义在Window资源中的Game对象的实例
		/// </summary>
		/// <value>游戏实例.</value>
		Game Game
		{
			get
			{
				return (Game)TryFindResource("sokobanGame");
			}
		}

Game属性的get获取器使用TryFindResource() 方法,传入指定的查找对象实例,并转换为Game对象。因为TryFindResource()方法返回object类型的实例。

 //
        // 摘要:
        //     搜索具有指定键的资源,如果找到,则返回该资源。
        //
        // 参数:
        //   resourceKey:
        //     要查找的资源的键标识符。
        //
        // 返回结果:
        //     找到的资源;如果未找到具有所提供 key 的资源,则为 null。
        public object TryFindResource(object resourceKey);


3、3  程序启动时加载关卡

Window_Loaded 将使用Game对象加载游戏关卡,并初始化用户界面。

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事件处理代码便会执行,该事件中的代码将开始游戏界面的更新工作。

/// <summary>
		/// 通过加载第一关来开始游戏
		/// </summary>
		public void Start()
		{
			if (sokobanService != null)
			{   //获取关卡数
				LevelCount = sokobanService.LevelCount;
			}
			else
			{	//得到关卡总数,通过获取关卡文件的个数来得到
				string[] files = Directory.GetFiles(levelDirectory, "*.skbn");
				LevelCount = files.Length;
			}
			LoadLevel(0);//加载关卡
		}


 

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的实现如下:

	/// <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是一个自定义的用于游戏界面显示消息的用户控件,该控件主要用于在用户控件上显示一些消息。

<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控件相关联。

		/// <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事件代码如下:

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事件处理代码。

	/// <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代码:

C#入门学习-推箱子游戏(WPF技术兑现)

通过WPF提供的强大的样式触发器,当移动角色或推动箱子是地,UI端会自动变更显示内容,不需要编程实现设置图形的位置。

 

上面讲到Button的Style是Rectangle,这是在InitialiseLevel()方法中,为每一个单元格创建按钮时,指定了按钮的默认模式为Cell时指定的。

button.Style = (Style)Resources["Cell"];//指定按钮样式


Cell样式指定了按钮的呈现外观,因为按钮是一个内容控件,可以通过指定其Template来改变按钮的默认呈现。Cell样式如下:

C#入门学习-推箱子游戏(WPF技术兑现)

方块显示的Rectangle指定的Style为"CellStyle",它会根据Button所关联的不同的Cell类型来显示不同的方块样式。如下:

 C#入门学习-推箱子游戏(WPF技术兑现)

 

通过这些样式触发器的设置,游戏关卡内容一旦回去,就自动使用各自不同的方块外观来显示整个游戏的布局。

当鼠标或键盘移动时,会根据CellContents的Name属性自动设置移动,实现了游戏的运行显示效果。