zoukankan      html  css  js  c++  java
  • [翻译]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 }

    代码

        略,参看上面的代码!
  • 相关阅读:
    C# FTP功能实现(转载)
    Invoke和BeginInvoke的使用(转载)
    .NET中各种数据库连接大全(转载)
    最近关注的网络资料(书签)
    SQL语句总结(转载)
    线程池和定时器——多线程的自动管理(转载)
    C#程序安装部署(转载)
    TcpClient.Connect函数连接超时的问题(转载)
    C# 各种定时器比较(转载)
    SQL SERVER中对日期字段(datetime)比较(转载)
  • 原文地址:https://www.cnblogs.com/315358525/p/1531073.html
Copyright © 2011-2022 走看看