MonoGame – Meny

Inledning

Vi ska i denna artikel göra ett enklare menysystem. Det skall gå att välja med mus eller med tangentbord. För att göra det lite mer intressant så kommer vi att låta meny-systemet ärva av klassen DrawableGameComponent. Förhoppningsvis kan du återanvända komponenten i andra MonoGame-projekt.

I kommande artiklar är det mycket möjligt att vi själva återanvänder just denna meny-komponent.

Projektet finns som vanligt att ladda ned i slutet av artikeln inklusive all ”Content” som använts.

Förberedelser

Vi skapar ett ”MonoGame Windows Project” projekt. Två stycken fonter skall användas; en för omarkerade meny-alternativ och en för markerade. Dessa ska läggas in i er ”Content” via MonoGame Pipeline Tool (se tidigare tutorials).

Vi skapade våra fonter med hjälp av Fancy Bitmap Font Generator. Ett bra verktyg för att skapa snygga fonter. Programmet genererar Bitmap-bilder som kan konverteras till spriteFont. Viktigt att tänka på är att du sätter rätt ”Content Processor” annars blir det en Texture2D och inte en SpriteFont av det hela.

KeyboardComponent – GameComponent

Vi börjar med att presentera en komponent för att enklare hantera tangentbordet; KeyboardComponent. Denna ärver från klassen GameComponent och är inte uppritningsbar.

KeyboardComponent.cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
 
 
namespace MonoGame.Meny
{
    public class KeyboardComponent : Microsoft.Xna.Framework.GameComponent
    {
        public static KeyboardState CurrentState { get; set; }
        public static KeyboardState LastState { get; set; }
 
        public KeyboardComponent(Game game)
            : base(game)
        {
        }
 
        public override void Update(GameTime gameTime)
        {
            LastState = CurrentState;
            CurrentState = Keyboard.GetState();
 
            base.Update(gameTime);
        }
 
        public static bool KeyPressed(Keys key)
        {
            return CurrentState.IsKeyDown(key) && !LastState.IsKeyDown(key);
        }
    }
}

Via den statiska metoden KeyPressed kan vi nu enklare se när en tangent trycks ned. Tanken med komponenten är att den lever sitt eget liv och håller endast reda på tangentbordet.

Data till Menyn

Vi gör en data-klass för att beskriva ett meny-alternativ. Vad skall vi ha med? En text, position (X, Y) samt en indikation på om meny-alternativet är markerat; Selected.

En HitBox som beskriver det område som meny-alternativet tar upp på skärmen. Användbart för att avgöra om vi har fört muspekaren över alternativet vilket då borde göra alternativet markerat.

Vi använder här klassen Action för att kunna binda en metod till alternativet. Vi vill ju att något skall hända när vi klickar på alternativet med musen alternativt trycker ENTER på tangentbordet.

MenuChoice.cs
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace MonoGame.Meny
{
    class MenuChoice
    {
        public float X { get; set; }
        public float Y { get; set; }
 
        public string Text { get; set; }
        public bool Selected { get; set; }
 
        public Action ClickAction { get; set; }
        public Rectangle HitBox { get; set; }
    }
}

Nu till själva hjärtat av projektet. Denna gång ärver vi från DrawableGameComponent som till skillnad från GameComponent då är uppritningsbar. Vi har utelämnat några områden av kod () som kommer att kompletteras. Själva grunden har vi nedan.

MenuComponent.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
 
namespace MonoGame.Meny
{
    public class MenuComponent : Microsoft.Xna.Framework.DrawableGameComponent
    {
        SpriteBatch _spriteBatch;
        SpriteFont _normalFont;
        SpriteFont _selectedFont;
        List<MenuChoice> _choices;
        Color _backgroundColor;
        MouseState _previousMouseState;
 
        public MenuComponent(Game game)
            : base(game)
        {
        }
 
        public override void Initialize()
        {
            _choices = new List<MenuChoice>();
            _choices.Add(new MenuChoice() { Text = "START", Selected = true, ClickAction = MenuStartClicked });
            _choices.Add(new MenuChoice() { Text = "SELECT LEVEL", ClickAction = MenuSelectClicked });
            _choices.Add(new MenuChoice() { Text = "OPTIONS", ClickAction = MenuOptionsClicked });
            _choices.Add(new MenuChoice() { Text = "QUIT", ClickAction = MenuQuitClicked });
             
            base.Initialize();
        }
         
        // ... Komplettering #1
 
        protected override void LoadContent()
        {
            _spriteBatch = new SpriteBatch(GraphicsDevice);
            _normalFont = Game.Content.Load<SpriteFont>("menuFontNormal");
            _selectedFont = Game.Content.Load<SpriteFont>("menuFontSelected");
            _backgroundColor = Color.White;
 
            //... Komplettering #2
             
            _previousMouseState = Mouse.GetState();
            base.LoadContent();
        }
 
        public override void Update(GameTime gameTime)
        {
            if (KeyboardComponent.KeyPressed(Keys.Down))
                NextMenuChoice();
            if (KeyboardComponent.KeyPressed(Keys.Up))
                PreviousMenuChoice();
            if (KeyboardComponent.KeyPressed(Keys.Enter))
            {
                var selectedChoice = _choices.First(c => c.Selected);
                selectedChoice.ClickAction.Invoke();
            }
 
            var mouseState = Mouse.GetState();
 
            //... Komplettering #3
             
            _previousMouseState = mouseState;
 
            base.Update(gameTime);
        }
 
