20 febrero 2011

Un juego modular con XNA (Convertir el juego de SDL a XNA - 2)


El esqueleto que hemos creado con XNA sirve para juegos sencillos, pero un único fuente de gran tamaño no es lo más sencillo de mantener. En cuanto la lógica del juego se complica, convendrá desglosar en clases que encapsulen los detalles del comportamiento de nuestro personaje, de nuestro(s) enemigo(s), etc.

Así, podríamos comenzar por crear una clase "Elemento gráfico", similar a la que ya usábamos con SDL (aunque todavía mucho más simple), de la que heredáramos para crear nuestro Personaje, los enemigos, los objetos del fondo, etc. Esta clase podría ser así de momento:


class ElemGrafico
{
protected Texture2D miImagen;
protected int x, y;
protected int incrX, incrY;

public ElemGrafico(string nombreFichero)
{
x = 0; y = 0;
incrX = 0; incrY = 0;
CargarImagen(nombreFichero);
}

/// Mueve el elemento grafico a otra posicion
public void MoverA(int nuevaX, int nuevaY)
{
x = (short)nuevaX;
y = (short)nuevaY;
}

/// Carga la imagen que representara a este elemento grafico
public void CargarImagen(string nombre)
{
miImagen = Texture2D.FromStream(miPartida.GetGraphics(),
new FileStream ( nombre ,FileMode.Open ) );

contieneImagen = true;
contieneSecuencia = false;
}


/// Dibuja en pantalla oculta, como parte de un "SpriteBatch"
public void DibujarOculta(SpriteBatch listaSprites)
{
listaSprites.Draw(miImagen, new Vector2(x, y), Color.White);
}
}
Y a partir de ella, podríamos crear clase como la que representa a nuestro enemigo, así:

class Enemigo : ElemGrafico
{
// Constructor
public Enemigo(Partida p)
: base("imagenes/enemigo.png")
{
x = 400; // Valores iniciales
y = 400;
incrX = 2;
}

// Métodos de movimiento
public void Mover()
{
x += incrX;

if ((x <> 700))
incrX = (short)(-incrX);
}
}


De forma que el cuerpo del programa tendría cosas como:


protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);

miPersonaje = new Personaje();
miPersonaje.MoverA(400, 300);

miEnemigo = new Enemigo();
}


protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}


protected override void Update(GameTime gameTime)
{
MoverElementos();
ComprobarTeclas();

base.Update(gameTime);
}


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

spriteBatch.Begin();
miPersonaje.DibujarOculta(spriteBatch);
miEnemigo.DibujarOculta(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}


public void MoverElementos()
{
miEnemigo.Mover();
}


public void ComprobarTeclas()
{
if (Keyboard.GetState().IsKeyDown(Keys.Left))
miPersonaje.MoverIzquierda();
if (Keyboard.GetState().IsKeyDown(Keys.Right))
miPersonaje.MoverDerecha();
...
}


Es decir, creará el personaje, el enemigo, los moverá y dibujará, y podemos incluso crear funciones auxiliares MoverElementos, ComprobarTeclas, ComprobarColisiones, etc., que acerquen la rígida estructura que propone XNA a la estructura "clásica" que habíamos creado por encima de SDL.

Si quieres ver el fuente completo, puedes descargar el mini-proyecto desde su página en Google Code, con el nombre miner002XNA:

http://code.google.com/p/manic-miner/downloads/list

07 febrero 2011

Convertir el juego de SDL a XNA (1)


En este primer acercamiento, veremos las pautas básicas de cómo hacer que un personaje se mueva por la pantalla al pulsar las flechas del teclado. Posteriormente lo ampliaremos para se que parezca un poco más (dentro de lo posible) a las clases que estábamos usando para ocultar SDL.

El primer paso es descargar las herramientas necesarias para crear juegos con XNA: Visual Studio como entorno de desarrollo y XNA Game Studio como biblioteca de juegos.

En primer lugar, descargamos e instalamos Visual Studio. Si queremos ir a la última versión (2010), en su variante “Express” (de evaluación), la podemos encontrar aquí:

http://www.microsoft.com/express/Downloads/#2010-Visual-CS

Podemos optar entre descargar sólo Visual C# en castellano (se descarga un instalador de pequeño tamaño, que luego va descargando progresivamente todo lo que necesita) o bien una imagen ISO que contiene todo Visual Studio, para instalar posteriormente en local.

Después descargamos e instalamos XNA. Su última versión en estos momentos es la 4, sólo en inglés, que podemos encontrar aquí:

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=9ac86eca-206f-4274-97f2-ef6c8b1f478f


Si ahora entramos a Visual Studio e indicamos que queremos crear un Nuevo proyecto. Entre las plantillas que se nos proponen, debería aparecer una llamada “Windows Game (4.0)”.

Damos un nombre al proyecto y aparecerá un fuente llamado “Game1.cs”, que contiene algo similar a (eliminando los comentarios):

public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

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

protected override void Initialize()
{
// TODO: Add your initialization logic here
base.Initialize();
}

protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
}

protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}

protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
base.Draw(gameTime);
}
}

Las ideas básicas son:
  • “Initialize” es la función de inicialización. En principio, debería crear la infraestructura que fuera necesaria, a excepción de los gráficos.
  • “LoadContent” se encarga de cargar el “contenido” que fueramos a emplear. Ahí preparemos nuestras imágenes.
  • “UnloadContent” se encargará del paso contrario, si fuera necesario (en nuestros ejemplos simples no lo será).
  • “Update” (actualizar) se debe encargar de toda la lógica de juego: actualizar el mundo, comprobar colisiones, comprobar entrada del usuario -teclas, joystick, ratón-, reproducir sonido, etc.
  • “Draw” dibuja los elementos en pantalla.


Si queremos acercarlo un poco a nuestro estilo, la imagen del personaje sería un “Texture2D” (una textura en dos dimensiones), que declararíamos en la parte de atributos:

Texture2D imagenPersonaje;

Para cargar la imagen, podemos “esquivar” los “contenidos” de XNA y hacerlo a nuestra manera, cargando un fichero PNG desde el programa. Esto, en XNA 3.1 se puede conseguir con Texture2D.FromFile:

imagenPersonaje = Texture2D.FromFile(graphics, "imagenes\\personaje.png");

pero esta sintaxis no es válida en XNA 4, sino que tenemos que usar “FromStream”, un poco más incómodo, porque deberemos leer la imagen desde un “FileStream”, así por ejemplo:

imagenPersonaje = Texture2D.FromStream(graphics.GraphicsDevice,
new FileStream ("imagenes\\personaje.png",FileMode.Open));

(como usamos un FileStream, deberemos añadir “using System.IO;” al principio de nuestro fuente).

La alternativa fácil es dejar las imágenes prefijadas, como parte del “contenido” (content) del proyecto, pero no estamos buscando la solución más fácil, sino una que recuerde a nuestra forma anterior de trabajar, cuando usábamos Tao.SDL..

Para la posición, podemos usar dos números enteros, x e y, o podemos emplear un tipo de datos existente en XNA, el “Vector2” (un vector de 2 componentes): lo declararíamos como atributo:

Vector2 posicion;

al que daremos valor desde LoadContent:

posicion = new Vector2(400, 300);

Desde “Update” podemos comprobar si se pulsa ESC para salir o alguna de las flechas de dirección para cambiar la posición del personaje:

if (Keyboard.GetState().IsKeyDown(Keys.Escape))
this.Exit();

if (Keyboard.GetState().IsKeyDown(Keys.Left))
posicion.X -= 5;
if (Keyboard.GetState().IsKeyDown(Keys.Right))
posicion.X += 5;
if (Keyboard.GetState().IsKeyDown(Keys.Up))
posicion.Y -= 5;
if (Keyboard.GetState().IsKeyDown(Keys.Down))
posicion.Y += 5;

Y para dibujar el personaje, y los demás “sprites” del juego, lo haremos desde “Draw”, como parte del “lote de sprites” (spriteBatch), así:

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

spriteBatch.Begin();
spriteBatch.Draw(imagenPersonaje, posicion, Color.White);
spriteBatch.End();

base.Draw(gameTime);
}

Si todos los cambios han ido bien, nuestro proyecto debería compilar correctamente y quizá hasta funcionar, con dos consideraciones:
  • Deberemos crear una carpeta “imagenes” dentro de la carpeta del ejecutable de nuestro proyecto (bin/debug), y copiar en ella la imagen llamada “personaje.png”.
  • Es posible que al lanzar el proyecto, se queje de que nuestra tarjeta gráfica no es lo suficientemente potente y “no soporta un perfil HiDef”. No es problema: vamos al menú Proyecto, escogemos la opción “Propiedades de... (el nombre del proyecto)” y en la primera pestaña, en el apartado “Game profile”, dejamos activada la opción “Use Reach” en vez de “Use HiDef”.


Puedes descargar el mini-proyecto desde su página en Google Code, con el nombre miner001XNA:

http://code.google.com/p/manic-miner/downloads/list


05 febrero 2011

Descubrir tiles en un juego

Si estamos pensando hacer un remake, o al menos tomar ideas de un juego clásico, nos puede interesar tratar de descubrir que casillas repetitivas (tiles) formaban la pantalla de ese juego.

GIMP tiene una opción que puede ser de gran ayuda para conseguirlo. Se trata del filtro de "rejilla", que superpone una cuadrícula a nuestra imagen.

