Академический Документы
Профессиональный Документы
Культура Документы
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 corresponde
Una historia de fondo con los personajes principales
Una lista de tems, power-ups u otras cosas que modifiquen a los personajes, la
mecnica o el mundo del juego
Diseo grfico de la historia y los personajes
Bocetos de todas las pantallas del juego y transiciones entre ellas
Transformar el juego en un archivo ejecutable
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.
1. Interface FileIO
import 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;
}
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);
}
}
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.
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.
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.
Empezamos creando una variable int, que sera donde almacenaremos la id de nuestro efecto de
sonido y creamos un objeto soundPool que 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.
Ya en el constructor primero iniciamos nuestro mediaplayer y encapsulamos todo en un bloque trycatch 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 mtodo getFileDescriptor que
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
mtodo getStartOffset establecemos el inicio) y el final del archivo de msica (con el
mtodo getLength establecemos 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();
}
Comprobaremos nuestra variable booleana y nos devolver false en caso de que se este
reproduciendo.
public void pause() {
if (mediaPlayer.isPlaying())
mediaPlayer.pause();
}
synchronized (this) {
if (!isPrepared)
mediaPlayer.prepare();
mediaPlayer.start();
}
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
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;
}
}
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);
}
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.
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, ");
else
builder.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;
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();
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.
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<T> {
public interface PoolObjectFactory<T> {
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<T> freeObjects;
private final PoolObjectFactory<T> 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<T> factory, int maxSize) {
this.factory = factory;
this.maxSize = maxSize;
this.freeObjects = new ArrayList<T>(maxSize);
}
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).
import com.example.slange.interfaces.Pool;
import com.example.slange.interfaces.Pool.PoolObjectFactory;
public class KeyboardHandler implements OnKeyListener {
boolean[] pressedKeys = new boolean[128];
Pool<KeyEvent> keyEventPool;
List<KeyEvent> keyEventsBuffer = new ArrayList<KeyEvent>();
List<keyEvent> keyEvents = new ArrayList<KeyEvent>();
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
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];
}
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.
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<TouchEvent> factory = new
PoolObjectFactory<TouchEvent>() {
public TouchEvent createObject() {
return new TouchEvent();
}
};
touchEventPool = new Pool<TouchEvent>(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,
Usando estos dos mtodos podremos conocer las coordenadas del puntero. Nos devolver el valor
que haya almacenado en touchX y touchY.
public List<TouchEvent> 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.
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<TouchEvent> factory = new
PoolObjectFactory<TouchEvent>() {
public TouchEvent createObject() {
return new TouchEvent();
}
};
touchEventPool = new Pool<TouchEvent>(factory, 100);
view.setOnTouchListener(this);
this.scaleX = scaleX;
this.scaleY = scaleY;
}
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;
else
return isTouched[index];
}
}
public int getTouchX(int pointer) {
synchronized (this) {
int index = getIndex(pointer);
if (index < 0 || index >= MAX_TOUCHPOINTS)
return 0;
else
return touchX[index];
}
}
public int getTouchY(int pointer) {
synchronized (this) {
int index = getIndex(pointer);
if (index < 0 || index >= MAX_TOUCHPOINTS)
return 0;
else
return 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<TouchEvent> 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;
}
}
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.
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).
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();
}
}
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();
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;
else
config = 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;
else
format = 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);
}
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.
Para terminar la clase sobreescribimos los mtodos detWidth y getHeight que nos devolvern el
tamao de pantalla de dibujo.
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;
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.
Los cuatro primeros mtodos nos devolvern una instancia de cada una de las interfaces para poder
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.
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
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();
}
}
public FileIO getFileIO() {
return fileIO;
}
public Graphics getGraphics() {
return graphics;
}
public Audio getAudio() {
return audio;
}
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.