Como hacer un menú en XNA


Uno de los elementos necesarios en la construcción de videojuegos son los menús de opciones que permiten al jugador iniciar la partida, configurar las opciones del juego, consultar las estadísticas, etc. Es a esta tarea en particular a la que nos vamos a dedicar con el siguiente tutorial.

Como vamos a ver la construcción del menú lo vamos a hacer encapsulándolo dentro de un componente de XNA, de tal manera que no solo veremos cómo está construido por dentro sino que también podremos disponer de un menú en alguno de nuestros proyectos muy fácilmente.

Bien, empecemos por el principio que es ¿cómo pensamos hacer el menú?, bueno pues la idea es la siguiente, vamos a diseñar un objeto con las siguientes características:
  • Número de elementos que tiene el menú.
  • El elemento que está seleccionado.
  • Color del elemento seleccionado y deseleccionado.
  • Posición en la que se sitúa el menú en pantalla.
  • Separación entre los ítems del menú.
  • El tipo de fuente con el que vamos a escribir.
  • Un listado con los nombres de cada ítem del menú.

Os habréis dado cuenta de que este menú no carga ninguna imagen que tengamos en memoria sino que cada ítem del menú se escribe con el tipo de fuente indicado. En un futuro es posible que se haga necesario cargar imágenes en el menú por lo que habría que añadir algún método más a nuestro menú.

Lo primero que tenemos que hacer es abrir Visual C# Express y crear un nuevo proyecto:

imagen1.jpg

Elegimos como tipo de proyecto Windows Game 2.0 y damos un nombre al proyecto, luego pulsamos en Aceptar.
Para construir un Game Component tenemos que agregar un nuevo elemento de este tipo a nuestra solución por lo que pulsaremos con el botón derecho del ratón sobre el icono del proyecto y pulsaremos en Agregar -> Nuevo elemento.

imagen2.jpg

Pulsamos sobre el elemento Game Component y le damos un nombre más concreto como por ejemplo “menuComponent”.

imagen3.jpg

Si pulsamos en Agregar lo que nos aparece es una plantilla del tipo Game Component que como podéis ver hereda de Microsoft.Xna.Framework.GameComponent , como nosotros necesitamos que este componente pueda ser dibujado en pantalla tenemos que cambiar la herencia de GameComponent a DrawableGameComponent.
Ahora lo que vamos a hacer es declararnos las variables necesarias en base a las características que he comentado anteriormente para el menú. Tendríamos las siguientes:

private int number_elements;
private int element_active = 0;
private string[] elements;
private Color selected_color;
private Color unselected_color;
private Vector2 free_position;
private SpriteFont font;
private int separation;
private anchor position;

Hay dos variables parecidas, free_position y position , la primera indica una posición al menú que indicamos nosotros mismos en el constructor y la segunda es una enumeración de valores que utilizaremos para colocar el menú en posiciones predefinidas de la pantalla y no tener que indicar la coordenada.

imagen4.jpg

Estas serían las posiciones a las que nos referiremos si utilizamos position al instanciar el menú. Y el código que tenemos que escribir para tener esto es el que sigue:

public enum anchor
{
  top_left = 1,
  top_center = 2,
  top_right = 3,
  half_left = 4,
  half_center = 5,
  half_right = 6,
  bottom_left = 7,
  bottom_center = 8,
  bottom_right = 9,            
};

A continuación vamos a escribir el código de los constructores que necesitamos, uno tendrá en cuenta la posición que nosotros le indicamos y el otro la posición que indicamos mediante la enumeración.

       public menuComponent(Game game, int Num_elements, Color selected_c, Color unselected_c, SpriteFont _font, int _separation, anchor _position)
            : base(game)
        {
            number_elements = Num_elements;
            element_active = 0;
            elements = new string[number_elements];
            selected_color = selected_c;
            unselected_color = unselected_c;
            font = _font;
            separation = _separation;
            position = _position;
        }

        public menuComponent(Game game, int Num_elements, Color selected_c, Color unselected_c, Vector2 _free_position, SpriteFont _font, int _separation)
            : base(game)
        {
            number_elements = Num_elements;
            element_active = 0;
            elements = new string[number_elements];
            selected_color = selected_c;
            unselected_color = unselected_c;
            free_position = _free_position;
            font = _font;
            separation = _separation;
        }



En este momento tenemos un objeto menú que esta vacio, es decir, no tiene ningún ítem que podamos utilizar por lo que vamos a implementar un método que nos permita hacer esto mismo.

   public void AddElement(int element_number,string element_name)
    {
       if ((element_number > -1)&&(element_number < number_elements))
       {
           elements[element_number] = element_name;
       } 
    }


Bastante sencillo, le decimos el lugar que va a ocupar y el nombre que tenemos que mostrar.

