Creacin de un juego (1): Introduccion Antes de empezar a crear
un juego deberemos pensar en todos los aspectos que incluir, por lo
tanto es mas que recomendable que cojas papel y lpiz y empieces a
pensar en los siguientes conceptos:Mecnica bsica del juego,
incluyendo un concepto de nivel si correspondeUna historia de fondo
con los personajes principalesUna lista de tems, power-ups u otras
cosas que modifiquen a los personajes, la mecnica o el mundo del
juegoDiseo grfico de la historia y los personajesBocetos de todas
las pantallas del juego y transiciones entre ellasTransformar el
juego en un archivo ejecutable
1. Mecnica bsica del juego
En este apartado nos centraremos en disear la mecnica de nuestro juego. Botones necesarios para manejar a nuestro personajes o personajes, botones para acceder a otras pantallas, mens de pausa, ... Que pasa con los tems cuando se usan o que pasa con el juego cuando se termina. Estos serian algunos aspectos a tener en cuenta a la hora de crear la mecnica de nuestro juego.
2. Historia y diseo
Esta parte depende de nuestra habilidades como dibujante, pero esta orientada al diseo de nuestros personajes, tems, power-ups y el mundo que lo rodea. A parte tendremos que pensar la historia o trama principal de nuestro juego. Tamin podremos incluir sonidos o msica que harn mucho mas llamativa nuestra creacin.
3. Pantallas y transiciones
Una vez tenemos diseada la mecnica del juego, la historia y los
personajes, llega el momento de plantearse como sern nuestras
pantallas y transiciones entre ellas. Pero deberemos tener en
cuenta varias cosas: una pantalla es una unidad llena de elementos
y es responsable de una parte del juego, cada pantalla puede estar
compuesta de mltiples componentes y una pantalla permite al usuario
interactuar con los elementos que la componen.
Sabiendo esto ya podremos disear las pantallas que tendr nuestro
juego, como por ejemplo puede ser un men principal donde habr dos
botones (uno iniciara el juego y otro mostrara las puntuaciones) a
parte mostrara el titulo del juego y otro botn de opciones. Una
pantalla donde el usuario podr interactuar y jugar con nuestra
creacin, compuesta de varios botones (pausa, movimiento de nuestro
personaje, ...). Podramos disear pantallas de ayuda, de pausa, de
seleccin de nivel, de juego terminado, .... Todo depender de la
idea que tengamos de nuestro juego.
4. Programar el cdigo del juego
Llega la hora de transformar todos nuestros diseos del juego en
un archivo ejecutable y lo haremos a travs de herramientas Android
que son relevantes para la programacin de juegos. Para ello
usaremos interfaces por dos razones: nos permiten concentrar la
semntica del cdigo sin necesidad de conocer los detalles de
implementacin y por otro lado nos permitir intercambiar la
aplicacin mas tarde (es decir, en lugar de utilizar la
representacin 2D CPU, mas adelante podramos explotar OpenGL ES para
visualizar nuestro juego).
Podramos definir a una interface como un conjunto de mtodos
abstractos, es decir, en una interface declaramos los nombres de
los mtodos, la lista de argumentos y el tipo de retorno para cada
metodo, pero no escribimos el bloque de cdigo de cada mtodo. De
esto ultimo se encarga la clase que implemente la interface que
deber sobreescribir esos mtodos con bloques de cdigo o bien
declarar nuevamente los mtodos abstractos convirtindose en una
clase abstracta.
Siguiendo nuestro articulo podemos dividir las partes del cdigo en los siguientes mdulos (cada modulo constara de una interface como minimo), que sern los encargados de llevar a cabo todo el diseo de nuestro juego:Gestor de pantallas: sera el responsable de crear, cerrar, pausar o reanudar nuestras pantallas del juego.Input: este modulo esta relacionado con el gestor de ventanas y sera en encargado de manejar las entradas del usuario (eventos touch, key, acelerometro, ...)FileIO: encargado de leer, escribir en archivos. Tanto internal como external storage o incluso shared preferences.Graficos: este es probablemente el mas complejo, a parte del juego en si. Se encargara de cargar los diseos y dibujarlos en la pantalla.Audio: controlara todo efecto de sonido o msica de nuestro juego.Framework del juego: esta vinculado a todos los anteriores y proporcionara una base fcil de usar para escribir nuestros juegos.
Creacin de un juego (2): Modulo FileIO Empezamos con uno de los
mdulos mas fciles, que sera el encargado de leer o escribir en
archivos. Para ello crearemos InputStream y OutputStream, que se
podran definir como mecanismos estndar de Java para leer y escribir
en archivos. A parte aadiremos un nuevo mtodo relacionado con
SharedPrefernces.
La lectura de archivos sera muy usada para los archivos de nivel,
imagenes y archivos de audio. En cambio la escritura la usaremos
con menos frecuencia para mantener las puntuaciones,
configuraciones del juego o guardar un estado del juego para que el
usuario pueda volver donde lo haba dejado.
1. Interface FileIOimport java.io.IOException;import
java.io.InputStream;import java.io.OutputStream;
public interface FileIO {
public InputStream leerAsset(String fileName) throws IOException;
public InputStream leerFile(String fileName) throws IOException;
public OutputStream escribirFile(String fileName) throws
IOException;}
Como se puede observar hemos declarado 3 mtodos:leerAsset: lo
usaremos para leer los fichero de la carpeta assets.leerFile: nos
servir para leer los archivos que hemos creado.escribirFile: y el
ultimo lo usaremos para guardar en un archivo las puntuaciones,
configuraciones, ...Los dos primeros mtodos nos devolvern un
InputStream (una fuente donde leer bytes) y el ultimo un
OutputStream (una fuente donde escribir bytes). En los tres mtodos
le pasamos como parmetro "fileName" que sera el nombre del archivo
a leer o escribir. Tambin manejamos la IOException que se encargara
de manejar los errores que pueda haber en la lectura o
escritura.
2. Implementar interface FileIOimport java.io.File;import
java.io.FileInputStream;import java.io.FileOutputStream;import
java.io.IOException;import java.io.InputStream;import
java.io.OutputStream;
import android.content.Context;import android.content.SharedPreferences;import android.content.res.AssetManager;import android.os.Environment;import android.preference.PreferenceManager;
import com.example.slange.interfaces.FileIO;
public class AndroidFileIO implements FileIO {
Context mcontext;AssetManager assets;String externalStoragePath;
public AndroidFileIO(Context context) {this.context = context;this.assets = context.getAssets();this.externalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator;}
public InputStream leerAsset(String fileName) throws IOException {return assets.open(fileName);}
public InputStream leerFile(String fileName) throws IOException {return new FileInputStream(externalStoragePath + fileName);}
public OutputStream escribirFile(String fileName) throws IOException {return new FileOutputStream(externalStoragePath + fileName);}
public SharedPreferences getPreferences() {return
PreferenceManager.getDefaultSharedPreferences(context);}}
Primero creamos tres objetos: un Context, un AssetManager y un
String. Despus en el constructor de nuestra clase indicamos como
parmetro un context que sera almacenado en nuestro objeto mcontext,
seguimos almacenando una instancia AssetManager en el objeto
assets, y para terminar almacenamos la raz de nuestro
almacenamiento externo en el objeto externalStoragePath.
Con el mtodo leerAsset podremos abrir cualquier archivo ubicado en
la carpeta assets de nuestro proyecto, para ello hacemos uso del
mtodo open. Como parmetro indicaremos el archivo a abrir.
En el mtodo leerFile creamos un nuevo FileInputStream indicando
como parmetro el nombre del archivo. Esto nos devolver un archivo
para su lectura.
El mtodo escribirFile hace lo contrario, nos devolver un archivo
para su lectura.
Y por ultimo hemos aadido un nuevo mtodo, getPreferences, que nos
devuelve una instancia SharedPreferences que podremos usar tanto
para leer como para escribir en un archivo de preferencias.
Para terminar, comentar que podramos aadir un mtodo para comprobar
si el almacenamiento externo esta montado, es solo de lectura o
simplemente no se dispone de l.
Creacin de un juego (3): Modulo Audio Para la creacin de nuestro
juego no vamos a disear un proceso de audio avanzado, simplemente
nos centraremos en reproducir archivos de msica y efectos de
sonido. Para ello crearemos tres interfaces: Sonido, Msica y
Audio.Sonido: nos permitir reproducir efectos de sonido que
almacenaremos en la memoria RAM.Musica: reproducir archivos de gran
tamao directamente desde el disco a la tarjeta de sonido.Audio:
sera la responsable de crear sonidos y msica a partir de archivos
ubicados en la carpeta assets.
1.1 Interface de Sonidopublic interface Sound {
public void play(float volume);
public void dispose();}Simplemente necesitaremos dos mtodos para
los efectos de sonido, el mtodo play para reproducirlo y el mtodo
dispose que liberara el efecto de sonido de la memoria RAM.
1.2 Interface de Msicapublic interface Music {
public void play();
public void stop();
public void pause();
public void setLooping(boolean looping);
public void setVolume(float volume);
public boolean isPlaying();
public boolean isStopped();
public boolean isLooping();
public void dispose();}Aqu necesitaremos los controles tpicos de
un reproductor de msica: play, stop y pause. Despus el mtodo
setLooping lo usaremos para poner la msica en modo bucle/loop. Con
el mtodo setVolume indicaremos el volumen de salida de nuestra
msica. Continuamos creando tres mtodos (isPlaying, isStopped,
isLooping) que usaremos para comprobar si la msica se esta
reproduciendo, esta parada o esta en modo bucle/loop. Y para
terminar usaremos el mtodo dispose para liberar los recursos de
nuestro reproductor una vez que ya no lo necesitemos.
1.3 Interface de Audiopublic interface Audio {
public Music newMusic(String filename);
public Sound newSound(String filename);}Con esta interface
conseguiremos reducir en numero de instancias de nuestro cdigo y
simplemente la usaremos para crear nuevos objetos de Sonido o
Audio, pasndole como parmetro el nombre del archivo.
2.1 Implementar interface de Sonidoimport
android.media.SoundPool;
import com.example.slange.interfaces.Sound;
public class AndroidSound implements Sound {
int soundId;SoundPool soundPool;
public AndroidSound(SoundPool soundPool, int soundId) {this.soundId = soundId;this.soundPool = soundPool;}
public void play(float volume) {soundPool.play(soundId, volume, volume, 0, 0, 1);}
public void dispose() {soundPool.unload(soundId);}}Empezamos
creando una variable int, que sera donde almacenaremos la id de
nuestro efecto de sonido y creamos un objeto soundPoolque nos
ayudara a gestionar y reproducir los efectos de sonido.
En el constructor de la clase simplemente almacenamos los parmetros
del constructor en las dos variables que hemos creado para esta
clase.
Para reproducir los efectos de sonido usaremos el mtodo play de la
clase SoundPool:play(soundID, leftVolume, rightVolume, priority,
loop, rate)Nos pide como parmetro la id del sonido que la
conseguiremos mas adelante con el mtodo load de esta misma clase,
el volumen del canal izquierdo y derecho (usaremos los parmetros de
nuestro mtodo en este caso, el rango va desde 0.0 a 1.0), la
prioridad de reproduccin (0 es la mas baja), modo loop (0
desactivado y -1 activado) y para finalizar la tasa de reproduccin
(el valor normal es 1, pero su rango va desde 0.5 a 2.0).
Para liberar el recurso de la memoria en nuestro SoundPool usaremos
el mtodo unload que nos pide como parmetro la id del sonido a
liberar.
2.2 Implementar interface de Msicaimport java.io.IOException;
import android.content.res.AssetFileDescriptor;import android.media.MediaPlayer;import android.media.MediaPlayer.OnCompletionListener;
import com.example.slange.interfaces.Music;
public class AndroidMusic implements Music, OnCompletionListener {
MediaPlayer mediaPlayer;boolean isPrepared = false;Comentar que
a parte de implementar nuestra interface Msica, tambin
implementamos la interfaceOnCompletionListenerque nos pide
sobreescribir el mtodoonCompletion. Esta interface se encarga de
hacer una llamada a su mtodo una vez que la reproduccin a
terminado, es decir, cuando termina de sonar la msica porque a
llegado a su fin.
Y el bloque sinchronized se encargara automticamente de gestionar
los estados del reproductor (reproduciendo, parado, pausado,
preparado). Con ello conseguiremos no tener dos estados diferentes
a la vez y por lo tanto no tener excepciones
IllegalStateException.
Empezamos la interface creando un objeto MediaPlayer que sera el
encargado de preparar, reproducir, pausar y parar nuestra msica.
Tambin creamos un booleano para almacenar el estado preparado del
reproductor de msica.public AndroidMusic(AssetFileDescriptor
assetDescriptor) {mediaPlayer = new MediaPlayer();try
{mediaPlayer.setDataSource(assetDescriptor.getFileDescriptor(),assetDescriptor.getStartOffset(),assetDescriptor.getLength());mediaPlayer.prepare();isPrepared
= true;mediaPlayer.setOnCompletionListener(this);} catch (Exception
e) {throw new RuntimeException("Error al cargar la musica");}}Ya en
el constructor primero iniciamos nuestro mediaplayer y encapsulamos
todo en un bloque try-catch por si acaso hay problemas al cargar el
archivo de msica.
Establecemos nuestra fuente de msica con el metodo setDataSource,
que nos pide como parmetro un FileDescriptor (usamos el parmetro
AssetFileDescriptor con su mtodogetFileDescriptorque nos devuelve
un archivo de la carpeta assets para poder leer sus datos, as como
el desplazamiento y su longitud), el punto inicial de nuestro
archivo de msica (con el mtodogetStartOffsetestablecemos el inicio)
y el final del archivo de msica (con el mtodogetLengthestablecemos
el final).
Preparamos el archivo para su reproduccin con el mtodo prepare y
guardamos el estado true en nuestra variable booleana.
Para terminar establecemos la llamada al mtodo onCompletion y
creamos una nueva excepcin con un mensaje personalizado.public void
dispose() {if
(mediaPlayer.isPlaying())mediaPlayer.stop();mediaPlayer.release();}Comprobamossi
se esta reproduciendo y en este caso pararemos la reproduccin con
el mtodostopy liberaremos el recurso de nuestro mediaplayer con el
mtodorelease.public boolean isLooping() {return
mediaPlayer.isLooping();}Devuelve true en caso de que el
reproductor este en modo bucle/loop.public boolean isPlaying()
{return mediaPlayer.isPlaying();}Devuelve true en caso de que el
reproductor este reproduciendo la msica.public boolean isStopped()
{return !isPrepared;}Comprobaremos nuestra variable booleana y nos
devolver false en caso de que se este reproduciendo.public void
pause() {if
(mediaPlayer.isPlaying())mediaPlayer.pause();}Comprobaremos si se
esta reproduciendo y en este caso pausamos la reproduccin.public
void play() {if (mediaPlayer.isPlaying())return;try {synchronized
(this) {if
(!isPrepared)mediaPlayer.prepare();mediaPlayer.start();}} catch
(IllegalStateException e) {e.printStackTrace();} catch (IOException
e) {e.printStackTrace();}}Lo primero que hacemos es comprobar si se
esta reproduciendo y en este caso devolvemos la funcin y ya no hace
nada mas. En el caso contrario encapsulamos todo en un bloque
try-catch manejando dos excepciones. Una vez dentro del bloque,
sincronizamos el estado del reproductor y comprobamos si esta
preparado a travs de nuestra variable booleana, en caso de que no
lo este lo preparamos con el mtodo prepare y para terminar
empezamos la reproduccin con el mtodo start.public void
setLooping(boolean isLooping)
{mediaPlayer.setLooping(isLooping);}Ponemos el reproductor en modo
bucle/loop a travs del mtodo mediaplayer setLooping, indicndolo con
el parmetro booleano de nuestro mtodo isLooping.public void
setVolume(float volume) {mediaPlayer.setVolume(volume,
volume);}Indicamos el volumen derecho e izquierdo a travs del mtodo
mediaplayer setVolume y usando nuestro parmetro float volume.public
void stop() {mediaPlayer.stop();synchronized (this) {isPrepared =
false;}}Paramos la reproduccin con el mtodo mediaplayer stop y
seguidamente sincronizamos el estado del reproductor y ponemos
nuestra variable a false indicando que no esta preparada la
reproduccin.public void onCompletion(MediaPlayer player)
{synchronized (this) {isPrepared = false;}}}Para terminar
sobreescribimos el mtodo onCompletion y simplemente sincronizamos
el estado y ponemos nuestra variable a false.
2.3 Implementar interface de Audioimport java.io.IOException;
import android.app.Activity;import android.content.res.AssetFileDescriptor;import android.content.res.AssetManager;import android.media.AudioManager;import android.media.SoundPool;
import com.example.slange.interfaces.Audio;import com.example.slange.interfaces.Music;import com.example.slange.interfaces.Sound;
public class AndroidAudio implements Audio {
AssetManager assets;SoundPool soundPool;Comentar que a parte de
implementar nuestra interface Audio, estamos importando las
interfaces de Sonido y Msica.
Empezamos creando un objeto AssetManager y SoundPool, ya conocidos
en estos ltimos artculos.public AndroidAudio(Activity activity)
{activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);this.assets
= activity.getAssets();this.soundPool = new SoundPool(20,
AudioManager.STREAM_MUSIC, 0);}Ya en el constructor le aadimos como
parmetro una Activity y usamos el mtodosetVolumeControlStreamque
nos permitir cambiar el volumen con los controles de hardware del
dispositivo y nos pide como parmetro una fuente de msica que le
indicamos que va a ser un stream de msica a travs de la clase
AudioManager.
En nuestra variable assets almacenamos una instancia de la carpeta
assets.
Y en la variable soundPool almacenamos un nuevo sonido SoundPool
indicando como parmetro el numero mximo de sonidos simultneos (20),
el tipo de sonido (AudiosManager.STREAM_MUSIC) y la calidad de
nuestro sonido (actualmente no tiene ningn efecto, por lo que su
valor por defecto es 0).public Music newMusic(String filename) {try
{AssetFileDescriptor assetDescriptor =
assets.openFd(filename);return new AndroidMusic(assetDescriptor);}
catch (IOException e) {throw new RuntimeException("Error al cargar:
'" + filename + "'");}}Nos devolver un nuevo objeto AndroidMusic
que cargaremos desde la carpeta assets.public Sound newSound(String
filename) {try {AssetFileDescriptor assetDescriptor =
assets.openFd(filename);int soundId =
soundPool.load(assetDescriptor, 0);return new
AndroidSound(soundPool, soundId);} catch (IOException e) {throw new
RuntimeException("Error al cargar: '" + filename + "'");}}}Y para
terminar el mtodo newSound nos devolver un nuevo objeto
AndroidSound que cargaremos desde la carpeta assets, almacenando en
memoria su id con el mtodo load.
En ambos casos manejamos la excepcin IOException en caso de que
algo vaya mal a la hora de cargar los archivos.
Creacin de un juego (4): Modulo Input Modulo encargado de
recoger y procesar los eventos de entrada del usuario. En Android
tenemos tres mtodos de entrada principales: pantalla tctil, teclado
y acelermetro. Podremos recoger esos eventos de entrada de dos
maneras:Polling: solo comprobara el estado actual del evento,
cualquier estado entre el evento actual y la ultima comprobacin se
perder. Muy til para los eventos tctiles pero inadecuado para
recoger texto.Handling: este mtodo nos dar una historia cronolgica
completa de todos los eventos que se han producido. Mecanismo muy
adecuado para recoger texto o cualquier otra tarea que se base en
el orden de los acontecimientos.Por lo tanto en este modulo vamos a
crear tres manejadores (handlers) para recoger los eventos de
teclado, eventos de un toque tctil y eventos multi toque.
1.1 Interface Input
Esta interface sera la encargada de recoger todos los eventos de
entrada del usuario. Para ello creamos dos clases: una se encargara
de los eventos del teclado (KeyEvent) y la otra de los eventos
tctiles (TouchEvent). Tambin declararemos varios mtodos que se
explicaran a continuacin.public static class KeyEvent {public
static final int KEY_DOWN = 0;public static final int KEY_UP =
1;
public int type;public int keyCode;public char keyChar;
public String toString() {StringBuilder builder = new
StringBuilder();if (type == KEY_DOWN)builder.append("key down,
");elsebuilder.append("key up,
");builder.append(keyCode);builder.append(",");builder.append(keyChar);return
builder.toString();}}Empezamos creando un par de variables que nos
servirn para identificar el tipo de evento del teclado:KEY_DOWN:
sucede cuando el usuario mantiene el dedo en una tecla del
teclado.KEY_UP: este evento sucede cuando el usuario levanta ese
dedo de la tecla.Por lo tanto de este modo conseguimos registrar
los caracteres que va pulsando el usuario en su teclado, estos
caracteres los almacenaremos en la variable keyChar. La variable
type se usara para determinar el tipo de evento (0=KEY_DOWN y
1=KEY_UP). Y en la variable keyCode almacenaremos el valor Unicode
de cada carcter.
Creamos el mtodo toString para almacenar todos esos datos de un
evento de teclado en un StringBuilder que sera convertido a String
y devuelto por el mtodo.public static class TouchEvent {public
static final int TOUCH_DOWN = 0;public static final int TOUCH_UP =
1;public static final int TOUCH_DRAGGED = 2;
public int type;public int x, y;public int pointer;
public String toString() {StringBuilder builder = new
StringBuilder();if (type == TOUCH_DOWN)builder.append("touch down,
");else if (type == TOUCH_DRAGGED)builder.append("touch dragged,
");elsebuilder.append("touch up,
");builder.append(pointer);builder.append(",");builder.append(x);builder.append(",");builder.append(y);return
builder.toString();}}Clase muy similar a la anterior, pero aqu
tenemos tres tipos de evento tctil:TOUCH_DOWN: sucede cuando el
usuario mantiene un dedo en la pantalla.TOUCH_DRAGGED: sucede
cuando el usuario mueve un dedo por la pantalla. TOUCH_UP: sucede
cuando el usuario levanta el dedo de la pantalla. En las variables
x e y, almacenaremos las coordenadas de los eventos touch y en la
variable pointer la id de ese evento touch. Comentar que podemos
tener varios eventos touch simultneamente y para diferenciarlos lo
haremos por su id.
Terminamos con el mtodo to String, que nos devolver un String con
todos los datos de ese evento touch.public boolean isKeyPressed(int
keyCode)
public boolean isTouchDown(int pointer);
public int getTouchX(int pointer);
public int getTouchY(int pointer);
public float getAccelX();
public float getAccelY();
public float getAccelZ();
public List getKeyEvents();
public List getTouchEvents();Podramos decir que los siete
primeros mtodos son mtodos polling y los dos ltimos handling.
Con el metodo isKeyPressed comprobaremos si el parmetro keyCode
esta siendo pulsado o no.
Usando el metodo isTouchDown comprobaremos si hay un evento touch
en un puntero determinado.
Podremos conseguir las coordenadas X e Y de un evento touch con los
mtodos: getTouchX y getTouchY.
Tambin podremos consultar las coordenadas X, Y, y Z del acelermetro
con los mtodos: getAccelX, getAccelY y getAccelZ.
Los dos ltimos mtodos: getKeyEvents y getTouchEvents, nos devolvern
una lista de sus respectivos eventos.
1.2 Interface TouchHandlerimport java.util.List;
import com.example.slange.interfaces.Input.TouchEvent;
import android.view.View.OnTouchListener;
public interface TouchHandler extends OnTouchListener {
public boolean isTouchDown(int pointer);
public int getTouchX(int pointer);
public int getTouchY(int pointer);
public List getTouchEvents();}Interface que usa los mismos
mtodos que la anterior pero con la excepcin de que esta interface
extiende a OnTouchListener que nos ayudara a controlar los eventos
touch en una view. A parte importamos la clase TouchEvent de la
interface Input.
2 Clase Pool
Se podra decir que su funcin principal es reutilizar eventos de
usuario, esta clase tambin es conocida como recolector de basura.
Continuamente los eventos touch o key generan instancias que se van
acumulando en una lista y perjudicando a nuestro sistema Android.
Para solucionar esto vamos a implementar un concepto conocido como
Pooling. En lugar de ir acumulando nuevas instancias en una lista,
esta clase se encargara de reutilizar las instancias mas antiguas y
as conseguir un equilibrio para nuestro sistema. A continuacin
vamos a ver un ejemplo de este concepto explicando detenidamente su
metodologa:import java.util.ArrayList;import java.util.List;
public class Pool {
public interface PoolObjectFactory {public T
createObject();}Esta clase se denomina Genrica en Java y para
implementar clases u objetos genricos usamos: .
Es algo complejo as que recomiendo buscar informacin en
internet.
Pero para hacernos una idea, la T indica la clase de objeto que
usaremos en esta clase. Como nosotros vamos a usar esta clase para
reciclar los eventos touch y key, podemos sustituir esa T por
KeyEvent o TouchEvent y hacernos una idea de su funcionamiento. (En
los siguientes puntos veremos el uso de esta clase)
Lo primero que hacemos en esta clase es crear una interface genrica
PoolObjectFactory con un nico mtodo createObject que nos devolver
una nueva instancia del evento de usuario.private final List
freeObjects;private final PoolObjectFactory factory;private final
int maxSize;Primero creamos un ArrayList freeObjects que sera donde
se almacenen los eventos de usuario.
El segundo objeto factory, sera donde almacenemos las nuevas
instancias de la interface PoolObjectFactory.
Y el ultimo miembro maxSize, sera el numero mximo de eventos que
almacenara nuestra clase Pool.public Pool(PoolObjectFactory
factory, int maxSize) {this.factory = factory;this.maxSize =
maxSize;this.freeObjects = new ArrayList(maxSize);}El constructor
toma como parmetros las instancias PoolObjectFactory y el numero
mximo de eventos a almacenar (maxSize).
Dentro del constructor almacenamos estos parmetros es sus
respectivas variables (factory, maxSize) y creamos una nueva lista
(freeObjects) con el tamao mximo de eventos a recoger.public T
newObject() {T object = null;
if (freeObjects.isEmpty())object =
factory.createObject();elseobject =
freeObjects.remove(freeObjects.size() - 1);return object;}Este
mtodo sera el encargado de entregarnos un nuevo objeto del evento
de usuario.
Se comprueba si la lista freeObjects esta vaca y en ese caso se
crea una nueva instancia con ese evento. En caso contrario se borra
el ultimo evento de la lista.
Para finalizar el mtodo nos devolver el nuevo objeto.public void
free(T object) {if (freeObjects.size() <
maxSize)freeObjects.add(object);}}Para finalizar la clase, el mtodo
free sera el encargado de almacenar los eventos de usuario que ya
no necesitemos. El mtodo simplemente inserta la nueva instancia del
objeto en la lista freeObjects.
A continuacin veremos el uso de esta clase en nuestros manejadores
(Handlers).
3.1 Clase KeyBoardHandler
En los siguientes tres puntos vamos a crear tres clases que nos
servirn para manejar los eventos de usuario. A estas clases se les
denomina manejadores o mas bien "Handlers" en ingles. Primero
empezaremos con los eventos del teclado, vamos a ver esta clase en
detalle.import java.util.ArrayList;import java.util.List;
import android.view.View;import android.view.View.OnKeyListener;
import com.example.slange.interfaces.Input.KeyEvent;import com.example.slange.interfaces.Pool;import com.example.slange.interfaces.Pool.PoolObjectFactory;
public class KeyboardHandler implements OnKeyListener {
boolean[] pressedKeys = new boolean[128];Pool keyEventPool;List
keyEventsBuffer = new ArrayList(); List keyEvents = new
ArrayList();Empezamos implementando la interface OnKeyListener que
nos pide sobreescribir el mtodo onKey. Tambin importamos la clase
KeyEvent de nuestra interface Input, la clase Pool y la interface
PoolObjectFactory.
Creamos una variable booleana con 128 posibilidades, que nos servir
para conocer el estado actual (presionado o no) de cada tecla. Cada
tecla del teclado tiene asignada una constante (KEYCODE_XXX) que va
desde 0 hasta 127.
La siguiente se encargara de recoger los eventos del teclado y
llevarlos a nuestra clase Pool. Para ello indicamos entre los
eventos que queremos mandar a la clase Pool, en este caso
KeyEvent.
El primer ArrayList (keyEventsBuffer) es genrico y especificamos
que va a ser una lista con eventos KeyEvent. En esta lista se
almacenaran los eventos que aun no han sido consumidos por nuestro
juego, es decir, los nuevos eventos.
Y el segundo ArrayList (keyEvents) es un segundo buffer para
recoger los eventos del teclado, mas adelante veremos su uso.public
KeyboardHandler(View view) {PoolObjectFactory factory = new
PoolObjectFactory() {public KeyEvent createObject() {return new
KeyEvent();}};keyEventPool = new Pool(factory,
100);view.setOnKeyListener(this);view.setFocusableInTouchMode(true);view.requestFocus();}En
el constructor usaremos como parmetro la view de la que queremos
recibir los eventos del teclado.
Creamos un objeto PoolObjectFactory que sera el encargado de
recoger los eventos KeyEvent y devolvernos una nueva instancia
KeyEvent.
Continuamos creando nuestro recolector de eventos de teclado
(keyEventPool), indicando como parmetro la instancia KeyEvent que
debe almacenar nuestro Pool y como segundo parmetro el numero mximo
de instancias a almacenar.
Establecemos que la view registre los eventos de teclado con el
mtodo setOnKeyListener que har una llamada al mtodo oKey para
conocer el evento de teclado.
Nos aseguramos que la view puede recibe el foco aunque este en modo
tctil (setFocusableInTouchMode) y as pueda recibir los eventos de
teclado.
Y terminamos estableciendo que la view recibe el foco
(requestFocus).public boolean onKey(View v, int keyCode,
android.view.KeyEvent event) {if (event.getAction() ==
android.view.KeyEvent.ACTION_MULTIPLE)return false;
synchronized (this) {KeyEvent keyEvent =
keyEventPool.newObject();keyEvent.keyCode =
keyCode;keyEvent.keyChar = (char) event.getUnicodeChar();if
(event.getAction() == android.view.KeyEvent.ACTION_DOWN)
{keyEvent.type = KeyEvent.KEY_DOWN;if(keyCode > 0 &&
keyCode < 127)pressedKeys[keyCode] = true;}if (event.getAction()
== android.view.KeyEvent.ACTION_UP) {keyEvent.type =
KeyEvent.KEY_UP;if(keyCode > 0 && keyCode <
127)pressedKeys[keyCode] =
false;}keyEventsBuffer.add(keyEvent);}return false;}Este mtodo sera
llamado cada vez que la view registre un evento de teclado.
Lo primero que hacemos es comprobar si el evento es de accin
mltiple (ACTION_MULTIPLE) y directamente lo descartamos ya que
estos eventos no nos sirven.
Debemos utilizar un bloque synchronized ya que los eventos se
reciben en la interface de usuario y se leen en el hilo principal.
Tenemos que asegurarnos de que ninguno de los dos accede en
paralelo.
Dentro del bloque lo primero que hacemos es mandar el evento de
teclado a nuestro recolector de basura (este se encargara de crear
una nueva instancia o de reciclar una antigua).
Despus de registrar el evento, almacenamos el carcter del teclado
(keyCode) y su valor Unicode (keyChar) ayudandonos de los parmetros
del mtodo.
Lo siguiente es comprobar que tipo de evento a tenido lugar
(ACTION_DOWN, ACTION_UP) en los dos casos registramos en nuestro
keyEvent.type el tipo de evento. Comprobamos si la tecla pulsada
esta dentro del rango de constantes (0 hasta 127) y dependiendo del
tipo de evento devolveremos true o false a nuestra variable
booleana pressedKeys.
Para terminar almacenamos nuestro evento de teclado en la lista
keyEventBuffer.public boolean isKeyPressed(int keyCode) {if
(keyCode < 0 || keyCode > 127)return false;return
pressedKeys[keyCode];}Con este mtodo sabremos si una tecla es
pulsada o no.
Para ello comprobamos si el parmetro keyCode esta dentro del rango
(0 a 127) y en caso de que no lo este devolvemos el mtodo con un
false. En caso de que se encuentre dentro de ese rango, el mtodo
devolver true en la posicin keyCode de nuestra variable
pressedKeys.public List getKeyEvents() {synchronized (this) {int
len = keyEvents.size();for (int i = 0; i < len; i++)
{keyEventPool.free(keyEvents.get(i));}keyEvents.clear();keyEvents.addAll(keyEventsBuffer);keyEventsBuffer.clear();return
keyEvents;}}}Mtodo que nos devolver la lista de eventos de teclado
mas nuevos. Se encargara de pasar los eventos de nuestra lista a
nuestro recolector de basura Pool. A parte de limpiar nuestra
lista. Y aqu es donde entra en juego el segundo buffer de
almacenamiento de eventos. Comentar que deberemos llamar
frecuentemente a este metodo para limpiar los eventos de
teclado.
Como en el mtodo onKey, aqui tambin debemos usar un bloque
synchronized.
Con el bucle for mandamos todos los eventos de la lista keyEvents a
nuestro recolector de basura keyEventPool, haciendo uso del mtodo
free que se encargara de almacenarlos.
Despus limpiamos la lista keyEvents con el mtodo clear, aadimos a
la lista keyEvents los nuevos eventos keyEventsBuffer y limpiamos
nuestro buffer principal keyEventsBuffer.
Finalmente el mtodo devuelve la lista keyEvents con los ltimos
eventos producidos.
3.2 Clase SingleTouchHandler
En los dos siguientes puntos vamos a crear dos clases que se
encargaran de manejar los eventos touch (single y multi).
Antiguamente los eventos singletouch se usaban para un nivel de API
4 o inferior. Y lo eventos multitouch se implementaron a partir de
la API 5.
Actualmente hay un porcentaje muy pequeo de terminales en el
mercado que usa una versin de Android inferior a la 2.0 (0.2%) y
con el tiempo terminara por desaparecer del mercado. Por lo tanto
no tenemos que preocuparnos por esto. Pero vamos a conocer las dos
clases (multi y single) que nos sern de gran ayuda para crear
nuestros juegos. Vamos a ver la clase SingleTouch en detalle:import
java.util.ArrayList;import java.util.List;
import android.view.MotionEvent;import android.view.View;
import com.example.slange.interfaces.Pool;import com.example.slange.interfaces.TouchHandler;import com.example.slange.interfaces.Input.TouchEvent;import com.example.slange.interfaces.Pool.PoolObjectFactory;
public class SingleTouchHandler implements TouchHandler {
boolean isTouched;int touchX;int touchY;Pool touchEventPool;List
touchEvents = new ArrayList();List touchEventsBuffer = new
ArrayList();float scaleX;float scaleY;Empezamos implementando
nuestra interface TouchHandler que nos har sobreescribir sus cuatro
mtodos mas el mtodo onTouch de la interface OnTouchListener. Tambin
importamos la clase TouchEvent de nuestra interface Input, la clase
Pool y la interface PoolObjectFactory.
En el primer miembro isTouched almacenaremos si un puntero (un
dedo) esta tocando la pantalla. Y en las dos siguientes variables
(touchX, touchY) almacenaremos las coordenadas de ese
puntero.
Creamos los tres miembros ya vistos en el punto anterior, para
manejar las instancias de los eventos touch.
Y los dos ltimos miembros los usaremos para tratar con las
diferentes resoluciones de pantalla, mas adelante veremos su
uso.public SingleTouchHandler(View view, float scaleX, float
scaleY) {PoolObjectFactory factory = new PoolObjectFactory()
{public TouchEvent createObject() {return new TouchEvent();}
};touchEventPool = new Pool(factory,
100);view.setOnTouchListener(this);
this.scaleX = scaleX;this.scaleY = scaleY;}Constructor muy parecido al de KeyBoardHandler, a diferencia que en este registramos los eventos touch en nuestra view a travs del mtodo setOnTouchListener que har una llamada al mtodo onTouch. Y aqu almacenamos los parmetros (scaleX, scaleY) en sus respectivas variables.public boolean onTouch(View v, MotionEvent event) {synchronized(this) {TouchEvent touchEvent = touchEventPool.newObject();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:touchEvent.type = TouchEvent.TOUCH_DOWN;isTouched = true;break;case MotionEvent.ACTION_MOVE:touchEvent.type = TouchEvent.TOUCH_DRAGGED;isTouched = true;break;case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP:touchEvent.type = TouchEvent.TOUCH_UP;isTouched = false;break;}
touchEvent.x = touchX = (int)(event.getX() * scaleX);touchEvent.y = touchY = (int)(event.getY() * scaleY);touchEventsBuffer.add(touchEvent);
return true;}}Mtodo que consigue el mismo resultado que el mtodo
onKey de nuestra clase KeyboardHandler, pero en este caso manejamos
eventos TouchEvent.
Creamos un nuevo objeto TouchEvent que almacenamos en nuestro Pool
touchEventPool.
Con el bloque switch comprobamos que tipo de evento a tenido lugar
y lo almacenamos en la variable de nuestro objeto touchEvent.type.
Segn el tipo de evento almacenamos en nuestra variable isTouched
true o false.
Almacenamos las coordenadas de ese evento en las variables de
nuestro objeto (touchEvent.x, touchEvent.y) y tambin en las
variables de nuestra clase (touchX, touchY). Para conocer las
coordenadas usamos el parmetro del mtodo (event) con sus mtodos
(getX, getY) y esto lo multiplicamos por la escala de la pantalla
(mas adelante veremos como calcular la escala para los diferentes
tamaos de pantalla).
Para terminar aadimos nuestro objeto touchEvent a nuestra lista
touchEventsBuffer y devolvemos true al mtodo.public boolean
isTouchDown(int pointer) {synchronized(this) {if(pointer ==
0)return isTouched;elsereturn false;}}Con este mtodo comprobaremos
si el puntero esta tocando la pantallapublic int getTouchX(int
pointer) {synchronized(this) {return touchX;}}
public int getTouchY(int pointer) {synchronized(this) {return
touchY;}}Usando estos dos mtodos podremos conocer las coordenadas
del puntero. Nos devolver el valor que haya almacenado en touchX y
touchY.public List getTouchEvents() {synchronized(this) { int len =
touchEvents.size();for( int i = 0; i < len; i++
)touchEventPool.free(touchEvents.get(i));touchEvents.clear();touchEvents.addAll(touchEventsBuffer);touchEventsBuffer.clear();return
touchEvents;}}Mismo mtodo que la clase KeyboardHandler. Recordar
que deberemos llamar a este mtodo frecuentemente.
3.3 Clase MultiTouchHandler
Clase que va a ser muy similar a la anterior pero con la diferencia
que en esta clase vamos a usar mas de un puntero.import
java.util.ArrayList;import java.util.List;
import android.annotation.TargetApi;import android.view.MotionEvent;import android.view.View;
import com.example.slange.interfaces.Input.TouchEvent;import com.example.slange.interfaces.Pool;import com.example.slange.interfaces.TouchHandler;import com.example.slange.interfaces.Pool.PoolObjectFactory;
public class MultiTouchHandler implements TouchHandler {
private static final int MAX_TOUCHPOINTS = 10;boolean[]
isTouched = new boolean[MAX_TOUCHPOINTS];int[] touchX = new
int[MAX_TOUCHPOINTS];int[] touchY = new int[MAX_TOUCHPOINTS];int[]
id = new int[MAX_TOUCHPOINTS];Pool touchEventPool;List touchEvents
= new ArrayList();List touchEventsBuffer = new ArrayList();float
scaleX;float scaleY;Tenemos la misma implementacin e importacin que
la clase anterior.
Empezamos declarando el numero mximo de punteros que vamos a
controlar, en este caso sern 10.
Luego creamos cuatro variables para almacenar los datos de cada
puntero. Si esta tocando la pantalla (isTouched), sus coordenadas x
e y (touchX, touchY) y su identidad (id).
Lo siguiente es lo mismo que en la clase anterior.public
MultiTouchHandler(View view, float scaleX, float scaleY)
{PoolObjectFactory factory = new PoolObjectFactory() {public
TouchEvent createObject() {return new
TouchEvent();}};touchEventPool = new Pool(factory,
100);view.setOnTouchListener(this);
this.scaleX = scaleX;this.scaleY = scaleY;}El constructor es el mismo que la clase anterior.public boolean onTouch(View v, MotionEvent event) {synchronized (this) {int action = event.getAction() & MotionEvent.ACTION_MASK;int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;int pointerCount = event.getPointerCount();TouchEvent touchEvent;for (int i = 0; i < MAX_TOUCHPOINTS; i++) {if (i >= pointerCount) {isTouched[i] = false;id[i] = -1;continue;}int pointerId = event.getPointerId(i);if (event.getAction() != MotionEvent.ACTION_MOVE && i != pointerIndex) {continue;}switch (action) {case MotionEvent.ACTION_DOWN:case MotionEvent.ACTION_POINTER_DOWN:touchEvent = touchEventPool.newObject();touchEvent.type = TouchEvent.TOUCH_DOWN;touchEvent.pointer = pointerId;touchEvent.x = touchX[i] = (int) (event.getX(i) * scaleX);touchEvent.y = touchY[i] = (int) (event.getY(i) * scaleY);isTouched[i] = true;id[i] = pointerId;touchEventsBuffer.add(touchEvent);break;
case MotionEvent.ACTION_UP:case MotionEvent.ACTION_POINTER_UP:case MotionEvent.ACTION_CANCEL:touchEvent = touchEventPool.newObject();touchEvent.type = TouchEvent.TOUCH_UP;touchEvent.pointer = pointerId;touchEvent.x = touchX[i] = (int) (event.getX(i) * scaleX);touchEvent.y = touchY[i] = (int) (event.getY(i) * scaleY);isTouched[i] = false;id[i] = -1;touchEventsBuffer.add(touchEvent);break;
case MotionEvent.ACTION_MOVE:touchEvent =
touchEventPool.newObject();touchEvent.type =
TouchEvent.TOUCH_DRAGGED;touchEvent.pointer =
pointerId;touchEvent.x = touchX[i] = (int) (event.getX(i) *
scaleX);touchEvent.y = touchY[i] = (int) (event.getY(i) *
scaleY);isTouched[i] = true;id[i] =
pointerId;touchEventsBuffer.add(touchEvent);break;}}return
true;}}Mtodo que a priori puede parecer complicado pero es muy
similar al anterior, lo que en este caso almacenamos todos los
punteros que pueda haber en la pantalla.
En la variable action almacenaremos el tipo de evento que tiene
lugar. En pointerIndex almacenaremos el ndice del puntero, este
ndice puede cambiar si un puntero suelta la pantalla, por lo que
usamos la variable pointerId para almacenar la identidad real de
cada puntero.public boolean isTouchDown(int pointer) {synchronized
(this) {int index = getIndex(pointer);if (index < 0 || index
>= MAX_TOUCHPOINTS)return false;elsereturn
isTouched[index];}}
public int getTouchX(int pointer) {synchronized (this) {int index = getIndex(pointer);if (index < 0 || index >= MAX_TOUCHPOINTS)return 0;elsereturn touchX[index];}}
public int getTouchY(int pointer) {synchronized (this) {int
index = getIndex(pointer);if (index < 0 || index >=
MAX_TOUCHPOINTS)return 0;elsereturn touchY[index];}}Con estos
mtodos podremos saber si un puntero en concreto (lo indicaremos con
el parmetro pointer) esta tocando la pantalla y tambin podremos
conocer sus coordenadas x e y.public List getTouchEvents()
{synchronized (this) {int len = touchEvents.size();for (int i = 0;
i < len;
i++)touchEventPool.free(touchEvents.get(i));touchEvents.clear();touchEvents.addAll(touchEventsBuffer);touchEventsBuffer.clear();return
touchEvents;}}Mismo mtodo que en clases anteriores.private int
getIndex(int pointerId) {for (int i = 0; i < MAX_TOUCHPOINTS;
i++) {if (id[i] == pointerId) {return i;}}return -1;}Con este
ultimo mtodo podremos conocer el ndice de un puntero en concreto,
para ello usamos el parmetro pointerId.
Con esto terminamos el articulo, pero comentar que podramos hacer
mas clases Handler para controlar por ejemplo los sensores de un
terminal. En el cdigo de ejemplo tenemos dos clases mas para
manejar estos sensores.Creacin de un juego (5): Modulo Graficos
Este modulo es algo mas complejo de los que llevamos creados hasta
ahora. Y sera el responsable de cargar imgenes (bitmaps) en
nuestras pantallas. En principio si queremos grficos de alto
rendimiento primero deberemos saber por lo menos los fundamentos de
la programacin grfica y para ello vamos a conocer los conceptos
bsicos en grficos 2D. Crearemos dos interfaces y tres clases para
manejar los grficos de nuestro juego.
Para llevar a cabo esta tarea deberemos de ser capaces de realizar las siguientes operaciones:Almacenar imgenes en la RAM para su posterior uso.Limpiar nuestra pantalla de todos los elementos que la componen.Pintar un pixel de la pantalla con un color especifico.Dibujar lineas y rectngulos en nuestra pantalla.Dibujar imgenes en la pantalla cargadas previamente. Ya sea una imagen completa o una porcin de la misma. Tambin dibujar imgenes con y sin mezcla.Obtener el tamao de nuestra pantalla de dibujo.
1.1 Interface Pixmapimport com.example.slange.interfaces.Graphics.PixmapFormat;
public interface Pixmap {
public int getWidth();
public int getHeight();
public PixmapFormat getFormat();
public void dispose();}Primero importamos la interface Grficos
para tener acceso a su miembro PixmapFormat, se explicara en el
siguiente punto del articulo.
Esta interface nos permitir conocer el tamao en pxeles de una
imagen, a travs de los mtodos getWidth y getHeight. Tambin podremos
conoces su formato con el mtodo getFormat. Y para terminar, con el
mtodo dispose liberaremos las imgenes cargadas en la memoria.
1.2 Interface Grficospublic interface Graphics {
public static enum PixmapFormat {ARGB8888, ARGB4444, RGB565}
public Pixmap newPixmap(String fileName, PixmapFormat format);
public void clear(int color);
public void drawPixel(int x, int y, int color);
public void drawLine(int x, int y, int x2, int y2, int color);
public void drawRect(int x, int y, int width, int height, int color);
public void drawPixmap(Pixmap pixmap, int x, int y, int srcX, int srcY,int srcWidth, int srcHeight);
public void drawPixmap(Pixmap pixmap, int x, int y);
public int getWidth();
public int getHeight();}Interface que nos servir para dibujar en
pantalla tanto imgenes como pxeles a parte de poder recuperar el
tamao de nuestra pantalla de dibujo (framebuffer).
Primero creamos un enum PixmapFormat donde almacenamos 3 tipos de
formato de imagen. Los enum sirven para restringir el contenido de
una variable (en nuestro caso PixmapFormat) a una serie de valores
predefinidos (ARGB8888, ARGB4444, RGB565), es decir, nuestra
variable PixmapFormat solo tienes las 3 posibilidades, esto suele
ayudar a reducir los errores de nuestro cdigo. Esta variable la
usaremos para indicar el tipo de formato de imagen que necesitemos,
consiguiendo almacenar la imagen en un tamao menor o mayor:ARGB444:
cada pixel de la imagen se almacena en 2 bytes y los tres canales
de color RGB mas el canal alpha (A) se almacenan con una precisin
de 4 bits (16 posibilidades). til cuando queremos usar el menor
almacenamiento para nuestras imgenes, pero se recomienda usar
ARGB888.ARGB888: cada pixel se almacena en 4 bytes. Cada canal ARGB
se almacena con una precisin de 8 bits (256 posibilidades). Esta
configuracin es muy flexible y ofrece la mejor calidad pero mayor
tamao de almacenamiento.RGB565: cada pixel se almacena en 2 bytes y
solo disponemos de los canales RGB. el rojo se almacena con 5 bits
de precisin (32 posibilidades), el verde con 6 bits (64
posibilidades) y el azul con 5 bits. Esta configuracin es til
cuando se usan imgenes opacas que no requieren alta definicin de
color.Continuamos creando un mtodo newPixmap que nos devolver un
objeto Pixmap y como parmetros indicaremos el nombre del archivo de
imagen y el formato que necesitemos darle.
Los cuatro siguientes metodos (clear, drawPixel, drawLine,
drawRect) los usaremos para colorear pxeles con un color especifico
y de varias formas posibles (toda la pantalla, un solo pixel, una
linea o un rectngulo).
Despus tenemos dos mtodos drawPixmap que nos ayudaran a dibujar en
pantalla una imagen o una porcin de imagen en el sitio que le
indiquemos de nuestra pantalla de dibujo (framebuffer).
Y por ultimo dos mtodos que nos devolvern el ancho y el alto de la
pantalla de dibujo (framebuffer).
2.1 Implementar interface Pixmapimport android.graphics.Bitmap;
import com.example.slange.interfaces.Graphics.PixmapFormat;import com.example.slange.interfaces.Pixmap;
public class AndroidPixmap implements Pixmap {
Bitmap bitmap;PixmapFormat format;
public AndroidPixmap(Bitmap bitmap, PixmapFormat format)
{this.bitmap = bitmap;this.format = format;}A parte de implementar
la interface Pixmap, importamos a esta clase el enum PixmapFormat
de la interface Grficos.
Empezamos creando dos objetos: un Bitmap (que nos ayudara a
trabajar con archivos de mapa de bits, es decir, imgenes) y un
PixmapFormat (que usaremos para determinar que tipo de formato le
queremos dar a la imagen).
En el constructor simplemente almacenamos los dos parmetros que le
pasamos al constructor, en los dos objetos que hemos creado para
esta clase.public int getWidth() {return bitmap.getWidth();}
public int getHeight() {return bitmap.getHeight();}Aplicando los mtodos getWidth y getHeight nos devolver el ancho y alto de nuestra imagen bitmap.public PixmapFormat getFormat() {return format;}
public void dispose() {bitmap.recycle();} }El mtodo getFormat
nos devolver un PixmapFormat. El formato de nuestra imagen
bitmap.
Y para terminar con el mtodo dispose liberaremos de la memoria la
imagen asociada al bitmap gracias al mtodo recycle de la clase
Bitmap.
2.2 Implementar interface Grficosimport java.io.IOException;import
java.io.InputStream;
import android.content.res.AssetManager;import android.graphics.Bitmap;import android.graphics.Bitmap.Config;import android.graphics.BitmapFactory;import android.graphics.BitmapFactory.Options;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Paint.Style;import android.graphics.Rect;
import com.example.slange.interfaces.Graphics;import com.example.slange.interfaces.Pixmap;
public class AndroidGraphics implements Graphics {
AssetManager assets;Bitmap frameBuffer;Canvas canvas;Paint
paint;Rect srcRect = new Rect();Rect dstRect = new Rect();A parte
de implementar la interface Graficos, importamos tambin la
interface Pixmap ya que usaremos algn atributo suyo en esta
clase.
Empezamos creando un objeto AssetManager que a estas alturas ya
sabremos lo que es.
Creamos un objeto Bitmap que como iba indicando pasos atrs sera
nuestra pantalla de dibujo (framebuffer) donde colocaremos los
distintos elementos de nuestras pantallas del juego.
Otro objeto Canvas que atender a las llamadas de dibujo, es decir,
dibujara pxeles o imgenes en nuestro framebuffer.
Un objeto Paint que sera el encargado de darle estilo y color a lo
que dibujemos en nuestro framebuffer.
Y por ultimo creamos dos objetos Rect: srcRect se encargara de
almacenar las cuatro coordenadas de un rectngulo (que sern las 4
esquinas de nuestras imgenes) y dstRect se encargara de dibujar
esas imgenes en las coordenadas que le indiquemos.public
AndroidGraphics(AssetManager assets, Bitmap frameBuffer)
{this.assets = assets;this.frameBuffer = frameBuffer;this.canvas =
new Canvas(frameBuffer);this.paint = new Paint();}Almacenamos los
parmetros del constructor de la clase en sus respectivas variables
de la clase.
Iniciamos el objeto canvas indicando como parmetro la pantalla
donde podr dibujar.
Y terminamos iniciando el objeto Paint.public Pixmap
newPixmap(String fileName, PixmapFormat format) {Config config =
null;if (format == PixmapFormat.RGB565)config = Config.RGB_565;else
if (format == PixmapFormat.ARGB4444)config =
Config.ARGB_4444;elseconfig = Config.ARGB_8888;
Options options = new Options();options.inPreferredConfig = config;
InputStream in = null;Bitmap bitmap = null;try {in = assets.open(fileName);bitmap = BitmapFactory.decodeStream(in);if (bitmap == null)throw new RuntimeException("Error al cargar bitmap desde assets '"+ fileName + "'");} catch (IOException e) {throw new RuntimeException("Error al cargar bitmap desde assets '"+ fileName + "'");} finally {if (in != null) {try {in.close();} catch (IOException e) {}}}
if (bitmap.getConfig() == Config.RGB_565)format = PixmapFormat.RGB565;else if (bitmap.getConfig() == Config.ARGB_4444)format = PixmapFormat.ARGB4444;elseformat = PixmapFormat.ARGB8888;
return new AndroidPixmap(bitmap, format);}El mtodo newPixmap
intentara cargar una imagen desde la carpeta assets. Parece muy
complicado pero es mas fcil de lo que aparenta.
Empezamos creando un objeto Config que tiene los mismos tipos de
formato que nuestro objeto PixmapFormat. Comprobaremos en que
formato (ARGB444, ARGB888, RGB565) viene nuestra imagen y lo
almacenaremos en nuestro objeto config.
Seguidamente creamos un objeto Options para almacenar el tipo de
formato config preferido. Este objeto options se encargara
automticamente de establecer este formato si es posible.
Intentamos cargar una imagen desde la carpeta assets al objeto
bitmap a travs del mtodo decodeStream de la clase BitmapFactory que
nos pide como parmetro una fuente de datos a leer. Manejaremos la
IOException en caso de que ocurra algo y comprobaremos que nuestro
objeto bitmap es nulo por si acaso. Finalmente si nuestro
InputStream no es nulo, lo cerramos.
Tras cargar la imagen en nuestro bitmap, el BitmapFactory podra
hacer caso omiso del tipo de formato de nuestra imagen por lo que
tenemos que volver a comprobarlo y almacenarlo en nuestro parmetro
format.
Para terminar devolvemos un nuevo objeto AndroidPixmap indicando
los parmetros que hemos recogido.public void clear(int color)
{canvas.drawRGB((color & 0xff0000) >> 16, (color &
0xff00) >> 8,(color & 0xff));}Con este mtodo podremos
pintar toda la pantalla de nuestro framebuffer del color que se le
indique como parmetro, para ello haremos uso del mtodo drawRGB que
nos pide como parmetro un rango de 0 a 255 para cada color
primario(rojo, verde, azul).public void drawPixel(int x, int y, int
color) {paint.setColor(color);canvas.drawPoint(x, y,
paint);}Podremos pintar un pixel con el siguiente mtodo, indicando
como parmetro las coordenadas x e y de nuestro framebuffer. Como
parmetro color indicaremos el estilo de color a nuestro objeto
paint y realizaremos el dibujo con el mtodo drawPoint.public void
drawLine(int x, int y, int x2, int y2, int color)
{paint.setColor(color);canvas.drawLine(x, y, x2, y2,
paint);}Pintaremos una linea gracias a este mtodo, indicando las
coordenadas de inicio (x, y) y las coordenadas de destino (x2, y2).
Para ello hacemos uso del mtodo drawLine de la clase Canvas.public
void drawRect(int x, int y, int width, int height, int color)
{paint.setColor(color);paint.setStyle(Style.FILL);canvas.drawRect(x,
y, x + width - 1, y + width - 1, paint);}Comentar aqu que las
coordenadas 0, 0 de x e y de nuestro terminal, se encuentran en la
esquina superior izquierda de la pantalla (por lo tanto empezaremos
a sumar hacia abajo y hacia la derecha).
Sabiendo esto, con el siguiente mtodo podremos pintar un rectngulo
con el color que se le indique. El inicio del rectngulo y por lo
tanto su esquina superior izquierda la declararemos con los
parmetros x e y. La altura y anchura lo haremos con height y
width.
Si nos fijamos le estamos restando -1 a la altura y anchura porque
si declarramos un rectngulo de 5x5, el mtodo drawRect coje esos dos
pxeles de altura o anchura y le suma el punto de partida con lo
cual se con quedara un rectngulo de 6x6.public void
drawPixmap(Pixmap pixmap, int x, int y, int srcX, int srcY,int
srcWidth, int srcHeight) {
srcRect.left = srcX;srcRect.top = srcY;srcRect.right = srcX + srcWidth - 1;srcRect.bottom = srcY + srcHeight - 1;
dstRect.left = x;dstRect.top = y;dstRect.right = x + srcWidth - 1;dstRect.bottom = y + srcHeight - 1;
canvas.drawBitmap(((AndroidPixmap) pixmap).bitmap, srcRect,
dstRect, null);}Con este mtodo conseguiremos pintar en pantalla una
porcin de imagen y ocurre los mismo que en la pantalla, las
coordenadas 0,0 de x e y de una imagen se encuentran en la esquina
superior izquierda.
Primero deberemos seleccionar la porcin de imagen indicando su
esquina superior izquierda con los parmetros (srcX y srcY), para el
ancho usaremos (srcWidth) y para el alto (srcHeight).
Una vez seleccionada el mtodo drawPixmap almacena estos datos en el
objeto srcRect.
Y para indicar donde la queremos pintar en nuestro framebuffer
usaremos los parmetro (x, y), el propio mtodo sabiendo la esquina
superior izquierda, calculara las cuatro coordenadas y las
almacenara en el objeto dstRect.
Recordar que le restamos -1 ya que sino la imagen exceder en un
pixel.
Finalmente podemos pintar esa porcin de imagen con el mtodo
drawBitmap de la clase Canvas.public void drawPixmap(Pixmap pixmap,
int x, int y) {canvas.drawBitmap(((AndroidPixmap)pixmap).bitmap, x,
y, null);}Este mtodo es similar al anterior pero mucho mas
sencillo, con el simplemente dibujaremos en pantalla una imagen
completa en las coordenada (x, y) que se le indique.public int
getWidth() {return frameBuffer.getWidth();}
public int getHeight() {return frameBuffer.getHeight();}}Para
terminar la clase sobreescribimos los mtodos detWidth y getHeight
que nos devolvern el tamao de pantalla de dibujo.
2.3 Clase FastRenderView
Podramos decir que esta clase sera la encargada de pintar nuestro
framebuffer en la pantalla de nuestro terminal, colocando el
framebuffer en su lugar correspondiente y escalndolo a los
diferentes tamaos de pantalla si es necesario. Tambin nos permitir
conocer la pantalla activa del juego y podremos controlar el
DeltaTime en todo momento.
El DeltaTime es un concepto que se utiliza en programacin que
relaciona el hardware y su capacidad de respuesta. Y cuando
hablamos de mover grficos el deltatime se calcula llamando a un
temporizador que controlara los fotogramas por segundo del hardware
y coger el tiempo que pasa entre que se ve un fotograma y el
siguiente, esto nos servir para mostrar la misma cantidad de
cuadros por segundo.
A continuacin vamos a ir viendo la clase por partes, explicando su
metodologa:import android.graphics.Bitmap;import
android.graphics.Canvas;import android.graphics.Rect;import
android.view.SurfaceHolder;import android.view.SurfaceView;
public class AndroidFastRenderView extends SurfaceView implements Runnable {
AndroidGame game;Bitmap framebuffer;Thread renderThread =
null;SurfaceHolder holder;volatile boolean running =
false;Extendemos la clase SurfaceView que nos proporciona una
superficie de dibujo dentro de una view, a parte implementamos la
interface Runnable que nos servir para manejar hilos secundarios,
es decir, podremos ejecutar cdigo en un hilo diferente al
principal.Esta implementacin nos pide sobreescribir el mtodo
run.
Creamos una instancia de la clase AndroidGame (en el articulo 6
veremos el modulo juego y de que trata esta clase). Y un objeto
Bitmap que sera nuestra SurfaceView.
Continuamos creando un objeto Thread que usaremos para crear hilos
nuevos o eliminar los existentes. Un thread se puede definir como
una unidad de cdigo en ejecucin.
El objeto SurfaceHolder nos permitir controlar el tamao y formato
de la superficie de dibujo, editar los pxeles y vigilar los cambios
en dicha superficie. Esta interface esta disponible a travs de la
clase SurfaceView.
Y por ultimo creamos una variable booleana donde almacenaremos si
un hilo debe ser detenido o reanudado. Con el modificador volatile
indicamos que dicha variable puede ser modificada por varios hilos
(threads) de forma simultanea y asncrona, asegurando as su valor en
todo momento a costa de un pequeo impacto en el rendimiento.public
AndroidFastRenderView(AndroidGame game, Bitmap framebuffer)
{super(game);this.game = game;this.framebuffer =
framebuffer;this.holder = getHolder();}Con el modificador super
estamos llamando al constructor de la clase AndroidGame a travs del
parmetro game (AndroidGame se trata de una Activity que veremos en
el capitulo siguiente). Almacenamos tambin nuestro parmetro en la
variable game de la clase.
Tambin almacenamos el segundo parmetro en su variable framebuffer.
Y en la variable holder usamos el mtodo getHolder que nos devuelve
una instancia SurfaceHolder que almacenamos tambin.public void
resume() { running = true;renderThread = new
Thread(this);renderThread.start(); }Mtodo que usaremos para crear
nuevos hilos. En cada hilo pondremos su variable running a true
indicando que ese hilo esta en ejecucin.
Iniciamos un nuevo hilo y lo ejecutamos con el mtodo start (que
este a su vez har una llamada al mtodo run para el hilo
creado).
Con este mtodo resume nos aseguramos de que nuestro nuevo hilo acta
bien con el ciclo de vida de la Activity.public void run() {Rect
dstRect = new Rect();long startTime =
System.nanoTime();while(running) {
if(!holder.getSurface().isValid())continue;
float deltaTime = (System.nanoTime()-startTime) / 1000000000.0f;startTime = System.nanoTime();
game.getCurrentScreen().update(deltaTime);game.getCurrentScreen().present(deltaTime);
Canvas canvas =
holder.lockCanvas();canvas.getClipBounds(dstRect);canvas.drawBitmap(framebuffer,
null, dstRect, null);holder.unlockCanvasAndPost(canvas);}}Este
mtodo sera llamado cada vez que se cree un nuevo hilo o pantalla de
juego. Y sera el encargado de actualizar/renderizar los objetos en
la pantalla en cada momento. Se ira repitiendo continuamente en la
pantalla que lo llame.
Empezamos creando un objeto Rect que nos servir para almacenar los
limites de la pantalla. Y una variable startTime donde
almacenaremos la hora actual en nanosegundos. Un nanosegundo es una
mil millonsima de segundo.
Seguimos creando un bucle while que se repetir mientras nuestro
hilo este en ejecucin. Lo primero que hacemos dentro del hilo es
comprobar si existe una superficie valida gracias al bloque if, si
no existe una superficie valida entra en juego continue que hace
terminar el bucle.
Si existe una superficie valida calculamos el DeltaTime, lo
convertimos a segundos y seguidamente almacenamos de nuevo la hora
actual.
Una vez calculado el DeltaTime usamos los mtodos update y present
para ir actualizando la pantalla actual con el intervalo de tiempo
DeltaTime. (en el articulo 6 veremos estos mtodos en detalle)
Por ultimo creamos un objeto canvas al que le indicamos con el
mtodo lockCanvas que puede comenzar la edicin de pxeles en la
superficie de dibujo. Con el mtodo getClipBounds recuperamos los
limites de la superficie de dibujo. Y ya con el mtodo drawBitmap
haremos que pinte una pantalla del juego. Para finalizar llamamos
al mtodo unlockCanvasAndPost para terminar la edicin de pxeles y
conseguir mostrar una pantalla de nuestro juego.public void pause()
{ running = false; while(true) {try {renderThread.join();return;}
catch (InterruptedException e) {// retry}}} Con el mtodo pause
bloquearemos el hilo en ejecucin a la espera de que el usuario lo
finalice y termine por desaparecer de la memoria.
Creacin de un juego (6): Modulo Juego Con este modulo terminamos
la interface bsica para empezar a desarrollar juegos. Es el modulo
mas importante ya que se encarga de gestionar todas las clases que
hemos visto con anterioridad. Todo lo que tenemos que hacer es
encajar todas las piezas teniendo en cuenta varias cosas:Deberemos
realizar la gestin de ventanas (pantallas de juego) creando una
Activity y una instancia AndroidFastRenderView a parte de manipular
el ciclo de vida en la actividad de forma limpiaCrear y gestionar
un WakeLock para que la pantalla no se apague mientras jugamosCrear
instancias para manejar todos los mdulos anteriores (Grficos,
Audio, FileIO, ...)Administrar las pantallas e integrarlas con el
ciclo de vida de la actividad
1.1 Interface Game
Esta interface sera la encargada de darnos acceso a todos los
mdulos que hemos desarrollado anteriormente y sera capaz de una
gestin absoluta de las pantallas del juego. Veamos a continuacin
los componentes de esta interface:public interface Game {
public Input getInput();
public FileIO getFileIO();
public Graphics getGraphics();
public Audio getAudio();
public void setScreen(Screen screen);
public Screen getCurrentScreen();
public Screen getStartScreen();}Los cuatro primeros mtodos nos
devolvern una instancia de cada una de las interfaces para poder
trabajar con sus mtodos.
El mtodo setScreen nos permite configurar la pantalla actual del
juego, para ello lo indicaremos en su parmetro.
Con el mtodo getCurrentScreen podremos saber que pantalla esta
activa en ese momento.
Y con el ultimo mtodo getStartScreen iniciaremos nuestro
juego.
1.2 Interface Screen
La ultima pieza de nuestro rompecabezas es la clase abstracta
Screen. Esta vez creamos una clase abstracta en vez de una
interface porque habr veces que no necesitemos usar todos sus
mtodos. Esta clase nos ayudara a presentar los objetos en pantalla,
actualizar la pantalla segn el delta time, pausar o reanudar el
juego. Cada pantalla que creemos en nuestro juego deber implementar
esta interface excepto la primera pantalla o pantalla de inicio de
nuestro juego que deber extender la clase AndroidGame
sobreescribiendo el mtodo getStartScreen. Vamos a ver la
interface:public abstract class Screen {
protected final Game game;
public Screen(Game game) {this.game = game;}
public abstract void update(float deltaTime);
public abstract void present(float deltaTime);
public abstract void pause();
public abstract void resume();
public abstract void dispose();}Lo primero que hacemos es crear
una instancia de nuestra interface Juego y ya en el constructor
almacenamos su parmetro en ella. Con esto conseguimos dos cosas:
tener acceso a todos los mdulos de la interface juego (Input,
FileIO, Graficos, Audio) y a parte poder crear una nueva ventada
desde la pantalla actual, llamando al mtodo Game.setScreen.
Por lo tanto cuando implementamos esta interface necesitamos tener
acceso a todos los mdulos para crear y gestionar esa pantalla de
nuestro juego.
Con los mtodos update y present, podremos actualizar y presentar
los componentes en la pantalla. Se llamara a la instancia juego en
cada iteracin del bucle principal.
Los mtodos pause y resume actuaran cuando el juego se ponga en
pausa o se reanude. Esto se realiza de nuevo con la instancia juego
y se aplicara a la pantalla actual.
Para finalizar usaremos el mtodo dispose para liberar todos los
recursos de la memoria. Se deber llamar a este mtodo justo despus
de llamar a Game.setScreen. Y recordar que sera el ultimo momento
donde prodremos guardar los datos de esa pantalla.
2.1 Implementar interface Game
Como se puede observar en lo que llevamos de articulo, todo el
trabajo que hemos realizado esta dando sus frutos y con pocas
lineas de cdigo tenemos acceso a todos los mtodos de nuestras
interfaces. Para terminar vamos a crear una clase que practicamente
terminara de encajar todas las piezas de nuestro puzzle. Se encarga
de toda la gestin de nuestras interfaces, vamos a verla:import
android.app.Activity;import android.content.Context;import
android.content.res.Configuration;import
android.graphics.Bitmap;import
android.graphics.Bitmap.Config;import android.os.Bundle;import
android.os.PowerManager;import
android.os.PowerManager.WakeLock;import android.view.Window;import
android.view.WindowManager;
import com.example.slange.interfaces.Audio;import com.example.slange.interfaces.FileIO;import com.example.slange.interfaces.Game;import com.example.slange.interfaces.Graphics;import com.example.slange.interfaces.Input;import com.example.slange.interfaces.Screen;
public abstract class AndroidGame extends Activity implements Game {
AndroidFastRenderView renderView;Graphics graphics;Audio
audio;Input input;FileIO fileIO;Screen screen;WakeLock
wakeLock;Empezamos la clase extendiendo de Activity e implementando
la interface Game.
Creamos la instancia renderView que sera nuestra superficie de
dibujo y otra instancia graphics que nos ayudara a dibujar en esa
superficie.
Continuamos creando las instancias audio, input, fileIO y screen
para poder tener acceso a sus mtodos.
Y el ultimo miembro wakeLock lo usaremos para mantener la pantalla
encendia.public void onCreate(Bundle savedInstanceState)
{super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;int frameBufferWidth = isLandscape ? 480 : 320;int frameBufferHeight = isLandscape ? 320 : 480;Bitmap frameBuffer = Bitmap.createBitmap(frameBufferWidth,frameBufferHeight, Config.RGB_565);
float scaleX = (float) frameBufferWidth/ getWindowManager().getDefaultDisplay().getWidth();float scaleY = (float) frameBufferHeight/ getWindowManager().getDefaultDisplay().getHeight();
renderView = new AndroidFastRenderView(this, frameBuffer);graphics = new AndroidGraphics(getAssets(), frameBuffer);fileIO = new AndroidFileIO(this);audio = new AndroidAudio(this);input = new AndroidInput(this, renderView, scaleX, scaleY);screen = getStartScreen();setContentView(renderView);
PowerManager powerManager = (PowerManager)
getSystemService(Context.POWER_SERVICE);wakeLock =
powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "GLGame");}Lo
primero que hacemos en este mtodo es quitar la barra de titulo de
la aplicacin y ponerla a pantalla completa.
Lo siguiente que hacemos es comprobar que orientacin tiene la
pantalla, para ello consultamos su orientacin con el mtodo
getResources().getConfiguration().orientation y si es igual a la
orientacin landscape, almacenamos true en nuestra variable
isLandscape. En caso contrario se almacenara false.
Despus tenemos dos miembros (framebufferWidth y frameBufferHeight)
que nos servirn para delimitar el ancho y el alto de nuestra
superficie de dibujo. Dependiendo del valor isLandScape se
almacenara un valor u otro en cada variable.
Sabiendo esos datos creamos nuestra superficie de dibujo
frameBuffer. Con una configuracin RGB565, que har que nuestro
dibujo se complete un poco mas rpido.
En las dos siguientes variables (scaleX, scaleY) calculamos la
escala para los diferentes tamaos de pantalla. En nuestro caso
estamos usando una resolucin de 480x320 o 320x480. Con lo cual
debemos calcular la escala para tamaos de pantalla mas grandes o
mas pequeos.
Continuamos iniciando todos nuestros objetos y aplicando sus
parmetros si es necesario. Y terminamos estableciendo nuestra
superficie de dibujo renderView como la View principal.
Por ultimo almacenamos en nuestro wakeLock una instancia de
servicio que se encarga de mantener la pantalla encendida.public
void onResume()
{super.onResume();wakeLock.acquire();screen.resume();renderView.resume();}Con
el mtodo wakeLock.acquire creamos un bloqueo para que la pantalla
se quede encendida.
El mtodo screen.resume reanudara la Activity.
Y con renderView.resume reanudara nuestra superficie de
dibujo.public void onPause()
{super.onPause();wakeLock.release();renderView.pause();screen.pause();
if (isFinishing())screen.dispose();}Con el mtodo
wakeLock.release liberamos el bloqueo de iluminacin de pantalla,
ahora se apagara segn la configuracin de tiempo que tenga el
terminal del usuario.
Primero deberemos pausar la superficie de dibujo (renderView.pause)
y despus la pantalla de juego (screen.pause) sino podramos tener
problemas de concurrencia ya que trabajan en hilos diferentes, por
un lado tenemos el hilo de la interface y por el otro el hilo
principal.public Input getInput() {return input;}
public FileIO getFileIO() {return fileIO;}
public Graphics getGraphics() {return graphics;}
public Audio getAudio() {return audio;}No necesita explicacin, simplemente se devuelve la instancia correspondiente a cada uno. Mas tarde se podr llamar a estos mtodos desde una clase que implemente la interface Screen.public void setScreen(Screen screen) {if (screen == null)throw new IllegalArgumentException("Pantalla nula");
this.screen.pause();this.screen.dispose();screen.resume();screen.update(0);this.screen = screen;}
public Screen getCurrentScreen() {return screen;}Con el primer
mtodo podremos establecer una pantalla del juego, para ello lo
indicaremos en su parmetro screen. Lo primero que hacemos en el
mtodo es comprobar si esa pantalla es nula, en caso de que sea as
creamos una nueva excepcin.
A continuacin le decimos a la pantalla actual que entre en pausa y
libere sus recursos para que pueda dar cabida a la nueva
pantalla.
Seguidamente le pedimos que se reanude y actualice con un delta
time de 0 segundos. Y para terminar almacenamos la nueva pantalla
en nuestra variable screen.
Normalmente llamaremos a este mtodo dentro del mtodo
Screen.update.
Para finalizar usaremos el mtodo getCurrentScreen para conocer la
pantalla actual del juego.
Con esto concluimos nuestra interface y ya disponemos de una base
para crear juegos.
Top Related