Administrador de Escenas – XNA


Aunque en la red hay varias entradas de este tema, yo voy a crear otra :P, en esta entrada voy a mostrar cómo crear un administrador de escenas, pantallas, stages o como quieran llamarlas, para que en nuestro juego fluyan las escenas de Inicio, Ayuda, Cargar juego, Opciones, acción, etc.

Después de esto, se pueden crear otras escenas y dependiendo de la escena hacemos algo específico, así que comencemos.

Imagen sacada de http://zatungames.com/legend-vraz/game-information/

Voy a hacer que las clases hereden de DrawableGameComponent para sobrecargar varios métodos utiles como el Load, el Update, el Draw, el Show, etc.

Creamos una clase llamada GameScene que tendrá una lista de GameComponents y será la encargada de servir como base para las demás escenas:

public class GameScene : DrawableGameComponent
{
    /// <summary>
    /// Lista de componentes Hijos
    /// </summary>
    private List<GameComponent> componentes;
    public List<GameComponent> Componentes
    {
        get
        {
            return componentes;
        }
    }
    public GameScene(Game game)
        : base(game)
    {
        Visible = false;
        Enabled = false;
        componentes = new List<GameComponent>();
    }
    /// <summary>
    /// Muestra la escena del Juego
    /// </summary>
    public virtual void Show()
    {
        Visible = true;
        Enabled = true;
    }
    /// <summary>
    /// Oculta la escena del Juego
    /// </summary>
    public virtual void Hide()
    {
        Visible = false;
        Enabled = false;
    }
    /// <summary>
    /// Allows the game component to perform any initialization it needs to before starting
    /// to run.  This is where it can query for any required services and load content.
    /// </summary>
    public override void Initialize()
    {
        // TODO: Add your initialization code here
        base.Initialize();
    }
    /// <summary>
    /// Allows the game component to update itself.
    /// </summary>
    /// <param name="gameTime">Provides a snapshot of timing values.</param>
    public override void Update(GameTime gameTime)
    {
        for (int i = 0; i < componentes.Count; i++)
        {
            if (componentes[i].Enabled)
            {
                componentes[i].Update(gameTime);
            }
        }
         base.Update(gameTime);
    }
    public override void Draw(GameTime gameTime)
    {
        for (int i = 0; i < componentes.Count; i++)
        {
            GameComponent gc = componentes[i];
            if ((gc is DrawableGameComponent) && ((DrawableGameComponent)gc).Visible)
            {
                ((DrawableGameComponent)gc).Draw(gameTime);
            }
        }
        base.Draw(gameTime);
    }
}

La clase maneja los componentes que son guardados en la lista y si se encuentran habilitados (Enabled) o Visibles los dibuja y los actualiza.

Ahora voy a crear la escena de Inicio o pantalla del Menú, pero antes creo otro componente que será el Menú, así vamos encapsulando los componentes para usarlos luego, el menú está basado en la anterior entrada de crear un Menú Gráfico :

Componente Menú:

