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.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