Anuncios

Bienvenidos sean a este post, en el post anterior vimos como trabajar con SoundPool para manipular sonidos, en el dia de hoy veremos como agregar sonidos a nuestro segundo proyecto por medio de esta clase.

Anuncios

Antes de comenzar les recomiendo descargar el siguiente archivo donde contiene los distintos sonidos que usaremos:

Anuncios
Anuncios

Una vez descargado extraigan los archivos dentro del mismo y recuerden donde los dejaron porque mas adelante los necesitaremos, para continuar debemos volver a abrir nuestro proyecto Pong y procederemos a crear nuestra carpeta de assets, para ello volveremos a hacer lo mismo que vimos en el post anterior sobre nuestra raiz haremos click con el boton derecho y seleccionaremos New -> Folder -> Assets Folder, nos aparecera un nuevo cuadro lo dejan como aparece y presionen Finish, una vez creada nuestra carpeta debemos volver a la carpeta donde extrajimos los archivos anteriormente descargados, seleccionaremos los cuatro archivos y presionaremos Ctrl+C para copiarlas, luego vamos a la carpeta assets creada recientemente y presionamos Ctrl+V, nos aparecera un nuevo cuadro y presionen Ok para comenzar la copia de los archivos, una vez terminado deberan aparecer los cuatro archivos dentro de la carpeta.

Con nuestros recursos agregados procederemos a generar nuestro codigo encargado de manipular los sonidos en el juego, para ello vamos a modificar la clase PongJuego, primero vamos a agregar los objetos y variables encargadas de manejarlos junto a las demas declaraciones de nuestras variables:

    private SoundPool mSP;
    private int mBeepID = -1;
    private int mBoopID = -1;
    private int mBopID = -1;
    private int mMissID = -1;
Anuncios
Anuncios

En este caso volvemos a repetir lo hecho en el post anterior, primero creamos el objeto de tipo SoundPool y lo llamaremos mSP, luego declararemos cuatro variables para almacenar los id de cada uno de los sonidos que tenemos en assets, nuestra siguiente modificacion sera en el constructor de la clase donde agregaremos el siguiente bloque y este debe estar antes del llamado a iniciaNuevoJuego:

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{
    AssetManager assetManager = contexto.getAssets();
    AssetFileDescriptor descriptor;

    descriptor = assetManager.openFd("beep.wav");
    mBeepID = mSP.load(descriptor,0);

    descriptor = assetManager.openFd("boop.wav");
    mBoopID = mSP.load(descriptor,0);

    descriptor = assetManager.openFd("bop.wav");
    mBopID = mSP.load(descriptor,0);

    descriptor = assetManager.openFd("miss.wav");
    mMissID = mSP.load(descriptor,0);
} catch (IOException e){
    Log.e("Error","Fallo la carga de alguno de los archivos.");
}
Anuncios
Anuncios

En este caso primero tenemos un condicional donde verificaremos si la version es mayor o igual a LOLLIPOP, en caso de ser cierto usaremos la forma de construir nuestro objeto mSP por medio de Builder, tanto para los atributos como el objeto en si, en cambio si la version es anterior utilizara el constructor tradicional para crear nuestro objeto, luego crearemos un objeto de tipo AssetManager para objetos todos los elementos en la carpeta assets, y crearemos un descriptor de tipo AssetFileDescriptor, este sera usado para obtener el valor de descriptor de nuestros archivos los cuales usaremos para cargar por medio de load los archivos a cada uno de los ids, pero para hacer estas acciones utilizaremos un try/catch para en caso de ocurrir una excepcion poder manejar la excepcion y mostrar el mensaje en el log, nuestro siguiente paso sera modificar el metodo de detectarColisiones de la siguiente forma:

    private void detectarColisiones(){
        if(RectF.intersects(mBate.getmRect(),mPelota.getmRect())){
            mPelota.reboteBate(mBate.getmRect());
            mPelota.incremetarVelocidad();
            mPuntaje++;
            mSP.play(mBeepID,1,1,0,0,1);
        }

        if(mPelota.getmRect().bottom > mScreenY){
            mPelota.invertirYVelocidad();
            mVidas--;
            mSP.play(mMissID,1,1,0,0,1);
            if(mVidas==0){
                mPausado=true;
                iniciaNuevoJuego();
            }
        }

        if (mPelota.getmRect().top < 0){
            mPelota.invertirYVelocidad();
            mSP.play(mBoopID,1,1,0,0,1);
        }

        if (mPelota.getmRect().left < 0){
            mPelota.invertirXVelocidad();
            mSP.play(mBopID,1,1,0,0,1);
        }

        if (mPelota.getmRect().right > mScreenX){
            mPelota.invertirXVelocidad();
            mSP.play(mBopID,1,1,0,0,1);
        }
    }
