Mostrando entradas con la etiqueta XNA. Mostrar todas las entradas
Mostrando entradas con la etiqueta XNA. Mostrar todas las entradas

jueves, 8 de julio de 2010

Programando videojuegos: Tutorial XNA Game Studio 4.0 parte 5

crab-icon ball c#

¿Que tal pequeños cangrejillos? Disculpad la tardanza de esta quinta parte del tutorial, pero me ha dado mas problemas de los que me esperaba, y además he andado algo ocupadillo. En esta parte de tutorial vamos a aprender a manejar detección de colisiones, parte fundamental de prácticamente cualquier videojuego, así que nos ponemos serios, que empieza lo difícil. Empecemos por la teoría.

Existen numerosas técnicas de detección de colisiones entre sprites (no exclusivas de XNA, son comunes en el mundo del desarrollo de videojuegos). Lo más fácil y eficiente (de cara a procesamiento y obtención de buenos resultados de performance) es utilizar rectángulos, detectando colisión cuando estos se intersecan.

           

El problema obvio es la imprecisión en la detección de la colisión. Según esto, un impreciso acercamiento entre los dos objetos ya supondría una colisión, como se puede ver en las imágenes anteriores.

En muy pocas ocasiones, pues, esta técnica tan simple nos valdrá. Aunque existen otras variantes más precisas, y todavía eficientes, basadas en la detección de colisiones mediante rectángulos. Una de ellas sería dividir cada uno de los sprites en rectángulos más pequeños, y englobarlos todos en uno más grande. Entonces, primero intentaríamos detectar la colisión mediante los rectángulos grandes, y solo si esta es afirmativa, intentaríamos detectar la colisión entre los rectángulos más pequeños (y estos son los que decidirían si hay colisión o no).

           colisiones1

Como esta existen muchas variantes de la misma técnica, basadas en diferentes formas geométricas, y si bien esta técnica es mucho mas precisa que la anterior, aun puede ser más precisa, aplicando la detección de colisiones denominada “Perfecta” o Pixel a Pixel, en la cual se obtendrá precisión perfecta, activándose solo cuando se colisione a nivel de pixel.

La principal desventaja de esta técnica es que es más complicada que las anteriores, y por tanto tiene un coste computacional mucho mayor. Sin embargo en nuestro caso vamos a adoptar una solución intermedia, comprobando solo la colisión pixel a pixel solo si se detecta colisión con rectángulos. Evidentemente el rendimiento no es igual que con las técnicas más simples, pero se mejorará muchísimo.

Fuente: http://geeks.ms/blogs/jbosch/archive/2009/08/08/xna-pixel-perfect-collision-con-xna-en-base-a-un-mapa-de-colisiones-2d.aspxEnemigoBasico

Pues explicado esto, vamos al tema. Lo primero que habrá que hacer es una clase para crear enemigos. Esta parte voy rápido que no hay nada nuevo. Añadimos la imagen del enemigo como siempre (metiéndola en la carpeta content y arrastrándola al VS). Una vez esta esto, creamos la clase Enemigo1, algo así:



Imagino que a estas alturas sabréis por donde van los tiros. 40 y 57 son las dimensiones de la imagen. Además de los atributos de siempre vemos que tenemos un Rectangle, llamado bounds, en el que almacenamos el rectángulo que representa la imagen dibujada en pantalla y lo mantenemos actualizado (en update). Además añadimos varias propiedades para poder acceder a imagen, _posicion y bounds sin necesidad de implementar un método get (Gracias a esto se podra acceder simplemente con enemigo.Imagen o enemigo.Bounds. Este nuevo atributo, así como las diferentes propiedades también tienen que ser añadidas a las clases Nave y Disparo, pues nos hará falta para la detección de colisiones:







Solo muestro las partes que cambio por evitar hacer un post enorme, así que cuidado si copiáis pegáis. Con esto añadimos las nuevas propiedades, el nuevo atributo, y mantenemos actualizado este atributo. Es importante darse cuenta de la diferencia entre este rectángulo (bounds) que muestra el rectángulo ocupado por la imagen en la ventana, y el otro rectángulo que ya teníamos en Nave, que representa que parte de la imagen se va a dibujar.



Ahora que esta todo preparado, lo primero que hay que hacer es mostrar los enemigos por pantalla. Esto lo manejaremos en la clase Game1 de momento (En la próxima parte del tutorial se hará una pequeña reestructuración).





Añadimos a los atributos una lista de enemigos, que luego instanciamos en el constructor, además de un int para contar los frames (ahora veréis para que). Después, en Update, añadimos UpdateEnemigos, que hará varias cosas. Lo primero será aumentar frameCounter para así saber cuando pasan 60 frames.



Si el contador es mayor que 60 procederemos a añadir un nuevo enemigo (Con esto evitamos añadir sabedioscuantos enemigos por segundo). Para ello, instanciamos un objeto Random, y creamos un enemigo con una coordenada X aleatoria (Con la anchura de la pantalla de máximo) y –57 de coordenada Y para que entre poco a poco a la pantalla. Añadimos este enemigo recién creado a la lista enemigos, controlamos el evento FueraDePantalla (De igual manera que lo hacemos con los disparos, para borrar los enemigos que se salgan de la pantalla). Por ultimo ponemos el contador de frames a 0.



Después, fuera del If (esto se tiene que ejecutar siempre, no solo cada 60 frames) se invocara a Update de todos los enemigos. Una vez más no utilizamos foreach por que es posible que se dispare FueraDePantalla y que cambie enemigos, lo que haría que un foreach diese un error en tiempo de ejecución, pero la expresión lambda utilizada en esencia hace lo mismo.



image



Tras esto podemos probar a ejecutar y veremos que ya tenemos una serie de enemigos en pantalla cada X tiempo. ¿Va cogiendo forma no?. Ahora que ya está todo preparado podemos empezar con la detección de colisiones, manejada una vez mas desde Game1 temporalmente:





Añadimos a Update otro método en el cual manejaremos las colisiones. Como ya mencionamos anteriormente no se puede eliminar la lista que se este iterando dentro de un iterador foreach, pero esta vez no podremos usar una expresión lambda, así que utilizaremos un tag y unas variables locales para saber cuando se encuentra una colisión y borrar tanto el disparo como el enemigo después de salir del bucle.



En el método colisión enemigo disparo, usado por el método anterior, comprobamos si hay una colisión rectangular, y en caso de que sea así pasamos a ver si hay colisión pixel a pixel. En caso de que sea así devolvemos true y si no devolvemos false.



En cuanto al método estático ColisionPixel, almacenamos los datos de los pixeles en los arrays bitsA y bitsB, buscamos los limites del espacio que vamos a manejar, con las coordenadas x1, x2, y1 y y2. Con todo esto recorremos los arrays bitsA y bitsB con dos bucles for en los cuales comprobamos en cada iteración el canal Alpha de transparencia de las dos imágenes en un punto concreto. Por esto en caso de que ninguna de las dos imágenes sea transparente en cualquier punto significa que hay colisión, por lo que devolveremos true.



image image



Con esto la detección de colisiones esta completa, y lo podemos ver ejecutando el juego, aunque no se aprecie en la captura :P La colisión de los enemigos con las nave la pondremos mas adelante, cuando se añadan las vidas y la pantalla de gameover. No obstante esto empieza a coger forma ya, ¿no? Podéis bajar el código hasta ahora en el siguiente enlace:  http://www.megaupload.com/?d=OE6KUY4S



En la próxima entrega del tutorial menú, pantalla de pausa y pantalla de gameover. No os lo perdáis y ¡Sed buenos cangrejos!





EDIT: He cambiado algunas cosas en el código respecto a la parte 5 del tutorial original con la intención de obtener un código más limpio. Quite todas las _ de las variables, y me asegure de que todas las propiedades estaban en mayúscula. A parte de esto también cambie algunos puntos donde se accedía a las variables mediante las propiedades desde dentro de la misma clase.



Para los que hagan el tutorial a partir de ahora (22/09/2010) no lo notarán, pues ya esta actualizado, pero los que ya han hecho el tutorial que se bajen mi código, ya que esta corregido. Es posible que se me colase alguna errata, en tal caso hacédmelo saber.

martes, 22 de junio de 2010

Programando videojuegos: Tutorial XNA Game Studio 4.0 parte 4

crab-icon ball c#

Hoy en The Code Crab vamos a poner un fondo animado para nuestro futuro superventas. La técnica que vamos a seguir es bien sencilla: Tendremos una imagen que tenga el mismo ancho que la pantalla pero mucho mas alta, y la empezaremos a dibujar por abajo, subiendo poco a poco, hasta llegar a la parte superior que será similar a la parte inferior. Cuando pasa esto volvemos a la parte de abajo y ya tenemos efecto de scroll.

Dado que XNA solo soporta imágenes de resolución X x Y, donde ni X ni Y rebasan los 2000 y pico pixeles (no se cuantos exactamente) es posible que para un scroll largo haga falta dividir el fondo en diferentes imágenes. No vamos a dar un ejemplo de ese caso, pero que sepáis que sería exactamente lo mismo pero un poco mas complicado. Lo más importante es que si tenemos 3 imágenes A, B y C, para hacer una transición fluida de una a otra la ultima parte que se dibuje de una tiene que ser equivalente a la primera de la siguiente, esto es, el final de A y el principio de B coinciden, el final de B y el principio de C coinciden y el final de C y el principio de A coinciden.

Una vez tenemos claro lo que queremos hacer, necesitamos saber la resolución de nuestra ventana para preparar la imagen. Para ello, como recordareis, tenemos las propiedades PreferredBackBufferHeight y PreferredBackBufferWidth de graphics. Aunque si queréis se pueden alterar, yo no lo voy a hacer, voy a dejar la resolución por defecto. Como no la altero, voy a imprimir estos datos por pantalla, para saber que resolución tiene nuestra ventana:

 protected override void Initialize()
{
Console.WriteLine(graphics.PreferredBackBufferHeight + "x" + graphics.PreferredBackBufferWidth);
_fondo = new Fondo(graphics.PreferredBackBufferHeight, graphics.PreferredBackBufferWidth);
_nave = new Nave(graphics.PreferredBackBufferHeight, graphics.PreferredBackBufferWidth);
this.Window.Title="Space Burst";
base.Initialize();
}


Con esto, ejecutamos nuestro proyecto, y no ha cambiado nada, pero si vamos a la ventana de resultados veremos algo así:



image No os olvidéis de borrar la línea ahora que ya sabéis el tamaño de la ventana. Con esto ya sabemos la resolución de la ventana, 480x800, con lo que ahora necesitamos una imagen loquesea x 800 donde los primeros y los últimos 480 px sean iguales. A partir de una imagen real del espacio, algo de photoshopeo cutre (se me da fatal) y algo de paciencia, yo he conseguido esto, aunque si os apetece podéis haceros vuestro propio fondo.



spaceBackgr Con esto, tenemos ya un fondo de 1700x800 con el principio y el final igual (Hay que pinchar en la imagen para verla completa). Lo metemos en la carpeta y lo agregamos a los recursos del proyecto, como venimos haciendo con todas las imágenes. Tras esto ya estamos preparados para crear la clase Fondo (esta es bastante sencillita):



 private const int anchoImagen = 800;
private const int altoImagen = 1700;
private int altoVentana;
private Texture2D imagen;
private Rectangle rectangulo;
public Fondo(int altoVentana, int anchoVentana)
{
rectangulo = new Rectangle(0, altoImagen - altoVentana, anchoVentana, altoVentana);
this.altoVentana = altoVentana;
}


En cuanto a las propiedades, a estas alturas no os sorprenderá mucho, así que iré rápido. Guardamos el ancho y el alto de la imagen, así como el alto de la ventana (para saber cuanto tiene que medir el rectángulo) que será pasado en el constructor. También almacenamos un rectángulo con la sección de la imagen que se dibujara, empezando por la parte inferior de la imagen y un Texture2D con la imagen en sí.



        public void LoadContent(ContentManager Content)
{
imagen = Content.Load("spaceBackgr");
}
public void Update()
{
rectangulo.Y -= 1;
if (rectangulo.Y <= 0)
rectangulo.Y = ((altoImagen - altoVentana) -15);
}
public void Draw(SpriteBatch spbtch)
{
spbtch.Draw(imagen, new Vector2(0, 0), rectangulo, Color.White);
}


En cuanto a los métodos, tendremos los 3 tradicionales de XNA que ya conoceréis. LoadContent y Draw son iguales que en Nave, por lo que no merece mucho la pena pararse (en cuanto a Draw la única diferencia es que lo dibujamos en (0,0), ya que queremos que ocupe toda la pantalla).



Lo importante aquí (relativamente, pues es bastante simple en realidad) es el método Update, en el cual disminuimos en 1 la coordenada Y de rectángulo, para subir hacia arriba el rectángulo y crear un efecto scroll. A mi personalmente me gusta el efecto que queda al disminuir en 1 cada Update, pero si se aumenta el numero aumentara la velocidad del scroll. Eso queda a gusto de cada uno. También controlamos que si Y es 0 o menos, volvemos al principio, volviendo a mostrar la parte de abajo de la imagen, que como ya mencioné anteriormente debería ser igual. Ese –15 extra se debe a que si ponemos las coordenadas iniciales sin mas se apreciara un rebote, dado a que la imagen se esta moviendo constantemente, y que probablemente la parte de arriba y la de abajo no sean exactamente iguales pixel a pixel. A base de prueba y error para el caso de esta imagen he sacado que el numero apropiado son 15 px, pero si usáis otra imagen esto cambiara.



Ahora que ya tenemos lista la clase Fondo, solo nos queda agregar una instancia en la clase principal Game1, y las invocaciones a todos los métodos necesarios:



    public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Nave _nave;
Fondo _fondo;

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

protected override void Initialize()
{
//Recordar comentar la siguiente linea
//Console.WriteLine(graphics.PreferredBackBufferHeight + "x" + graphics.PreferredBackBufferWidth);
_fondo = new Fondo(graphics.PreferredBackBufferHeight, graphics.PreferredBackBufferWidth);
_nave = new Nave(graphics.PreferredBackBufferHeight, graphics.PreferredBackBufferWidth);
this.Window.Title="Space Burst";
base.Initialize();
}

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

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

protected override void Update(GameTime gameTime)
{
// Cuidado con esto, no usamos mando.
if (Keyboard.GetState().IsKeyDown(Keys.Escape))
this.Exit();
_fondo.Update();
_nave.Update();

base.Update(gameTime);
}

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

spriteBatch.Begin();
_fondo.Draw(spriteBatch);
_nave.Draw(spriteBatch);
spriteBatch.End();

base.Draw(gameTime);
}
}


Es importante recordar, que como se menciono anteriormente, para que la imagen A se dibuje por encima de la imagen B, hay que dibujar primero B y luego A. Por tanto, el Fondo debe ser lo primero en dibujarse, o sino tapara el resto de objetos. Por lo demás, nada nuevo.



image



Tras ejecutar vemos el efecto obtenido. ¿Que os parece? Yo creo que no esta mal, queda un poco raro la nave pixelada con la imagen real del espacio, pero bueno, siempre podéis cambiar vuestro fondo por otro :P



Podéis descargaros el código fuente del tutorial hasta este momento en el siguiente enlace: http://www.megaupload.com/?d=RB2TYYLJcangrejus guaperus



Con esto concluye esta parte del tut orial. En la siguiente parte nos ponemos serios, introduciendo enemigos y detección de colisiones, no os lo perdáis. ¡Sed buenos cangrejos!



PD: ¡¡Gracias a Sara por el aporte del cangrejo!!



Etiquetas de Technorati: ,,,,,