zoukankan      html  css  js  c++  java
  • 处理2D图像和纹理——创建2D菜单界面

    问题

    你想创建一个2D菜单界面,让你可以容易地添加新的菜单和指定它们的菜单选项。这个菜单允许用户使用控制器/键盘切换不同的选项和菜单,当用户从一个菜单切换到另一个菜单时还可以定义漂亮的过渡效果。

    解决方案

    你将创建一个新的类,MenuWindow,这个类保存所有与菜单相关的东西,诸如菜单的当前状态,菜单项,背景图像等。这个类让主程序可以容易地创建多个MenuWindow实例并将菜单项添加到这些实例中。菜单项可以使用你的系统中安装的字体用2D文字的方式显示。

    要实现过渡效果,一个窗口将拥有Starting、Ending、Active和Inactive状态。controller/keyboard状态会被传递到Active MenuWindow,可以通过传递选择的菜单让主程序知道用户是否选择了一个菜单项。

    让MenuWindow可以存储和显示背景图像可以增强最终效果,这还可以使用后期处理效果加以改进(见教程2-12)。

    工作原理

    主程序会创建几个MenuWindow对象,每个菜单项都会链接到另一个MenuWindow对象。所以首先定义一个MenuWindow类。 MenuWindow类创建一个叫做MenuWindow的新类。每个菜单能够存储它的菜单项。对每个菜单项,必须存储文字和指向的菜单。所以,在MenuWindow类中定义下述结构:

    private struct MenuItem 
    {
        public string itemText; 
        public MenuWindow itemLink; 
        public MenuItem(string itemText, MenuWindow itemLink) 
        
        {
            this.itemText = itemText; 
            this.itemLink = itemLink; 
        }
    } 

    每个菜单总是处于下面四个状态之一:

    • Starting:菜单刚刚被选择,正在淡入。
    • Active:此菜单为屏幕上显示的唯一一个菜单并处理用户输入。
    • Ending:此菜单中的一个菜单项被选择,因此它正在淡出。
    • Inactive:如果此菜单不在前面三个状态之中,那么它不被绘制。

    所以你需要一个枚举表示状态,枚举应放在类的外部:

    public enum WindowState 
    {
        Starting, 
        Active, 
        Ending, 
        Inactive 
    } 

    然后是使类正常工作所需的变量:

    private TimeSpan changeSpan; 
    private WindowState windowState; 
    private List<MenuItem> itemList; 
    private int selectedItem; 
    private SpriteFont spriteFont; 
    private double changeProgress;

    changeSpan表示淡入淡出持续的时间。然后你需要一些变量保存菜单的当前状态、菜单项的集合和当前选择的菜单项。变量changeProgress保存一个介于0和1之间的值表示在Starting或Ending状态时淡入淡出处在过程的何处。

    构造函数只是简单地初始化这些变量:

    public MenuWindow(SpriteFont spriteFont) 
    {
        itemList = new List<MenuItem>(); 
        changeSpan = TimeSpan.FromMilliseconds(800); 
        selectedItem = 0; 
        changeProgress = 0; 
        windowState = WindowState.Inactive; 
        this.spriteFont = spriteFont; 
    } 

    你指定了两个菜单间的过渡持续800毫秒的时间,菜单开始时的状态为Inactive。你可以在前一个教程中学到SpriteFont类和如何绘制文字。

    然后你需要一个方法添加菜单项:

    public void AddMenuItem(string itemText, MenuWindow itemLink) 
    {
        MenuItem newItem = new MenuItem(itemText, itemLink); 
        itemList.Add(newItem); 
    } 

    当用户选择菜单项时,菜单项上的文字和菜单需要被激活,被主程序传递。一个新的菜单项被创建并被添加到itemList中。你还需要一个方法激活一个Inactive菜单:

    public void WakeUp() 
    {
        windowState = WindowState.Starting; 
    } 

    像XNA程序中的大多数组件一样。这个类需要被更新:

    public void Update(double timePassedSinceLastFrame) 
    {
        if ((windowState == WindowState.Starting) || (windowState == WindowState.Ending)) 
            changeProgress += timePassedSinceLastFrame / changeSpan.TotalMilliseconds; 
        if (changeProgress >= 1.0f) 
        {
            changeProgress = 0.0f; 
            if (windowState == WindowState.Starting) 
                windowState = WindowState.Active; 
            else if (windowState == WindowState.Ending) 
                windowState = WindowState.Inactive; 
        }
    } 

    这个方法接受上一个update调用以来经历的毫秒数为参数(通常这个参数为1000/60 毫秒,见教程1-5)。如果菜单正在过渡模式中,变量changeProgress进行更新,导致在经过了存储在changeSpan (800,前面你已经定义了)中的毫秒后,这个值达到1。

    当这个值达到1,过渡结束,状态要么从Starting变换到Active,要么从Ending变换为Inactive。

    最后,你需要一些代码绘制菜单。当菜单处于Active状态时,必须显示菜单项,例如从位置(300,300)开始,每个菜单项位于前一个之下30个像素。

    当菜单在Starting状态时,菜单项应该淡入(它们的alpha值应该从0增加到1)并且从屏幕的左边移动至最终的位置。如果处于Ending状态,菜单项应该淡出 (它们的alpha值应该减少)并且移动到右方。

    public void Draw(SpriteBatch spriteBatch) 
    {
        if (windowState == WindowState.Inactive) 
            return; 
        
        float smoothedProgress = MathHelper.SmoothStep(0,1,(float)changeProgress); 
        int verPosition = 300; 
        float horPosition = 300; 
        float alphaValue; 
        
        switch (windowState) 
        {
            case WindowState.Starting: 
                horPosition -= 200 * (1.0f - (float)smoothedProgress); 
                alphaValue = smoothedProgress; 
                break; 
            case WindowState.Ending: 
                horPosition += 200 * (float)smoothedProgress; 
                alphaValue = 1.0f - smoothedProgress; 
                break;
            default: 
                alphaValue = 1; 
                break; 
        }
        
        for (int itemID = 0; itemID < itemList.Count; itemID++) 
        {
            Vector2 itemPostition = new Vector2(horPosition, verPosition); 
            Color itemColor = Color.White; 
            
            if (itemID == selectedItem) 
                itemColor = new Color(new Vector4(1,0,0,alphaValue)); 
            else 
                itemColor = new Color(new Vector4(1,1,1,alphaValue)); 
            spriteBatch.DrawString(spriteFont, itemList[itemID].itemText, itemPostition, itemColor, 0, Vector2.Zero, 1, SpriteEffects.None, 0); 
            verPosition += 30; 
        }
    } 

    当处于Starting或Ending状态时,changeProgress值会线性地从0增加到1,它工作正常但无法在开始或结束时产生平滑的效果。MathHelper. SmoothStep方法平滑曲线,让开始和结束时都能平滑过渡。当菜单处于Starting或Ending状态时,case结构中调整菜单项的水平位置和alpha值。然后,对每个菜单项,其上的文字被正确地绘制到屏幕上。绘制文字更多的信息可见前一个教程。如果菜单项没有被选择,文字是白色的,当选择时变为红色。

    以上就是MenuWindow类的基础!

    在主程序中,你只需一个集合存储所有的菜单:

    List<MenuWindow> menuList; 

    在LoadContent方法中,你可以创建菜单并将它们添加到menuList中。然后,你可以将菜单项添加到菜单中,让你可以在用户选择菜单项时指定激活哪个菜单。

    MenuWindow menuMain = new MenuWindow(menuFont, "Main Menu", backgroundImage); 
    MenuWindow menuNewGame = new MenuWindow(menuFont, "Start a New Game", bg); 
    menuList.Add(menuMain); 
    menuList.Add(menuNewGame); 
    menuMain.AddMenuItem("New Game", menuNewGame); 
    menuNewGame.AddMenuItem("Back to Main menu", menuMain); 
    menuMain.WakeUp();

    以上操作创建了两个菜单,每个菜单包含一个链接到另一个菜单的菜单项。初始化菜单结构后,激活mainMenu,使它处于Starting状态。现在,你需要在程序更新循环中更新所有菜单:

    foreach (MenuWindow currentMenu in menuList) 
                currentMenu.Update(gameTime.ElapsedGameTime.TotalMilliseconds); 

    并在Draw方法中绘制菜单:

    spriteBatch.Begin(); 
    
    foreach (MenuWindow currentMenu in menuList) 
        currentMenu.Draw(spriteBatch); 
    spriteBatch.End(); 

    当运行代码时,主菜单会从左侧淡入,因为你还没有处理用户输入,所以无法切换到其他菜单。

    允许用户通过菜单项导航

    你将在MenuWindow类中添加一个方法处理用户输入。注意这个方法只能被当前激活的菜单调用:

    public MenuWindow ProcessInput(KeyboardState lastKeybState, KeyboardState currentKeybState) 
    {
        if (lastKeybState.IsKeyUp(Keys.Down) && currentKeybState.IsKeyDown(Keys.Down)) 
            selectedItem++; 
        if (lastKeybState.IsKeyUp(Keys.Up) && currentKeybState.IsKeyDown(Keys.Up)) 
            selectedItem--; 
        if (selectedItem < 0) 
            selectedItem = 0; 
        if (selectedItem >= itemList.Count) 
            selectedItem = itemList.Count-1; 
        if (lastKeybState.IsKeyUp(Keys.Enter) && currentKeybState.IsKeyDown(Keys.Enter)) 
        { 
            windowState = WindowState.Ending; 
            return itemList[selectedItem].itemLink; 
        }
        else if (lastKeybState.IsKeyDown(Keys.Escape)) 
            return null; 
        else
            return this; 
    } 

    这里有许多有趣的东西。首先,你检查up或down键是否被按下。当用户按下一个键时,只要这个键一直被按着,那么这个键的IsKeyUp为true!因此,你需要检查上一帧这个键是否已经被按下。

    如果up或down键被按下,你需要对应地改变selectedItem变量。如果超出边界,需要将它返回到一个合理的位置。

    接下去的代码包含整个导航逻辑。你应该注意到这个方法需要返回一个MenuWindow对象到主程序中。因为这个方法只会被当前激活的菜单调用,这允许当前菜单将新选择的菜单返回到主程序。如果用户没有选择任何菜单项,当前菜单会保持激活并返回自己,这就是最后一行代码的操作。通过这种方式,主菜单在处理输入后知道哪个菜单是激活菜单。

    当用户按下Enter键后,当前激活菜单会从Active转到Ending状态,被选择菜单项链接的菜单被返回到主程序。如果用户按下Escape键,返回null,这回被后面的退出程序捕捉到。如果什么都没选,返回当前菜单自身,告知主程序当前菜单仍保持激活。

    这个方法需要从主程序调用,主程序需要两个变量:

    MenuWindow activeMenu; 
    KeyboardState lastKeybState; 

    第一个变量保存当前激活的菜单,在LoadContent 方法中进行初始化。lastKeybState 变量在Initialize方法中进行初始化。

    private void MenuInput(KeyboardState currentKeybState) 
    {
        MenuWindow newActive = activeMenu.ProcessInput(lastKeybState, currentKeybState); 
        
        if (newActive == null) 
            this.Exit(); 
        else if (newActive != activeMenu) 
            newActive.WakeUp(); 
        activeMenu = newActive; 
    } 

    这个方法调用当前激活菜单的ProcessInput方法,并传递前一个和当前的键盘状态。如前所述,如果用户按下Escape键这个方法会返回null,所以在这种情况中,应用程序会退出。否则,如果这个方法返回一个不同于当前菜单的菜单,说明用户做出了一个选择。在这种情况中,新选择的菜单会通过调用它的WakeUp方法从Inactive变化到Starting状态。如果两种情况都不是,则返回当前菜单,存储在activeMenu变量中。

    请确保在Update方法内部调用这个方法。运行这个代码让你可以在两个菜单间自由切换。

    在菜单中添加标题和背景图像

    菜单现在已经可以正常工作了,但没有背景图片菜单还不够完美。在MenuWindow类中添加两个变量:

    private string menuTitle; 
    private Texture2D backgroundImage; 

    这两个变量需要在Initialize方法中赋值:

    public MenuWindow(SpriteFont spriteFont, string menuTitle, Texture2D backgroundImage) 
    {
        //... 
        this.menuTitle = menuTitle; 
        this.backgroundImage = backgroundImage; 
    }

    显示标题很简单。但是,如果菜单使用不同的背景图片,那么在绘制背景图片时会遇到些麻烦。你想要的是在Active和Ending状态下显示图片。当处于Starting状态时,新的背景会混合在前一个的上面。当在第一张图片上混合第二张图片时,你想要保证第一张图像首先被绘制!这并不容易,因为这涉及到改变菜单的绘制顺序。

    一个简单的方法是使用SpriteBatch . Draw方法的layerDepth参数(见教程3-3)。当处于Active或Ending状态时,背景图像会在距离1绘制,即“最深的”层。在Starting模式中,图像会在距离0.5绘制,所有文字会在距离0绘制。当使用SpriteSortMode. BackToFront时,首先在深度1的Active或Ending菜单会被绘制。然后,Starting菜单被绘制(混合在已经存在的图像上),最后绘制所有文字。

    在MenuWindow的Draw方法中,创建两个变量:

    float bgLayerDepth; 
    Color bgColor;

    这两个变量保存背景图像的layerDepth和透明颜色值,在switch中设置这两个变量:

    switch (windowState) 
    {
        case WindowState.Starting: 
            horPosition -= 200 * (1.0f - (float)smoothedProgress); 
            alphaValue = smoothedProgress; 
            bgLayerDepth = 0.5f; 
            bgColor = new Color(new Vector4(1, 1, 1, alphaValue)); 
            break; 
        case WindowState.Ending: 
            horPosition += 200 * (float)smoothedProgress; 
            alphaValue = 1.0f - smoothedProgress; 
            bgLayerDepth = 1; 
            bgColor = Color.White; 
            break; 
        default: alphaValue = 1; 
            bgLayerDepth = 1; 
            bgColor = Color.White; 
            break; 
    } 

    Color. White与Color(new Vector4(1, 1, 1, 1))相同,表示完全alpha值。如果一个菜单处于Starting或Ending状态,会计算alphaValue。然后,使用透明颜色值绘制标题和背景图像。

    Color titleColor = new Color(new Vector4(1, 1, 1, alphaValue)); 
            
    spriteBatch.Draw(backgroundImage, new Vector2(), null, bgColor, 0, Vector2.Zero, 1, SpriteEffects.None, bgLayerDepth); 
    spriteBatch.DrawString(spriteFont, menuTitle, new Vector2(horPosition, 200), titleColor,0,Vector2.Zero, 1.5f, SpriteEffects.None, 0); 

    标题文字被缩放到1.5f,比菜单项的文字大。

    最后,你需要确保在主程序的Draw方法中将SpriteSortMode设置为BackToFront:

    spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.BackToFront, SaveStateMode.None); 
    从菜单移动到游戏

    至此你创建了一些漂亮的菜单,但如何创建一些菜单项开始游戏?这可以使用dummy 菜单,这个菜单应该存储在主程序中。例如,如果你想在Start New Game菜单中包含start an Easy,a Normal或a Hard game的菜单项,应该将下列代码添加到菜单中:

    MenuWindow startGameEasy; 
    MenuWindow startGameNormal; 
    MenuWindow startGameHard; 
    bool menusRunning; 

    在LoadContent方法中,你可以使用null参数实例化这些变量并将它们链接到menuNewGame中的菜单项上:

    startGameEasy = new MenuWindow(null, null, null); 
    startGameNormal = new MenuWindow(null, null, null);
    startGameHard = new MenuWindow(null, null, null); 
    menuNewGame.AddMenuItem("Easy", startGameEasy); 
    menuNewGame.AddMenuItem("Normal", startGameNormal); 
    menuNewGame.AddMenuItem("Hard", startGameHard); 
    menuNewGame.AddMenuItem("Back to Main menu", menuMain);

    这会在New Game菜单中添加四个菜单项。接下去你要做的就是检测哪一个dummy菜单被选择。因此,需要扩展一下MenuInput方法:

    private void MenuInput(KeyboardState currentKeybState) 
    {
        MenuWindow newActive = activeMenu.ProcessInput(lastKeybState, currentKeybState); 
        
        if (newActive == startGameEasy) 
        {
            //set level to easy 
            menusRunning = false; 
        }
        else if (newActive == startGameNormal) 
        {
            //set level to normal 
            menusRunning = false; 
        }
        else if (newActive == startGameHard) 
        {
            //set level to hard 
            menusRunning = false; 
        }
        else if (newActive == null) 
            this.Exit(); 
        else if (newActive != activeMenu) 
            newActive.WakeUp(); 
        activeMenu = newActive; 
    } 

    你可以使用menusRunning变量保证当用户在游戏中时不更新/绘制菜单:

    if (menusRunning) 
    {
        spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.BackToFront, SaveStateMode.None); 
        
        foreach (MenuWindow currentMenu in menuList) 
            currentMenu.Draw(spriteBatch); 
        spriteBatch.End(); 
        Window.Title = "Menu running ..."; 
    }
    else 
    {
        Window.Title = "Game running..."; 
    }
    代码
    MenuWindow类

    下面是简单、必须的方法:

    public MenuWindow(SpriteFont spriteFont, string menuTitle, Texture2D backgroundImage) 
    {
        itemList = new List<MenuItem>(); 
        changeSpan = TimeSpan.FromMilliseconds(800); 
        selectedItem = 0; 
        changeProgress = 0; 
        windowState = WindowState.Inactive; 
        this.spriteFont = spriteFont; 
        this.menuTitle = menuTitle; 
        this.backgroundImage = backgroundImage; 
    }
    
    public void AddMenuItem(string itemText, MenuWindow itemLink) 
    {
        MenuItem newItem = new MenuItem(itemText, itemLink); 
        itemList.Add(newItem); 
    }
    
    public void WakeUp() 
    {
        windowState = WindowState.Starting; 
    }

    然后是更新菜单的方法。注意只有当前激活的菜单才会调用ProcessInput方法。

    public void Update(double timePassedSinceLastFrame) 
    {
        if ((windowState == WindowState.Starting) || (windowState == WindowState.Ending)) 
            changeProgress += timePassedSinceLastFrame / changeSpan.TotalMilliseconds; 
        if (changeProgress >= 1.0f) 
        {
            changeProgress = 0.0f; 
            if (windowState == WindowState.Starting) 
                windowState = WindowState.Active; 
            else if (windowState == WindowState.Ending) 
                windowState = WindowState.Inactive; 
         }
    }
    
    public MenuWindow ProcessInput(KeyboardState lastKeybState, KeyboardState currentKeybState) 
    {
        if (lastKeybState.IsKeyUp(Keys.Down) && currentKeybState.IsKeyDown(Keys.Down)) 
            selectedItem++; 
        if (lastKeybState.IsKeyUp(Keys.Up) && currentKeybState.IsKeyDown(Keys.Up)) 
            selectedItem--; 
        if (selectedItem < 0) 
            selectedItem = 0; 
        if (selectedItem >= itemList.Count) 
            selectedItem = itemList.Count-1; 
        if (lastKeybState.IsKeyUp(Keys.Enter) && currentKeybState.IsKeyDown(Keys.Enter)) 
        {
            windowState = WindowState.Ending; 
            return itemList[selectedItem].itemLink; 
        }
        else if (lastKeybState.IsKeyDown(Keys.Escape)) 
            return null; 
        else 
            return this; 
    } 

    最后是绘制菜单的方法:

    public void Draw(SpriteBatch spriteBatch) 
    {
        if (windowState == WindowState.Inactive) 
            return; 
        float smoothedProgress = MathHelper.SmoothStep(0,1,(float)changeProgress); 
        int verPosition = 300; 
        float horPosition = 300; 
        float alphaValue; 
        float bgLayerDepth; 
        Color bgColor; 
        
        switch (windowState) 
        {
            case WindowState.Starting: 
                horPosition -= 200 * (1.0f - (float)smoothedProgress); 
                alphaValue = smoothedProgress; 
                bgLayerDepth = 0.5f; 
                bgColor = new Color(new Vector4(1, 1, 1, alphaValue)); 
                break; 
            case WindowState.Ending: 
                horPosition += 200 * (float)smoothedProgress; 
                alphaValue = 1.0f - smoothedProgress; 
                bgLayerDepth = 1; 
                bgColor = Color.White; 
                break; 
            default: 
                alphaValue = 1; 
                bgLayerDepth = 1; 
                bgColor = Color.White; 
                break; 
         }
         
         Color titleColor = new Color(new Vector4(1, 1, 1, alphaValue));
         spriteBatch.Draw(backgroundImage, new Vector2(), null, bgColor, 0, Vector2.Zero, 1, SpriteEffects.None, bgLayerDepth); 
         spriteBatch.DrawString(spriteFont, menuTitle, new Vector2(horPosition, 200), titleColor,0,Vector2.Zero, 1.5f, SpriteEffects.None, 0); 
         
         for (int itemID = 0; itemID < itemList.Count; itemID++) 
         {
             Vector2 itemPostition = new Vector2(horPosition, verPosition); 
             Color itemColor = Color.White; 
             
             if (itemID == selectedItem) 
                 itemColor = new Color(new Vector4(1,0,0,alphaValue)); 
             else 
                 itemColor = new Color(new Vector4(1,1,1,alphaValue)); 
             spriteBatch.DrawString(spriteFont, itemList[itemID].itemText, itemPostition, itemColor, 0, Vector2.Zero, 1, SpriteEffects.None, 0); 
             verPosition += 30; 
         }
    } 
    主程序

    你可以在LoadContent方法中创建菜单结构。更新方法必须调用每个菜单的更新方法和MenuInput方法:

    protected override void Update(GameTime gameTime) 
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) 
            this.Exit(); 
        KeyboardState keybState = Keyboard.GetState(); 
        if (menusRunning) 
        {
            foreach (MenuWindow currentMenu in menuList) 
                currentMenu.Update(gameTime.ElapsedGameTime.TotalMilliseconds); 
            MenuInput(keybState); 
        }
        else
        {
        }
        
        lastKeybState = keybState; 
        base.Update(gameTime); 
    } 

    MenuInput方法将用户输入传递到当前激活的菜单,当输入处理后接受返回的激活菜单:

    private void MenuInput(KeyboardState currentKeybState) 
    {
        MenuWindow newActive = activeMenu.ProcessInput(lastKeybState, currentKeybState); 
        if (newActive == startGameEasy) 
        {
            //set level to easy 
            menusRunning = false; 
        }
        else if (newActive == startGameNormal) 
        {
            //set level to normal 
            menusRunning = false; 
        }
        else if (newActive == startGameHard) 
        {
            //set level to hard 
            menusRunning = false; 
        }
        else if (newActive == null) 
            this.Exit(); 
        else if (newActive != activeMenu) 
            newActive.WakeUp(); 
        activeMenu = newActive; 
    } 
    扩展阅读

    虽然对一个基础菜单系统这个方法已经足够了,但对一个完整的游戏来说,MenuWindow类应该是一个抽象类,这样菜单不能作为这个类的实例。作为替代,你应该为菜单和游戏各创建一个新类,这两个类都从MenuWindow类继承。通过这种方式,键盘处理和绘制完全被这个方法处理,无需丑陋的menuRunning变量了。这也是http ://creators . xna . com网站上菜单示例的基础(译者注:这应该是指示例Game State Management)。

  • 相关阅读:
    Selenium断言的使用,等待
    Selenium的鼠标事件,键盘事件
    json,HTTP协议
    HTML,js的基础知识
    Selenium3详解:元素定位方法
    Python操纵Excel,数据库
    Spring拦截器(权限的管理)
    完成登陆功能
    配置使用sitemesh
    Hibernate+pager-taglib实现分页功能
  • 原文地址:https://www.cnblogs.com/AlexCheng/p/2120153.html
Copyright © 2011-2022 走看看