Anuncios

Lo primero que tendremos es un condicional donde verificaremos por medio de intersects de la clase RectF si se «cruzan» mPelota y mBate, para ello vemos que le enviamos los dos objetos en conjunto con el metodo getmRect. el cual le devuelve el objeto completo, e intersects se encarga de hacer la «magia», suponiendo que sea verdad procedera primero a:

  • Llama al metodo reboteBate de la clase Pelota y envia como argumento el objeto de mBate
  • Llama a incrementarVelocidad para incrementar la velocidad de la pelota
  • Incrementa el valor de mPuntaje
  • Reproduce el sonido mBeepID.
Anuncios

Con esto ya tenemos cubierto como va a ser la conducta de nuestra pelota cuando rebota en el bate, pasemos a hablar sobre el siguiente condicional.

Anuncios

Este condicional sera el encargado de verificar cuando la pelota toca el fondo de la pantalla, es decir que no le pegamos con el bate, en este caso ocurren varias cosas:

  • Se llama a invertirYVelocidad para cambiar la direccion de la pelota
  • Se le resta en uno a mVidas, es decir perdemos una vida
  • Reproducimos el sonido de mMissID
  • Hay un condicional que chequea si mVidas es igual a cero
  • En caso de ser cierto procede a setear a mPausado como true y llama a iniciaNuevoJuego, es decir reinicia la partida y es una especie de Game Over, man!
Anuncios

Con esto cubrimos dos aspectos fundamentales como son el rebote en el bate y cuando no le pegamos y a su vez se nos acaban las vidas, lo siguiente son una serie de condiciones para evitar que la pelota se nos vaya de la pantalla.

Anuncios
Anuncios

El primero verifica que el valor de top de mPelota no sea menor a cero, en caso de ser verdadero procede a llamar al metodo invertirYVelocidad para luego reproducir el sonido con el id mBoopID, el siguiente condicional verifica que el valor ahora de left de pelota no sea menor a cero, en caso de ser verdadero llama a invertirXVelocidad y reproduce el sonido mBopID, y el ultimo condicional se encarga de verificar que el valor de right de mPelota no sea mayor a mScreenX, en caso de ser cierto hace exactamente que en el condicional anterior.

Con esto ya tenemos cubierto como evitar que nuestra pelota se vaya de la pantalla, como rebotar con nuestro bate a la pelota, como nos castiga cuando fallamos y en todos los casos reproduce un sonido con cada evento, antes de probar nuestro juego veamos el codigo final de nuestra clase PongJuego:

PongJuego.java

package org.example.pong;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
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 android.widget.Toast;

import java.io.IOException;

class PongJuego extends SurfaceView implements Runnable {

    private final boolean DEPURANDO = true;

    private SurfaceHolder mNuestroCont;
    private Canvas mCanvas;
    private Paint mPincel;

    private long mFPS;

    private final int MILES_EN_SEGUNDO = 1000;

    private int mScreenX;
    private int mScreenY;

    private int mFontTam;
    private int mFontMargen;

    private Bate mBate;
    private Pelota mPelota;

    private int mPuntaje;
    private int mVidas;

    private Thread mJuegoThread = null;
    private volatile boolean mJugando;
    private boolean mPausado = true;

    private SoundPool mSP;
    private int mBeepID = -1;
    private int mBoopID = -1;
    private int mBopID = -1;
    private int mMissID = -1;

    public PongJuego(Context contexto, int x, int y){
        super(contexto);

        mScreenX = x;
        mScreenY = y;

        mFontTam = mScreenX / 20;
        mFontMargen = mScreenX /75;

        mNuestroCont = getHolder();
        mPincel = new Paint();

        mPelota = new Pelota(mScreenX);
        mBate = new Bate(mScreenX,mScreenY);

        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{
            AssetManager assetManager = contexto.getAssets();
            AssetFileDescriptor descriptor;

            descriptor = assetManager.openFd("beep.wav");
            mBeepID = mSP.load(descriptor,1);

            descriptor = assetManager.openFd("boop.wav");
            mBoopID = mSP.load(descriptor,1);

            descriptor = assetManager.openFd("bop.wav");
            mBopID = mSP.load(descriptor,1);

            descriptor = assetManager.openFd("miss.wav");
            mMissID = mSP.load(descriptor,1);

        } catch (IOException e){
            Log.e("Error","Fallo la carga de alguno de los archivos.");
        }
        iniciaNuevoJuego();
    }

    private void iniciaNuevoJuego(){

        mPuntaje = 0;
        mVidas = 3;
        mPelota.reset(mScreenX,mScreenY);
    }

    private void dibujar(){
        if (mNuestroCont.getSurface().isValid()){
            mCanvas = mNuestroCont.lockCanvas();
            mCanvas.drawColor(Color.argb(255,26,128,182));
            mPincel.setColor(Color.argb(255,255,255,255));
            mCanvas.drawRect(mPelota.getmRect(),mPincel);
            mCanvas.drawRect(mBate.getmRect(),mPincel);
            mPincel.setTextSize(mFontTam);
            mCanvas.drawText("Puntaje: " + mPuntaje
                            + "  Vidas: " + mVidas,
                    mFontMargen,mFontTam,mPincel);
            if (DEPURANDO){
                imprimirDepuracion();
            }
            mNuestroCont.unlockCanvasAndPost(mCanvas);
        }
    }

    private void imprimirDepuracion(){
        int debugTam = mFontTam / 2;
        int debugComienzo = 150;
        mPincel.setTextSize(debugTam);
        mCanvas.drawText("FPS: " + mFPS,
                10,
                debugComienzo + debugTam,
                mPincel);
    }

    @Override
    public void run(){
        while(mJugando){
            long frameInicio = System.currentTimeMillis();
            if (!mPausado){
                actualizar();
                detectarColisiones();
            }
            dibujar();
            long esteFrameTiempo = System.currentTimeMillis() - frameInicio;
            if (esteFrameTiempo > 0){
                mFPS = MILES_EN_SEGUNDO / esteFrameTiempo;
            }
        }
    }

    private void actualizar(){
        mPelota.actualizar(mFPS);
        mBate.actualizar(mFPS);
    }

    private void detectarColisiones(){
        if(RectF.intersects(mBate.getmRect(),mPelota.getmRect())){
            mPelota.reboteBate(mBate.getmRect());
            mPelota.incremetarVelocidad();
            mPuntaje++;
            mSP.play(mBeepID,1,1,0,0,1);
        }

        if(mPelota.getmRect().bottom >= mScreenY){
            mPelota.invertirYVelocidad();
            mVidas--;
            mSP.play(mMissID,1,1,0,0,1);
            if(mVidas==0){
                mPausado=true;
                iniciaNuevoJuego();
            }
        }

        if (mPelota.getmRect().top <= 0){
            mPelota.invertirYVelocidad();
            mSP.play(mBoopID,1,1,0,0,1);
        }

        if (mPelota.getmRect().left <= 0){
            mPelota.invertirXVelocidad();
            mSP.play(mBopID,1,1,0,0,1);
        }

        if (mPelota.getmRect().right >= mScreenX){
            mPelota.invertirXVelocidad();
            mSP.play(mBopID,1,1,0,0,1);
        }
    }

    public void pause(){
        mJugando = false;
        try{
            mJuegoThread.join();
        } catch (InterruptedException e){
            Log.e("Error","uniendo a thread");
        }
    }

    public void resume(){
        mJugando = true;
        mJuegoThread = new Thread(this);
        mJuegoThread.start();
    }

    @Override
    public boolean onTouchEvent(MotionEvent motionEvent){
        switch (motionEvent.getAction()
                & MotionEvent.ACTION_MASK){
            case MotionEvent.ACTION_DOWN:
                mPausado = false;
                if(motionEvent.getX() > (mScreenX/2)){
                    mBate.setEstadoMovimiento(mBate.DERECHA);
                } else {
                    mBate.setEstadoMovimiento(mBate.IZQUIERDA);
                }
                break;
            case MotionEvent.ACTION_UP:
                mBate.setEstadoMovimiento(mBate.DETENIDO);
                break;
        }
        return true;
    }

}
Anuncios

Con esto podemos proceder a probar nuestro juego, vamos a compilarlo y probarlo tal como se ve en el siguiente video

Anuncios

Aca podemos ver como funciona nuestra aplicacion, podemos ver tambien un pequeño fallo donde si la pelota rebota y pega en el borde del bate nos queda trabado hasta terminar las vidas restantes, tambien hemos visto como se incrementa la velocidad pero les voy a comentar un solo detalle, cuando lo probe en un celular con un API menor a 21 no me anduvo el sonido pero esto tiene solucion y veremos como hacerlo.

Anuncios

En realidad, el inconveniente en mi caso fue debido a que por alguna extraña razon si los sonidos estan en assets no los detecta, para solucionar esto debemos crear una carpeta de tipo raw en los recursos, para ello debemos hacer click con el boton derecho sobre la carpeta res y seleccionen New -> Android Resource Directory, nos aparecera una nueva ventana y completaran estos campos:

  • Directory Name: raw
  • Resource Type: raw
  • Source set: main
Anuncios

Cuando estos tres datos esten seteados presionen Ok para proceder a crear el nuevo directorio, una vez creado volvemos a hacer lo mismo que antes vamos al lugar donde descomprimimos los archivos de sonido, los seleccionamos apretamos Ctrl+C, vamos al nuevo directorio y presionamos Ctrl+V para pegarlos, una vez realizada la accion nos aparecera un pequeño cuadro donde presionamos Ok y ya tenemos nuestros recursos disponibles, nuestra unica modificacion va a ser en el bloque try del constructor donde lo cambiaremos de la siguiente forma:

        try{

            AssetManager assetManager = contexto.getAssets();
            AssetFileDescriptor descriptor;

            descriptor = assetManager.openFd("beep.wav");
            mBeepID = mSP.load(contexto,R.raw.beep,1);

            descriptor = assetManager.openFd("boop.wav");
            mBoopID = mSP.load(contexto,R.raw.boop,1);

            descriptor = assetManager.openFd("bop.wav");
            mBopID = mSP.load(contexto,R.raw.bop,1);

            descriptor = assetManager.openFd("miss.wav");
            mMissID = mSP.load(contexto,R.raw.miss,1);

        } catch (IOException e){
            Log.e("Error","Fallo la carga de alguno de los archivos.");
        }

En este caso pueden observar que modificamos las lineas de load donde en lugar de usar a descriptor usamos al constructor donde le enviamos a contexto y la direccion de nuestro recurso:

R.raw.beep

Y por ultimo la prioridad, con este simple cambio hacemos que funcione tanto en las versiones viejas como en las nuevas pero porque no lo hicimos asi en lugar de usar assets? Porque la realidad es que esta forma de trabajar es para las versiones viejas de SoundPool y la forma de trabajar con los assets esta apuntada para la nueva version del SoundPool, por lo tanto podemos tomar la siguiente consideracion:

Si la API de nuestro juego es igual o mayor a 21 es recomendable ubicar los recursos en la carpeta assets, en caso de que la API sea menor a la version antes citada se recomienda usar almacenar los recursos en raw.

El Tinchicus

Recuerden que por el momento la forma de trabajar con raw todavia funciona y es compatible con versiones mas modernas de Android pero esto puede cambiar con el tiempo, veamos el siguiente video donde lo pruebo en un equipo viejo

Anuncios

Con esto ya tenemos nuestro juego completamente funcional, de forma basica y simple pero a lo largo de estos posts, aprendimos el concepto de Game Engine, como delimitar los bordes, aprendimos los conceptos de colisiones y como agregar efectos de sonidos para nuestros juegos, si se animan traten de agregar sonidos para el primer proyecto como es el cazador de submarinos, un ejemplo puede ser la explosion.

Anuncios

En resumen, hoy hemos visto como agregar sonidos a nuestro juego, como implementar una deteccion de colisiones simple y basica, como en base a esa deteccion activamos los sonidos y como afecta al resto de nuestros valores, como son puntaje y vidas, 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

Anuncio publicitario