[翻译]XNA 3.0 Game Programming Recipes之sixteen
PS:自己翻译的,转载请著明出处
3-6 创建一个2D菜单界面
问题
你想建立一个2D菜单界面,允许你很容易添加新的菜单和指定他们的项目。菜单应该允许用户翻阅不同的项目和菜单使用控制器/键盘来控制。当用户从一个菜单浏览到另一个时,您希望有一个很好的过度效果:
解决方案
您将创建一个新的类,MenuWindow,这将跟踪一切与菜单有关的事物,如当前的菜单状态,在菜单中的项目,背景图像,以及更多。这个类允许主程序轻松地创建多个实例,这样一个MenuWindow和新增项目添加到这些实例。该项目将以您安装在您的系统喜欢的字体,使用纯文本显示。
为了过度,一个窗口将有Starting和Ending状态,和Active和Inactive状态。控制器/键盘状态被传递到ActiveMenuWindwo中,这会使主要的程序知道用户是否选择了其中一项,并传递选项到主程序中。
给予MenuWindow储存和显示背景图象的能力这将极大加强最终的结果,同时也能提高使用post-processing的效果。
它是如何工作的
主程序将创建一些MenuWindow对象,每一个项目都会连接到另外一个MenuWindow对象。所以首先,要定义个MenuWindow类。
MenuWindow类
建立一个新的类命名为MenuWindow.每个菜单有保存项目的能力。每一个项目,文本和菜单它们都被存储。所以,在你的MenuWindow类中定义字母的结构:
1 private struct MenuItem
2 {
3 public string itemText;
4 public MenuWindow itemLink;
5 public MenuItem(string itemText,MenuWindow itemLink)
6 {
7 this.itemText=itemText;
8 this.itemLink=itemLink;
9 }
10 }
每个菜单是4个状态中的一个:2 {
3 public string itemText;
4 public MenuWindow itemLink;
5 public MenuItem(string itemText,MenuWindow itemLink)
6 {
7 this.itemText=itemText;
8 this.itemLink=itemLink;
9 }
10 }
1。Starting:这菜单已经被选择了颜色变淡了。
2。Active:这个菜单是唯一的显示在屏幕上它将是用户的输入。
3。Ending:一个项目在此菜单中被选中,因此这个菜单淡出。
4。Inactive:如果菜单上没有三个中的一个状态,它不会被绘制出。
因此,你需要枚举显示出这个状态,它将被输出这个类:
1 public enum WindowState{Starting,Active,Ending,Inactive}
下一步是变量所需要的类到工作的属性:
1 private TimeSpan changeSpan;
2 private WindowState windowState;
3 private List<MenuItem> itemList;
4 private int selectedItem;
5 private SpriteFont spriteFont;
6 private double changeProgress;
changeSpane表明淡入和淡出需要花费多少时间。你需要一些变量保持当前菜单的状态,菜单选项的列表,它是当前被选定的。changeProgress变量将保持一个在0和1之间的值,说明在开始和结束状态的情况下淡出状态有多久。2 private WindowState windowState;
3 private List<MenuItem> itemList;
4 private int selectedItem;
5 private SpriteFont spriteFont;
6 private double changeProgress;
这个结构很容易初始化这些值:
1 public MenuWindow(SpriteFont spriteFont)
2 {
3 itemList=new List<MenuItem>();
4 changeSpan=TimeSpan.FromMilliseconds(800);
5 selectedItem=0;
6 changeProgress=0;
7 windowState=WindowState.Inactive;
8 this.spriteFont=spriteFont;
9 }
在两个菜单之间的转变显示在800毫秒中完成,在Inactive的模式下一个菜单开始。你可以读所有关于SpriteFont类和在当前的节里有,如何呈现文本。2 {
3 itemList=new List<MenuItem>();
4 changeSpan=TimeSpan.FromMilliseconds(800);
5 selectedItem=0;
6 changeProgress=0;
7 windowState=WindowState.Inactive;
8 this.spriteFont=spriteFont;
9 }
下一步,你需要一个方法,这个方法允许你添加项目到菜单里:
1 public void AddMenuItem(string itemText,MenuWindow itemLink)
2 {
3 MenuItem newItem=new MenuItem(itemText,itemLink);
4 itemList.Add(newItem);
5 }
项目的标题和菜单都需要被激活,当用户选者了项目时,它被主程序传入。一个新的MenuItem被创建,添加它到itemList.2 {
3 MenuItem newItem=new MenuItem(itemText,itemLink);
4 itemList.Add(newItem);
5 }
你同样需要一个方法,它在Inactive菜单里被激活:
1 public void WakeUp()
2 {
3 windowState=WindowState.Starting;
4 }
像几乎所有的一个XNA程序组成部分,这个类也必须更新:
2 {
3 windowState=WindowState.Starting;
4 }
1 public void Update(double timePassedSinceLastFrame)
2 {
3 if((windowState==WindowState.Starting)||(windowState==WindowState.Ending))
4 changeProgress+=timePassedSinceLastFrame/changeSpan.TotalMilliseconds;
5 if(changeProgress>=1.0f)
6 {
7 changeProgress=0.0f;
8 if(windowState==WindowState.Starting)
9 windowState==WindowState.Active;
10 else if(windowState==WindowState.Ending)
11 windowState=WindowState.Inactive;
12 }
13 }
这个方法将得到直到上一次更新调用已经过去的毫秒数(见1-6节,一般来说是1000/60豪秒)。如果菜单在转换的模式下,changeProgress变量被更新,所以在大量的毫秒被存储在changSpan之后,这个值达到1。2 {
3 if((windowState==WindowState.Starting)||(windowState==WindowState.Ending))
4 changeProgress+=timePassedSinceLastFrame/changeSpan.TotalMilliseconds;
5 if(changeProgress>=1.0f)
6 {
7 changeProgress=0.0f;
8 if(windowState==WindowState.Starting)
9 windowState==WindowState.Active;
10 else if(windowState==WindowState.Ending)
11 windowState=WindowState.Inactive;
12 }
13 }
当这个值达到1,这个转变结束,这个状态既不从Starting到Active被改变也不会从Ending到Inactive被改变。
最后,你把想一些代码绘制到菜单。当菜单是Active,项目应显示,从例如,位置(300,300 ),每个项目低于上一个项目30像素。
当菜单在Starting模式下,项目应淡入(其Alpha值应增加从0到1 ) ,从屏幕的左侧到最终的位置。当在Ending模式下,项目应淡出(其Alpha值应减少) ,他们应该移动到右边。
1 public void Draw(SpriteBatch spriteBatch)
2 {
3 if(windowState==WindowState.Inactive)
4 return;
5 float smoothedProgress=MathHelper.SmoothStep(0,1,(float)changeProgress);
6 int verPosition=300;
7 float horPosition=300;
8 float alphaValue;
9 switch(windowState)
10 {
11 case WindowState.Starting:
12 horPosition-=200*(1.0f-(float)smoothedProgress);
13 alphaValue=smoothedProgress;
14 break;
15 case WindowState.Ending:
16 horPosition+=200*(float)smoothedProgress;
17 alphaValue=1.0f-smoothedProgress;
18 break;
19 default:
20 alphaValue=1;
21 break;
22 }
23 for(int itemID=0;itemID<itemList.Count;itemID++)
24 {
25 Vector2 itemPosition=new Vector2(horPosition,verPosition);
26 Color itemColor=Color.White;
27 if(itemID==selectedItem)
28 itemColor=new Color(new Vector4(1,0,0,alphaValue));
29 else
30 itemColor=new Color(new Vector4(1,1,1,alphaValue));
31 spriteBatch.DrawString(spriteFont,itemList[itemID].itemText,itemPostition,itemColor,0,Vector2.Zero,1,SpriteEffects.None,0);
32 verPosition+=30;
33 }
34 }
在Starting或Ending状态,changeProgress值将从0到1线形的增加,这是好的,但不会顺畅的开始或结尾(译者:突然开始或者结尾)。2 {
3 if(windowState==WindowState.Inactive)
4 return;
5 float smoothedProgress=MathHelper.SmoothStep(0,1,(float)changeProgress);
6 int verPosition=300;
7 float horPosition=300;
8 float alphaValue;
9 switch(windowState)
10 {
11 case WindowState.Starting:
12 horPosition-=200*(1.0f-(float)smoothedProgress);
13 alphaValue=smoothedProgress;
14 break;
15 case WindowState.Ending:
16 horPosition+=200*(float)smoothedProgress;
17 alphaValue=1.0f-smoothedProgress;
18 break;
19 default:
20 alphaValue=1;
21 break;
22 }
23 for(int itemID=0;itemID<itemList.Count;itemID++)
24 {
25 Vector2 itemPosition=new Vector2(horPosition,verPosition);
26 Color itemColor=Color.White;
27 if(itemID==selectedItem)
28 itemColor=new Color(new Vector4(1,0,0,alphaValue));
29 else
30 itemColor=new Color(new Vector4(1,1,1,alphaValue));
31 spriteBatch.DrawString(spriteFont,itemList[itemID].itemText,itemPostition,itemColor,0,Vector2.Zero,1,SpriteEffects.None,0);
32 verPosition+=30;
33 }
34 }
如果菜单中Starting或Ending模式,在结构调整的情况下改变水平位置和Alpha值的菜单项。其次,把每个菜单上的项目,标题呈现在屏幕上的正确位置。欲了解更多有关渲染信息,见以往的章节。如果该项目没有选定的项目,其文字用白色绘制,而选定项目将被绘制成红色。
它是MenuWindow类的基础!
在您的主程式,您只需列出所有存储菜单的列表:
1 List<MenuWindow> menuList;
在你的LoadContent方法中,你可以建立一个你的菜单并添加他们到menuList中。下一步,你可以添加项目到菜单中,允许你去指定哪个菜单应该被激活情况,则用户选择了这个项目。
1 MenuWindow menuMain=new MenuWindow(menuFont,"Mian Menu",backgroundImage);
2 MenuWindow menuNewGame=new MenuWindow(menuFont,"Start a New Game",bg);
3 menuList.Add(menuMain);
4 menuList.Add(menuNewGame);
5 menuMain.AddMenuItem("New Game",menuNewGame);
6 menuNewGame.AddMenuItem("Back to Main menu",menuMain);
7 menuMain.WakeUp();
你将会创建两个菜单,每个都包含一个项目它连接到另一个菜单。在你的菜单结构做了初始化之后,mainMenu被激活,使其在开始状态。2 MenuWindow menuNewGame=new MenuWindow(menuFont,"Start a New Game",bg);
3 menuList.Add(menuMain);
4 menuList.Add(menuNewGame);
5 menuMain.AddMenuItem("New Game",menuNewGame);
6 menuNewGame.AddMenuItem("Back to Main menu",menuMain);
7 menuMain.WakeUp();
现在你需要更新所有你的菜单在你程序的更新循环中:
1 foreach(MenuWindow currentMenu in menuList)
2 currentMenu.Update(gameTime.ElapseGameTime.TotalMilliseconds);
在你程序的绘制阶段去呈现它们:
2 currentMenu.Update(gameTime.ElapseGameTime.TotalMilliseconds);
1 spriteBatch.Begin();
2 foreach(MenuWindow currentMenu in menuList)
3 currentMenu.Draw(spriteBatch);
4 spriteBatch.End();
当您运行此代码,在主菜单应该从左侧淡入。您还不能移动到其他菜单,仅仅是因为你不能处理没有任何用户的输入。2 foreach(MenuWindow currentMenu in menuList)
3 currentMenu.Draw(spriteBatch);
4 spriteBatch.End();
从而使用户能够浏览菜单
您将您的MenuWindow类k扩展成一个方法处理用户的输入。请注意,此方法将被要求只能在当前活动的菜单内:
1 public MenuWindow ProcessInput(KeyboardStatee lastKeybState,KeyboardState currentKeybState)
2 {
3 if(lastKeybState.IsKeyUp(Keys.Down)&¤tKeybState.IsKeyDown(Keys.Down))
4 selectedItem++;
5 if(lastKeybState.IsKeyUp(Keys.Up)&¤tKeybState.IsKeyDown(Keys.Up))
6 selectedItem--;
7 if(selectedItem<0)
8 selectedItem=0;
9 if(selectedItem>=itemList.Count)
10 selectedItem=itemList.Count-1;
11 if(lastKeybState.IsKeyUp(Key.Enter)&¤tKeybState.IsKeyDown(Keys.Enter))
12 {
13 windowState=WindowState.Ending;
14 return itemList[selectedItem].itemLink;
15 }
16 else if(lastKeybState.IsKeyDown(Keys.Escape))
17 return null;
18 else
19 return this;
20 }
很多有趣的东西是这里发生。首先,你要检查向上或向下键是否按下。当使用者按下一个按钮不放,则IsKeyUp的键仍保持为true,只要键仍然是压下的!所以,你需要检查以往的时间,按键是不是一直都是按下的。2 {
3 if(lastKeybState.IsKeyUp(Keys.Down)&¤tKeybState.IsKeyDown(Keys.Down))
4 selectedItem++;
5 if(lastKeybState.IsKeyUp(Keys.Up)&¤tKeybState.IsKeyDown(Keys.Up))
6 selectedItem--;
7 if(selectedItem<0)
8 selectedItem=0;
9 if(selectedItem>=itemList.Count)
10 selectedItem=itemList.Count-1;
11 if(lastKeybState.IsKeyUp(Key.Enter)&¤tKeybState.IsKeyDown(Keys.Enter))
12 {
13 windowState=WindowState.Ending;
14 return itemList[selectedItem].itemLink;
15 }
16 else if(lastKeybState.IsKeyDown(Keys.Escape))
17 return null;
18 else
19 return this;
20 }
如果向上或向下键被按下,因此你改变了selectedItem变量。如果它已经出界,你把它重新放回一个合理的范围内。
下列几行包含整个导航机制。你应该注意到,这方法返回一个MenuWindow对象到主程序。由于这一方法将调用只对当前活动的菜单,这使得菜单通过新选定菜单中的主要程序。如果用户没有选择任何项目,菜单将保持活跃, 返回到本身,在最后一行实现。通过这种方式,主程序知道这菜单上是active的菜单后,输入后处理。
因此,如果用户按下回车键,当前活动的菜单是从Active到Ending模式,菜单上选定的项目链接,返回到主程序。 如果使用者按下按钮是Escape , null被返回,这将是被捕捉后退出应用程序。如果没有被选中,返回自己的菜单,通知的主要程序,此菜单仍然是active的。
这种方法需要从主要程序上调用,这需要掌握两个或更多的变量:
1 MenuWindow activeMenu;
2 KeyboardState lastKeybState;
第一个变量保存菜单是当前的active并在LoadContent方法中初始化到mainMenu。在初始方法中lastKeybState应该被初始化。
2 KeyboardState lastKeybState;
1 private void MenuInput(KeyboardState currentKeybState)
2 {
3 MenuWindow newActive=activeMenu.ProcessInput(lastKeybState,currentKeybState);
4 if(newActive==null)
5 this.Exit();
6 else if(newActive!=activeMenu)
7 newActive.WakeUp();
8 activeMenu=newActive;
9 }
此方法调用Process方法,当前active菜单和传递它以往和当前的键盘状态。正如以前所讨论,这个方法返回null 如果使用者按下按钮Escape,因此,如果是这种情况,应用程序退出。否则,如果该方法返回一个菜单不同于active菜单上,这表明该用户已取得选择。在这种情况下,新选定的菜单是从Inactive到Starting状态靠调用其WakeUp方法。无论哪种方式,菜单,返回包含Active 菜单在这个时刻,因此它需要储存在activeMenu变量中。2 {
3 MenuWindow newActive=activeMenu.ProcessInput(lastKeybState,currentKeybState);
4 if(newActive==null)
5 this.Exit();
6 else if(newActive!=activeMenu)
7 newActive.WakeUp();
8 activeMenu=newActive;
9 }
保证从Update方法中调用这个方法。运行这个代码允许你选择在两个菜单中。
添加菜单标题和背景图像菜单
该机制运作,但为什么没有菜单的背景图片?添加这两个变量到MenuWindow类:
1 private string menuTitle;
2 private Texture2D backgroundImage;
要添加到初始方法中
2 private Texture2D backgroundImage;
1 public MenuWindow(SpriteFont spriteFont,string menuTitle,Texture2D backgroundImage)
2 {
3 //
4 this.menuTite=menuTitle;
5 this.backgroundImage=backgroundImage;
6 }
显示标题应该容易。然而,绘制背景图片很麻烦,如果您使用不同的菜单背景图片。你想要的是,在这两个Active和Ending状态,图像显示。当在Starting模式,新的背景图像与前面的图象混合。当混合第二图像和第一个图像,您需要确认您的第一个图象实际上是第一个绘制的!做到这一点并不容易, 因为这将涉及改变菜单绘制顺序。2 {
3 //
4 this.menuTite=menuTitle;
5 this.backgroundImage=backgroundImage;
6 }
一个简单的方法是使用SpriteBatch.Draw方法的layerDepth参数。当在Active或Ending模式下,图片将会在距离1绘制,"deepest"层。在Starting模式,图象将会在0.5f深度绘制,所有的文本将会在距离为0处绘制。当使用SpriteSortMode.BackToFront,首先Active到Ending菜单在深度为1处将被绘制出。下一步,如果可以适用,Starting菜单将会被绘制(混合的图象已经存在了),最后所有文字被渲染在你的MenuWindow类的Draw方法,跟踪这两个变量。
1 float bgLayerDepth;
2 Color bgColor;
这里包含有背景图象的layerDepth和渐变的值,它设置有开关的模式:
2 Color bgColor;
1 switch(windowState)
2 {
3 case WindowState.Starting:
4 horPosition-=200*(1.0f-(float)smoothedProgress);
5 alphaValue=smoothedProgress;
6 bgLayerDepth=0.5f;
7 bgColor=new Color(new Vector4(1,1,1,alphaValue));
8 break;
9 case WindowState.Ending:
10 horPosition+=200*(float)smoothedProgress;
11 alphaValue=1.0f-smoothedProgress;
12 bgLayerDepth=1f;
13 bgColor=Color.White;
14 break;
15 default:
16 alphaValue=1;
17 bgLayerDepth=1;
18 bgColor=Color.White;
19 break;
20 }
Color.White和Color(new Vector4(1,1,1,1))颜色一样,意思是说alpha值最大。如果一个菜单是Starting或者Ending状态,alphaValue被计算。下一步,你使用这个渐变的值去绘制标题和绘制背景图象。
2 {
3 case WindowState.Starting:
4 horPosition-=200*(1.0f-(float)smoothedProgress);
5 alphaValue=smoothedProgress;
6 bgLayerDepth=0.5f;
7 bgColor=new Color(new Vector4(1,1,1,alphaValue));
8 break;
9 case WindowState.Ending:
10 horPosition+=200*(float)smoothedProgress;
11 alphaValue=1.0f-smoothedProgress;
12 bgLayerDepth=1f;
13 bgColor=Color.White;
14 break;
15 default:
16 alphaValue=1;
17 bgLayerDepth=1;
18 bgColor=Color.White;
19 break;
20 }
1 Color titleColor=new Color(new Vector4(1,1,1,alphaValue));
2 spriteBatch.Draw(backgroundImage,new Vector2(),null,bgColor,0,Vector2.Zero,1,SpriteEffects.None,bgLayerDepth);
3 spriteBatch.DrawString(spriteFont,menuTilte,new Vector2(horPosition,200),titleColor,0,Vector2.Zero,1.5f,SpriteEffects.None,0);
你可以看见这个标题被1.5f倍缩放,这将会使它比正常菜单项目看上去大了。2 spriteBatch.Draw(backgroundImage,new Vector2(),null,bgColor,0,Vector2.Zero,1,SpriteEffects.None,bgLayerDepth);
3 spriteBatch.DrawString(spriteFont,menuTilte,new Vector2(horPosition,200),titleColor,0,Vector2.Zero,1.5f,SpriteEffects.None,0);
最后,你需要确认你设置的主程序的Draw方法
1 //SpriteSortMode 到BackToFront:
2 spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.BackToFront,SaveStateMode.None);
2 spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.BackToFront,SaveStateMode.None);
从菜单到游戏
在这一点上,您可以创建了一些不错的前瞻性菜单,但你怎么能真正的用菜单项目启动游戏?这项工作可以使用虚拟菜单完成,它需要储存在主程序。例如,如果您想启动新游戏菜单包含项目启动一种简单,一个正常,或一场非常难的游戏,加上这些菜单:
1 MenuWindow startGameEasy;
2 MenuWindow startGameNormal;
3 MenuWindow startGameHard;
4 bool menusRunning;
在你的LoadContent方法中,你可以实例化这些变量并且没有参数连接到项目在menuNewGame。
2 MenuWindow startGameNormal;
3 MenuWindow startGameHard;
4 bool menusRunning;
1 startGameEasy=new MenuWindow(null,null,null);
2 startGameNormal=new MenuWindow(null,null,null);
3 startGameHard=new MenuWindow(null,null,null);
4 menuNewGame.AddMenuItem("Easy",startGameEasy);
5 menuNewGame.AddMenuItem("Normal",startGameNormal);
6 menuNewGame.AddMenuItem("Hard",startGameHard);
7 menuNewGame.AddMenuItem("Back to Main menu",menuMain);
这四个项目将新增到新的游戏菜单中。您需要做的下一步是检测是否任一虚拟菜单已经选定。因此,扩展您的MenuInput方法:
2 startGameNormal=new MenuWindow(null,null,null);
3 startGameHard=new MenuWindow(null,null,null);
4 menuNewGame.AddMenuItem("Easy",startGameEasy);
5 menuNewGame.AddMenuItem("Normal",startGameNormal);
6 menuNewGame.AddMenuItem("Hard",startGameHard);
7 menuNewGame.AddMenuItem("Back to Main menu",menuMain);
1 private void MenuInput(KeyboardState currentKeybState)
2 {
3 MenuWindow newActive=activeMenu.ProcessInput(lastKeybState,currentKeybState);
4 if(newActive==startGameEasy)
5 {
6 //set level to easy
7 menusRunning=false;
8 }
9 else if(newActive==startGameNormal)
10 {
11 //set level to normal
12 menusRunning=false;
13 }
14 else if(newActive==startGameHard)
15 {
16 //set level to hard
17 menusRunning=false;
18 }
19 else if(newActive==null)
20 this.Exit();
21 else if(newActive!=activeMenu)
22 newActive.WakeUp();
23 activeMenu=newActive;
24 }
当用户在游戏中时,你可以使用menusRunning变量确保你不会更新/绘制你菜单:
2 {
3 MenuWindow newActive=activeMenu.ProcessInput(lastKeybState,currentKeybState);
4 if(newActive==startGameEasy)
5 {
6 //set level to easy
7 menusRunning=false;
8 }
9 else if(newActive==startGameNormal)
10 {
11 //set level to normal
12 menusRunning=false;
13 }
14 else if(newActive==startGameHard)
15 {
16 //set level to hard
17 menusRunning=false;
18 }
19 else if(newActive==null)
20 this.Exit();
21 else if(newActive!=activeMenu)
22 newActive.WakeUp();
23 activeMenu=newActive;
24 }
1 if(menusRunning)
2 {
3 spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.BackToFront,SaveStateMode.None);
4 foreach(MenuWindow currentMenu in menuList)
5 currentMenu.Draw(spriteBatch);
6 spriteBatch.End();
7 Window.Title="Menu running..";
8 }
9 else
10 {
11 window.Title="Game running.";
12 }
2 {
3 spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.BackToFront,SaveStateMode.None);
4 foreach(MenuWindow currentMenu in menuList)
5 currentMenu.Draw(spriteBatch);
6 spriteBatch.End();
7 Window.Title="Menu running..";
8 }
9 else
10 {
11 window.Title="Game running.";
12 }
代码
略,参看上面的代码!