[翻译]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个状态中的一个:
               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之间的值,说明在开始和结束状态的情况下淡出状态有多久。
               这个结构很容易初始化这些值:
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类和在当前的节里有,如何呈现文本。
                      下一步,你需要一个方法,这个方法允许你添加项目到菜单里:
1 public void AddMenuItem(string itemText,MenuWindow itemLink)
2 {
3      MenuItem newItem=new MenuItem(itemText,itemLink);
4      itemList.Add(newItem);
5 }
                      项目的标题和菜单都需要被激活,当用户选者了项目时,它被主程序传入。一个新的MenuItem被创建,添加它到itemList.
                     你同样需要一个方法,它在Inactive菜单里被激活:
1 public void WakeUp()
2 {
3      windowState=WindowState.Starting;
4 }
                  像几乎所有的一个XNA程序组成部分,这个类也必须更新:
 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
                       当这个值达到1,这个转变结束,这个状态既不从StartingActive被改变也不会从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线形的增加,这是好的,但不会顺畅的开始或结尾(译者:突然开始或者结尾)。
                            如果菜单中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被激活,使其在开始状态。
                        现在你需要更新所有你的菜单在你程序的更新循环中:
1 foreach(MenuWindow currentMenu in menuList)
2     currentMenu.Update(gameTime.ElapseGameTime.TotalMilliseconds);
                        在你程序的绘制阶段去呈现它们:
1 spriteBatch.Begin();
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)&&currentKeybState.IsKeyDown(Keys.Down))
 4          selectedItem++;
 5     if(lastKeybState.IsKeyUp(Keys.Up)&&currentKeybState.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)&&currentKeybState.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,只要键仍然是压下的!所以,你需要检查以往的时间,按键是不是一直都是按下的。
                       如果向上或向下键被按下,因此你改变了selectedItem变量。如果它已经出界,你把它重新放回一个合理的范围内。
                       下列几行包含整个导航机制。你应该注意到,这方法返回一个MenuWindow对象到主程序。由于这一方法将调用只对当前活动的菜单,这使得菜单通过新选定菜单中的主要程序。如果用户没有选择任何项目,菜单将保持活跃, 返回到本身,在最后一行实现。通过这种方式,主程序知道这菜单上是active的菜单后,输入后处理。
                       因此,如果用户按下回车键,当前活动的菜单是从Active到Ending模式,菜单上选定的项目链接,返回到主程序。 如果使用者按下按钮是Escape , null被返回,这将是被捕捉后退出应用程序。如果没有被选中,返回自己的菜单,通知的主要程序,此菜单仍然是active的。
                      这种方法需要从主要程序上调用,这需要掌握两个或更多的变量:
1 MenuWindow activeMenu;
2 KeyboardState lastKeybState;
                       第一个变量保存菜单是当前的active并LoadContent方法中初始化到mainMenu。在初始方法中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变量中。
                     保证从Update方法中调用这个方法。运行这个代码允许你选择在两个菜单中。

添加菜单标题和背景图像菜单
                   该机制运作,但为什么没有菜单的背景图片?添加这两个变量到MenuWindow类:
1 private string menuTitle;
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模式,新的背景图像与前面的图象混合。当混合第二图像和第一个图像,您需要确认您的第一个图象实际上是第一个绘制的!做到这一点并不容易, 因为这将涉及改变菜单绘制顺序。
                    一个简单的方法是使用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和渐变的值,它设置有开关的模式:
 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.WhiteColor(new Vector4(1,1,1,1))颜色一样,意思是说alpha值最大。如果一个菜单是Starting或者Ending状态,alphaValue被计算。下一步,你使用这个渐变的值去绘制标题和绘制背景图象。
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倍缩放,这将会使它比正常菜单项目看上去大了。
                     最后,你需要确认你设置的主程序的Draw方法
1 //SpriteSortMode 到BackToFront:
2 spriteBatch.Begin(SpriteBlendMode.AlphaBlend,SpriteSortMode.BackToFront,SaveStateMode.None);

从菜单到游戏

                      在这一点上,您可以创建了一些不错的前瞻性菜单,但你怎么能真正的用菜单项目启动游戏?这项工作可以使用虚拟菜单完成,它需要储存在主程序。例如,如果您想启动新游戏菜单包含项目启动一种简单,一个正常,或一场非常难的游戏,加上这些菜单:
1 MenuWindow startGameEasy;
2 MenuWindow startGameNormal;
3 MenuWindow startGameHard;
4 bool menusRunning;
                       在你的LoadContent方法中,你可以实例化这些变量并且没有参数连接到项目在menuNewGame
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方法:
 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变量确保你不会更新/绘制你菜单:
 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 }

代码

    略,参看上面的代码!

posted on 2009-07-25 22:56  一盘散沙  阅读(321)  评论(0编辑  收藏  举报

导航