Creación de un juego (6): Modulo Juego

Con este modulo terminamos la interface básica para empezar a desarrollar juegos. Es el modulo mas importante ya que se encarga de gestionar todas las clases que hemos visto con anterioridad. Todo lo que tenemos que hacer es encajar todas las piezas teniendo en cuenta varias cosas:
Deberemos realizar la gestión de ventanas (pantallas de juego) creando una Activity y una instancia AndroidFastRenderView a parte de manipular el ciclo de vida en la actividad de forma limpia
Crear y gestionar un WakeLock para que la pantalla no se apague mientras jugamos
Crear instancias para manejar todos los módulos anteriores (Gráficos, Audio, FileIO, ...)
Administrar las pantallas e integrarlas con el ciclo de vida de la actividad


1.1 Interface Game

Esta interface sera la encargada de darnos acceso a todos los módulos que hemos desarrollado anteriormente y sera capaz de una gestión absoluta de las pantallas del juego. Veamos a continuación los componentes de esta interface:

public interface Game {
    
    public Input getInput();

    public FileIO getFileIO();

    public Graphics getGraphics();

    public Audio getAudio();

    public void setScreen(Screen screen);

    public Screen getCurrentScreen();

    public Screen getStartScreen();
}
Los cuatro primeros métodos nos devolverán una instancia de cada una de las interfaces para poder trabajar con sus métodos.

El método setScreen nos permite configurar la pantalla actual del juego, para ello lo indicaremos en su parámetro.

Con el método getCurrentScreen podremos saber que pantalla esta activa en ese momento.

Y con el ultimo método getStartScreen iniciaremos nuestro juego.


1.2 Interface Screen

La ultima pieza de nuestro rompecabezas es la clase abstracta Screen. Esta vez creamos una clase abstracta en vez de una interface porque habrá veces que no necesitemos usar todos sus métodos. Esta clase nos ayudara a presentar los objetos en pantalla, actualizar la pantalla según el delta time, pausar o reanudar el juego. Cada pantalla que creemos en nuestro juego deberá implementar esta interface excepto la primera pantalla o pantalla de inicio de nuestro juego que deberá extender la clase AndroidGame sobreescribiendo el método getStartScreen. Vamos a ver la interface:

public abstract class Screen {
    
    protected final Game game;

    public Screen(Game game) {
        this.game = game;
    }

    public abstract void update(float deltaTime);

    public abstract void present(float deltaTime);

    public abstract void pause();

    public abstract void resume();

    public abstract void dispose();
}
Lo primero que hacemos es crear una instancia de nuestra interface Juego y ya en el constructor almacenamos su parámetro en ella. Con esto conseguimos dos cosas: tener acceso a todos los módulos de la interface juego (Input, FileIO, Graficos, Audio) y a parte poder crear una nueva ventada desde la pantalla actual, llamando al método Game.setScreen.

Por lo tanto cuando implementamos esta interface necesitamos tener acceso a todos los módulos para crear y gestionar esa pantalla de nuestro juego.

Con los métodos update y present, podremos actualizar y presentar los componentes en la pantalla. Se llamara a la instancia juego en cada iteración del bucle principal.

Los métodos 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 método dispose para liberar todos los recursos de la memoria. Se deberá llamar a este método justo después de llamar a Game.setScreen. Y recordar que sera el ultimo momento donde prodremos guardar los datos de esa pantalla.


2.1 Implementar interface Game

Como se puede observar en lo que llevamos de articulo, todo el trabajo que hemos realizado esta dando sus frutos y con pocas lineas de código tenemos acceso a todos los métodos de nuestras interfaces. Para terminar vamos a crear una clase que practicamente terminara de encajar todas las piezas de nuestro puzzle. Se encarga de toda la gestión de nuestras interfaces, vamos a verla:

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.view.Window;
import android.view.WindowManager;

import com.example.slange.interfaces.Audio;
import com.example.slange.interfaces.FileIO;
import com.example.slange.interfaces.Game;
import com.example.slange.interfaces.Graphics;
import com.example.slange.interfaces.Input;
import com.example.slange.interfaces.Screen;

public abstract class AndroidGame extends Activity implements Game {
    
    AndroidFastRenderView renderView;
    Graphics graphics;
    Audio audio;
    Input input;
    FileIO fileIO;
    Screen screen;
    WakeLock wakeLock;
Empezamos la clase extendiendo de Activity e implementando la interface Game.

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 métodos.

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 método es quitar la barra de titulo de la aplicación y ponerla a pantalla completa.

Lo siguiente que hacemos es comprobar que orientación tiene la pantalla, para ello consultamos su orientación con el método getResources().getConfiguration().orientation y si es igual a la orientación landscape, almacenamos true en nuestra variable isLandscape. En caso contrario se almacenara false.

Después tenemos dos miembros (framebufferWidth y frameBufferHeight) que nos servirán para delimitar el ancho y el alto de nuestra superficie de dibujo. Dependiendo del valor isLandScape se almacenara un valor u otro en cada variable.

Sabiendo esos datos creamos nuestra superficie de dibujo frameBuffer. Con una configuración RGB565, que hará que nuestro dibujo se complete un poco mas rápido.

En las dos siguientes variables (scaleX, scaleY) calculamos la escala para los diferentes tamaños de pantalla. En nuestro caso estamos usando una resolución de 480x320 o 320x480. Con lo cual debemos calcular la escala para tamaños de pantalla mas grandes o mas pequeños.

Continuamos iniciando todos nuestros objetos y aplicando sus parámetros si es necesario. Y terminamos estableciendo nuestra superficie de dibujo renderView como la View principal.

Por ultimo almacenamos en nuestro wakeLock una instancia de servicio que se encarga de mantener la pantalla encendida.

    public void onResume() {
        super.onResume();
        wakeLock.acquire();
        screen.resume();
        renderView.resume();
    }
Con el método wakeLock.acquire creamos un bloqueo para que la pantalla se quede encendida.
El método 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();
    }
Con el método wakeLock.release liberamos el bloqueo de iluminación de pantalla, ahora se apagara según la configuración de tiempo que tenga el terminal del usuario.
Primero deberemos pausar la superficie de dibujo (renderView.pause) y después la pantalla de juego (screen.pause) sino podríamos tener problemas de concurrencia ya que trabajan en hilos diferentes, por un lado tenemos el hilo de la interface y por el otro el hilo principal.

    public Input getInput() {
        return input;
    }

    public FileIO getFileIO() {
        return fileIO;
    }

    public Graphics getGraphics() {
        return graphics;
    }

    public Audio getAudio() {
        return audio;
    }
No necesita explicación, simplemente se devuelve la instancia correspondiente a cada uno. Mas tarde se podrá llamar a estos métodos desde una clase que implemente la interface Screen.

    public void setScreen(Screen screen) {
        if (screen == null)
            throw new IllegalArgumentException("Pantalla nula");

        this.screen.pause();
        this.screen.dispose();
        screen.resume();
        screen.update(0);
        this.screen = screen;
    }

    public Screen getCurrentScreen() {
        return screen;
    }
Con el primer método podremos establecer una pantalla del juego, para ello lo indicaremos en su parámetro screen. Lo primero que hacemos en el método es comprobar si esa pantalla es nula, en caso de que sea así creamos una nueva excepción.

A continuación 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 método dentro del método Screen.update.

Para finalizar usaremos el método getCurrentScreen para conocer la pantalla actual del juego.


Con esto concluimos nuestra interface y ya disponemos de una base para crear juegos.


CODIGO DE EJEMPLO: DESCARGAR

No hay comentarios:

Publicar un comentario