pixelboyz logo
Desarrollo de Videojuegos

Tutorial MonoGame: Creación de una aplicación

image

Índice de contenido


En este capítulo vamos a ver de cerca la estructura de un juego típico de XNA. Al final, debería tener una mejor idea del ciclo de vida de una aplicación típica de MonoGame, desde la creación del programa, la carga de contenido, el bucle del juego, la descarga de contenido y la salida.

Si prefieres los videos a los mensajes de texto, puedes ver esto contenido en video HD aquí mismo.

Comencemos mirando el código de una aplicación generada automáticamente, sin comentarios. Hay dos archivos de código en el proyecto generado, Program.cs y [YourProjectName].cs. Comencemos con Program.cs

using System;

namespace Example1
{
#if WINDOWS || LINUX
    public static class Program
    {
        [STAThread]
        static void Main()
        {
            using (var game = new Game1())
                game.Run();
        }
    }
#endif
}

El corazón de este código es la creación de nuestro objeto Game, luego llamando a su método Run(), que inicia la ejecución de nuestro juego, iniciando el ciclo del juego hasta que finaliza la ejecución del juego. Hablaremos sobre el bucle del juego con un poco más de detalle más adelante. De lo contrario, esta es una aplicación de estilo C# estándar, siendo Main() el punto de entrada de la aplicación. Esto es cierto al menos para las aplicaciones de Windows y Linux, que es el motivo de la directiva de preprocesador #if. Discutiremos el punto de entrada de varias otras plataformas más adelante, así que no se preocupe demasiado por esto. También tenga en cuenta que si no seleccionó Windows como su plataforma al crear este proyecto, el contenido de su propio archivo Program.cs puede verse ligeramente diferente. Nuevamente, no se preocupe por esto ahora, lo más importante es darse cuenta de que Program crea y ejecuta su clase derivada de Game.

Valores de plataforma predefinidos

Una de las características principales de MonoGame over XNA es la adición de varias otras plataformas compatibles. Además de los símbolos WINDOWS y LINUX, se han definido las siguientes plataformas:

  • ANDROIDE
  • iOS
  • LINUX
  • MONOMACO
  • OUYA
  • PSM
  • VENTANAS
  • TELEFONO WINDOWS
  • WINDOWS_PHONE81
  • WINDOWSRT
  • WEB

Por supuesto, se agregan nuevas plataformas todo el tiempo, por lo que es posible que esta lista aún no esté actualizada. Puede buscar las definiciones en las fuentes de MonoGame en el archivo /Build/Projects/MonoGame.Framework.definition en el repositorio de MonoGame GitHub.

Tenga en cuenta que hay muchas otras definiciones por plataforma, por ejemplo, iOS, Android, MacOS, Ouya y WindowsGL también definen OPENGL. Puede utilizar estos símbolos predefinidos para implementar código específico de plataforma o biblioteca.

Ahora pasemos a nuestra clase de juego.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Example1
{
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == 
                ButtonState.Pressed || Keyboard.GetState().IsKeyDown(
                Keys.Escape))
                Exit();
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            base.Draw(gameTime);
        }
    }
}

Nuestro juego se deriva de la clase Microsoft.Xna.Framework.Juego y es el corazón de nuestra aplicación. La clase Game es responsable de inicializar el dispositivo gráfico, cargar contenido y, lo que es más importante, ejecutar el bucle de juego de la aplicación. La mayoría de nuestro código se implementa anulando varios de los métodos protegidos de Game.

Echemos un vistazo al código, comenzando desde arriba. Creamos dos variables miembro, gráficos y spriteBatch. GraphicsDeviceManager requiere una referencia a una instancia de Juego. Cubriremos las clases GraphicsDeviceManager y SpriteBatch en breve en el capítulo de gráficos, así que ignórelas por ahora.

A continuación, anulamos los métodos Initialize(), LoadContent y UnLoadContent. La pregunta más inmediata que probablemente tenga es, ¿por qué tener un método Initialize()? ¿Por qué no simplemente inicializar en el constructor? En primer lugar, el comportamiento de llamar a una función virtual desde un constructor puede generar todo tipo de errores difíciles de encontrar en una aplicación de C#. En segundo lugar, generalmente no desea hacer ningún trabajo pesado en un constructor. Sin embargo, la existencia de LoadContent() a menudo dejará un método Initialize() vacío. Como regla general, realice las inicializaciones que sean necesarias (como la asignación de GraphicsDeviceManager) para que el objeto sea válido en el constructor, realice inicializaciones de ejecución prolongada (como la generación de terreno por procedimientos) en Initialize() y cargue todo el contenido del juego en LoadContent(). .

