Bienvenidos sean a este post, hoy al igual que cuando trabajamos con Pong crearemos nuestro engine para poder manejar nuestro juego.
Tal como hicimos antes tendremos una actividad principal, BalaceraActivity, la cual iniciara nuestro juego en si y tambien definiremos nuestro «Game Engine» el cual se encargara de manejar tanto los elementos del juego (Pepe y Bala), tambien la interaccion del jugador y todo lo relacionado con la actualizacion, tiempo y grabacion de los datos, comencemos primero con el codigo de BalaceraActivity:
BalaceraActivity.java
package org.example.balacera;
import android.app.Activity;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Display;
public class BalaceraActivity extends Activity {
private BalaceraJuego mBJuego;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Display display = getWindowManager()
.getDefaultDisplay();
Point tamano = new Point();
display.getSize(tamano);
mBJuego = new BalaceraJuego(this,tamano.x,tamano.y);
setContentView(mBJuego);
}
@Override
protected void onResume(){
super.onResume();
mBJuego.retomar();
}
@Override
protected void onPause(){
super.onPause();
mBJuego.pausa();
}
}
Este codigo es muy parecido al de Pong porque primero crearemos un objeto de tipo BalaceraJuego al cual llamaremos mBJuego, despues vendra el metodo onCreate, al cual como primera linea nueva es la creacion de un objeto de tipo Display llamado display que nos servira para obtener la pantalla predeterminada, despues crearemos un objeto llamado tamano de tipo Point que lo usaremos para que recorra la pantalla, y atraves de este objeto y el metodo getSize obtendremos el tamaño real de nuestra pantalla, por ultimo definiremos a nuestro objeto mBJuego al cual le enviaremos tres datos:
- El contexto del juego por medio de this
- El valor maximo del eje X
- El valor maximo del eje Y
Para finalmente setear como la vista actual al objeto mBJuego pero por ahora estas dos lineas nos devolveran un error, nuestros siguientes dos metodos son para manipular nuestro thread:
- el metodo onResume sera para retomar nuestro juego cuando lo volvamos al primer plano
- onPause sera el encargado de detener o «pausar» a nuestro thread cuando no este adelante en la pantalla
En ambos casos tendremos errores porque por ahora estos metodos no existen pero esto lo solucionaremos a continuacion, nuestro siguiente paso sera la modificacion de nuestra clase BalaceraJuego, para ello primero declaramos las variables que necesitaremos:
public class BalaceraJuego extends SurfaceView implements Runnable {
boolean mDepurando = true;
private Thread mJuegoTrread=null;
private volatile boolean mJugando;
private boolean mPausado;
private SurfaceHolder mNuestroHolder;
private Canvas mCanvas;
private Paint mPincel;
private long mFPS;
private final int MILES_EN_SEGUNDOS = 1000;
private int mScreenX;
private int mScreenY;
private int mFontTamano;
private int mFontMargen;
private SoundPool mSP;
private int mBeepID = -1;
private int mTeleportID = -1;
}
Lo primera modificacion es hacerla heredera de SurfaceView para poder manipular nuestra pantalla y a su vez poder usarlo como vista, principalmente para poder asignarlo al setContentView de BalaceraActivity, tambien implementamos una interfaz pero por el momento quedara marcada como error pero lo solucionaremos mas adelante, despues tendremos este bloque de variables:
private Thread mJuegoTrread=null;
private volatile boolean mJugando;
private boolean mPausado;
La primera sera para crear nuestro objeto de tipo Thread para poder manipular al thread del juego, la segunda (mJugando) al declararla como volatile nos permite manejarla tanto por dentro como por fuera del thread y esta sera para indicar si estamos jugando o no, la ultima sera para indicar si lo pausamos o no, nuestro siguiente bloque de variables sera este:
private SurfaceHolder mNuestroHolder;
private Canvas mCanvas;
private Paint mPincel;
Estos seran los objetos encargados de dibujar a todos los elementos, el primero sera para nuestro contenedor o lo mismo que decir el lugar donde dibujaremos todo, luego tendremos el objeto Canvas encargado de dibujarlos y por ultimo el pincel que utiliza el Canvas, despues tendremos el siguiente bloque de variables:
private long mFPS;
private final int MILES_EN_SEGUNDOS = 1000;
En este caso tendremos una variable para almacenar los frames-per-second (fps) y la siguiente sera la constante que usaremos para indicar el valor de un segundo, este es nuestro siguiente bloque:
private int mScreenX;
private int mScreenY;
Estas dos variables las usaremos para almacenar el tamaño de la pantalla, primero el eje X y el segundo para el eje Y, veamos el siguiente bloque:
private int mFontTamano;
private int mFontMargen;
Estas son las que usaremos para definir primero el tamaño de la fuente y el segundo para establecer el margen de nuestro texto, por ultimo nos queda este bloque de variables:
private SoundPool mSP;
private int mBeepID = -1;
private int mTeleportID = -1;
Este bloque sera la encargada de manejar el sonido en el juego, el primero es el objeto de tipo SoundPool al cual llamaremos mSP, despues tendremos los dos identificadores para nuestro sonido, el primero sera para el «beep» de la bala, el segundo sera para cuando se teletransporta nuestro personaje, hasta aca la declaracion de las variables/objetos, el siguiente paso sera agregar nuestro constructor pero antes debemos hacer un par de modificaciones, para ello debemos descargar el siguiente archivo:
Nota: Hasta ahora tenemos algunos errores pero no se preocupen porque en este post los iremos corrigiendo.
Una vez descargado estos archivos deben extraerlos en alguna carpeta de la pc, ya seguiremos con esto pero primero debemos crear un nuevo recurso en el proyecto, primero vayan a la carpeta res, presionen click con el boton derecho y seleccionen New -> Android Resource Directory, nos aparecera una nueva ventana y en este caso deben completar los siguientes datos:
- Directory name: raw
- Resource type: raw
- Source set: main
El resto quedara como aparece y no se debe agregar nada mas, presionen Ok para generar el nuevo recurso, nuestro siguiente paso sera ir a donde descomprimimos estos archivos, los seleccionan y presionen Ctrl+C para copiarlos, volvemos a nuestro editor y vamos a la carpeta raw y presionamos Ctrl+V para pegarlos, nos aparecera una nueva ventana, lo dejan como aparece y presionen Ok para copiarlos, si todo sale bien nos quedara de la siguiente forma

Con esto establecido nuestro siguiente paso sera la creacion del constructor de nuestra clase, veamos como es su codigo:
public BalaceraJuego(Context contexto, int x, int y){
super(contexto);
mScreenX = x;
mScreenY = y;
mFontTamano = mScreenX / 20;
mFontMargen = mScreenX / 50;
mNuestroHolder = getHolder();
mPincel = new Paint();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
AudioAttributes atributos = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
mSP = new SoundPool.Builder().setMaxStreams(5)
.setAudioAttributes(atributos)
.build();
} else {
mSP = new SoundPool(5, AudioManager.STREAM_MUSIC,0);
}
try{
mBeepID = mSP.load(contexto,R.raw.beep,0);
mTeleportID = mSP.load(contexto,R.raw.teleport,0);
} catch (Exception e){
Log.e("Error","Ha fallado la carga de alguno de los archivos");
}
iniciaJuego();
}
En este caso tenemos la primer linea que sera la encargada de llamar al constructor predeterminado y le enviaremos el contexto de nuestro juego, despues le asignaremos los valores a mScreenX y mScreenY con los valores informados en las variables x e y respectivamente, despues definiremos el tamaño y el margen de nuestras fuentes, por medio de getHolder obtendremos a nuestro contenedor y lo asignaremos a mNuestroHolder, para finalmente crear nuestro pincel por medio de new, despues tendremos un condicional que verificara cual version de Android poseemos si esta es mayor o igual a LOLLIPOP procedera a definir a mSP por medio del metodo Builder donde primero definiremos los atributos del audio y luego usaremos a este valor junto con maxStreams, en caso de que la version sea menor a LOLLIPOP procedera a crearlo por medio del constructor, esto lo hacemos porque esta forma de hacerlo a quedado obsoleta a partir de la version antes mencionada, despues tendremos un bloque try/catch para verificar cuando carguemos los sonidos y ante cualquier eventualidad nos notifica en el log un error, para cargar los sonidos usamos al metodo load donde primero informaremos el contexto que nos pasaron en los parametros del constructor, luego pasamos el archivo almacenado en raw y por ultimo la prioridad, esto lo hacemos para ambos sonidos en lo unico que varia es el nombre del recurso, por ultimo llamaremos al metodo iniciaJuego que por ahora no existe y nos devuelve un error, nuestro siguiente paso sera agregar los metodos que necesitamos para nuestra clase, veamos el siguiente bloque de codigo:
public void iniciaJuego(){
}
private void generaBalas(){
}
@Override
public void run(){
while(mJugando){
long frameInicioTiempo=System.currentTimeMillis();
if (!mPausado){
actualizar();
detectarColisiones();
}
dibujar();
long frameEsteTiempo=System.currentTimeMillis()
- frameInicioTiempo;
if (frameEsteTiempo>=1)
mFPS = MILES_EN_SEGUNDOS/frameEsteTiempo;
}
}
private void actualizar(){
}
private void detectarColisiones(){
}
Por ahora simplemente agregamos cuatro funciones para que no genere errores y en los proximos posts los iremos definiendo, vamos a comentar la unica definida como es run, esta funcion la debemos definir principalmente para poder implementar a la interfaz Runnable que implementamos al comienzo, en este caso tendremos un while que correra siempre que mJugando sea verdadero, dentro del bucle primero crearemos una variable llamada frameInicioTiempo al cual le asignaremos el valor del reloj actual, despues tendremos un condicional donde verifica que mPausado sea distinto de verdadero (por medio del signo de negacion) y en caso de ser verdadera la situacion llamara a actualizar y detectarColisiones, para luego llamar a dibujar, este sigue con error porque no existe, despues tenemos una variable llamada frameEsteTiempo que sera la diferencia del tiempo actual del reloj y el valor anteriormente almacenado (frameInicioTiempo) para despues verificarlo y si este es mayor o igual a uno establece el valor para mFPS por medio de la division de la constante MILES_EN_SEGUNDOS y el valor de frameEsteTiempo, nuestra siguiente modificacion sera agregar dos metodos mas como son dibujar y onTouchEvent:
private void dibujar(){
if (mNuestroHolder.getSurface().isValid()){
mCanvas = mNuestroHolder.lockCanvas();
mCanvas.drawColor(Color.argb(255,243,111,36));
mPincel.setColor(Color.argb(255,255,255,255));
if (mDepurando)
imprimirDepuracion();
mNuestroHolder.unlockCanvasAndPost(mCanvas);
}
}
@Override
public boolean onTouchEvent(MotionEvent evento){
return true;
}
En el primer metodo, dibujar, tendremos un condicional donde verifica si la superficie de nuestro contenedor es valida, en caso de ser cierto procede a ejecutar su bloque, la primera linea bloquea al contenedor para nuestro Canvas, despues setearemos el valor de nuestro color para el Canvas, luego seteamos el valor de nuestro pincel para dibujar en el Canvas, despues tenemos un condicional donde verifica si mDepurando tiene el valor de true, en caso de ser cierto llama a imprimirDepuracion, por ultimo desbloqueamos al contenedor y mostramos el Canvas en el mismo, por ultimo tenemos al metodo onTouchEvent que unicanente devuelve el valor true, para ir finalizando vamos a definir nuestros ultimos tres metodos:
public void pausa(){
mJugando = false;
try{
mJuegoTread.join();
} catch (InterruptedException e){
Log.e("Error","Joining el thread");
}
}
public void retomar(){
mJugando = true;
mJuegoTread = new Thread(this);
mJuegoTread.start();
}
private void imprimirDepuracion(){
int depuraTamano = 35;
int depuraInicio = 150;
mPincel.setTextSize(depuraTam);
mCanvas.drawText("FPS: " + mFPS,
10,depuraInicio + depuraTamano, mPincel);
}
El primer metodo es para pausar a nuestro thread, setea a mJugando como false para indicar que no estamos jugando, luego por medio de un try/catch verificamos que el thread se una al thread principal y se ponga en pausa de lo contrario notificara un error, el siguiente metodo es para retomar a nuestro thread del juego, vuelve a setear a mJugando como true, genera un nuevo thread y lo comienza, el ultimo metodo sera para mostrar la «depuracion», en este caso creamos dos variables, la primera es para el tamaño de la fuente y la segunda es para donde va a estar ubicada, la siguiente linea setea el tamaño del texto del Canvas, y la ultima la usamos para mostrar el valor de mFPS en pantalla, solo nos falta una ultima modificacion, vamos a modificar el inicio de nuestra clase:
public class BalaceraJuego extends SurfaceView implements Runnable {
De la siguiente forma:
class BalaceraJuego extends SurfaceView implements Runnable {
Con esto estableceremos a nuestra clase como privada, antes de probar nuestro juego veamos como quedo el codigo final, por ahora, de nuestra clase BalaceraJuego:
BalaceraJuego.java
package org.example.balacera;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
class BalaceraJuego extends SurfaceView implements Runnable {
boolean mDepurando = true;
private Thread mJuegoTread=null;
private volatile boolean mJugando;
private boolean mPausado;
private SurfaceHolder mNuestroHolder;
private Canvas mCanvas;
private Paint mPincel;
private long mFPS;
private final int MILES_EN_SEGUNDOS = 1000;
private int mScreenX;
private int mScreenY;
private int mFontTamano;
private int mFontMargen;
private SoundPool mSP;
private int mBeepID = -1;
private int mTeleportID = -1;
public BalaceraJuego(Context contexto, int x, int y){
super(contexto);
mScreenX = x;
mScreenY = y;
mFontTamano = mScreenX / 20;
mFontMargen = mScreenX / 50;
mNuestroHolder = getHolder();
mPincel = new Paint();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
AudioAttributes atributos = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
mSP = new SoundPool.Builder().setMaxStreams(5)
.setAudioAttributes(atributos)
.build();
} else {
mSP = new SoundPool(5, AudioManager.STREAM_MUSIC,0);
}
try{
mBeepID = mSP.load(contexto,R.raw.beep,0);
mTeleportID = mSP.load(contexto,R.raw.teleport,0);
} catch (Exception e){
Log.e("Error","Ha fallado la carga de alguno de los archivos");
}
iniciaJuego();
}
public void iniciaJuego(){
}
private void generaBalas(){
}
@Override
public void run(){
while(mJugando){
long frameInicioTiempo=System.currentTimeMillis();
if (!mPausado){
actualizar();
detectarColisiones();
}
dibujar();
long frameEsteTiempo=System.currentTimeMillis()
- frameInicioTiempo;
if (frameEsteTiempo>=1)
mFPS = MILES_EN_SEGUNDOS/frameEsteTiempo;
}
}
private void actualizar(){
}
private void detectarColisiones(){
}
private void dibujar(){
if (mNuestroHolder.getSurface().isValid()){
mCanvas = mNuestroHolder.lockCanvas();
mCanvas.drawColor(Color.argb(255,243,111,36));
mPincel.setColor(Color.argb(255,255,255,255));
if (mDepurando)
imprimirDepuracion();
mNuestroHolder.unlockCanvasAndPost(mCanvas);
}
}
@Override
public boolean onTouchEvent(MotionEvent evento){
return true;
}
public void pausa(){
mJugando = false;
try{
mJuegoTread.join();
} catch (InterruptedException e){
Log.e("Error","Joining el thread");
}
}
public void retomar(){
mJugando = true;
mJuegoTread = new Thread(this);
mJuegoTread.start();
}
private void imprimirDepuracion(){
int depuraTamano = 35;
int depuraInicio = 150;
mPincel.setTextSize(depuraTamano);
mCanvas.drawText("FPS: " + mFPS,
10,depuraInicio + depuraTamano, mPincel);
}
}
Nota: A partir de la version 4.0, Android Studio no permite realizar modificaciones al momento de crear las clases y estas debemos hacerlas nosotros despues de generadas.
Con todo esto podemos hacer la primera prueba y se vera de la siguiente manera
En este caso solamente veremos como varia mFPS, si lograron lo mismo Felicitaciones!!! Vamos por el buen camino.
En resumen, hoy hemos completado de gran forma nuestro game engine o lo mismo que decir BalaceraJuego, hemos dejado operativo el juego, hemos implementado tanto a SurfaceView como a Runnable, tambien hemos logrado los metodos para manejar cuando nuestro juego deja de estar activo y volvemos al mismo, tambien hemos hecho la base mas importante del mismo, espero les haya gustado sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.
Tengo un Patreon donde podes acceder de manera exclusiva a material para este blog antes de ser publicado, sigue los pasos del link para saber como.

Tambien podes donar
Es para mantenimiento del sitio, gracias!
$1.00