Ejemplo XNA parte 8 – Implementado la acción disparar del Jugador

Ya se puede observar el progreso de nuestro juego ejemplo. Ya cuenta con elementos animados como la nave y los enemigos, un fondo que le da una profundidad y una sensación de mayor movimiento en el entorno. Por los momentos, la única forma de destruir los enemigos es estrellando la nave contra ellos. Es hora de equipar la nave con un arma para disparar proyectiles contra las minas explosivas.

Comienza agregando una nueva clase llamada Projectile.cs; y elimina las declaraciones using que trae por defecto, y agrega las siguientes:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

Ya que estaremos trabajando con clases y metodos del framework XNA. Ahora prosigue a agregar los siguientes atributos de la clase Projectile. En los comentarios observa la importancia de cada atributo.

// Imagen del proyectil
public Texture2D Texture;

// Posicion del proyectil
public Vector2 Position;

// Estado del proyectil
public bool Active;

// Cantidad de daño del proyectil
public int Damage;

// Limite visible del juego
Viewport viewport;

// Ancho del proyectil
public int Width
{
	get { return Texture.Width; }
}

// Alto del proyectil
public int Height
{
	get { return Texture.Height; }
}

// Velocidad de desplazamiento
float projectileMoveSpeed;

Ahora agreguemoe el metodo Initialize para inicializar las propiedades de la clase. Observa que le llegan tres parametros, el viewport que son las dimensiones de la escena del juego, la textura del proyectil que en nuestro caso es un rayo laser azul, y por ultimo la posicion inicial del proyectil.

public void Initialize(Viewport viewport, 
	Texture2D texture, Vector2 position)
{
	Texture = texture;
	Position = position;
	this.viewport = viewport;
	Active = true;
	Damage = 2;
	projectileMoveSpeed = 20f;
}

Como la idea de los proyectiles es que viajen de izquierda a derecha contra las minas enemigas, en el método Update evidentemente debemos incrementar la posición con respecto al eje X de acuerdo a la velocidad establecida en el método Initialize (proyectileMoveSpeed = 20f). Por último verificamos si el proyectil se ha salido de la escena para desactivar su despliegue, con el propósito de evitar desplegar imágenes que no podremos visualizar en la escena.

public void Update()
{
	// Projectiles always move to the right
	Position.X += projectileMoveSpeed;
	// Deactivate the bullet if it goes out of screen
	if (Position.X + Texture.Width / 2 > viewport.Width)
	Active = false;
}

Finalmente para esta clase, agregamos el método Draw para visualizar un proyectil en la pantalla.

public void Draw(SpriteBatch spriteBatch)
{
	spriteBatch.Draw(Texture, Position, null, Color.White, 0f,
	new Vector2(Width / 2, Height / 2), 1f, SpriteEffects.None, 0f);
}

Ya con nuestra nueva clase Projectile lista, proseguimos a hacer unas modificaciones en la clase principal.

En la Clase Game1

Comenzamos declarando los siguientes atributos.

// Textura de un proyectil
Texture2D projectileTexture;
// Lista de proyectiles
List <Projectile>projectiles;
// Tasa de proyectiles a disparar
TimeSpan fireTime;
// Tiempo de disparo anterior
TimeSpan previousFireTime;

Nota que agregamos una lista de proyectiles. Hacemos esto porque planeamos desplegar múltiples proyectiles disparados por la nave, por ende esta estructura de dato dinámica es ideal para llevar control de cada proyectil en escena en todo momento. fireTime nos dice en cada cuanto intervalo de tiempo el jugador disparará un proyectil en escena, ya verás cómo eso funciona más adelante. En el método Initialize, al final, agrega las siguientes instrucciones:

projectiles = new List<Proyectile>();
// Un laser se dispara cada cuarto de segundo
fireTime = TimeSpan.FromSeconds(.15f);

Observas el valor de fireTime? Esto quiere decir que la nave disparará 4 láseres por segundo. Ahora, En el LoadContent, al final del método, proseguimos a cargar la textura del láser, con la siguiente instrucción.

projectileTexture = Content.Load<Texture2D>("laser");

Como cuando trabajamos con el despliegue de enemigos, debemos agregar un método para AddProjectile, para instanciar y agregar nuevos proyectiles en la lista, inicializándolos con el viewport, su textura y su posición inicial.

