Quantcast
Channel: vidasConcurrentes » Programación
Viewing all articles
Browse latest Browse all 11

Creando una aplicación de Android: mejoras (parte 1)

$
0
0

Bienvenidos a la quinta parte de la serie de entradas en el ciclo Creando una aplicación de Android. Si aún no lo has hecho, comienza desde la primera entrada mostrada en el menú inmediatamente superior.

En esta entrada vamos a continuar con el resultado de la entrada anterior, en la que habíamos acabado la lógica de movimientos y las colisiones. Añadiremos funcionalidades durante esta entrada y la siguiente para acabar la aplicación.
Para esta entrada añadiremos vibración del dispositivo, reproducción de sonidos y uso del acelerómetro para los movimientos de las raquetas.

¡Comenzamos!

Importante: esta entrada va a basarse en los códigos creados en las
anteriores entradas del ciclo al que pertenece. Tienes disponible pinchando aquí (MD5: 31b264db2de4125fa01c52d4d169bc99) el proyecto de Eclipse con todo lo hecho hasta ahora, el cual puedes importar a tu Eclipse y comenzar a trabajar.

El Android SDK incluye una gran cantidad de clases que hace muy sencillo el uso de los sensores del dispositivo, la vibración, la reproducción de audio… Para mostrar cómo de sencillo es, comenzamos añadiéndole vibración a nuestra aplicación. Comenzamos abriendo nuestro Eclipse.

Vibración

Para que una aplicación pueda vibrar, ya que necesita acceder a un elemento del sistema, necesita que el usuario acepte el uso de este elemento. Para ello tenemos que añadir al AndroidManifest.xml que queremos usar vibración. Tenemos dos formas, la primera es añadiendo <uses-sdk android:minSdkVersion=”7″ /> justo detrás de la etiqueta <manifest … >.
La segunda forma consiste en ir a la pestaña Permissions del manifest, pulsar en Add, elegir Uses Permission y aceptar, y luego escribir android.permission.VIBRATE en el cuadro de la derecha. Lo siguiente es programar el uso del vibrador.

El uso de la vibración está definido por un Context, por lo que vamos a necesitar usar el contexto de nuestra aplicación para poder obtener el vibrador y posteriormente hacerlo vibrar. La vibración la vamos a realizar solamente cuando la Bola rebote en una de las dos Raquetas. Si recordamos las entradas anteriores, nuestra Bola y nuestra Raqueta tenían una función llamada puedoMover(), heredada de su superclase ElementoPong, que decía si se podía mover este elemento sin salirse de la pantalla. Sabemos que cuando una bola rebota es porque se ha chocado con algo. Si se ha chocado con algo pero aún se puede mover por la pantalla significa que, por eliminación, se ha chocado con una raqueta. Ya tenemos planteado lo que vamos a hacer, así que hagámoslo.

Primero vamos a añadir un nuevo atributo a la clase BolaMoveThread de la siguiente forma:

private Vibrator v = null;

Este será nuestro vibrador. Ahora necesitamos inicializarle, puesto que está a null y si lo usáramos ahora la aplicación se moriría debido a un NullPointerException (de ahí que se inicialice a null, para debuggearlo más fácil). Para inicializarle necesitamos el Context de la aplicación, de modo que al constructor de BolaMoveThread le vamos a pasar el Context, quedando así:

public BolaMoveThread(Bola bola, Raqueta izda, Raqueta dcha,
        Rect screen, Context context) {
	this.bola = bola;
	this.raquetaIzda = izda;
	this.raquetaDcha = dcha;
	this.screen = screen;
	this.run = false;
	this.speed = 1;
	this.v = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
}

Faltaría cambiar la forma en que se crea este Thread de la siguiente forma (en el método surfaceCreated() del PongGameView:

bolaThread = new BolaMoveThread((Bola)bola, (Raqueta)raquetaIzda,
        (Raqueta)raquetaDcha, new Rect(0,0,getWidth(),getHeight()),
        this.getContext());

Por último sólo queda hacer que vibre, para ello vamos a modificar el código del run() del BolaMoveThread de la siguiente forma:

@Override
public void run() {
	while(run) {
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if(!bola.puedoMover(speed, speed, screen, raquetaIzda.getRectElemento(), raquetaDcha.getRectElemento())) {
			bola.rebota(speed, speed, screen, raquetaIzda.getRectElemento(), raquetaDcha.getRectElemento());
			if(bola.puedoMover(speed, speed, screen))
					v.vibrate(50);
		}
		bola.move(speed, speed);
	}
}

Con esto estamos haciendo una vibración breve (50 milisegundos) cada vez que rebotamos contra una raqueta. Fácil, ¿verdad?

Como información extra tenemos que saber que un objeto de la clase Vibrator también puede vibrar dado un patrón, utilizando la función vibrate(pattern, repeat), siendo pattern un long[] y repeat un int que dice cuántas veces se repite (-1 si no queremos repetir).

Sonido

El siguiente paso consistirá en añadir sonido a los rebotes, el típico sonido metálico. Existen diversas formas de usar sonidos en Android, pero vamos a usar la más simple de todas: el MediaPlayer. Lo primero que tenemos que hacer es buscar un sonido que nos valga. Por lo general se recomienda usar ficheros codificados en formato .ogg (Ogg Vorbis) por ser el más compatible y tener una gran compresión. Yo he elegido el siguiente sonido. Si quieres usar el mismo, haz click derecho sobre el enlace y selecciona Guardar enlace como (y guárdalo con un nombre distinto, en minúsculas, sin espacios y sin tildes).

Ahora crearemos una nueva carpeta dentro de la carpeta res de nuestro proyecto, y la llamaremos raw. Dentro de ella meteremos nuestro fichero de audio (en mi caso pong.ogg). A partir de ahora podremos acceder a ello referenciando a R.raw.pong. Añadimos un nuevo atributo a BolaMoveThread:

private MediaPlayer mp = null;

Y el constructor queda así:

public BolaMoveThread(Bola bola, Raqueta izda,
        Raqueta dcha, Rect screen, Context context) {
	this.bola = bola;
	this.raquetaIzda = izda;
	this.raquetaDcha = dcha;
	this.screen = screen;
	this.run = false;
	this.speed = 1;
	this.v = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
	this.mp = MediaPlayer.create(context, R.raw.pong);
}

La llamada a MediaPlayer.create() crea y prepara el audio local para ser reproducido. Existe una función de los objetos MediaPlayer llamada prepare() que hace lo mismo (pero sólo hay que llamarla en ciertos momentos, ahora explicamos más). Falta hacer que reproduzca el sonido, de modo que vamos a la función run(), que queda:

@Override
public void run() {
	while(run) {
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if(!bola.puedoMover(speed, speed, screen, raquetaIzda.getRectElemento(), raquetaDcha.getRectElemento())) {
			mp.start();
			bola.rebota(speed, speed, screen, raquetaIzda.getRectElemento(), raquetaDcha.getRectElemento());
			if(bola.puedoMover(speed, speed, screen))
					v.vibrate(50);
		}
		bola.move(speed, speed);
	}
}

Podríamos hacer un stop() y prepare() después y antes (respectivamente) de cada start(). Sin embargo eso sería un consumo muy grande de los recursos, y start() ya mantiene preparado el audio para hacer un replay. Sería interesante, sin embargo, hacer una llamada a stop() y prepare() dentro del setRunning() dependiendo de si paramos o iniciamos el thread. Agregar sonidos simples ha resultado ser también muy fácil, ¿no?

Activar y desactivar

Ahora mismo tenemos que la aplicación hace que el dispositivo vibre al rebotar con una pala, y hace que reproduzca un sonido de choque metálico cada vez que rebote con algo (sea pared o raqueta). Recordemos que tenemos un menú en el cual teníamos una parte de Opciones, pero que no tenía nada dentro. Es ahora el momento de crear las opciones del juego, permitiendo activar y desactivar estas dos funcionalidades.

Comenzamos creando un nuevo layout que tendrá el siguiente código:

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="fill_parent">
	<TableRow android:id="@+id/tableRow1" android:layout_width="wrap_content"
		android:layout_height="wrap_content">
		<TextView android:textAppearance="?android:attr/textAppearanceLarge"
			android:layout_width="wrap_content" android:layout_height="wrap_content"
			android:width="250dip" android:paddingLeft="30dip" android:id="@+id/labelSonido"
			android:text="@string/sonidoMenu"></TextView>
		<CheckBox android:layout_width="wrap_content" android:id="@+id/checkBoxSonido"
			android:layout_height="wrap_content" android:checked="true"></CheckBox>
	</TableRow>
	<TableRow android:id="@+id/tableRow2" android:layout_width="wrap_content"
		android:layout_height="wrap_content">
		<TextView android:textAppearance="?android:attr/textAppearanceLarge"
			android:layout_width="wrap_content" android:layout_height="wrap_content"
			android:paddingLeft="30dip" android:id="@+id/labelVibracion"
			android:text="@string/vibracionMenu"></TextView>
		<CheckBox android:layout_width="wrap_content" android:id="@+id/checkBoxVibracion"
			android:layout_height="wrap_content" android:checked="true"></CheckBox>
	</TableRow>
</TableLayout>

Teniendo en cuenta que tenemos que añadir los Strings a strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
	<string name="app_name">Pong</string>
	<string name="menu_play">Jugar</string>
	<string name="menu_options">Opciones</string>
	<string name="menu_exit">Salir</string>
	<string name="sonidoMenu">Sonido</string>
	<string name="vibracionMenu">Vibración</string>
</resources>

Tenemos que añadir la Activity al AndroidManifest.xml igual que hicimos en otra de las entradas. Para ello abrimos el AndroidManifest.xml y añadimos lo siguiente antes de </application>:

<activity android:name=".PongOpcionesActivity" android:screenOrientation="portrait"></activity>

Lo siguiente que necesitamos hacer es añadir el código necesario para crear una actividad nueva que muestre este nuevo layout. Creamos una nueva clase llamada PongOpcionesActivity, cuyo código es el siguiente:

package com.vidasconcurrentes.pongvc;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

public class PongOpcionesActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                            WindowManager.LayoutParams.FLAG_FULLSCREEN);
		setContentView(R.layout.options);
	}
}

Ahora sólo falta hacer el código en la actividad principal para que se ejecute esta. Para ello vamos a la clase PongvCActivity (la principal), y añadimos la función:

private void muestraOpciones() {
	Intent opciones = new Intent(this, PongOpcionesActivity.class);
	this.startActivity(opciones);
}

Ahora en el onCreate() de esta clase, vamos al lugar donde registramos el listener para el botón de Opciones y cambiamos el contenido por lo siguiente:

TextView options = (TextView)findViewById(R.id.options_button);
options.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		muestraOpciones();
	}
});

Ahora sí, si hacemos una ejecución, obtendremos esto tras pulsar en el botón Opciones del menú principal:

Ahora tenemos que programar qué pasa cuando activamos y desactivamos estos checkboxes. He elegido hacer lo siguiente con un patrón Singleton. A grandes rasgos, un patrón Singleton ofrece la garantía de que existe como máximo una instancia de una clase y que es accesible globalmente. Nosotros queremos poder acceder al estado de estas opciones desde el juego, pero modificarlas desde esta actividad. De modo que comenzamos creando nuestra clase PongOpciones dentro de un nuevo paquete llamado com.vidasconcurrentes.pongvc.opciones:

package com.vidasconcurrentes.pongvc.opciones;

public class PongOpciones {

	private static PongOpciones opciones = null;
	private boolean sonido;
	private boolean vibracion;

	private PongOpciones() {
		sonido = true;
		vibracion = true;
	}

	public static synchronized PongOpciones getInstance() {
		if(opciones == null)
			opciones = new PongOpciones();
		return opciones;
	}

	public void toggleSound() {
		sonido = !sonido;
	}

	public void toggleVibration() {
		vibracion = !vibracion;
	}

	public boolean soundEnabled() {
		return sonido;
	}

	public boolean vibrationEnabled() {
		return vibracion;
	}
}

Las ponemos inicializadas a true porque, por defecto, nuestra aplicación va a tener ambas activadas. Ahora añadimos el comportamiento al pulsar sobre los checkboxes. Para ello vamos al onCreate() de la clase PongOpcionesActivity y añadimos lo siguiente al final:

CheckBox sonido = (CheckBox) findViewById(R.id.checkBoxSonido);
sonido.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		PongOpciones.getInstance().toggleSound();
	}
});

CheckBox vibracion = (CheckBox) findViewById(R.id.checkBoxVibracion);
vibracion.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		PongOpciones.getInstance().toggleVibration();
	}
});

sonido.setChecked(PongOpciones.getInstance().soundEnabled());
vibracion.setChecked(PongOpciones.getInstance().vibrationEnabled());

Como vemos, lo último que hacemos es poner el estado de cada CheckBox acorde con el estado de la instancia del patrón Singleton. Cuando pulsamos en un CheckBox, cambiamos el valor de la variable a la que se refiere de true a false y viceversa.

Efectivamente, si ejecutásemos ahora, comprobaríamos que podemos tener ambas activas, desactivadas o una activa y otra no.

Acelerómetro

El acelerómetro del dispositivo va a permitirnos registrar movimientos de éste para saber si se ha cambiado la inclinación con respecto de la posición inicial con la que se comenzó el juego. Existen varias formas de usar el acelerómetro, y por supuesto existen muchísimas e infinitas formas de programar dónde debe ir cada cosa en nuestro proyecto.
Mis decisiones han sido las siguientes:

  • Queremos que se ejecute un Thread que controle la raqueta izquierda con el acelerómetro.
  • Queremos quitar el registro del acelerómetro cuando no lo necesitemos.
  • Queremos reanudar el uso del acelerómetro al volver al juego.

Al igual que hicimos al crear los hilos anteriores (pintado y movimiento de la bola), vamos a crear un hilo nuevo para las raquetas:

package com.vidasconcurrentes.pongvc.juego;

import android.graphics.Rect;

public class RaquetaMoveThread extends Thread {

	private Raqueta raqueta;
	private Rect screen;

	private boolean run;

	public RaquetaMoveThread(Raqueta r, Rect s) {
		raqueta = r;
		screen = s;
	}

	public void setRunning(boolean run) {
		this.run = run;
	}

	public void run() {
		while(run) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// nuestro codigo va aqui
		}
	}
}

En PongGameView creamos el hilo en surfaceCreated() y lo matamos en surfaceDestroyed():

// esto dentro de surfaceCreated()
raquetaThread = new RaquetaMoveThread((Raqueta)raquetaIzda,
                new Rect(0,0,getWidth(),getHeight()));
raquetaThread.setRunning(true);
raquetaThread.start();

// esto seria el surfaceDestroyed():
@Override
public void surfaceDestroyed(SurfaceHolder arg0) {
	boolean retry = true;
	paintThread.setRunning(false);
	bolaThread.setRunning(false);
	raquetaThread.setRunning(false);
	while (retry) {
		try {
			paintThread.join();
			bolaThread.join();
			raquetaThread.join();
			retry = false;
		} catch (InterruptedException e) { }
	}
}

Ahora es el momento de crear nuestro acelerómetro. Para ello vamos a crear una clase envolvente para la funcionalidad que queremos que nos ofrezca. Un dispositivo emulado no puede usar acelerómetro, así que será necesario el uso de un dispositivo físico.
Nosotros vamos a querer que se mueva la raqueta. Esta raqueta pertenece a una actividad que está en modo landscape (apaisado) continuamente. Un dispositivo físico tiene su eje X a lo alto, es decir, es una línea imaginaria que va desde los botones del dispositivo hasta el auricular. Si lo preferís: de abajo a arriba. Su eje Y es el perpendicular a éste que cruza de lado a lado. Como hasta ahora hemos visto a la hora de pintar, con el dispositivo en modo apaisado, el eje X será el ancho y el eje Y será el alto. De modo que si nosotros rotamos el dispositivo hacia delante en modo apaisado, queremos que nuestra pala suba. Si rotamos el dispositivo hacia nosotros en modo apaisado, queremos que nuestra pala baje.
Dicho en otras palabras: si la rotación del eje X es negativa, queremos que vaya arriba; si es positiva, queremos que vaya abajo. Para más información sobre los ejes de coordenadas tridimensionales pulsa aquí.

Por tanto sólo nos interesa la rotación del eje X para nuestra pala, pero podemos acceder a los valores de Y y Z consultando event.values[1] y event.values[2] respectivamente. Creamos una nueva clase AcelerometroPong en el paquete com.vidasconcurrentes.pongvc.juego:

package com.vidasconcurrentes.pongvc.juego;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

public class AcelerometroPong implements SensorEventListener {

	private SensorManager sm = null;
	private int x;

	public AcelerometroPong(Context context) {
		sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
	}

	public void register() {
		sm.registerListener(this, sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                SensorManager.SENSOR_DELAY_GAME);
	}

	public void unregister() {
		sm.unregisterListener(this);
	}

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) { }

	@Override
	public void onSensorChanged(SensorEvent event) {
		if(event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
			x = Math.round(event.values[0] * 100);
		}
	}

	public int getXInclination() {
		return x;
	}
}

Desde la Activity PongJuego vamos a usar esta clase como atributo, y añadiremos dos nuevos métodos de modo que quedará así:

package com.vidasconcurrentes.pongvc;

import android.app.Activity;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

import com.vidasconcurrentes.pongvc.juego.AcelerometroPong;
import com.vidasconcurrentes.pongvc.pintado.PongGameView;

public class PongJuego extends Activity {

	private AcelerometroPong acelerometro;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                            WindowManager.LayoutParams.FLAG_FULLSCREEN);
        acelerometro = new AcelerometroPong(this.getApplicationContext());
		setContentView(new PongGameView(this, acelerometro));
	}

	@Override
	protected void onResume() {
		super.onResume();
		acelerometro.register();
	}

	@Override
	protected void onStop() {
		super.onStop();
		acelerometro.unregister();
	}
}

Es importante no usar el sensor si no es necesario, de ahí onResume() y onStop().
Nuestro PongGameView ahora recibe también el acelerómetro, así que tendremos que modificar el constructor para que así lo reciba y añadir un atributo nuevo.

private Integer xInit = null;
private AcelerometroPong acelerometro;

public PongGameView(Context context, AcelerometroPong acelerometro) {
	super(context);
	getHolder().addCallback(this);

	this.acelerometro = acelerometro;
}

Ahora, queremos que sea el Thread que controla el movimiento de la raqueta el encargado de consultar el acelerómetro, así que igualmente cambiamos el constructor de éste y lo añadimos como atributo a RaquetaMoveThread:

private AcelerometroPong acelerometro;

public RaquetaMoveThread(Raqueta r, Rect s, AcelerometroPong a) {
	raqueta = r;
	screen = s;
	this.acelerometro = a;
}

Además, añadimos esta nueva variable xInit de tipo Integer (para poder inicializarla a null). ¿Y por qué queremos inicializarla a null? Es tan sólo una pequeña treta que se me ha ocurrido para poder realizar una calibración en cada ejecución. Si es null es que aún no hemos calibrado la posición en la que está el dispositivo al arrancar el Thread, si no es null es que ya está calibrado.

Ahora modificaremos el run() de este Thread, de la siguiente forma:

public void run() {
	if(xInit == null)
		xInit = acelerometro.getXInclination();

	while(run) {
		try {
			if(Math.abs(xInit - acelerometro.getXInclination()) < 200)
				Thread.sleep(5);
			else
				Thread.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if(xInit < acelerometro.getXInclination() - UMBRAL ||
		   xInit < acelerometro.getXInclination() + UMBRAL)
			if(raqueta.puedoMover(0, 1, screen))
					raqueta.move(0, 1);
		if(xInit > acelerometro.getXInclination() - UMBRAL ||
		   xInit > acelerometro.getXInclination() + UMBRAL)
			if(raqueta.puedoMover(0, -1, screen))
				raqueta.move(0, -1);
	}
}

Lo primero que hacemos es inicializar la posición de calibrado, si es null. Lo siguiente que hacemos es hacer que el movimiento de la raqueta sea más o menos rápido dependiendo de si hemos inclinado mucho el dispositivo o no. En nuestro caso, si hay una diferencia absoluta (positiva o negativa) de menos de 200 unidades, entonces nos vamos a mover a una velocidad normal. Si esto es mayor significa que hemos inclinado el dispositivo mucho más, por lo que deseamos que se mueva más rápido. La variable UMBRAL no es más que una variable final de la clase que en este caso tiene el valor de 20. Este UMBRAL nos sirve para no mover la raqueta si el movimiento es demasiado pequeño (quizá por errores de medida o de redondeo). Además añadimos la comprobación de si puedoMover() para evitar salirse de la pantalla.

Con esto ya tenemos la raqueta izquierda funcionando para moverse con el acelerómetro, pero a la vez funciona con el dedo y esto no es lo deseable.

Activar / desactivar acelerómetro

Igual que hicimos con el sonido y la vibración, ahora queremos poder cambiar acelerómetro por táctil y viceversa. Para ello vamos a nuestra clase PongOpciones y añadimos un nuevo atributo, de modo que queda:

private static PongOpciones opciones = null;
private boolean sonido;
private boolean vibracion;
private boolean acelerometro;

private PongOpciones() {
	sonido = true;
	vibracion = true;
	acelerometro = false;
}

Añadimos también los siguientes dos métodos, para cambiar el valor y consultarlo:

public void toggleAcelerometro() {
	acelerometro = !acelerometro;
}

public boolean accelerometerEnabled() {
	return acelerometro;
}

Ahora tenemos que llamar a estos métodos desde la actividad de las opciones, la cual necesita ser cambiada para añadir una nueva fila. Por tanto, antes del </TableLayout> de options.xml, añadimos:

<TableRow android:id="@+id/tableRow3" android:layout_width="wrap_content" android:layout_height="wrap_content">
	<TextView android:textAppearance="?android:attr/textAppearanceLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/labelAccel" android:text="@string/accel" android:layout_marginLeft="30dip"></TextView>
	<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/checkBoxAccel"></CheckBox>
</TableRow>

Hay que añadir también el String al fichero strings.xml. De esta forma veríamos la siguiente imagen al ejecutar:

En el onCreate() de PongOpcionesActivity, añadimos:

CheckBox acelerometro = (CheckBox) findViewById(R.id.checkBoxAccel);
acelerometro.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v) {
		PongOpciones.getInstance().toggleAcelerometro();
	}
});