A continuación, anulamos Update() y Draw(), que es esencialmente el corazón del bucle de juego de su aplicación. Update() es responsable de actualizar el estado del mundo de tu juego, cosas como entradas de sondeo o entidades móviles, mientras que Draw es responsable de dibujar el mundo de tu juego. En nuestra llamada Update() predeterminada, verificamos si el jugador presiona el botón Atrás o la tecla Escape y salimos si lo hace. No se preocupe por los detalles, cubriremos Input en breve. En Draw() simplemente limpiamos la pantalla a CornFlower Blue (una tradición de XNA). Notará que en ambos ejemplos también llamamos a la clase base.

¿Qué es un bucle de juego?

Un bucle de juego es esencialmente el corazón de un juego, lo que hace que el juego realmente se ejecute. El siguiente es un bucle de juego bastante típico:

void gameLoop(){
   while (game != DONE){
      getInput();
      physicsEngine.stepForward();
      updateWorld();
      render();
   }
   cleanup();
}

Como puede ver, es literalmente un bucle que llama a las diversas funciones que hacen que su juego sea un juego. Obviamente, este es un ejemplo bastante primitivo, pero en realidad el 90% de los bucles de juego terminan luciendo muy similares a este.

Sin embargo, una vez que usa un motor de juego o un marco, las cosas se comportan de manera ligeramente diferente. Todo esto todavía sucede, simplemente ya no es responsabilidad de su código crear el bucle. En su lugar, el motor del juego realiza el bucle y cada iteración vuelve a llamar al código del juego. Aquí es donde se llaman las diversas funciones anuladas, como actualizar() y dibujar(). Sin embargo, al mirar nuestro ciclo de muestra anterior, es posible que note una llamada al motor de física. XNA no tiene un motor de física integrado, por lo que en lugar de que el bucle del juego actualice la física, tendrás que hacerlo tú mismo en la llamada de actualización() de tu juego.

Cuando ejecute este código, debería ver:

Tenga en cuenta que, según la plataforma en la que se esté ejecutando, esta ventana puede o no crearse a pantalla completa. En Windows, está predeterminado en ventana, mientras que en MacOS está predeterminado en pantalla completa. Presione la tecla Escape o el botón Atrás si tiene un controlador instalado para salir de la aplicación.

Echemos un vistazo rápido al ciclo de vida de un programa con este práctico gráfico.

flujo de programa

En pocas palabras, se crea el juego, se inicializa, se carga el contenido, el bucle del juego se ejecuta hasta que se llama a Exit(), luego el juego se limpia y se cierra. En realidad, hay algunos métodos más detrás de escena, como BeginDraw() y EndDraw(), pero para la mayoría de los juegos, esto es suficiente detalle.

Nuestro ejemplo actual no es exactamente emocionante porque no pasa absolutamente nada. Vamos a crear un ejemplo un poco más interesante, uno que dibuje un rectángulo en la pantalla y lo haga rodar por la pantalla. No se preocupe por los detalles, cubriremos los gráficos con más detalle en breve.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

// This example simply adds a red rectangle to the screen
// then updates it's position along the X axis each frame.
namespace Example2
{
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Texture2D texture;
        Vector2 position;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            position = new Vector2(0, 0);
        }

        protected override void Initialize()
        {
            texture = new Texture2D(this.GraphicsDevice, 100, 100);
            Color[] colorData = new Color[100 * 100];
            for (int i = 0; i < 10000; i++)
                colorData[i] = Color.Red;

            texture.SetData<Color>(colorData);
            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == 
                ButtonState.Pressed || Keyboard.GetState().IsKeyDown(
                Keys.Escape))
                Exit();

            position.X += 1;
            if (position.X > this.GraphicsDevice.Viewport.Width)
                position.X = 0;
            base.Update(gameTime);
        }

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

            spriteBatch.Begin();
            spriteBatch.Draw(texture,position);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Cuando ejecutemos esto veremos:

gif1

Nada emocionante, pero al menos nuestro juego lo hace. algo ahora. Nuevamente, no se preocupe demasiado por los detalles de cómo, cubriremos todo esto más adelante. Lo que queremos hacer es ver algunos temas clave cuando se trata de lidiar con el ciclo del juego.

Pausar tu juego

Pausar tu juego es una tarea bastante común, especialmente cuando una aplicación pierde el foco. Si toma el ejemplo anterior, minimice la aplicación, luego restáurela y notará que la animación continúa, incluso cuando el juego no tenía foco. Sin embargo, implementar la funcionalidad de pausa es bastante simple, echemos un vistazo a cómo:

        protected override void Update(GameTime gameTime)
        {
            if (IsActive)
            {
                if (GamePad.GetState(PlayerIndex.One).Buttons.Back == 
                    ButtonState.Pressed || Keyboard.GetState().
                    IsKeyDown(Keys.Escape))
                    Exit();
                position.X += 1;
                if (position.X > this.GraphicsDevice.Viewport.Width)
                    position.X = 0;
                base.Update(gameTime);
            }
        }

Bueno, eso fue bastante simple. Hay una bandera puesta, Está activo, cuando tu juego está activo o no. La definición de IsActive depende de la plataforma en la que se esté ejecutando. En una plataforma de escritorio, una aplicación está activa si no está minimizada Y tiene foco de entrada. En la consola, está activo si no se muestra una superposición como la guía de XBox, mientras que en los teléfonos está activo si se está ejecutando la aplicación en primer plano y no muestra ningún tipo de diálogo del sistema. Como puede ver, pueden pausar el juego simplemente sin realizar llamadas Game::Update().

Puede haber momentos en los que desee realizar alguna actividad cuando gane/pierda el estado activo. Esto se puede hacer con un par de controladores de eventos:

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            position = new Vector2(0, 0);

            this.Activated += (sender, args) => {  this.Window.Title = 
                              "Active Application"; };
            this.Deactivated += (sender, args) => { this.Window.Title 
                                = "InActive Application"; };
        }

O anulando las funciones OnActivated y OnDeactivated, que es el enfoque recomendado:

        protected override void OnActivated(object sender, System.
                                            EventArgs args)
        {
            this.Window.Title = "Active Application";
            base.OnActivated(sender, args);
        }

        protected override void OnDeactivated(object sender, System.
                                              EventArgs args)
        {
            this.Window.Title = "InActive Application";
            base.OnActivated(sender, args);
        }

Controlar el bucle del juego

Otro desafío al que se enfrentan los juegos es controlar la velocidad a la que se ejecutan en una variedad de dispositivos diferentes. En nuestro ejemplo relativamente simple no hay problema por dos razones. Primero, es una aplicación muy simple y no particularmente exigente, lo que significa que cualquier máquina debería poder ejecutarla extremadamente rápido. En segundo lugar, la velocidad en realidad está limitada por dos factores: estamos ejecutando en un paso de tiempo fijo (más sobre eso más adelante) y tenemos vsync habilitado, que en muchos monitores modernos, se actualiza a una velocidad de 59 o 60 Hz. Si desactivamos ambas funciones y dejamos que nuestro juego se ejecute a la máxima velocidad, de repente la velocidad de la computadora en la que se ejecuta se vuelve increíblemente importante:

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            position = new Vector2(0, 0);
            this.IsFixedTimeStep = false;
            this.graphics.SynchronizeWithVerticalRetrace = false;
        }

Al establecer IsFixedTimeStep en falso y Graphics.SyncronizeWithVerticalRetrace en falso, nuestra aplicación ahora se ejecutará tan rápido como el juego sea capaz. El problema es que esto dará como resultado que nuestro rectángulo se dibuje extremadamente rápido y a diferentes velocidades en diferentes máquinas. Obviamente no queremos esto, pero afortunadamente hay una solución fácil. Eche un vistazo a Update() y notará que se pasa en un parámetro GameTime. Este valor contiene la cantidad de tiempo transcurrido desde la última vez que se llamó a Update(). También puede notar que Draw() también tiene este parámetro. Este valor se puede usar para suavizar el movimiento, de modo que funcione a una velocidad constante en todas las máquinas. Cambiemos nuestra llamada de actualización para que en lugar de recibir ++ en cada cuadro, ahora nos movamos a una velocidad fija, digamos 200 píxeles por segundo. Eso se puede lograr con este cambio en su código de posición Update():

