Anuncios

Bienvenidos sean a este post, en el post haremos el game engine de manera muy similar a como lo hicimos en nuestros ultimos dos juegos.

Anuncios

Para comenzar haremos que nuestra clase sea de tipo privada tambien la haremos heredera de SurfaceView e implementaremos a la interface Runnable, para ello modificaremos el comienzo de la clase:

public class SnakeJuego {
Anuncios

De la siguiente forma:

class SnakeJuego extends SurfaceView implements Runnable {
Anuncios

Con esto lo que logramos que la clase sea similar a una vista (permitiendo implementarla en el SnakeActivity) y la interfaz que implementamos nos servira para manejar el thread del juego, nuestro siguiente paso sera declarar las variables y objetos que usaremos agregando el siguiente bloque despues de la modificacion anterior:

    private Thread mThread = null;
    private long mProxTiempoCuadro;
    private volatile boolean mPausado = true;
    private volatile boolean mJugando = false;
    private SoundPool mSP;
    private int mComer_ID = -1;
    private int mMordida_ID = -1;
    private final int ANCHO_NUM_BLOQUES = 30;
    private int mNumeroBloquesAlto;
    private int mPuntaje;
    private Canvas mCanvas;
    private SurfaceHolder mSurfaceHolder;
    private Paint mPincel;
    private Snake mSnake;
    private Manzana mManzana;
Anuncios
Anuncios

Todas estas variables son muy similares a las vistas en casos anteriores pero tambien tendremos algunas nuevas, la primer linea sera para el objeto que manejara el thread, la segunda es de control pausando entre actualizaciones, las siguientes dos son para saber si el juego esta pausado o no, las siguientes tres seran para manejar nuestros sonidos, las proximas dos seran las que definen nuestra area jugable, la siguiente linea almacena el puntaje, despues tendremos tres lineas que se encargan de manejar el Canvas para mostrar los elementos en pantalla, la penultima linea es para el objeto encargado de manejar a la serpiente (Snake) y la ultima sera para la manzana, con esto tenemos los elementos declarados pasemos a crear el constructor agregando el siguiente bloque:

    public SnakeJuego(Context contexto, Point tamano) {
        super(contexto);

        int tamanoBloque = tamano.x / ANCHO_NUM_BLOQUES;
        mNumeroBloquesAlto = tamano.y / tamanoBloque;

        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()
                    .setAudioAttributes(atributos)
                    .setMaxStreams(5)
                    .build();
        } else {
            mSP = new SoundPool(5, AudioManager.STREAM_MUSIC,0);
        }
        try {
            mComer_ID = mSP.load(contexto,R.raw.apple,0);
            mMordida_ID = mSP.load(contexto,R.raw.bitten,0);
        } catch (Exception e){
            Log.e("Error","Ha ocurrido un error con la carga de los archivos");
        }
        mSurfaceHolder=getHolder();
        mPincel = new Paint();
    }
Anuncios
Anuncios

Primero recibiremos dos atributos, si recuerdan en el post anterior hicimos el constructor para recibir dos valores, uno sera el contexto y el otro el tamaño de la pantalla, la primera linea invoca al constructor de la clase maestra lo cual nos soluciona el error en el constructor, la siguiente linea se encarga de crear una variable que almacenara cuantos pixeles almacena un bloque, una vez obtenido el valor lo usaremos con la siguiente linea para calcular cuantos bloques con el mismo tamaño encajan en la pantalla, el siguiente es el condicional donde verifica la version del Android y en base al resultado utiliza el Builder para definir a mSP o de lo contrario lo hace a la forma antigua, si quieren saber mas les recomiendo ver los posts de los proyectos anteriores, despues usaremos un try/catch para cargar los sonidos en mSP, en esta ocasion seguimos usando el metodo con el directorio raw porque nos ofrece mas compatibilidad con la mayor cantidad de sistemas, por ultimo iniciaremos a mSurfaceHolder con el metodo getHolder y por ultimo definiremos a mPincel, con esto terminamos nuestro constructor, nuestro siguiente paso sera redefinir a run y para ello vamos a agregar el siguiente bloque:

    @Override
    public void run(){
        while(mJugando){
            if(!mPausado){
                if(requiereActualizar()){
                    actualizar();
                }
            }
            dibujar();
        }
    }
Anuncios
Anuncios

Este metodo nos permitira solucionar la implementacion de la interfaz Runnable, dentro usaremos un bucle while que verifica si mJugando es igual a true, es decir que estamos jugando, dentro tendra un condicional donde verifica que mPausado sea distinto de true, y a su vez tendremos otro condicional donde verifica si requiereActualizar devuelve una condicion verdadera y en caso de ser cierta llama a actualizar, despues explicaremos porque todo esto, por ultimo despues de estos condicionales llamamos a dibujar, hasta aca tenemos muchos errores pero no se preocupen porque esto los solucionaremos a continuacion, lo siguiente sera el metodo requiereActualizar y para ello agregaremos el siguiente bloque:

    public boolean requiereActualizar(){
        final long BLANCO_FPS = 10;
        final long MILES_POR_SEGUNDO = 1000;
        if(mProxTiempoCuadro<=System.currentTimeMillis()){
            mProxTiempoCuadro=System.currentTimeMillis()
                    + MILES_POR_SEGUNDO / BLANCO_FPS;
            return true;
        }
        return false;
    }
Anuncios
Anuncios

En este caso la primera linea setea una constante la cual indicara que se correra a diez fps (frames-per-seconds), la segunda sera para indicar que un segundo equivale a 1000 milisegundos, el condicional verifica si debemos actualizarlo, para ello verifica si mProxTiempoCuadro es menor o igual que el tiempo actual, en caso de ser cierto habra pasado un decimo de un segundo y mediante la formula en el bloque establecemos cuando la proxima actualizacion se disparara, por ultimo el return true se encarga de indicar que se ejecuten la actualizacion y el metodo dibujar, el ultimo return false es para el caso de no cumplirse la condicion, recuerden que al ser de tipo booleano debe devolver un valor de este estilo, nuestra siguiente modificacion sera agregar el metodo actualizar que por ahora sera simplemente en blanco como se ve a continuacion:

    public void actualizar(){
        
    }
Anuncios

En este caso lo agregamos para solucionar el inconveniente en el metodo run pero mas adelante ingresaremos las acciones que realizara, nuestra siguiente implementacion sera agregar el metodo encargado de dibujar y para ello agregaremos el siguiente bloque:

    public void dibujar(){
        if(mSurfaceHolder.getSurface().isValid()){
            mCanvas = mSurfaceHolder.lockCanvas();
            mCanvas.drawColor(Color.argb(255,26,128,182));
            mPincel.setColor(Color.argb(255,255,255,255));
            mPincel.setTextSize(120);
            mCanvas.drawText("" + mPuntaje,20,120,mPincel);
            if (mPausado){
                mPincel.setColor(Color.argb(255,255,255,255));
                mPincel.setTextSize(100);
                mCanvas.drawText("Toca para Jugar!",20,300,mPincel);
            }
            mSurfaceHolder.unlockCanvasAndPost(mCanvas);
        }
Anuncios
Anuncios

Este metodo nos permitira dibujar en pantalla pero ahora hara algo muy basico procedamos a hablar sobre esto, primero chequea si contenedor es valido, si es verdadero primero bloqueara al contenedor para poder trabajar con el Canvas, despues estableceremos el color del Canvas, para despues setear el color de nuestro pincel, mPincel, luego el tamaño de texto del pincel para finalmente “dibujar” un texto que representara a nuestro puntaje, luego tendremos un condicional donde verificamos si esta pausado y en caso de ser verdadero seteara los valores del pincel y mostrara un mensaje en pantalla, este sera el que nos recibira cada vez que perdamos o iniciemos el juego, por ultimo saldremos de este ultimo condicional para ejecutar un metodo que desbloqueara el Canvas y le asignara lo generado en nuestro Canvas, para nuestra siguiente modificacion agregaremos el metodo encargado de monitorear el toque de pantalla a traves del siguiente bloque:

    @Override
    public boolean onTouchEvent(MotionEvent evento){
        switch (evento.getAction() & MotionEvent.ACTION_MASK){
            case MotionEvent.ACTION_UP:
                if (mPausado){
                    mPausado = false;
                    nuevoJuego();
                    return true;
                }
                break;
            default:
                break;
        }
        return true;
    }
Anuncios
Anuncios

En este caso usaremos un switch donde por medio de getAction y la constante ACTION_MASK podemos registrar las acciones de tocar la pantalla, en este caso tendremos un case para cuando sacamos el dedo de la pantalla, en este caso tenemos un condicional donde verifica si mPausado es verdadero, en caso de ser cierto setea a mPausado como false para indicar que el juego no esta mas pausado, llama a nuevoJuego para iniciar un juego nuevo y por ultimo devuelve un valor true para ejecutar lo anterior correctamente, fuera de este case tenemos un default pero con un break solamente y por ultimo retornamos true en caso de no haber hecho nada, para la siguiente modificacion agregaremos el metodo para crear una nueva sesion en nuestro juego:

    public void nuevoJuego(){
        mPuntaje = 0;
        mProxTiempoCuadro = System.currentTimeMillis();
    }
Anuncios

Este metodo sera bien simple, primero resetea el valor del puntaje, para luego establecer el valor de mProxTiempoCuadro con el tiempo actual de nuestra CPU, nuestra ultima modificacion sera implementar los dos metodos encargados de manejar el thread del juego:

    public void pausar(){
        mJugando=false;
        try{
            mThread.join();
        } catch (InterruptedException e){
            Log.e("Error", "no se pudo detener el thread.");
        }
    }

    public void retomar(){
        mJugando=true;
        mThread = new Thread(this);
        mThread.start();
    }
Anuncios

El primer metodo sera para pausar o detener el thread, para ello seteamos a mJugando como false para indicar que no estamos jugando y luego por medio de join pausamos a nuestro thread, observen que usamos un try/catch para interceptarlo en caso de cualquier error, y por ultimo tenemos el metodo retomar que se encargar de setear mJugando como true, luego creara un nuevo trhead donde asignara al juego y por ultimo lo inicia, estos dos metodos son para manejar cuando el juego esta en la pantalla o no, con esto ya tenemos parcialmente creada nuestra clase veamos como quedo, por ahora, su codigo final:

SnakeJuego.java

package org.example.snake;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
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;

import java.io.IOException;

class SnakeJuego extends SurfaceView implements Runnable {

    private Thread mThread = null;
    private long mProxTiempoCuadro;
    private volatile boolean mPausado = true;
    private volatile boolean mJugando = false;
    private SoundPool mSP;
    private int mComer_ID = -1;
    private int mMordida_ID = -1;
    private final int ANCHO_NUM_BLOQUES = 40;
    private int mNumeroBloquesAlto;
    private int mPuntaje;
    private Canvas mCanvas;
    private SurfaceHolder mSurfaceHolder;
    private Paint mPincel;
    private Snake mSnake;
    private Manzana mManzana;

    public SnakeJuego(Context contexto, Point tamano) {
        super(contexto);

        int tamanoBloque = tamano.x / ANCHO_NUM_BLOQUES;
        mNumeroBloquesAlto = tamano.y / tamanoBloque;

        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()
                    .setAudioAttributes(atributos)
                    .setMaxStreams(5)
                    .build();
        } else {
            mSP = new SoundPool(5, AudioManager.STREAM_MUSIC,0);
        }
        try {
            mComer_ID = mSP.load(contexto,R.raw.apple,0);
            mMordida_ID = mSP.load(contexto,R.raw.bitten,0);
        } catch (Exception e){
            Log.e("Error","Ha ocurrido un error con la carga de los archivos");
        }
        mSurfaceHolder=getHolder();
        mPincel = new Paint();
    }

    @Override
    public void run(){
        while(mJugando){
            if(!mPausado){
                if(requiereActualizar()){
                    actualizar();
                }
            }
            dibujar();
        }
    }

    public boolean requiereActualizar(){
        final long BLANCO_FPS = 10;
        final long MILES_POR_SEGUNDO = 1000;
        if(mProxTiempoCuadro<=System.currentTimeMillis()){
            mProxTiempoCuadro=System.currentTimeMillis()
                    + MILES_POR_SEGUNDO / BLANCO_FPS;
            return true;
        }
        return false;
    }

    public void actualizar(){

    }

    public void dibujar(){
        if(mSurfaceHolder.getSurface().isValid()){
            mCanvas = mSurfaceHolder.lockCanvas();
            mCanvas.drawColor(Color.argb(255,26,128,182));
            mPincel.setColor(Color.argb(255,255,255,255));
            mPincel.setTextSize(120);
            mCanvas.drawText("" + mPuntaje,20,120,mPincel);
            if (mPausado){
                mPincel.setColor(Color.argb(255,255,255,255));
                mPincel.setTextSize(100);
                mCanvas.drawText("Toca para Jugar!",20,300,mPincel);
            }
            mSurfaceHolder.unlockCanvasAndPost(mCanvas);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent evento){
        switch (evento.getAction() & MotionEvent.ACTION_MASK){
            case MotionEvent.ACTION_UP:
                if (mPausado){
                    mPausado = false;
                    nuevoJuego();
                    return true;
                }
                break;
            default:
                break;
        }
        return true;
    }

    public void nuevoJuego(){
        mPuntaje = 0;
        mProxTiempoCuadro = System.currentTimeMillis();
    }

    public void pausar(){
        mJugando=false;
        try{
            mThread.join();
        } catch (InterruptedException e){
            Log.e("Error", "no se pudo detener el thread.");
        }
    }

    public void retomar(){
        mJugando=true;
        mThread = new Thread(this);
        mThread.start();
    }
}
Anuncios

Con todo esto modificado ahora solo nos resta probarlo, con lo cual lo compilaremos y veremos en accion a traves del siguiente video

Anuncios

En resumen, tenemos un gran porcentaje del juego ya realizado, en este caso en particular nos centramos en el Game Engine del juego, hemos agregado todo lo necesario para realizar algunas tareas, tambien configuramos las bases para nuestras proximas modificaciones, 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.

Anuncios

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