public class MenuComponent : DrawableGameComponent
{
    SpriteBatch spriteBatch = null;
    SpriteFont spriteFont;
    KeyboardState oldState;
    Int32 indiceSeleccionado = 0;
    private StringCollection menuItems = new StringCollection();
    private List<Double> escala; // es la escala de cada menú
    public Int32 Ancho { get; set; }
    public Int32 Alto { get; set; }
    public Color ColorNormal { get; set; }
    public Color ColorSeleccionado { get; set; }
    public Vector2 Posicion { get; set; }
    public MenuComponent(Game game, SpriteFont spriteFont)
        : base(game)
    {
        escala = new List<double>();
        this.spriteFont = spriteFont;
        ColorNormal = Color.Yellow;
        ColorSeleccionado = Color.Red;
        spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
    }
    public Int32 IndiceSeleccionado
    {
        get
        {
            return indiceSeleccionado;
        }
        set
        {
            indiceSeleccionado = Convert.ToInt32(MathHelper.Clamp(value, 0, menuItems.Count - 1));
        }
    }
    public void AdicionarMenuItems(String[] items)
    {
        menuItems.Clear();
        menuItems.AddRange(items);
        for (int i = 0; i < items.Length; i++)
        {
            escala.Add(1.0f);
        }
        CalcularLimites();
    }
    /// <summary>
    /// Calcula los límites de los Items del Menú
    /// </summary>
    private void CalcularLimites()
    {
        Ancho = 0;
        Alto = 0;
        foreach (String item in menuItems)
        {
            Vector2 tamano = spriteFont.MeasureString(item);
            if (tamano.X > Ancho)
            {
                Ancho = (Int32)tamano.X;
                Alto += spriteFont.LineSpacing;
            }
        }
    }
    public override void Initialize()
    {
        base.Initialize();
    }
    public Boolean verificarTeclado(Keys teclas)
    {
        KeyboardState newState = Keyboard.GetState();
        return oldState.IsKeyDown(teclas) && newState.IsKeyUp(teclas);
    }
    public override void Update(GameTime gameTime)
    {
       KeyboardState newState = Keyboard.GetState();
        if (verificarTeclado(Keys.Down))
        {
            indiceSeleccionado++;
            if (indiceSeleccionado == menuItems.Count)
            {
                indiceSeleccionado = 0;
            }
        }
        if (verificarTeclado(Keys.Up))
        {
            indiceSeleccionado--;
            if (indiceSeleccionado == -1)
            {
                indiceSeleccionado = menuItems.Count - 1;
            }
        }
        for (int x = 0; x < menuItems.Count; x++)
        {
            if (x == indiceSeleccionado)
            {
                if (escala[x] < 2.0f)
                {
                    escala[x] += 0.04 + 10.0f * gameTime.ElapsedGameTime.Seconds;
                }
            }
            else if (escala[x] > 1.0f && x != indiceSeleccionado)
            {
                escala[x] -= 0.04 + 10.0f * gameTime.ElapsedGameTime.Seconds;
            }
        }
        oldState = newState;
        base.Update(gameTime);
    }
    public override void Draw(GameTime gameTime)
    {
        Vector2 menuPosition = Posicion;
        Color miColor;
        for (int i = 0; i < menuItems.Count; i++)
        {
            if (i == IndiceSeleccionado)
            {
                miColor = ColorSeleccionado;
                menuPosition.X -= (float)(escala[i] / 2);
                menuPosition.Y -= (float)(escala[i] / 2);
            }
            else
            {
                miColor = ColorNormal;
            }
            spriteBatch.DrawString(spriteFont, menuItems[i], menuPosition, miColor, 0.0f,
                Vector2.Zero, (float)escala[i], SpriteEffects.None, 0);
            menuPosition.Y += spriteFont.LineSpacing * 3;
        }
        base.Draw(gameTime);
    }
}

El componente, recibe como parámetro el juego actual y el la fuente (Font) de los ítems del Menú, se tiene un método público que recibe una array de Strings, este array tendrá todos los Items del Menú, los ítems son adicionados en una colección y luego se les calcula los limites para evitar que se solapen los ítems, así se tendría una buena distancia entre cada ítem.

También voy a hacer un Componente para los fondos, y hacerlo reutilizable, el componente recibirá una textura y la dibujara en todo el ancho y alto de la pantalla:

public class BackgroundComponent : DrawableGameComponent
{
    Texture2D fondo;
    SpriteBatch spriteBatch = null;
    Rectangle bgRect;
    public BackgroundComponent(Game game, Texture2D textura)
        : base(game)
    {
        this.fondo = textura;
        spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
        bgRect = new Rectangle(0, 0, Game.Window.ClientBounds.Width, Game.Window.ClientBounds.Height);
    }
    public override void Initialize()
    {
        base.Initialize();
    }
    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);
    }
    public override void Draw(GameTime gameTime)
    {
        spriteBatch.Draw(fondo, bgRect, Color.White);
        base.Draw(gameTime);
    }
}

Ahora que tenemos el Componente del Menú y el del Fondo, vamos a crear la escena de Inicio, que tendrá el componente Menú y un componente para el fondo:

class MenuScene : GameScene
{
    MenuComponent menu;
    public MenuScene(Game game, SpriteFont spriteFont, Texture2D fondo)
        : base(game)
    {
        Componentes.Add(new BackgroundComponent(game, fondo));
        String[] items = { "Nuevo Juego", "Continuar","Ayuda", "Salir" };
        menu = new MenuComponent(game, spriteFont);
        menu.AdicionarMenuItems(items);
        Componentes.Add(menu);
    }
    public Int32 IndiceSeleccionado
    {
        get
        {
            return menu.IndiceSeleccionado;
        }
    }
    public override void Show()
    {
        menu.Posicion = new Vector2((Game.Window.ClientBounds.Width - menu.Ancho) / 2, 100);
        base.Show();
    }
    public override void Hide()
    {
        base.Hide();
    }
}

La clase hereda de GameScene, y sobrecarga el método Show, inicializando la posición del componente menú, se va a mostrar en la mitad de la pantalla.

La Clase recibe en el constructor la fuente y la textura del fondo como parámetro, luego se inicializa el componente background con la fuente recibida y se crean los ítems para el menú.

Podemos crear otra escena llamada HelpScene que servirá para mostrar una imagen de la ayuda del juego o un texto o lo que queramos, la clase también hereda de GameScene y solo tendrá un fondo en este ejemplo:

class HelpScene : GameScene
{
    public HelpScene(Game game, Texture2D background)
        : base(game)
    {
        Componentes.Add(new BackgroundComponent(game, background));
        base.Hide();
    }
}

Creamos otra escena, que servirá para luego adicionar el flujo del juego, el manejo de los juegadores, Sprites y otros elementos que vayamos creando:

class ActionScene : GameScene
{
    ContentManager Content;
    SpriteBatch spriteBatch;
    public ActionScene(Game game)
        : base(game)
    {
        Content = (ContentManager)Game.Services.GetService(typeof(ContentManager));
        spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
        LoadContent();
    }
    protected override void LoadContent()
    {
        base.LoadContent();
    }
    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);
    }
    public override void Draw(GameTime gameTime)
    {
            base.Draw(gameTime);
   }
    public override void Show()
    {
        base.Show();
        Enabled = true;
        Visible = true;
    }
    public override void Hide()
    {
        base.Hide();
        Enabled = false;
        Visible = false;
    }
}

La clase por el momento no tiene nada, pero sirve de ejemplo de cómo ir creando diferentes escenas para el juego.

Ahora para modificamos la clase game1 para usar las escenas:

public class Game1 : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    MenuScene escenaInicio;
    HelpScene escenaAyuda;
    GameScene escenaActiva;
    ActionScene escenaAccion;
    SpriteFont fuenteNormal;
    Texture2D fondo;
    KeyboardState newState;
    KeyboardState oldState;
    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }
    protected override void Initialize()
    {
        base.Initialize();
    }
    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        Services.AddService(typeof(SpriteBatch), spriteBatch);
        Services.AddService(typeof(ContentManager), Content);
        fuenteNormal = Content.Load<SpriteFont>("fuente");
        fondo = Content.Load<Texture2D>("fondo1");
        escenaInicio = new MenuScene(this, fuenteNormal, fondo);
        Components.Add(escenaInicio);
        fondo = Content.Load<Texture2D>("fondo2");
        escenaAyuda = new HelpScene(this, fondo);
        Components.Add(escenaAyuda);
        escenaAccion = new ActionScene(this);
        Components.Add(escenaAccion);
        escenaAccion.Hide();
        escenaInicio.Show();
        escenaAyuda.Hide();
        escenaActiva = escenaInicio;
    }
    protected override void UnloadContent()
    {
    }
    protected override void Update(GameTime gameTime)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
            this.Exit();
        newState = Keyboard.GetState();
        if (escenaActiva == escenaInicio)
        {
            if (verificarTeclado(Keys.Enter) || verificarTeclado(Keys.Space))
            {
                switch (escenaInicio.IndiceSeleccionado)
                {
                    case 0:
                        escenaActiva.Hide();
                        escenaActiva = escenaAccion;
                        escenaActiva.Show();
                        break;
                    case 2:
                        escenaActiva.Hide();
                        escenaActiva = escenaAyuda;
                        escenaActiva.Show();
                        break;
                    case 3:
                        Exit();
                        break;
                }
            }
        }
        else if (escenaActiva == escenaAyuda)
        {
            if (verificarTeclado(Keys.Space) || verificarTeclado(Keys.Enter) || verificarTeclado(Keys.Escape))
            {
                escenaActiva.Hide();
                escenaActiva = escenaInicio;
                escenaActiva.Show();
            }
        }
        oldState = newState;
        base.Update(gameTime);
    }
    private bool verificarTeclado(Keys tecla)
    {
        return oldState.IsKeyDown(tecla) && newState.IsKeyUp(tecla);
    }
    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);
        spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
        base.Draw(gameTime);
        spriteBatch.End();
     }
}

