Sin duda una de las aplicaciones que más famosas se han hecho con el surgimiento de los teléfonos móviles MIDP son los juegos Java. Con estos teléfonos los usuarios pueden descargar estos juegos de Internet, normalmente previo pago de una tarifa, e instalarlos en el teléfono. De esta forma podrán añadir fácilmente al móvil cualquier juego realizado en Java, sin limitarse así a tener únicamente el típico juego de "la serpiente" y similares que vienen preinstalados en determinados teléfonos.
Vamos a ver en esta sección los conceptos básicos de la programación de videojuegos y las APIs de MIDP dedicadas a esta tarea. Comenzaremos viendo una introducción a los tipos de videojuegos que normalmente encontraremos en los teléfonos móviles.
Podemos distinguir diferentes tipos de juegos:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Figura 18. Capturas de juegos para diferentes modelos de móviles
Los primeros juegos que podíamos encontrar en los móviles eran normalmente juegos muy sencillos tipo puzzle o de mesa, o en todo caso juegos de acción muy simples similares a los primeros videojuegos aparecidos antes de los 80. En los móviles con soporte para Java podremos tener juegos más complejos, como los que se podían ver en los ordenadores y consolas de 8 bits, y estos juegos irán mejorando conforme evolucionen los teléfonos móviles.
Además tenemos las ventajas de que existe una gran comunidad de programadores en Java, a los que no les costará aprender a desarrollar este tipo de juegos para móviles, por lo que el número de juegos disponible crecerá rápidamente. El poder descargar y añadir estos juegos al móvil de forma sencilla, como cualquier otra aplicación Java, hará estos juegos especialmente atractivos para los usuarios, ya que de esta forma podrán estar disponiendo continuamente de nuevos juegos en su móvil.
Los juegos que se ejecutan en un móvil tendrán distintas características que los juegos para ordenador o videoconsolas, debido a las peculiaridades de estos dispositivos.
Ante todo, estos videojuegos deben ser atractivos para los jugadores, ya que su única finalidad es entretener.
Hemos visto que los juegos son aplicaciones que deben resultar atractivas para los usuarios. Por lo tanto deben tener gráficos personalizados e innovadores. La API de bajo nivel de LCDUI nos ofrece el control suficiente sobre lo que dibujamos en pantalla para poder crear cualquier interfaz gráfica que queramos que tengan nuestros juegos. Esto, junto al control a bajo nivel que nos ofrece sobre los eventos de entrada, hace que esta API sea suficiente para desarrollar videojuegos para móviles.
Además, en MIDP 2.0 se incluyen una serie de clases adicionales en el
paquete javax.microedition.lcdui.game
que basándose en la
API a bajo nivel de LCDUI nos ofrecerán facilidades para el desarrollo
de juegos. En estas clases tendremos implementados los objetos genéricos
que solemos encontrar en todos los juegos. Si queremos desarrollar juegos con
MIDP 1.0, deberemos implementar nosotros manualmente todos estos objetos.
Los juegos se instalarán en el móvil como cualquier otra aplicación Java. En el caso concreto de los teléfonos Nokia, podemos establecer en el fichero JAD un atributo propio de esta marca con el que especificamos el tipo de aplicación:
Nokia-MIDlet-Category: Game
Especificando que se trata de un juego, cuando lo instalemos en el teléfono los guardará directamente en la carpeta juegos, y no en la carpeta aplicaciones como lo haría por defecto.
Aplicación conducida por los datos
Cuando desarrollamos juegos, será conveniente llevar a la capa de datos todo lo que podamos, dejando la parte del código lo más simple y genérica posible.
Por ejemplo, podemos crear ficheros de datos donde se especifiquen las características de cada nivel del juego, el tipo y el comportamiento de los enemigos, los textos, etc.
Normalmente los juegos consisten en una serie de niveles. Cada vez que superemos un nivel, entraremos en uno nuevo en el que se habrá incrementado la dificultad, pero la mecánica del juego en esencia será la misma. Por esta razón es conveniente que el código del programa se encargue de implementar esta mecánica genérica, que conoceremos como motor del juego, y lea de ficheros de datos todas las características de cada nivel concreto.
De esta forma, si queremos añadir o modificar niveles del juego, cambiar la inteligencia artificial de los enemigos, añadir nuevos tipos de enemigos, o cualquier otro cambio de este tipo, no tendremos que modificar el código fuente, simplemente bastará con cambiar los ficheros de datos.
Es recomendable también centralizar la carga y la gestión de los recursos en una única clase. De esta forma quedará más claro qué recursos carga la aplicación, ya que no tendremos la carga de recursos dispersa por todo el código de las clases del juego. En este mismo fichero podemos tener los textos que se muestren en pantalla, lo que nos facilitará realizar traducciones del juego, ya qué sólo tendremos que modificar este fichero.
Optimización
Los juegos deben funcionar de manera fluida y dar una respuesta rápida al usuario para que estos resulten jugables y atractivos. Por lo tanto, será conveniente optimizar el código todo lo posible, sobretodo en el caso de los dispositivos móviles en el que trabajamos con poca memoria y CPUs lentas.
No debemos cometer el error de intentar escribir un código optimizado desde el principio. Es mejor comenzar con una implementación clara, y una vez funcione esta implementación, intentar optimizar todo lo que sea posible.
Para optimizar el código deberemos detectar primero en qué lugar se está invirtiendo más tiempo. Por ejemplo, si lo que está ralentizando el juego es el volcado de los gráficos, deberemos optimizar esta parte, mientras que si es la lógica interna del juego la que requiere un tiempo alto, deberemos fijarnos en cómo optimizar este código.
El dibujado de los gráficos suele ser bastante costoso. Para optimizar
este proceso deberemos intentar dibujar sólo aquello que sea necesario,
es decir, lo que haya cambiado de un fotograma al siguiente. Muchas veces en
la pantalla se está moviendo sólo un personaje pequeño,
sería una pérdida de tiempo redibujar toda la pantalla cuando
podemos repintar únicamente la zona en la que se ha movido este personaje.
Esto lo podemos hacer con una variante del método repaint
que nos permite redibujar sólo el área de pantalla indicada.
Por otro lado, dentro del código del juego deberemos utilizar las técnicas de optimización que conocemos, propias del lenguaje Java. Es importante intentar crear el mínimo número de objetos posibles, reutilizando objetos siempre que podamos. Esto evita el tiempo que requiere la instanciación de objetos y su posterior eliminación por parte del colector de basura.
También deberemos tener en cuenta que la memoria del móvil es
muy limitada, por lo que deberemos permitir que se desechen todos los objetos
que ya no necesitamos. Para que un objeto pueda ser eliminado por el colector
de basura deberemos poner todas las referencias que tengamos a dicho objeto
a null
, para que el colector de basura sepa que ya nadie va a poder
usar ese objeto por lo que puede eliminarlo.
Cuando diseñemos un juego deberemos identificar las distintas entidades que encontraremos en él. Normalmente en los juegos de acción en 2D tendremos una pantalla del juego, que tendrá un fondo y una serie de personajes u objetos que se mueven en este escenario. Estos objetos que se mueven en el escenario se conocen como sprites. Además, tendremos un motor que se encargará de conducir la lógica interna del juego. Podemos abstraer los siguientes componentes:
Figura 19. Componentes de un juego
A continuación veremos con más detalle cada uno de estos componentes, viendo como ejemplo las clases que MIDP 2.0 nos proporciona para crear cada uno de ellos.
Esto es lo que encontraremos en la pantalla de juego, mientras dure la partida. Sin embargo los juegos normalmente constarán de varias pantallas. Las más usuales son las siguientes:
Figura 20. Mapa de pantallas típico de un juego
Hemos de decidir qué API utilizar para desarrollar cada pantalla. La pantalla de juego claramente debe realizarse utilizando la API de bajo nivel. Sin embargo con esta API hemos visto que no podemos introducir texto de una forma sencilla. Si utilizamos la API de alto nivel para la pantalla de puntuaciones más altas se nos facilitará bastante la tarea. El inconveniente es que siempre tendrá un aspecto más atractivo y propio del juego si utilizamos la API de bajo nivel, aunque nos cueste más programarla.
Es importante poder salir en todo momento del juego, cuando el usuario quiera de terminar de jugar. Durante la partida se deberá permitir volver al menú principal, o incluso salir directamente del juego. Para las acciones de salir y volver al menú se deben utilizas las teclas asociadas a las esquinas de la pantalla (soft-keys). No es recomendable utilizar estas teclas para acciones del juego.
Los sprites hemos dicho que son todos aquellos objetos que aparecen en la escena que se mueven y/o podemos interactuar con ellos de alguna forma.
Animación
Estos objetos pueden estar animados. Para ello deberemos definir los distintos fotogramas (o frames) de la animación. Podemos definir varias animaciones para cada sprite, según las acciones que pueda hacer. Por ejemplo, si tenemos un personaje podemos tener una animación para andar hacia la derecha y otra para andar hacia la izquierda.
El sprite tendrá un determinado tamaño (ancho y alto), y cada fotograma será una imagen de este tamaño. Para no tener un número demasiado elevado de imágenes lo que haremos será juntar todos los fotogramas del sprite en una misma imagen, dispuestos como un mosaico.
Figura 21. Mosaico con los frames de un sprite
Cambiando el fotograma que se muestra del sprite en cada momento podremos
animarlo. En MIDP 2.0 se proporciona la clase Sprite
que nos permite
manejar este tipo de mosaicos para definir los fotogramas del sprite
y animarlo. Podemos crear el sprite de la siguiente forma:
Sprite personaje = new Sprite(imagen, ancho_fotograma, alto_fotograma);
Proporcionamos la imagen donde tenemos el mosaico de fotogramas, y definimos las dimensiones de cada fotograma. De esta forma esta clase se encargará de separar los fotogramas que hay dentro de esta imagen.
Cada fotograma tendrá un índice que se empezará a numerar
a partir de cero. La ordenación de los frames en la imagen se
realiza por filas y de izquierda a derecha, por lo que el frame de
la esquina superior izquierda será el frame 0
.
Podemos establecer el frame a mostrar actualmente con:
personaje.setFrame(indice);
Podremos definir determinadas secuencias de frames para crear animaciones.
Desplazamiento
Además el sprite se podrá desplazar por la pantalla, por lo que deberemos tener algún método para moverlo. El sprite tendrá una cierta localización, dada en coordenadas (x,y) de la pantalla, y podremos o bien establecer una nuevas coordenadas o desplazar el sprite respecto a su posición actual:
personaje.setPosition(x, y); personaje.move(dx, dy);
Con el primer método damos la posición absoluta donde queremos posicionar el sprite. En el segundo caso indicamos un desplazamiento, para desplazarlo desde su posición actual. Normalmente utilizaremos el primer método para posicionarlo por primera vez en su posición inicial al inicio de la partida, y el segundo para moverlo durante el transcurso de la misma.
Colisiones
Otro aspecto de los sprites es la interacción entre ellos.
Nos interesará saber cuando somos tocados por un enemigo o una bala para
disminuir la vida, o cuando alcanzamos nosotros a nuestro enemigo. Para ello
deberemos detectar las colisiones entre sprites. La colisión
con sprites de formas complejas puede resultar costosa de calcular.
Por ello se suele realizar el cálculo de colisiones con una forma aproximada
de los sprites con la que esta operación resulte más
sencilla. Para ello solemos utilizar el bounding box, es decir, un
rectángulo que englobe el sprite. La intersección de
rectángulos es una operación muy sencilla, podremos comprobarlo
simplemente con una serie de comparaciones menor que <
y mayor
que >
.
La clase Sprite
nos permite realizar esta comprobación
de colisiones utilizando un bounding box. Podemos comprobar si nuestro
personaje está tocando a un enemigo con:
personaje.collidesWith(enemigo, false);
Con el segundo parámetro a true
le podemos decir que compruebe
la colisión a nivel de pixels, es decir, que en lugar de usar
el bounding box compruebe pixel a pixel si ambos
sprites colisionan. Esto será bastante más costoso. Si
necesitamos hacer la comprobación a este nivel, podemos comprobar primero
si colisionan sus bounding boxes para descartar así de forma
eficiente bastantes casos, y en caso de que los bounding boxes si que
intersecten, hacer la comprobación a nivel de pixels para comprobar
si realmente colisionan o no. Normalmente las implementaciones harán
esto internamente cuando comprobemos la colisión a nivel de pixels.
En los juegos normalmente tendremos un fondo sobre el que se mueven los personajes. Muchas veces los escenarios del juego son muy extensos y no caben enteros en la pantalla. De esta forma lo que se hace es ver sólo la parte del escenario donde está nuestro personaje, y conforme nos movamos se irá desplazando esta zona visible para enfocar en todo momento el lugar donde está nuestro personaje. Esto es lo que se conoce como scroll.
El tener un fondo con scroll será más costoso computacionalmente, ya que siempre que nos desplacemos se deberá redibujar toda la pantalla, debido a que se está moviendo todo el fondo. Además para poder dibujar este fondo deberemos tener una imagen con el dibujo del fondo para poder volcarlo en pantalla. Si tenemos un escenario extenso, sería totalmente prohibitivo hacer una imagen que contenga todo el fondo. Esta imagen podría llegar a ocupar el tamaño máximo permitido para los ficheros JAR es muchos móviles.
Para evitar este problema lo que haremos normalmente en este tipo de juegos es construir el fondo como un mosaico. Nos crearemos una imagen con los elementos básicos que vamos a necesitar para nuestro fondo, y construiremos el fondo como un mosaico en el que se utilizan estos elementos.
Figura 22. Mosaico de elementos del fondo
Al igual que hacíamos con los sprites, tomaremos estos distintos elementos de una misma imagen. A cada elemento se le asociará un índice con el que lo referenciaremos posteriormente. El fondo lo crearemos como un mosaico del tamaño que queramos, en el que cada celda contendrá el índice del elemento que se quiere mostrar en ella.
La clase TiledLayer
de MIDP 2.0 nos permite realizar esto. Deberemos
especificar el número de filas y columnas que va a tener el mosaico,
y después la imagen que contiene los elementos y el ancho y alto de cada
elemento.
TiledLayer fondo = new TiledLayer(columnas, filas, imagen, ancho, alto);
Figura 23. Índices de los elementos del mosaico
De esta forma el fondo tendrá un tamaño en pixels de
(columnas*ancho)x(filas*alto)
. Ahora podemos establecer el elemento
que contendrá cada celda del mosaico con el método:
fondo.setCell(columna, fila, indice);
Como indice
especificaremos el índice del elemento en la
imagen que queremos incluir en esa posición del mosaico, o -1
si queremos dejar esa posición vacía con el color del fondo.
Figura 24. Ejemplo de fondo constuido con los elementos anteriores
En la pantalla se dibujarán todos los elementos anteriores para construir la escena del juego. De esta manera tendremos el fondo, nuestro personaje, los enemigos y otros objetos que aparezcan durante el juego, además de marcadores con el número de vidas, puntuación, etc.
La pantalla la vamos a dibujar por capas. Cada sprite y cada fondo
que incluyamos será una capa, de esta forma poniendo una capa sobre otra
construiremos la escena. Tanto la clase Sprite
como la clase TiledLayer
heredan de Layer
, que es la clase que define de forma genérica
las capas, por lo que podrán comportarse como tales. Todas las capas
podrán moverse o cambiar de posición, para mover de esta forma
su contenido en la pantalla.
Construiremos el contenido de la pantalla superponiendo una capa sobre otra.
Tenemos la clase LayerManager
en MIDP 2.0 que nos permitirá
construir esta superposición de capas. Este objeto contendrá todo
lo que se vaya a mostrar en pantalla, encargándose él internamente
de gestionar y dibujar esta superposición de capas. Podemos crear este
objeto con:
LayerManager escena = new LayerManager();
Ahora deberemos añadir por orden las capas que queramos que se muestren. El orden en el que las añadamos indicará el orden z, es decir, la profundidad de esta capa en la escena. La primera capa será la más cercana al punto de vista del usuario, mientras que la última será la más lejana. Por lo tanto, las primeras capas que añadamos quedarán por delante de las siguientes capas. Para añadir capas utilizaremos:
escena.append(personaje); escena.append(enemigo); escena.append(fondo);
También podremos insertar capas en una determinada posición de
la lista, o eliminar capas de la lista con los métodos insert
y remove
respectivamente.
El área dibujada del LayerManager
puede ser bastante extensa,
ya que abarcará por lo menos toda la extensión del fondo que hayamos
puesto. Deberemos indicar qué porción de esta escena se va a mostrar
en la pantalla, especificando la ventana de vista. Moviendo esta ventana podremos
implementar scroll. Podemos establecer la posición y tamaño
de esta ventana con:
escena.setViewWindow(x, y, ancho, alto);
La posición de esta ventana de vista es referente al sistema de coordenadas
de la escena (de la clase LayerManager
).
Debemos tener en cuenta al especificar el tamaño del visor que diferentes modelos de móviles tendrán pantallas de diferente tamaño. Podemos hacer varias cosas para que el juego sea lo más portable posible. Podríamos crear un visor del tamaño mínimo de pantalla que vayamos a considerar, y en el caso de que la pantalla sea de mayor tamaño mostrar este visor centrado. Otra posibilidad es establecer el tamaño de la ventana de vista según la pantalla del móvil, haciendo que en los móviles con pantalla más grande se vea un mayor trozo del escenario.
Una vez hemos establecido esta ventana de vista, podemos dibujarla en el contexto
gráfico g
con:
escena.paint(g, x, y);
Donde daremos las coordenadas donde dibujaremos esta vista, dentro del espacio de coordenadas del contexto gráfico indicado.
Durante la ejecución de los juegos podemos distinguir diferentes estados. En la mayoría de los casos tenemos una pantalla de título, la pantalla en la que se desarrolla la partida, la pantalla de game over, una pantalla de demo en la que vemos el juego funcionar automáticamente como ejemplo, etc.
Normalmente lo primero que veremos será el título, de aquí podremos pasar al juego o al modo demo. Si transcurre un determinado tiempo sin que el usuario pulse ninguna tecla pasará a demo, mientras que si el usuario pulsa la tecla start comienza el juego. La demo finalizará pasado un tiempo determinado, tras lo cual volverá al título. El juego finalizará cuando el jugador pierda todas sus vidas, pasando a la pantalla de game over, y de ahí volverá al título.
Podemos implementar este funcionamiento como una máquina de estados. Cada estado será una de las distintas pantallas que hemos visto, y determinadas acciones o condiciones que se produzcan causarán la transición de un estado a otro. Deberemos diseñar el diagrama de estados de nuestro juego y establecer qué posibles transiciones tendremos, y qué condiciones deben cumplirse para que se produzcan.
Ciclo del juego
Vamos a centrarnos en cómo desarrollar la pantalla en la que se desarrolla la partida. Aquí tendremos lo que se conoce como ciclo del juego (o game loop). Se trata de un bucle infinito en el que tendremos el código fuente que implementa el funcionamiento del juego. Dentro de este bucle se efectúan las siguientes tareas básicas:
La clase GameCanvas
es un tipo especial de canvas para
el desarrollo de juegos presente en MIDP 2.0. Esta clase nos ofrecerá
los métodos necesarios para realizar este ciclo del juego, ofreciéndonos
un acceso a la entrada del usuario y un método de render adecuados
para este tipo de aplicaciones. Para hacer la pantalla del juego crearemos una
subclase de GameCanvas
en la que introduciremos el ciclo del juego.
La forma en la que dibujaremos los gráficos utilizando esta clase será
distinta a la que hemos visto para la clase Canvas
. En el caso
del Canvas
utilizamos render pasivo, es decir, nosotros
definimos en el método paint
la forma en la que se dibuja
y es el sistema el que llama a este método. Ahora nos interesa poder
dibujar en cada iteración los cambios que se hayan producido en la escena
directamente desde el bucle del ciclo del juego. Es decir, seremos nosotros
los que decidamos el momento en el que dibujar, utilizaremos render
activo. Para ello en cualquier momento podremos obtener un contexto gráfico
asociado al GameCanvas
desde dentro de este mismo objeto con:
Graphics g = getGraphics();
Este contexto gráfico estará asociado a un backbuffer del canvas de juegos. De esta forma durante el ciclo del juego podremos dibujar el contenido de la pantalla en este backbuffer. Cuando queramos que este backbuffer se vuelque a pantalla, llamaremos al método:
flushGraphics();
Existe también una versión de este método en la que especificamos la región que queremos que se vuelque, de forma que sólo tenga que volcar la parte de la pantalla que haya cambiado.
Vamos a ver ahora el esqueleto de un ciclo del juego básico que podemos realizar:
Graphics g = getGraphics();
while(true) { leeEntrada();
actualizaEscena();
dibujaGraficos(g); flushGraphics(); }
Será conveniente dormir un determinado tiempo tras cada iteración
para controlar así la velocidad del juego. Vamos a considerar que CICLO
es el tiempo que debe durar cada iteración del juego. Lo que podremos
hacer es obtener el tiempo que ha durado realmente la iteración, y dormir
el tiempo restante hasta completar el tiempo de ciclo. Esto podemos hacerlo
de la siguiente forma:
Graphics g = getGraphics(); long t1, t2, td;
while(true) { t1 = System.currentTimeMillis();
leeEntrada();
actualizaEscena();
dibujaGraficos(); flushGraphics(); t2 = System.currentTimeMillis(); td = t2 - t1; td = td<CICLO?td:CICLO; try { Thread.sleep(CICLO - td); } catch(InterruptedException e) { } }
Todo este bucle donde se desarrolla el ciclo del juego lo ejecutaremos como un hilo independiente. Un buen momento para poner en marcha este hilo será el momento en el que se muestre el canvas:
public class Juego extends GameCanvas implements Runnable { public void showNotify() {
Thread t = new Thread(this);
t.start();
}
public void run() {
// Ciclo del juego
...
}
}
La clase Canvas
en MIDP 1.0 ofrece facilidades para leer la entrada
de usuario para juegos. Hemos visto en el tema anterior que asocia a los códigos
de las teclas lo que se conoce como acciones de juegos, lo cual nos facilitará
el desarrollo de aplicaciones que se controlan mediante estas acciones (arriba,
abajo, izquierda, derecha, fuego), que principalmente son juegos.
Cuando se lee una tecla en cualquier evento de pulsación de teclas (keyPressed
,
keyRepeated
o keyReleased
), se nos proporciona como
parámetro el código de dicha tecla. Podemos comprobar a qué
acción de juego corresponde dicha tecla de forma independiente a la plataforma
con el método getGameAction
como se muestra en el siguiente
ejemplo:
public void keyPressed(int keyCode) {
int action = getGameAction(keyCode);
if (action == LEFT) { moverIzquierda(); } else if (action == RIGHT) { moverDerecha();
} else if (action == FIRE) { disparar();
} }
También podemos obtener la tecla principal asociada a una acción
de juego con el método getKeyCode
, pero dado que una acción
puede estar asociada a varias teclas, si usamos este método no estaremos
considerando las teclas secundarias asociadas a esa misma acción. A pesar
de que en algunos ejemplos podamos ver código como el que se muestra
a continuación, esto no debe hacerse:
public class MiCanvas extends Canvas {
// NO HACER ESTO!
int izq, der, fuego;
public MiCanvas() { izq = getKeyCode(LEFT); der = getKeyCode(RIGHT); fuego = getKeyCode(FIRE); }
public void keyPressed(int keyCode) { if (keyCode == izq) { moverIzquierda(); } else if (keyCode = der) { moverDerecha(); } else if (keyCode = fuego) { disparar(); } }
}
Hemos de tener en cuenta que muchos modelos de móviles no nos permiten mantener pulsadas más de una tecla al mismo tiempo. Hay otros que, aunque esto se permita, el joystick no puede mantener posiciones diagonales, sólo se puede pulsar una de las cuatro direcciones básicas al mismo tiempo.
Esto provoca que si no tenemos suficiente con estas cuatro direcciones básicas y necesitamos realizar movimientos en diagonal, tendremos que definir nosotros manualmente los códigos de las teclas para cada una de estas acciones. Esto reducirá portabilidad a la aplicación, será el precio que tendremos que pagar para poder establecer una serie de teclas para movimiento diagonal.
Si definimos nosotros las acciones directamente a partir de los códigos de teclas a bajo nivel deberemos intentar respetar en la medida de lo posible el comportamiento que suele tener cada tecla en los móviles. Por ejemplo, las teclas asociadas a las esquinas de la pantalla (soft keys) deben utilizarse para terminar el juego y volver al menú principal o bien salir directamente del juego.
Acceso al teclado con MIDP 2.0
Hasta ahora hemos visto las facilidades que nos ofrece MIDP para leer la entrada
del usuario en los juegos desde su versión 1.0. Sin embargo, MIDP 2.0
incluye facilidades adicionales en la clase GameCanvas
.
Podremos leer el estado de las teclas en cualquier momento, en lugar de tener que definir callbacks para ser notificados de las pulsaciones de las teclas. Esto nos facilitará la escritura del código, pudiendo obtener directamente esta información desde el ciclo del juego.
Podemos obtener el estado de las teclas con el método getKeyStates
como se muestra a continuación:
int keyState = getKeyStates();
Esto nos devolverá un número entero en el que cada uno de sus
bits representa una tecla. Si el bit vale 0
la
tecla está sin pulsar, y si vale 1
la tecla estará
pulsada. Tenemos definidos los bits asociados a cada tecla como constantes,
que podremos utilizar como máscaras booleanas para extraer el
estado de cada tecla a partir del número entero de estado obtenido:
GameCanvas.LEFT_PRESSED |
Movimiento a la izquierda |
GameCanvas.RIGHT_PRESSED |
Movimiento a la derecha |
GameCanvas.UP_PRESSED |
Movimiento hacia arriba |
GameCanvas.DOWN_PRESSED |
Movimiento hacia abajo |
GameCanvas.FIRE_PRESSED |
Fuego |
Por ejemplo, para saber si están pulsadas las teclas izquierda o derecha haremos la siguiente comprobación:
if ((keyState & LEFT_PRESSED) != 0) { moverIzquierda(); } if ((keyState & RIGHT_PRESSED) != 0) { moverDerecha(); }
Vamos a ver ahora un ejemplo de ciclo de juego sencillo completo:
public class CanvasJuego extends GameCanvas implements Runnable {
private final static int CICLO = 50;
public CanvasJuego() { super(true); } public void showNotify() { Thread t = new Thread(this); t.start();
} public void run() { Graphics g = getGraphics(); long t1, t2, td;
// Carga sprites
Image imgPersonaje = null;
try { imgPersonaje = Image.createImage("/personaje.png"); } catch(IOException e) {}
Sprite personaje = new Sprite(imgPersonaje); personaje.setPosition(50,50);
// Crea escena
LayerManager escena = new LayerManager();
escena.append(personaje);
while(true) { t1 = System.currentTimeMillis();
// Lee entrada del teclado int keyState = getKeyStates();
if ((keyState & LEFT_PRESSED) != 0) { personaje.move(-1, 0); } else if ((keyState & RIGHT_PRESSED) != 0) { personaje.move(1, 0); } else if ((keyState & UP_PRESSED) != 0) { personaje.move(0, -1); } else if ((keyState & DOWN_PRESSED) != 0) { personaje.move(0, 1); }
escena.paint(g); flushGraphics();
t2 = System.currentTimeMillis(); td = t2 - t1; td = td<CICLO?td:CICLO;
try { Thread.sleep(CICLO - td); } catch(InterruptedException e) {} } }
}