private void AddProjectile(Vector2 position)
{
	Projectile projectile = new Projectile(); 
	projectile.Initialize(GraphicsDevice.Viewport,
	projectileTexture,position); 
	projectiles.Add(projectile);
}

Ahora debemos buscar donde invocar este método. Ya que el jugador es el único elemento que estará disparando rayos láser, tiene sentido ir al método UpdatePlayer, y agregar las siguientes instrucciones al final:

// Disparar bajo el valor de fireTime
if (gameTime.TotalGameTime - previousFireTime > fireTime)
{
	// Reiniciamos nuestro tiempo actual
	previousFireTime = gameTime.TotalGameTime;
	// Agregamos el proyectil al frente de la nave
	AddProjectile(player.Position + new Vector2(player.Width / 2, 0));
}

Aquí estamos agregando nuevos proyectiles en la cola, y disparando a la tasa del valor de fireTime. Ahora necesitamos actualizar las posiciones de cada proyectil en la escena. Como ya hicimos antes con los enemigos, debemos agregar un nuevo método llamado UpdateProjectiles y se verá de la siguiente manera:

private void UpdateProjectiles()
{
	// Actualizar los Proyectiles
	for (int i = projectiles.Count - 1; i >= 0; i--) 
	{
		projectiles[i].Update();
		if (projectiles[i].Active == false)
		{
			projectiles.RemoveAt(i);
		} 
	}
}

Observa que se invoca el método Update por cada proyectil en la escena, donde estos se desplazarán de izquierda a derecha a una velocidad uniforme, y cuando Active se encuentre en false, son removidos de la lista de proyectiles. En cuanto al método Update, proseguimos a invocar UpdateProjectiles al final:

// Actualizar proyectiles
UpdateProjectiles();

Hasta este punto los proyectiles se dibujan, se mueven, pero no colisionan con los enemigos. Dirígete al método UpdateCollision y agrega las siguientes intrucciones al final:

// Colision entre Proyectiles vs Enemigos
for (int i = 0; i < projectiles.Count; i++)
{
	for (int j = 0; j < enemies.Count; j++)
	{
		// Creamos los rectangulos para validar si hay colision
		rectangle1 = new Rectangle(
			(int)projectiles[i].Position.X - 
			projectiles[i].Width / 2,
			(int)projectiles[i].Position.Y - 
			projectiles[i].Height / 2,
			projectiles[i].Width, 
			projectiles[i].Height);

		rectangle2 = new Rectangle(
			(int)enemies[j].Position.X - 
			enemies[j].Width / 2,
			(int)enemies[j].Position.Y - 
			enemies[j].Height / 2,
			enemies[j].Width, 
			enemies[j].Height);

		// Determinamos si se intersectan
		if (rectangle1.Intersects(rectangle2))
		{
			enemies[j].Health -= projectiles[i].Damage;
			projectiles[i].Active = false;
		}
	}
}

Entiendes la lógica? Simplemente se está creando un rectángulo por cada proyectil, y se crea un rectángulo por cada enemigo visible en esa iteración (para capturar su posición en ese instante de tiempo). Luego se verifica si algún rectángulo enemigo se intersecta con el rectángulo del proyectil, y en caso afirmativo, se le resta vida al enemigo colisionado, y se desactiva el proyectil para que sea removido de la lista.

Finalmente, implementamos la logica para desplegar los proyectiles en escena. Vamos al metodo Draw, y debajo de Jugador.Draw(), inserta las siguientes instrucciones:

for (int i = 0; i < projectiles.Count; i++)
{
	projectiles[i].Draw(spriteBatch);
}

Al ejecutar nuestro juego se vera algo así:

Observa la colisión de los proyectiles contra los enemigos, pero veras que solo desaparecen cuando son destruidos. En la siguiente parte de este juego ejemplo haremos que las minas muestren un efecto de explosión cuando son destruidas por el jugador. Si estas teniendo problemas armando la solución, puedes descargarte el proyecto haciendo clic en el siguiente enlace:

Proyecto Shooter Parte 8

Ejemplo XNA parte 4 – Animación del elemento Jugador

Hasta ahora tenemos una aeronave que se puede mover a lo largo de la pantalla, pero sin ninguna animación, no se ve interesante ni llamativo para el usuario. Eso lo vamos arreglar ahora. Utilizaremos la clase “Animacion” que importamos desde la parte 1 de este tutorial. En caso que no hiciste el tutorial de animación en este blog, es recomendable que lo leas para que entiendas el mecanismo utilizado para desplegar objetos animados en la pantalla. Accede dicho tutorial dando click en el siguiente enlace: Despliegue de sprites animados 2D.

En la clase jugador

En esta clase Jugador, lo primero que haremos es reemplazar la variable

public Textured2D PlayerTexture

Por esta variable:

public Animation PlayerAnimation;

Esto es necesario ya que no trabajaremos desplegando una Textura estatica de la nave. Trabajaremos con la clase animacion para trabajar con el mecanismo de animacion de sprites. Debido a este cambio de atributo, debemos modificar unas cuantas instrucciones en otros metodos. Ve al metodo Initialize, y reemplazalo por el siguiente:

public void Initialize(Animation animation, Vector2 position)
{
	PlayerAnimation = animation;
	Position = position;
	// Set the player to be active
	Active = true;
	// Set the player health
	Health = 100;
}

Aquí lo que hicimos fue asignar el atributo Animacion que nos fue pasado por parámetro, al igual que la posición. Hacemos true al atributo active para que sea desplegado por defecto, y le inicializamos la vida de la nave a 100. También debemos modificar los atributos Width y Height en la clase jugador. Reemplázalas con lo siguiente:

// Get the width of the player ship
public int Width
{
	get { return PlayerAnimation.FrameWidth; }
}
// Get the height of the player ship
public int Height
{
	get { return PlayerAnimation.FrameHeight; }
}

Ya que no nos interesa saber el alto ni ancho de la imagen Texture2d que teníamos antes, sino el ancho y alto de una imagen en nuestra clase Animacion. Con respecto a Updatedebemos actualizar la posición e invocar el método Update de PlayerAnimation para desplazar el sprite a lo largo de la pantalla con el campo Position, e invocamos Update para el efecto de animación. El método Update queda de la siguiente manera:

public void Update(GameTime gameTime)
{
	PlayerAnimation.Position = Position;
	PlayerAnimation.Update(gameTime);
}

Finalmente, reemplazamos el método Draw que nos desplegaba la Textura2D estática, llamando al método Draw de PlayerAnimation para que despliegue el sprite animado. El método Draw quedaría de la siguiente manera:

public void Draw(SpriteBatch spriteBatch)
{
	PlayerAnimation.Draw(spriteBatch);
}

En la clase Game1

Por último nos queda modificar la clase principal, que involucra la carga de las imágenes de animación, y mandarlas al objeto Jugador por el método Initialize. Dirígete a LoadContent, y reemplaza el método completo por este:

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

	Animation playerAnimation = new Animation();
	Texture2D playerTexture = Content.Load<Texture2D>("Imagenes/animacionNave");
	playerAnimation.Initialize(playerTexture, 
		Vector2.Zero, 115, 69, 8, 30, Color.White, 1f, true);

	Vector2 playerPosition = new Vector2(
		GraphicsDevice.Viewport.TitleSafeArea.X +
		GraphicsDevice.Viewport.TitleSafeArea.Width / 2,
		GraphicsDevice.Viewport.TitleSafeArea.Y +
		GraphicsDevice.Viewport.TitleSafeArea.Height / 2);
	jugador.Initialize(playerAnimation, playerPosition);
}

Nada de otro mundo, simplemente instanciamos un objeto local de la clase Animacion, cargamos la tira de imágenes de la nave en playerTexture, e invocamos el método Initialize de playerAnimation, enviándole por parámetros:

  • playerTexture, el cual es la tira completa de imágenes de la nave
  • Vector2.Zero para la inicializar la posición
  • 115, que es el ancho de una sola imagen expresados en pixeles
  • 69, es el alto de una sola imagen expresados en pixeles
  • 8, representa el numero de imágenes que componen la animación
  • 30, tiempo de despliegue por imagen expresados en milisegundos
  • Color.White, para desplegar la imagen en sus colores reales
  • 1f, para el valor de escala del elemento
  • true, para especificar si queremos que se repita la animación múltiples veces

Se instancia un Vector2 para la posición, para desplegar la nave en el centro de la pantalla, y finalmente se invoca el Initialize de jugador, enviandole playerAnimation playerPosition. Ahora busca el método UpdatePlayer, y al comienzo del método, agrega la siguiente línea:

jugador.Update(gameTime);

Esto invoca el Update de jugador, que este a su vez hace Update a su atributo playerAnimation para refrescar las imágenes de despliegue para dar el efecto de animación de nuestra nave. Ahora no queda más sino armar la solución y ejecutar la aplicación. Ahora veras la nave animada en el centro de la pantalla, con un efecto de movimiento en las alas y el propulsor en la parte trasera. También observa que lo puedes controlar con las teclas de dirección del teclado.

Hasta este punto de nuestro juego de ejemplo, hemos logrado desplegar una nave animada, e interactiva con el usuario. De la misma manera como animamos el sprite del jugador, se harán con los demás elementos animados, como los enemigos. Puede descargar nuestro proyecto de ejemplo hasta este punto dando clic en el siguiente enlace:

Proyecto Shooter Parte 4

Ejemplo XNA parte 2 – Creación de la Clase Jugador

Haz un clic derecho sobre el proyecto y agrega una nueva clase llamada «Jugador», y agrega los métodos Initialize, Update y Draw. Deberás importar las librerías de XNA.

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Shooter
{
	class Jugador
	{
		public void Initialize()
		{
		}

		public void Update()
		{
		}

		public void Draw()
		{
		}
	}
}

Esta es la estructura básica de nuestra clase Jugador, que funcionará para desplegar el elemento que controlará el usuario, que en nuestro caso es la aeronave. Ahora agrega los siguientes atributos (observa la importancia de cada uno que se encuentran comentados)

// Animacion representando al jugador
public Texture2D PlayerTexture;

// Posicion del jugador
public Vector2 Position;

// Estado del jugador
public bool Active;

// Cantidad de vida del jugador
public int Health;

// Obtener el ancho del jugador
public int Width
{
	get { return PlayerTexture.Width; }
}

// Obtener el alto del jugador
public int Height
{
	get { return PlayerTexture.Height; }
}

Estos son algunos atributos fundamentales para nuestra clase jugador, pero obviamente se pueden agregar mas dependiendo del control que queramos tener sobre este elemento. Ahora reemplaza el método Initialize (de la clase Jugador) con lo siguiente:

public void Initialize(Texture2D texture, Vector2 position)
{
	PlayerTexture = texture; 
	// Asignar la posicion del jugador
	Position = position;
	// Activar el jugador
	Active = true;
	// Inicializar la vida del jugador
	Health = 100;
}

Como se puede ver claramente, lo que hace el metodo Initialize es establecer las propiedades iniciales de nuestra clase Jugador. Ahora reemplaza el metodo Draw con el siguiente:

public void Draw(SpriteBatch spriteBatch)
{ 
	spriteBatch.Draw(PlayerTexture, Position, null, 
		Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f);
}

Ya con la clase Jugador con sus atributos y funciones listas, pasaremos a la clase principal para instanciar un objeto Jugador.

En la clase Game1.cs

Ya que tenemos todo preparada la clase Jugador, debemos instanciarla, inicializarla y por último, invocar su método Draw desde esta clase principal para desplegar la nave en la pantalla. Ve al inicio de esta clase, y debajo de la declaración del atributo SpriteBatch spriteBatch; agrega nuestra variable tipo Jugador.

Jugador jugador;

Y ahora en el método Initialize, instanciamos nuestra clase:

jugador = new Jugador();

Siguiente, debemos cargar la textura de nuestra nave que se encuentra en el Content de nuestro proyecto. En el método LoadContent, agrega las siguientes líneas:

// Load the player resources 
Vector2 playerPosition = new Vector2(
	GraphicsDevice.Viewport.TitleSafeArea.X,
	GraphicsDevice.Viewport.TitleSafeArea.Y + 
	GraphicsDevice.Viewport.TitleSafeArea.Height / 2);
jugador.Initialize(Content.Load<Texture2D>("Imagenes/nave"), playerPosition);

Ya que tenemos nuestro objeto Jugador instacianciado, y cargado, ve al metodo Draw y agrega las siguientes instrucciones:

spriteBatch.Begin();
jugador.Draw(spriteBatch);
spriteBatch.End();

Ahora finalmente compila el proyecto, y ejecútalo. Si todo funciona correctamente, verás algo como esto:

Si tu proyecto presenta errores y no encuentra como solucionarlos, no entres en pánico, puedes descargar el proyecto de acá en su estado final de este tutorial, simplemente da clic al siguiente enlace:

Proyecto Shooter Parte 2

Diseña un sitio como este con WordPress.com
Comenzar