acelerometro.setChecked(PongOpciones.getInstance().accelerometerEnabled());

De esta forma cambiamos la variable de activado a desactivado y viceversa.

Es el momento ahora de hacer que la aplicación use una u otra. Todo el código se va a poner por tanto en PongGameView. El Thread de la raqueta sólo se ejecuta cuando está activado el acelerómetro, por tanto modificamos el código de surfaceCreated() y cambiamos:

if(PongOpciones.getInstance().accelerometerEnabled()) {
	raquetaThread = new RaquetaMoveThread((Raqueta)raquetaIzda,
       new Rect(0,0,getWidth(),getHeight()), acelerometro);
	raquetaThread.setRunning(true);
	raquetaThread.start();
}

Además, si este Thread no se ha iniciado, no se puede matar. Así que en surfaceDestroyed():

@Override
public void surfaceDestroyed(SurfaceHolder arg0) {
	boolean retry = true;
	paintThread.setRunning(false);
	bolaThread.setRunning(false);
	if(PongOpciones.getInstance().accelerometerEnabled())
		raquetaThread.setRunning(false);
	while (retry) {
		try {
			paintThread.join();
			bolaThread.join();
			if(PongOpciones.getInstance().accelerometerEnabled())
				raquetaThread.join();
			retry = false;
		} catch (InterruptedException e) { }
	}
}

Además, si activamos el acelerómetro no queremos poder mover la raqueta con el dedo. De modo que en el onTouchEvent() vamos a envolverlo todo con un:

if(!PongOpciones.getInstance().accelerometerEnabled() {
	// aqui todo el codigo anterior, incluyendo el switch
}
return true;

Hecho esto podemos ejecutar la aplicación y trastear con ella activando y desactivando cosas, reiniciando el juego… La idea es iniciar el juego, pulsar la tecla de “actividad anterior” que nos lleva al menú, elegir Opciones y cambiarlas, dar a “actividad anterior” y luego a Jugar.

Aquí llega el final de la entrada de hoy. En ella hemos visto cómo usar distintos sensores del sistema como la vibración o el acelerómetro, además de las herramientas que nos ofrece para reproducir sonidos locales. En la siguiente entrada acabaremos la aplicación añadiendo las últimas mejoras como un pequeño umbral para el juego táctil, el marcador de juego y la posibilidad de que se cuele la bola y por último una pequeña Inteligencia Artificial para la raqueta derecha.

Como siempre, podéis descargar el proyecto de Eclipse desde aquí (MD5: 1e3eeda28b041716b8526aa59f1aa312) e importarlo a vuestro Eclipse.

Muchas gracias por la lectura y ¡hasta la próxima semana!


Viewing all articles
Browse latest Browse all 11

Trending Articles