Lo podemos encontrar en el menú Filtros, dentro de la opción Filtros -> Renderizado -> Patrón -> Rejilla.


Nos preguntará:
  • El espaciado horizontal y vertical, que normalmente será 8 o 16 píxeles en una imagen de un juego de 8 bits que no se haya redimensionado, o bien 16 o 32 píxeles si la imagen está a doble tamaño, como ocurre con algunas que exporta CPCE y algún otro emulador.
  • El desplazamiento inicial, tanto en horizontal como en vertical, que suele ser 0.
  • La anchura de las líneas, que debería bastar con 1 píxel, para que no nos tape mucho de la imagen original.
  • El color de las líneas, que depende de cómo sea el juego: en los que tienen una pantalla predominantemente negra, podríamos dibujar líneas blancas, amarillas o incluso rojas.

El resultado debería ser algo parecido a:

En el que se ve claramente las casillas repetitivas que forman el fondo.

Si ahora queremos extraer esas casillas para usarlas nosotros en nuestro remake, tenemos dos opciones (ambas partiendo de la imagen original, no de la recuadrada):
  • Extraer los fragmentos que nos interesan, con un poco de paciencia y la herramienta de recorte, como ya vimos.
  • Crear un programita que extraiga todos los tiles por nosotros, y luego eliminamos los repetidos.
Ese tipo de programas puede ser muy fácil de hacer si nos apoyamos en herramientas como "nconvert" (www.xnview.com), que sean capaces de convertir una imagen a PNG y de cambiarle el tamaño o recortarla, todo ello a partir de los parámetros que le indiquemos. Así, nuestro programa simplemente tendría que llamar a "nconvert" (o la herramienta similar que escojamos) de forma repetitiva, indicando qué fragmento queremos extraer cada vez.

Un programa básico en C# que nos ayudara en esta tarea podría ser así:


// Recortador de imágenes en tiles
// (se apoya en nconvert)

using System; // WriteLine
using System.IO; // Path
using System.Diagnostics; // Process

public class RecortadorDeFicheros
{
public static void Main( string[] args )
{
if (args.Length < 1)
{
Console.WriteLine("Indica el nombre de fichero a recortar");
return;
}

int anchoImagen = 768;
int altoImagen = 576;

int anchoTile = 32;
int altoTile = 32;

int cantidadColumnas = anchoImagen / anchoTile;
int cantidadFilas = altoImagen / altoTile;

string nombre = args[0];
string nombreSalida = Path.GetFileNameWithoutExtension(nombre);
string extension = Path.GetExtension(nombre);


for (int fila = 0; fila < cantidadFilas; fila++)
for (int columna = 0; columna < cantidadColumnas; columna++)
{
Console.WriteLine("Fragmento: "+fila+","+columna);
string filaTexto = fila.ToString("00");
string columnaTexto = columna.ToString("00");
string nombreFinal = nombreSalida + filaTexto +
columnaTexto + "." + extension;
File.Copy(nombre, nombreFinal);
Process.Start("nconvert.exe",
"-out png -D -crop "
+columna*anchoTile + " " // x
+fila*altoTile + " " // y
+anchoTile + " " // w
+altoTile + " " // h
+nombreFinal
);
}
}
}




03 febrero 2011

Convertir una imagen en transparente

Para hacer que una imagen tenga zonas transparentes con GIMP (y así poderla usar como "sprite" en un juego, o como parte de una Web), los pasos a seguir serían:

0.- Abrir la imagen en The GIMP.

1.- En la ventana de capas hacer Clic con el botón derecho del ratón sobre la única capa que tiene nuestra imagen, y escoger la opción "Añadir canal alfa":


2.- En la barra de herramientas escoger la herramienta "Borrador":



y comenzar a borrar con ella. Lo que ya no es parte de la imagen (sino del fondo transparente) debería verse como un trama de cuadraditos grises claros y grises oscuros:


Generalmente será preferible no borrar "a pulso", así que podemos...

3.- Escoger la herramienta de "Selección difusa" (la varita mágica")


Hacemos clic en una parte del fondo que queramos borrar. Todos los píxeles colindantes que sean del mismo color se quedarán seleccionados también, así que podemos usar un pincel más grande para borrar rápidamente:


4.- Cuando la imagen ya está a nuestro gusto, vamos a "Guardar como" (en el menú Archivo) y le damos un nombre que termine en ".png", para que GIMP sepa que queremos usar dicho formato gráfico, que sí permite transparencia, aunque nuestra imagen original fuera un BMP p cualquier otra que no la permitiera:


Y si todo ha ido bien, nos preguntará ciertos detalles adicionales (que normalmente no será necesario cambiar), entre los que está si queremos guardar los datos de transparencia:



¡ Eso es todo !