Creamos las instancias de las escenas, luego cargamos los fondos y la fuente del menú, vamos a tener una variable que será la encargada de tener la escena activa, dependiendo del ítem seleccionado en el menú, asignamos la escena activa a la variable, y luego la dejamos habilitada y visible, para que en la clase GameScene se dibuje y se actualice.

Código Fuente Aquí: http://cid-bcf2b4912b193ad2.office.live.com/self.aspx/Escarbando%20C%c3%b3digo/SceneManager.zip

Anuncios

12 pensamientos en “Administrador de Escenas – XNA

  1. Muchas gracias por la ayuda. En estos momentos le estoy echando un ojo a lo que publicaste! Comento luego como me fue.

    Edit: Ahora revisé completo el tutorial. Me parece buena la idea, pero me quedan dos dudas. Tu solución como manera la comunicación entre stages? Es decir, si en una por ejemplo tengo que… no se… escribir varios textos… y necesito pasar el texto escrito por el usuario a otra stage para hacer algo con ellos.. como lo lograría hacer?

    Y lo otro, por lo que entiendo, sería necesario dejar todo inicializado desde un comienzo? Me refiero a que si es así tendré el gasto de memoria de todos los elementos, lo cual en un juego grande sería un problema =S.

    Muchas gracias!

    Saludos!

    • Hola,
      Para el primer punto, puedes declarar una propiedad mensaje o parámetros que sea String o dependiendo de lo que quieras tener, así cada Stage que heredé de la clase GameScene tendrá disponible la propiedad y se podrá usar para enviar mensajes.
      Para el segundo punto, no se si te refieras a que en cada Stage tengo que inicializar digamos la textura o los textos del menú, pero como el punto anterior puedes declarar propiedades en cada Stage dependiendo de lo que quieras hacer, y luego desde la clase game1 u otra que uses para manejar los flujos, empiezas a inicializar las propiedades.
      Lo que quise hacer es tener una clase Base (GameScene) y luego poder crear más clases dependiendo de lo que yo quería hacer, por ejemplo la clase ActionScene puede ser usara para inicializar y manejar el flujo de los sprites y las puntuaciones.

  2. Pingback: Colisión sobre Plataformas 2D – XNA « Escarbando Código

  3. Tengo un problema con el dibujo del menu.

    String[] items = { “Nuevo Juego”, “Continuar”,”Ayuda”, “Salir” };

    El tema es que, no se dibujan completamente todos los textos, solo se dibuja “Nue Juego” y “Con” en pantalla, queda así

    Estuve probando cambiar la fuente, pero nada…Si cambias los valores de “Nuevo” se cambia el texto, pero siempre aparece cortado.

    Sospeche que era “Add_item” o los limites del dibujo, pero nada de eso..

    Ayuda 😀

  4. Quiero hacer un juego por reconocimiento de voz, ya hice este tuto y le implemente la referencia system.speech
    lo que quiero hacer es que diciendo: “ayuda” vaya a la pantalla de ayuda, “jugar” empiece el juego y “salir” se salga del juego. Podrías ayudarme con esto?

  5. Hola, excelente tutorial pero tengo una duda.
    ¿Cómo sería si quisiera cargar texturas específicas en determinada escena desde el Content.Load??
    Cuando intento hacerlo me salta una excepción diciéndome que es null

Deseas comentar o sugerir algo?

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s