miércoles, 28 de abril de 2010

Desarrollo de un juego 2D con XNA VIII - Disparando balas


Estimados amigos, nuevamente, bienvenidos a este, su blog, XNAExplorer.

En el último post, quedamos con un cañón que rota entre 0 y 90 grados sobre un fondo espacial. Nada muy excitante aún.

Bien, en el capítulo de hoy vamos(por fín) a disparar.

Lo primero que debemos considerar es que nuestra clase GameObject queda corta para describir los comportamientos de una bala pues no contiene las propiedades que un objeto que se mueve por la pantalla posee. Hasta ahora no hemos lanzado nuestro cañón por los aires asi que no había sido necesario. A diferencia del cañón, una bala tendrá una trayectoria, bien podríamos ir representando cada punto de una trayectoria actualizando el valor de position de nunestro objeto de juego, pero aún es insuficiente. Por otro lado, la bala disparada, en algún momento "muere" ya sea al abandonar la pantalla o al colisionar con algún objeto la bala debe dejar de estar activa y por tanto dejar de ser dibujada.

Para cubrir estas necesidades, modificaremos nuestra clase GameObject agregando las propiedades: Velocity y Alive. donde Velocity nos permitirá ir calculando la trayectoria sumándola a una posición inicial de lanzamiento y Alive nos permitirá saber si el objeto está activo o no para dibujarlo.

Entonces, la clase GameObject quedará de este modo:



   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using Microsoft.Xna.Framework;
   5:  using Microsoft.Xna.Framework.Audio;
   6:  using Microsoft.Xna.Framework.Content;
   7:  using Microsoft.Xna.Framework.GamerServices;
   8:  using Microsoft.Xna.Framework.Graphics;
   9:  using Microsoft.Xna.Framework.Input;
  10:  using Microsoft.Xna.Framework.Media;
  11:  using Microsoft.Xna.Framework.Net;
  12:  using Microsoft.Xna.Framework.Storage;
  13:   
  14:  namespace JuegoXNADemo1
  15:  {
  16:      class GameObject
  17:      {
  18:          public Texture2D Sprite;
  19:          public Vector2 Position;
  20:          public float Rotation;
  21:          public Vector2 Center;
  22:          public Vector2 Velocity;
  23:          public bool Alive;
  24:   
  25:          public GameObject(Texture2D LoadedTexture)
  26:          {
  27:              Rotation = 0.0f;
  28:              Position = Vector2.Zero;
  29:              Sprite = LoadedTexture;
  30:              Center = new Vector2(Sprite.Width / 2, Sprite.Height / 2);
  31:              Velocity = Vector2.Zero;
  32:              Alive = false;
  33:          }
  34:   
  35:      }
  36:  }

Ahora, en nuestra clase Game1 agregaremos una constante que limite la cantidad de balas que pueden estar simultáneamente en pantalla, para el caso del ejemplo, fijaremos este valor en 3. Si quieren que el cañçon pueda disparar mas o menos balas por vez, tan solo cambien el valor de la constante y voilá.

Además, agregaremos un array de GameObject que represente nuestro conjunto de balas en juego.

Entonces, la sección de variables globales nos quedará así:



   1:  public class Game1 : Microsoft.Xna.Framework.Game
   2:      {
   3:          GraphicsDeviceManager graphics;
   4:          SpriteBatch spriteBatch;
   5:          Texture2D backgroundTexture;
   6:          Rectangle viewPortRect;
   7:          GameObject Cannon;
   8:          const int MaxCannonBalls = 3;
   9:          GameObject[] CannonBalls;
  10:   
  11:   
  12:  //Resto del programa

Como podrán suponer en base a lo visto en los post anteriores, la mecánica general es:

Actualizar datos en el método Update() y luego dibujar en base a estos datos en el método Draw()

Así es y seguiremos haciéndolo aunque con una salvedad.

Verán, en la medida que uno comienza a desarrollar y el código comienza a crecer y crecer el código se va haciendo menos mantenible(Esto es: se hace mas dificil encontrar errores o secciones de interés y mas dificil de modificar sin provocar errores) para evitar esto se acostumbra ir encerrando aquellos fragmentos de código relacionados con una tarea específica dentro de métodos específicos. Por ejemplo, si en el método update y en el método draw colocamos el código completo necesario para hacer lo propio con cada uno de los distintos objetos manejados(por ejemplo cañón, balas, naves, textos, otros enemigos, etc) Estos metodos crecerán tanto que luego se nos hará dificil de leer.

Si se hace dificil leer un parrafo como el anterior, que está en español y lenguaje natural, imaginen lo que pasa con código de programación.

Para concluir la idea anterior, diré que agregaremos 2 métodos que se ocuparan de:
1 - Actualizar los datos de posición y status de las balas(UpdateCannonBall())
2 - De inicializar los datos de las balas cuando se presione la barra espaciadora para dispararlas(FireCannonBall())

Los métodos son estos:


   1:  public void FireCannonBall()
   2:          { 
   3:              foreach (GameObject cannonBall in CannonBalls )
   4:              {
   5:                  if (!cannonBall.Alive)
   6:                  {
   7:                      //activamos la bala
   8:                      cannonBall.Alive = true;
   9:                      //Establecemos posición inicial segun la posición del cañón
  10:                      cannonBall.Position = Cannon.Position - cannonBall.Center;
  11:                      //Determinamos la velocidad de la bala segun la posición del cañón
  12:                      cannonBall.Velocity = new Vector2((float)Math.Cos(Cannon.Rotation ),
  13:                          (float)Math.Sin(Cannon.Rotation )) * 5;
  14:                      return;
  15:                  }
  16:              }
  17:          }
  18:          public void UpdateCannonBall()
  19:          {
  20:              //Revisaremos cada cannoBall existente dentro del array de GameObject llamado CannonBalls
  21:              foreach (GameObject cannonBall in CannonBalls)
  22:              {
  23:                  if (cannonBall.Alive)//Si la bala está activa, modificaremos su posición
  24:                  {
  25:                      cannonBall.Position += cannonBall.Velocity;
  26:                  }
  27:                  if (!viewPortRect.Contains(new Point((int)cannonBall.Position.X,(int)cannonBall.Position.Y)))
  28:                  {
  29:                     //Si la bala sale de la pantalla, la desactivaremos
  30:                      cannonBall.Alive = false;
  31:                  }
  32:              }
  33:          }

La gracia de definir estos métodos es que luego podemos invocarlos por su nombre y así ejecutar el código que contienen sin contaminar al método que los invoque con código que pudiera hacer compleja su lectura.

En este caso, ambos métodos serán invocados por el método Update(), cuyo código dejo aquí:



   1:  protected override void Update(GameTime gameTime)
   2:          {
   3:              // Allows the game to exit
   4:              if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
   5:                  this.Exit();
   6:   
   7:              KeyboardState keyboardState = Keyboard.GetState();
   8:              if(keyboardState.IsKeyDown(Keys.Left ))
   9:              {
  10:                  Cannon.Rotation -= 0.1f;
  11:              }
  12:              if (keyboardState.IsKeyDown(Keys.Right ))
  13:              {
  14:                  Cannon.Rotation += 0.1f;
  15:              }
  16:              if (keyboardState.IsKeyDown(Keys.Space) && !previousKeyboardState.IsKeyDown(Keys.Space ))
  17:              {
  18:                  //si la barra espaciadora se ha presionado y antes no estaba presionada
  19:                  //Disparamos una bala
  20:                  FireCannonBall();
  21:              }
  22:              // TODO: Add your update logic here
  23:              Cannon.Rotation = MathHelper.Clamp(Cannon.Rotation, -MathHelper.PiOver2, 0);
  24:              //Actualizaremos los estados y posiciones de las balas invocando el método UpdateCannonBall
  25:              UpdateCannonBall();
  26:              //Almacenamos el estado actual del teclado
  27:              previousKeyboardState = keyboardState;
  28:              base.Update(gameTime);
  29:          }

Por si no lo notaron, ha aparecido una nueva variable( previousKeyboardState) que es una variable global de tipo KeyboardState, lo que hace es, en cada ciclo, almacenar el último estado del teclado. La usaremos para controlar que las balas se disparen solo cuando se presiona la barra espaciadora y no cuando se deja presionada, sin este control, dada la velocidad a la que se ejecutan estos métodos, las tres balas saldrían disparadas juntas y nos daría la impresión de ser una única bala alargada.

Para que no les de error, han de agregarla tambien a la sección de variables globales, con lo que el encabezado de la clase Game1 queda así:



   1:      public class Game1 : Microsoft.Xna.Framework.Game
   2:      {
   3:          GraphicsDeviceManager graphics;
   4:          SpriteBatch spriteBatch;
   5:          Texture2D backgroundTexture;
   6:          Rectangle viewPortRect;
   7:          GameObject Cannon;
   8:          const int MaxCannonBalls = 3;
   9:          GameObject[] CannonBalls;
  10:          KeyboardState previousKeyboardState = Keyboard.GetState();
  11:  //Resto del programa

Ahora, solo nos queda modificar el método draw para que revise el status de las balas y si están activas, las dibuje en pantalla.

Entonces, el nuevo método Draw es este:



   1:  protected override void Draw(GameTime gameTime)
   2:          {
   3:              GraphicsDevice.Clear(Color.CornflowerBlue);
   4:   
   5:              spriteBatch.Begin();
   6:              spriteBatch.Draw(backgroundTexture, viewPortRect, Color.White);
   7:              spriteBatch.Draw(Cannon.Sprite, Cannon.Position, null, Color.White, Cannon.Rotation, Cannon.Center, 1.0f, SpriteEffects.None, 0);
   8:              //Revisamos cada cannonBall existente en el array CannonBalls
   9:              foreach (GameObject cannonball in CannonBalls)
  10:              {
  11:                  if (cannonball.Alive)//Si la bala está activa
  12:                  {
  13:                      //La dibujamos
  14:                      spriteBatch.Draw(cannonball.Sprite, cannonball.Position, null, Color.White, cannonball.Rotation, cannonball.Center, 1.0f, SpriteEffects.None, 0);
  15:                  }
  16:              }
  17:   
  18:              spriteBatch.End();
  19:   
  20:              // TODO: Add your drawing code here
  21:              base.Draw(gameTime);
  22:          }

Bueno, ahora, si han modificado sus códigos del modo correcto, nada mas presionen F5, con las teclas de dirección muevan su cañón y con la barra espaciadora disparen sus balas. ¿A qué ahora si da mas gustito?.. Como diría don Raúl Guerrero:"Tasstyy! "("teeeiiistii!").

Aviso: Por una molesta tos que me impide hablar 5 minutos continuos sin que me de un ataque horripilante de abuelito montepiado, solo incluiré código por ahora, los videos se los debo.

Como siempre, gracias por su tiempo e interés, un abrazo y recuerden "El volar es un arte o, mejor dicho, un don. El don consiste en aprender a tirarse al suelo y fallar."

Bonus track

JA, aqui un extracto de uno de mis libros favoritos de Douglas Adams, "La guía del viajero intergaláctico"

Instrucciones de como Volar

El volar es un arte o, mejor dicho, un don.
El don consiste en aprender a tirarse al suelo y fallar.
Elija un día que haga bueno -sugiere- e inténtelo.
La primera parte es fácil.
Lo único que se necesita es simplemente la habilidad de tirarse hacia adelante con todo el peso del cuerpo, y buena voluntad para que a uno no le importe que duela.
Es decir, dolerá si no se logra evitar el suelo.
La mayoría de la gente no consigue evitar el suelo, y si de verdad lo intenta como es debido, lo más probable es que no logra evitarlo de ninguna manera.
Está claro que la segunda parte, la de evitar el suelo, es la que presenta dificultades.


....

2 comentarios:

  1. Estoy leyendo tu tutorial, se ve bueno se agradece la publicacion

    lo que si como consejo para cualquier buen tutorial de programacion se debe adjuntar el codigo fuente por si se omitio algo o modifico algo mientras se hacia le tutorial

    saludos

    ResponderEliminar
  2. Gracias! Excelente tuto! me ayudó muchisimo!
    la mayoria de tutos que habia encontrado eran para juegos 2D en donde el personaje no rotaba

    ResponderEliminar

-__-