Ahora que ya podemos indicar las características y los ítems del menú, necesitamos dibujarlo en la pantalla y además distinguir el ítem activo de los demás.
public void Draw(SpriteBatch spriteBatch)
{
    spriteBatch.Begin();

    for (int i = 0; i < number_elements; i++)
    {
      if (element_active == i)
      {
        spriteBatch.DrawString(font, elements[i].ToString(),
                               new Vector2(free_position.X,
                               free_position.Y + (separation * i)),
                               selected_color);
      }else{

        spriteBatch.DrawString(font, elements[i].ToString(),
                               new Vector2(free_position.X,
                               free_position.Y + (separation * i)), 
                               unselected_color);
      }
    }
    spriteBatch.End();
}


Con el método Draw dibujamos el menú teniendo en cuenta el ítem que está activo, cuando nos encontramos con el utilizamos el color indicado en el constructor del menú para distinguirlo de los demás.

Vamos a ver cómo va nuestro trabajo, para ello nos colocamos en la clase Game.cs, donde vamos a instanciar e inicializar nuestro componente.
Nos declaramos al inicio de la clase un objeto de tipo menuComponent y lo inicializamos en el método Initialize.
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        menuComponent menu_principal;


Y en el método Initialize construimos el objeto:

        protected override void Initialize()
        {
            menu_principal = new menuComponent(this, 5, Color.Yellow, Color.Red, new Vector2(300, 50), Content.Load<SpriteFont>("font"), 40);
            menu_principal.AddElement(0, "New Game");
            menu_principal.AddElement(1, "Load Game");
            menu_principal.AddElement(2, "Options");
            menu_principal.AddElement(3, "High Scores");
            menu_principal.AddElement(4, "Exit");

            base.Initialize();
        }


Ahora vamos al método Draw de esta clase y cambiamos el color de fondo para que se muestre mejor en la pantalla, e indicamos al objeto menú_principal que llame a su método Draw.

        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(Color.Black);

            menu_principal.Draw(spriteBatch);

            base.Draw(gameTime);
        }


Por último agregamos un nuevo recurso a la carpeta Content de nuestro proyecto, pulsamos con el botón derecho en Agregar -> Nuevo elemento.

imagen5.jpg
imagen6.jpg

El recurso será de tipo Sprite Font, renombramos a font y pulsamos en Agregar, tendremos que tener dentro de Content un archivo Font.spritefont.
Si pulsamos sobre él, veremos un archivo XML en el que tenemos que indicar el tipo de letra y el tamaño de la misma, esto hacedlo al gusto de cada uno.

imagen7.jpg

Ahora podemos pulsar F5 y ver el resultado de lo que vamos haciendo.

imagen8.jpg

De momento no hace nada, por defecto muestra como activo el primer elemento, pero tenemos que agregarle los métodos necesarios para que responda a las teclas Up y Down con las que nos podremos mover por el menú.

Volvemos entonces a la clase de nuestro componente menuComponent.cs.

Lo primero que vamos a hacer es implementar los métodos para movernos entre los ítems:

        private void next_item()
        {
            if (element_active < number_elements-1)
            {
                element_active++;
            }
            else
            {
                element_active = 0;
            }
        }

        private void previus_item()
        {
            if (element_active > 0)
            {
                element_active--;
            }
            else
            {
                element_active = number_elements-1;
            }
        }


Sobre estos métodos creo que hay poco que comentar, lo único es decir que son privados porque los manejo desde otro método que si es público y que se encarga de controlar el estado del teclado.

Es el siguiente:
       public void Press_keys(KeyboardState keys)
        {
            if (keys.IsKeyDown(Keys.Up) && key_Up_press)
            {
                this.previus_item();
                key_Up_press = false;
            }
            else
            {
                if (keys.IsKeyUp(Keys.Up))
                {
                    key_Up_press = true;
                }
            }

            if (keys.IsKeyDown(Keys.Down) && key_Down_press)
            {
                this.next_item();
                key_Down_press = false;
            }
            else
            {
                if (keys.IsKeyUp(Keys.Down))
                {
                    key_Down_press = true;
                }
            }
        }


Vale, he añadido dos variables más a la clase, así que vosotros también tenéis que hacer lo mismo:

        private Boolean key_Up_press;
        private Boolean key_Down_press;


Son dos variables booleanas que nos permiten saber que tecla es la que se presiono anteriormente, de esta manera como podéis ver en el método nos movemos arriba o abajo fácilmente.
Para usar el método Press_keys vamos al método Update de la clase principal y lo llamamos de la siguiente manera:

   menu_principal.Press_keys(Keyboard.GetState());


Ahora si pulsáis F5 veréis que podéis ir arriba y abajo por el menú, pero ¿Cómo sabemos en qué opción del menú estamos? Bueno pues simplemente tenemos que devolver el valor del elemento activo desde el componente y según ese valor realizar la acción que más nos convenga.

        public int Element_active()
        {
            return element_active + 1;
        }


Por ejemplo para salir del programa:

    If (Keyboard.GetState().IsKeyDown(Keys.Enter) && menu_principal.Element_active() == 5) this.Exit();


Lo que hago es comprobar que se pulsa la tecla Enter y ver cuál es la opción activa, si nos devuelve en este caso concreto un 5 es que estamos en la opción de salir por lo que terminamos la ejecución del programa.


Ahora solo queda añadir un método que coloque automáticamente el menú en una parte de la pantalla según el valor que tome position. El código necesario es el siguiente:

        public void position_screen(anchor a)
        {
            position = a;

            switch (position)
            {
                case anchor.top_left:
                    free_position = new Vector2(0, 0);
                    break;
                case anchor.top_center:
                    free_position = new Vector2((Game.Window.ClientBounds.Width / 2) - (size_menu().X / 2), 0);
                    break;
                case anchor.top_right:
                    free_position = new Vector2(Game.Window.ClientBounds.Width - size_menu().X, 0);
                    break;
                case anchor.half_left:
                    free_position = new Vector2(0, (Game.Window.ClientBounds.Height / 2) - size_menu().Y);
                    break;
                case anchor.half_center:
                    free_position = new Vector2((Game.Window.ClientBounds.Width / 2) - (size_menu().X / 2), (Game.Window.ClientBounds.Height / 2) - size_menu().Y);
                    break;
                case anchor.half_right:
                    free_position = new Vector2(Game.Window.ClientBounds.Width - size_menu().X, (Game.Window.ClientBounds.Height / 2) - size_menu().Y);
                    break;
                case anchor.bottom_left:
                    free_position = new Vector2(0, Game.Window.ClientBounds.Height - size_menu().Y - 80);
                    break;
                case anchor.bottom_center:
                    free_position = new Vector2((Game.Window.ClientBounds.Width / 2) - (size_menu().X / 2), Game.Window.ClientBounds.Height - size_menu().Y - 80);
                    break;
                case anchor.bottom_right:
                    free_position = new Vector2(Game.Window.ClientBounds.Width - size_menu().X, Game.Window.ClientBounds.Height - size_menu().Y - 80);
                    break;
                default:
                    break;
            }
        }

        private Vector2 size_menu()
        {
            float width = 0, high = 0;

            for (int i = 0; i < number_elements; i++)
            {
                if (font.MeasureString(elements[i].ToString()).X > width) width = font.MeasureString(elements[i]).X;
                if (font.MeasureString(elements[i].ToString()).Y > high) high = font.MeasureString(elements[i]).Y;
            }

            high = high * (number_elements / 2);

            return new Vector2(width, high);

        }


El método sizemenu lo he utilizado para obtener las dimensiones del menú y poder colocarlo correctamente en la pantalla con positionscreen.
Para ver mejor como funcionan estos métodos podemos agregar al inicio del método Update las siguientes líneas:

            if (Keyboard.GetState().IsKeyDown(Keys.Q)) menu_principal.position_screen(menuComponent.anchor.top_left);
            if (Keyboard.GetState().IsKeyDown(Keys.W)) menu_principal.position_screen(menuComponent.anchor.top_center);
            if (Keyboard.GetState().IsKeyDown(Keys.E)) menu_principal.position_screen(menuComponent.anchor.top_right);

            if (Keyboard.GetState().IsKeyDown(Keys.A)) menu_principal.position_screen(menuComponent.anchor.half_left);
            if (Keyboard.GetState().IsKeyDown(Keys.S)) menu_principal.position_screen(menuComponent.anchor.half_center);
            if (Keyboard.GetState().IsKeyDown(Keys.D)) menu_principal.position_screen(menuComponent.anchor.half_right);

            if (Keyboard.GetState().IsKeyDown(Keys.Z)) menu_principal.position_screen(menuComponent.anchor.bottom_left);
            if (Keyboard.GetState().IsKeyDown(Keys.X)) menu_principal.position_screen(menuComponent.anchor.bottom_center);
            if (Keyboard.GetState().IsKeyDown(Keys.C)) menu_principal.position_screen(menuComponent.anchor.bottom_right);



Como comprobareis según pulse Q,W,E,A,S,D,Z,X ó C, el menú se posicionará automáticamente en una parte de la pantalla.

imagen9.jpg

De todas formas podéis ver todo el código en el proyecto de ejemplo en la sección de componentes, cualquier cuestión, petición o mejora sobre el componente podéis enviármela a cesarreneses@gmail.com e iré actualizándolo para que tenga más y mejores funciones.

Saludetes

César Reneses Cárcamo
Coordinador Albacete DotNetClub

Web: http://labloguera.net
Blog: http://labloguera.net/blogs/csharp
E-mail: cesarreneses@gmail.com

Last edited Sep 24, 2008 at 6:22 PM by csharp, version 8

Comments

No comments yet.