Sprites
Durante los capítulos siguientes se profundiza en los diferentes aspectos concernientes a la programación de videojuegos. Ya dispones de las herramientas necesarias para emprender la aventura, así que siéntate cómodamente, flexiona tus dedos y prepárate para la diversión. Para ilustrar las técnicas que se describirán en los próximos capítulos desarrollaremos un pequeño videojuego. Va a ser un juego sin grandes pretensiones, pero que nos va a ayudar a entender los diferentes aspectos que encierra este fascinante mundo. Nuestro juego va a consistir en lo que se ha dado en llamar shooter en el argot de los videojuegos. Quizás te resulte más familiar “matamarcianos”. En este tipo de juegos manejamos una nave que tiene que ir destruyendo a todos los enemigos que se pongan en su camino. En nuestro caso, va a estar ambientado en la segunda guerra mundial, y pilotaremos un avión que tendrá que destruir una orda de aviones enemigos. El juego es un homenaje al mítico 1942.
Este capítulo lo vamos a dedicar a los sprites. Seguro que alguna vez has jugado a Space Invaders. En este juego, una pequeña nave situada en la parte inferior de la pantalla dispara a una gran cantidad de naves enemigas que van bajando por la pantalla hacia el jugador. Pues bien, nuestra nave es un sprite, al igual que los enemigos, las balas y los escudos. Podemos decir que un sprite es un elemento gráfico determinado (una nave, un coche, etc...) que tiene entidad propia y sobre la que podemos definir y modificar ciertos atributos, como la posición en la pantalla, si es o no visible, etc... Un sprite, pues, tiene capacidad de movimiento. Distinguimos dos tipos de movimiento en los sprites: el movimiento externo, es decir, el movimiento del sprite por la pantalla, y el movimiento interno o animación.
Para posicionar un sprite en la pantalla hay que especificar sus coordenadas. Es como el juego de los barquitos, en el que para identificar un cuadrante hay que indicar una letra para el eje vertical (lo llamaremos eje Y) y un número para el eje horizontal (al que llamaremos eje X). En un ordenador, un punto en la pantalla se representa de forma parecida. La esquina superior izquierda representa el centro de coordenadas. La figura siguiente muestra el eje de coordenadas en una pantalla con una resolución de 320 por 200 píxeles.
Un punto se identifica dando la distancia en el eje X al lateral izquierdo de la pantalla y la distancia en el eje Y a la parte superior de la pantalla. Las distancias se miden en píxeles. Si queremos indicar que un sprite está a 100 píxeles de distancia del eje vertical y 150 del eje horizontal, decimos que está en la coordenada (100,150).
Imagina ahora que jugamos a un videjuego en el que manejamos a un hombrecillo. Podremos observar cómo mueve las piernas y los brazos según avanza por la pantalla. Éste es el movimiento interno o animación. La siguiente figura muestra la animación del sprite de un gato.
Otra característica muy interesante de los sprites es que nos permiten detectar colisiones entre ellos. Esta capacidad es realmente interesante si queremos conocer cuando nuestro avión ha chocado con un enemigo o con uno de sus misiles.
Control de sprites
Vamos a realizar una pequeña librería (y cuando digo pequeña, quiero decir realmente pequeña) para el manejo de los sprites. Luego utilizaremos esta librería en nuestro juego, por supuesto, también puedes utilizarla en tus propios juegos, así como ampliarla, ya que cubrirá sólo los aspectos básicos en lo referente a sprites.
Dotaremos a nuestra librería con capacidad para movimiento de sprites, animación (un soporte básico) y detección de colisiones.
Para almacenar el estado de los Sprites utilizaremos las siguientes variables.
private int posx,posy; private boolean active; private int frame,nframes; private Image[] sprites;
Necesitamos la coordenada en pantalla del sprite (que almacenamos en posx y posy. La variable active nos servirá para saber si el sprite está activo. La variable frame almacena el frame actual del sprite, y nframes el número total de frames de los que está compuesto. Por último, tenemos un array de objetos Image que contendrá cada uno de los frames del juego.
Como puedes observar no indicamos el tamaño del array, ya que aún no sabemos cuantos frames tendrá el sprite. Indicaremos este valor en el constructor del sprite.
// constructor. 'nframes' es el número de frames del Sprite. public Sprite(int nframes) { // El Sprite no está activo por defecto. active=false; frame=1; this.nframes=nframes; sprites=new Image[nframes+1]; }
El constructor se encarga de crear tantos elementos de tipo Image como frames tenga el sprite. También asignamos el estado inicial del sprite.
La operación más importante de un sprite es el movimiento por la pantalla. Veamos los métodos que nos permitirán moverlo.
public void setX(int x) { posx=x; } public void setY(int y) { posy=y; } int getX() { return posx; } int getY() { return posy; }
Como puedes observar, el código para posicionar el sprite en la pantalla no puede ser más simple. Los métodos setX() y setY() actualizan las variables de estado del sprite (posx,posy). Los métodos getX() y getY() realizan la operación contraria, es decir, nos devuelve la posición del sprite. Además de la posición del sprite, nos va a interesar en determinadas condiciones conocer el tamaño del mismo.
int getW() { return sprites[nframes].getWidth(); } int getH() { return sprites[nframes].getHeight(); }
Los métodos getW() y getH() nos devuelven el ancho y el alto del sprite en píxeles. Para ello recurrimos a los métodos getWidth() y getHeigth() de la clase Image.
Otro dato importante del sprite es si está activo en un momento determinado.
public void on() { active=true; } public void off() { active=false; } public boolean isActive() { return active; }
Necesitaremos un método que active el sprite, al que llamaremos on(), y otro para desactivarlo, que como podrás imaginar, llamaremos off(). Nos resta un método para conocer el estado del sprite. Hemos llamado al método isActive().
En lo referente al estado necesitamos algún método para el control de frames, o lo que es lo mismo, de la animación interna del sprite.
public void selFrame(int frameno) { frame=frameno; } public int frames() { return nframes; } public void addFrame(int frameno, String path) { try { sprites[frameno]=Image.createImage(path); } catch (IOException e) { System.err.println("Can`t load the image " + path + ": " + e.toString()); } }
El método selFrame() fija el frame actual del sprite, mientras que el método frame() nos devolverá el número de frames del sprite.
El método addFrame() nos permite añadir frames al sprite. Necesita dos parámetros. El parámetro frameno, indica el número de frame, mientras que el parámetro path indica el camino y el nombre del gráfico que conformará dicho frame.
Para dibujar el sprite, vamos a crear el método draw(). Lo único que hace este método es dibujar el frame actual del sprite en la pantalla.
public void draw(Graphics g) { g.drawImage (sprites[frame], posx, posy, Graphics.HCENTER|Graphics.VCENTER); }
Nos resta dotar a nuestra librería con la capacidad de detectar colisiones entre sprites. La detección de colisiones entre sprites puede enfocarse desde varios puntos de vista. Imaginemos dos sprites, nuestro avión y un disparo enemigo. En cada vuelta del game loop tendremos que comprobar si el disparo ha colisionado con nuestro avión. Podríamos considerar que dos sprites colisionan cuando alguno de sus píxeles visibles (es decir, no transparentes) toca con un píxel cualquiera del otro sprite. Esto es cierto al 100%, sin embargo, la única forma de hacerlo es comprobando uno por uno los píxeles de ambos sprites. Evidentemente esto requiere un gran tiempo de computación, y es inviable en la práctica. En nuestra librería hemos asumido que la parte visible de nuestro sprite coincide más o menos con las dimensiones de la superficie que lo contiene. Si aceptamos esto, y teniendo en cuenta que una superficie tiene forma cuadrangular, la detección de una colisión entre dos sprites se simplifica bastante. Sólo hemos de detectar el caso en el que dos cuadrados se solapen.
En la primera figura no existe colisión, ya que no se solapan las superficies (las superficies están representadas por el cuadrado que rodea al gráfico). La segunda figura muestra el principal problema de este método, ya que nuestra librería considerará que ha habido colisión cuando realmente no ha sido así. A pesar de este pequeño inconveniente, este método de detección de colisiones es el más rápido. Es importante que la superficie tenga el tamaño justo para albergar el gráfico. Este es el aspecto que tiene nuestro método de detección de colisiones.
boolean collide(Sprite sp) { int w1,h1,w2,h2,x1,y1,x2,y2; w1=getW(); // ancho del sprite1 h1=getH(); // altura del sprite1 w2=sp.getW(); // ancho del sprite2 h2=sp.getH(); // alto del sprite2 x1=getX(); // pos. X del sprite1 y1=getY(); // pos. Y del sprite1 x2=sp.getX(); // pos. X del sprite2 y2=sp.getY(); // pos. Y del sprite2 if (((x1+w1)>x2)&&((y1+h1)>y2)&&((x2+w2)>x1)&&((y2+h2)>y1)) { return true; } else { return false; } }
Se trata de comprobar si el cuadrado (superficie) que contiene el primer sprite, se solapa con el cuadrado que contiene al segundo.
Hay otros métodos más precisos que nos permiten detectar colisiones. Consiste en dividir el sprite en pequeñas superficies rectangulares tal y como muestra la próxima figura.
Se puede observar la mayor precisión de este método. El proceso de detección consiste en comprobar si hay colisión de alguno de los cuadros del primer sprite con alguno de los cuadrados del segundo utilizando la misma comprobación que hemos utilizado en el primer método para detectar si se solapan dos rectangulos. Se deja como ejercicio al lector la implementación de este método de detección de colisiones. A continuación se muestra el listado completo de nuestra librería.
import javax.microedition.midlet.*; import javax.microedition.lcdui.*; import java.util.*; import java.io.*; class Sprite { private int posx,posy; private boolean active; private int frame,nframes; private Image[] sprites; // constructor. 'nframes' es el número de frames del Sprite. public Sprite(int nframes) { // El Sprite no está activo por defecto. active=false; frame=1; this.nframes=nframes; sprites=new Image[nframes+1]; } public void setX(int x) { posx=x; } public void setY(int y) { posy=y; } int getX() { return posx; } int getY() { return posy; } int getW() { return sprites[nframes].getWidth(); } int getH() { return sprites[nframes].getHeight(); } public void on() { active=true; } public void off() { active=false; } public boolean isActive() { return active; } public void selFrame(int frameno) { frame=frameno; } public int frames() { return nframes; } // Carga un archivo tipo .PNG y lo añade al sprite en // el frame indicado por 'frameno' public void addFrame(int frameno, String path) { try { sprites[frameno]=Image.createImage(path); } catch (IOException e) { System.err.println("Can`t load the image " + path + ": " + e.toString()); } } boolean collide(Sprite sp) { int w1,h1,w2,h2,x1,y1,x2,y2; w1=getW(); // ancho del sprite1 h1=getH(); // altura del sprite1 w2=sp.getW(); // ancho del sprite2 h2=sp.getH(); // alto del sprite2 x1=getX(); // pos. X del sprite1 y1=getY(); // pos. Y del sprite1 x2=sp.getX(); // pos. X del sprite2 y2=sp.getY(); // pos. Y del sprite2 if (((x1+w1)>x2)&&((y1+h1)>y2)&&((x2+w2)>x1)&&((y2+h2)>y1)) { return true; } else { return false; } } // Dibujamos el Sprite public void draw(Graphics g) { g.drawImage(sprites[frame],posx,posy,Graphics.HCENTER|Graphics.VCENTER); }
Veamos un ejemplo práctico de uso de nuestra librería. Crea un nuevo proyecto en KToolBar, y añade el programa siguiente en el directorio ‘src’, junto con la librería Sprite.java. Por supuesto necesitarás incluir el gráfico hero.png en el directorio ‘res’.
En los siguientes capítulos vamos a basarnos en esta librería para el control de los Sprites del juego que vamos a crear.
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class SpriteTest extends MIDlet implements CommandListener {
private Command exitCommand, playCommand, endCommand;
private Display display;
private SSCanvas screen;
public SpriteTest() {
display=Display.getDisplay(this);
exitCommand = new Command("Salir",Command.SCREEN,2);
screen=new SSCanvas();
screen.addCommand(exitCommand);
screen.setCommandListener(this);
}
public void startApp() throws MIDletStateChangeException {
display.setCurrent(screen);
}
public void pauseApp() {}
public void destroyApp(boolean unconditional) {}
public void commandAction(Command c, Displayable s) {
if (c == exitCommand) {
destroyApp(false);
notifyDestroyed();
}
}
}
class SSCanvas extends Canvas {
private Sprite miSprite=new Sprite(1);
public SSCanvas() {
// Cargamos los sprites
miSprite.addFrame(1,"/hero.png");
// Iniciamos los Sprites
miSprite.on();
}
public void paint(Graphics g) {
// Borrar pantalla
g.setColor(255,255,255);
g.fillRect(0,0,getWidth(),getHeight());
// situar y dibujar sprite
miSprite.setX(50);
miSprite.setY(50);
miSprite.draw(g);
}
}
import javax.microedition.lcdui.*;
public class SpriteTest extends MIDlet implements CommandListener {
private Command exitCommand, playCommand, endCommand;
private Display display;
private SSCanvas screen;
public SpriteTest() {
display=Display.getDisplay(this);
exitCommand = new Command("Salir",Command.SCREEN,2);
screen=new SSCanvas();
screen.addCommand(exitCommand);
screen.setCommandListener(this);
}
public void startApp() throws MIDletStateChangeException {
display.setCurrent(screen);
}
public void pauseApp() {}
public void destroyApp(boolean unconditional) {}
public void commandAction(Command c, Displayable s) {
if (c == exitCommand) {
destroyApp(false);
notifyDestroyed();
}
}
}
class SSCanvas extends Canvas {
private Sprite miSprite=new Sprite(1);
public SSCanvas() {
// Cargamos los sprites
miSprite.addFrame(1,"/hero.png");
// Iniciamos los Sprites
miSprite.on();
}
public void paint(Graphics g) {
// Borrar pantalla
g.setColor(255,255,255);
g.fillRect(0,0,getWidth(),getHeight());
// situar y dibujar sprite
miSprite.setX(50);
miSprite.setY(50);
miSprite.draw(g);
}
}
No hay comentarios:
Publicar un comentario