        private void PreviousMenuChoice()
        {
            // ... Komplettering #4
        }
 
        private void NextMenuChoice()
        {
            // ... Komplettering #4
        }
 
        public override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(_backgroundColor);
            _spriteBatch.Begin();
 
            // ... Komplettering #5
             
            _spriteBatch.End();
            base.Draw(gameTime);
        }
    }
}

Vi laddar våra fonter samt skapar de meny-alternativ vi vill ha. Vi har också med hanteringen av tangentbordet med hjälv av KeyboardComponent.

Trycker vi ENTER så letar vi reda på det markerade alternativet och kör den Action som finns tilldelad med metoden Invoke. Om du tycker det ser aningen konstigt ut så är det för att vi använder oss av LINQ, vilket i sig är ämne för både en eller flera separata artiklar. Observera att vi alltid har ett meny-alternativ markerat.

Komplettering #1

Vi måste ha metoder på plats som hanterar klicken som görs. Alla meny-alternativ, utom ”QUIT”, sätter en ny bakgrundsfärg.

Komplettering #1 - kod
  #region Menu Clicks
 
  private void MenuStartClicked()
  {
      _backgroundColor = Color.Turquoise;
  }
 
  private void MenuSelectClicked()
  {
      _backgroundColor = Color.Teal;
  }
 
  private void MenuOptionsClicked()
  {
      _backgroundColor = Color.Silver;
  }
 
  private void MenuQuitClicked()
  {
      this.Game.Exit();
  }
 
  #endregion

Komplettering #2

Positionen samt området (HitBox) för varje alternativ måste beräknas. Vi försöker att centrera varje alternativ på skärmen samtidigt som alternativen separeras i höjdled. För att mäta hur stor en utskrift blir så har vi till hjälp metoden MeasureString från klassen SpriteFont.

Komplettering #2 - kod
  float startY = 0.2f * GraphicsDevice.Viewport.Height;
 
  foreach (var choice in _choices)
  {
      Vector2 size = _normalFont.MeasureString(choice.Text);
      choice.Y = startY;
      choice.X = GraphicsDevice.Viewport.Width / 2.0f - size.X / 2;
      choice.HitBox = new Rectangle((int)choice.X, (int)choice.Y, (int)size.X, (int)size.Y);
      startY += 70;
  }

Komplettering #3

För att hantera muspekaren så undersöker vi varje alternativ för att se om muspekaren befinner sig i HitBox. I så fall markeras alternativet. Vi passar även på att kolla om vänster musknapp trycks ned och kör då ClickAction.

Komplettering #3 - kod
  foreach(var choice in _choices)
  {
      if(choice.HitBox.Contains(mouseState.X, mouseState.Y))
      {
          _choices.ForEach(c => c.Selected = false);
          choice.Selected = true;
 
          if (_previousMouseState.LeftButton == ButtonState.Released
              && mouseState.LeftButton == ButtonState.Pressed)
              choice.ClickAction.Invoke();
      }
  }

Komplettering #4

Nästa och föregående alternativ måste hanteras. Trycker vi UPP när vi står överst så hamnar vi på det nedersta alternativet. Trycker vi NED när vi står nederst så hamnar vi på det översta alternativet. För att hitta det markerade alternativet använder vi oss återigen av lite LINQ .

Komplettering #4 - kod
  private void PreviousMenuChoice()
  {
      int selectedIndex = _choices.IndexOf(_choices.First(c => c.Selected));
      _choices[selectedIndex].Selected = false;
      selectedIndex--;
      if (selectedIndex < 0)
          selectedIndex = _choices.Count - 1;
      _choices[selectedIndex].Selected = true;
  }
  
  private void NextMenuChoice()
  {
      int selectedIndex = _choices.IndexOf(_choices.First(c => c.Selected));
      _choices[selectedIndex].Selected = false;
      selectedIndex++;
      if (selectedIndex >= _choices.Count)
          selectedIndex = 0;
      _choices[selectedIndex].Selected = true;
  }

Komplettering #5

Sist men inte minst så ritar vi ut alternativen. Är alternativet markerat så använder vi oss av en annan font. Läs mer om ?-operatorn i 10 Bra programmeringstekniker i C#.

Komplettering #5 - kod
  foreach (var choice in _choices)
  {
      _spriteBatch.DrawString(choice.Selected ? _selectedFont : _normalFont, 
          choice.Text, new Vector2(choice.X, choice.Y), Color.White);
  }

Registrering av komponenter

För att få allt att fungera så behöver endast komponenterna registreras i vår Game-klass. I vårt projekt har vi döpt om den från Game1.cs till MenuTest.cs. Registreringen görs enligt:

Registrering - kod
  protected override void Initialize()
  {
      Components.Add(new MenuComponent(this));
      Components.Add(new KeyboardComponent(this));
 
      base.Initialize();
  }

Avslutning

Varför använder vi oss av komponenter? Jo det blir en tydligare uppdelning mellan ansvarsområden samt att komponenterna går oftast bra att återanvända om de görs någorlunda generella.

Spel-klassen blir nästan ödsligt tom men då skall man komma ihåg att det faktiskt saknas ett spel också.. I kommande artiklar skall vi se hur bra komponenterna fungerar tillsammans med s.k. ”Game States”.

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *

Scroll to top