juego en android

Download Juego en Android

If you can't read please download the document

Upload: supermarinting

Post on 08-Nov-2015

19 views

Category:

Documents


4 download

TRANSCRIPT

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.