position.X += 200.0f * (float)gameTime.ElapsedGameTime.TotalSeconds;  

TotalSeconds contendrá la fracción de segundo que ha transcurrido desde que se llamó a Update() por última vez, ¡asumiendo, por supuesto, que su juego se está ejecutando al menos 1 cuadro por segundo! Por ejemplo, si su juego se actualiza a 60 Hz (segundos veces por segundo), entonces TotalSeconds tendrá un valor de 0.016666 (1/60). Suponiendo que se mantenga bastante estable a 60 fps, esto da como resultado que su código de actualización actualice la posición en 3,2 píxeles por cuadro (200 * 0,016). Sin embargo, en una computadora que funciona a 30 fps, esta misma lógica actualizaría la posición en 6,4 píxeles por cuadro (2000 * (1/30)). El resultado final es que el comportamiento del juego en ambas máquinas es idéntico, aunque una dibuja el doble de rápido que la otra.

La clase GameTime contiene un par de datos útiles:

imagen

ElapsedGameTime contiene información sobre cuánto tiempo ha pasado desde la última llamada a Update (o Draw, por cierto, valores completamente separados). Como acaba de ver, este valor se puede usar para normalizar el comportamiento independientemente de qué tan rápido funcione realmente la máquina subyacente. El valor TotalGameTime, por otro lado, es la cantidad de tiempo transcurrido desde que comenzó el juego, incluido el tiempo transcurrido en pausa. Finalmente, está el indicador IsRunningSlowly, que se establece si el juego no está alcanzando su objetivo de tiempo transcurrido, algo que discutiremos en un momento. El juego también tiene una propiedad llamada InactiveSleepTime, que junto con TotalGameTime se puede usar para calcular la cantidad de tiempo que un juego pasó ejecutándose.

Fijo vs. Paso de tiempo variable

Finalmente, analicemos el uso de un bucle de juego FixedStep en su lugar. En lugar de jugar con todas estas cosas de normalización de fotogramas, puedes decir «Oye, Monogame, quiero que ejecutes mi juego a esta velocidad» y lo hará lo mejor posible. Veamos este proceso:

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            position = new Vector2(0, 0);
            this.IsFixedTimeStep = true;
            this.graphics.SynchronizeWithVerticalRetrace = true;
            this.TargetElapsedTime = new System.TimeSpan(0, 0, 0, 0, 33); // 33ms = 30fps
        }

Esto intentará llamar a Update() exactamente 30 veces por segundo. Si no puede mantener esta velocidad, establecerá el indicador IsRunningSlowly en verdadero. Al mismo tiempo, el motor intentar para llamar a Draw() tanto como sea posible, pero si la máquina no es lo suficientemente rápida para alcanzar el TargetElapsedTime, comenzará a saltarse los marcos de Draw(), llamando solo a Update() hasta que esté «alcanzado». Si puede controlar la cantidad máxima de llamadas de sorteo que se omitirán por Actualización () configurando MaxElapsedTime. Este valor es una extensión específica de MonoGame y no está en el XNA original. Si no especifica un TargetElapsedTime, el valor predeterminado es 60 fps (1/60).

En última instancia, la decisión entre usar un bucle de juego de paso fijo o no depende en última instancia de usted. Un bucle de juego de pasos fijos tiene la ventaja de ser más fácil de implementar y proporciona una experiencia más consistente. Un bucle de paso variable es un poco más complicado, pero puede dar como resultado gráficos más suaves en máquinas de gama alta. El resultado es más pronunciado en un juego 3D que en uno 2D, lo que da como resultado controles más fluidos y efectos visuales ligeramente más limpios. En un juego 2D la diferencia es mucho menos pronunciada.

En el próximo capítulo pasaremos al tema mucho más interesante de los gráficos 2D.

El video




Source link

Tags :
aplicación,Creación,Monogame,Tutorial,una

Comparte :

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *