domingo, 6 de octubre de 2013

Screens y Spawners

Hola de nuevo! Éste artículo lo voy a dedicar a "partir" nuestra classe principal del juego en varias pantallas, para poder repartir el menu principal, la pantalla de puntuaciones, etc. Vamos a ello.  

Screens y Game 

Como su nombre indica, las pantallas nos permiten dividir el juego en diferentes "menus" o partes lógicas y simplificar nuestra lógica. Antes de crear nuestras primeras pantallas, hablaremos de otra classe que nos permitirá navegar por las mismas, ésta clase se llama "Game".

Como hemos podido observar en nuestra classe principal actual, implementamos una
"ApplicationListener" ésta interfaz nos genera los métodos que estamos usando, create, resize, render, etc... pero no nos permite navegar por pantallas, así que en vez de implementar la interfaz directamente, heredaremos de la classe "Game" la cual ya implementa la interfaz "ApplicationListener", para heredarla eliminaremos el "implements ApplicationListener" y añadiremos "extends Game", importando la classe "com.badlogic.gdx.Game" a continuación.

Al realizarlo vereís que no provoca problema alguno, ya que como he escrito antes, Game ya implementa ésta interfaz, no obstante, si escribimos en cualquier lado de algunos de nuestros métodos "this." observaremos que disponemos de varios métodos más entre ellos getScreen(Screen) y setScreen(Screen), efectivamente éstos métodos son los que usaremos para navegar entre pantallas, pero antes debemos crear alguna pantalla ¿no?.


Creando pantallas.

Para crear nuestra primera pantalla:

- Crearemos una clase nueva dentro de nuestro paquete, junto a nuestra classe principal.
- Implementaremos la interfaz "Screen" a nuestra clase, en mi caso :

public class PlayScreen implements Screen{


- Importaremos la classe "import com.badlogic.gdx.Screen;" y añadiremos los métodos de la interfa.

Todo ésto lo podemos hacer rapidamente con el solucionador de problemas de eclipse, en unos segundos tendremos creada nuestra primera pantalla.

package com.Firedark.libgdxspain;

import com.badlogic.gdx.Screen;

public class PlayScreen implements Screen{

    @Override
    public void render(float delta) {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void resize(int width, int height) {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void show() {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void hide() {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void pause() {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void resume() {
        // TODO Auto-generated method stub
       
    }

    @Override
    public void dispose() {
        // TODO Auto-generated method stub
       
    }

}



Como podeís observar en las pantallas tenemos un par de métodos más que los que ya nos resultan familiar.


Método Show() , se ejecuta al "mostrar" la pantalla, sustituye un poco al método Create() de la classe principal.


Método Hide() , se ejecuta al "ocultar" la pantalla.


Bien, una vez creada nuestra pantalla en blanco, vamos a traernos nuestra lógica a ésta pantalla y dejar "vacia" la classe principal, pero tenemos un pequeño problema que resolver, aquí tenemos que distinguir los atributos "Generales del juego" a los atributos "por pantalla", que quiero decir con ésto, lo más normal es que al crear yo el juego le asigne una resolución 800x480 y sea la misma en cada pantalla, el assets Manager, el cual uso para cargar los assets en memoria de manera rápida y desde cualquier lado ¿debería instanciarlo también en cada pantalla? lógicamente no, debemos crear el constructor de nuestra pantalla, añadiendole por parámetro nuestra classe "Game" para así acceder desde cualquier lugar a nuestros assets y nuestros datos generales. Para ello añadiremos a las pantallas nuestro constructor:


public "ClassePantalla"("Classe de Tu Juego" game){
// Y desde aqui podemos acceder a los assets desde la variable local game
// game.assets."musica"
// game.AltodePantalla, etc.
}


Para acceder los atributos de la classe game desde cualquier pantalla aseguraos de que los atributos sean publicos y no privados, en nuestro caso ( Y si habeís seguido mis otros tutoriales ) tendremos que cambiar a public el atributo de assets.

Por último y antes de añadir todo el código de ejemplo, crearemos en la classe principal el atributo de nuestra pantalla y los instanciaremos pasandole por parámetro como hemos dicho nuestra classe, luego "entraremos" en nuestra pantalla ya creada.

package com.Firedark.libgdxspain;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Rectangle;

public class PlayScreen implements Screen{


    private OrthographicCamera camera;
    private SpriteBatch batch;
    private ShapeRenderer shrend;
    private Rectangle recP,recG;
    private boolean debug;
    // Como veis e creado un atributo de nuestra classe principal para toda la pantalla, nos sirve
    // Como variable global dentro de nuestra classe pantalla.
    private LibgdxSpain game;
    //Constructor Creado, por parámetro requiero de la instancia de mi clase principal.
    public PlayScreen(LibgdxSpain game){
        //Ésto (para los mas novatos en java) nos sirve para al crear nuestra classe asignar la variable
        //local del constructor a nuestra variable global dentro de la classe, para poder usarla
        //En todos los métodos.
        this.game = game;
    }
   
    @Override
    public void render(float delta) {
       
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

        batch.setProjectionMatrix(camera.combined);

      
        if(Gdx.input.isTouched()){
       
            recP.x = Gdx.input.getX() - (recP.height/2);
            //Aqui accedo a la variable h de nuestro objeto principal.
            recP.y = game.h - Gdx.input.getY() - (recP.width/2);
           
        }
       
        if(recP.x < 0){
            recP.x = 0;
        }
       
        //Acordaros de direccionar bien todas las variables que dejeis en la classe principal.
        if(recP.x + recP.width > game.w){
            recP.x = game.w - recP.width;
        }
       
       
        if(recP.y < 0){
            recP.y = 0;
        }
       
        if(recP.y + recP.height > game.h){
            recP.y = game.h - recP.height;
        }
       
       
       
        recG.x = recG.x - 0.5f;
       
        if(recG.x < -60){
            recG.x = 800f;
        }
       
       
        if(recP.overlaps(recG)){
            recP.x = 400;
            recP.y = 400;
        }
       
       
        batch.begin();
        //Aqui hago más de lo mismo, busco el assets alojado en game y lo uso cuando quiero.
        batch.draw(game.assets.background,0,0);
        batch.draw(game.assets.pez,recP.x,recP.y);
        batch.draw(game.assets.gamba,recG.x,recG.y);
        batch.end();
       
        if(debug){
           
        shrend.begin(ShapeType.Rectangle);
        shrend.setColor(Color.BLUE);
        shrend.rect(recP.x, recP.y, recP.width, recP.height);
        shrend.end();
         
        shrend.begin(ShapeType.Rectangle);
        shrend.setColor(Color.RED);
        shrend.rect(recG.x, recG.y, recG.width, recG.height);
        shrend.end();
        }
       
       
    }

    @Override
    public void resize(int width, int height) {
   
       
    }

    @Override
    public void show() {
        shrend = new ShapeRenderer();
       
        recP= new Rectangle();
        recP.height = 64;
        recP.width = 64;
        recP.x = 400;
        recP.y = 400;
       
        recG = new Rectangle(800,0,60,60);
       
       
        debug = true;
   
   
        camera = new OrthographicCamera();
        camera.setToOrtho(false,game.w,game.h);

        batch = new SpriteBatch();   
        //Más de lo mismo.
        game.assets.musica.setLooping(true);
        game.assets.musica.play();
       
    }

    @Override
    public void hide() {
       
       
    }

    @Override
    public void pause() {
       
       
    }

    @Override
    public void resume() {

       
    }

    @Override
    public void dispose() {
        batch.dispose();
        shrend.dispose();
       
       
    }

}


Y nuestra nueva classe principal, e eliminado varios métodos que no uso.

package com.Firedark.libgdxspain;

import com.badlogic.gdx.Game;


public class LibgdxSpain extends Game {
   
   //Seguimos Creando aqui el assets Manager fijaros que ahora es Public
    public AssetsManager assets;
    public float h,w;
    //Nuevo atributo de pantalla.
    public PlayScreen pJuego;

    @Override
    public void create() {   
        //Asignamos resolucion, instanciamos y cargamos imagenes
        h = 480;
        w = 800;
        assets = new AssetsManager();
        assets.cargarAssets();
   
        //Instanciamos pantalla y la seteamos a game, ésto nos "llevará" a la pantalla.
        pJuego = new PlayScreen(this);
        this.setScreen(pJuego);
   

    }
   
    @Override
    public void dispose(){
        //Aqui eliminamos imagenes de memoria
        assets.disposeAssets();
        //Las pantallas también se liberan.
        pJuego.dispose();
    }
}




Spawners

Vamos a crear un spawn de objetos, un spawner, el cual nos servirá para añadir objetos "Rectángulos" a nuestro juego de manera sencilla, pudiendo tener por ejemplo 20 enemigos en la misma pantalla y no teniendo que crear los 20 rectángulos y las 20 colisiones, lógica de cada rectángulo por separado.

Para crear un spawner necesitaremos un array de rectángulos, donde almacenaremos nuestros mobs, disparos, etc...

private Array<Rectangle> mobsMalotes;

A continuación crearemos un método en nuestra classe o en alguna classe aparte.

public void spawnMobMalote(){
 //Dentro de éste metodo deberiamos colocar donde se va a spawnear el mob, en caso de un disparo
// del personaje por ejemplo, le podemos pasar por parámetro los valores de ubicacion del personaje
// así al generar el rectángulo lo generamos justo donde está el personaje.

// Aparte podemos darle valores aleatorios a las coordenadas con la utilidad matemática de libgdx
// int x = MathUtils.random(0,100); generará un int entre 0 y 100.

Rectangle rec = new Rectangle(x,y,height,width);
mobsMalotes.add(rec);
}


Una vez cumplimentado el método debemos dibujarlo en pantalla, ya sean disparos, mobs, rocas.
Para ello iremos entre el batch.begin y el batch.end, aqui hay varias maneras de recorrer el array
explicaré por lo menos 2.

Bucle for each.
El Bucle for each nos recorrerá los elementos del array, así que para dibujar los rectángulos con su textura ya haremos ésto:

for(Rectangle rec : mobsMalotes){
            batch.draw(game.assets.textura,rec.x,rec.y,rec.height,rec.width);
           //Podemos añadir algún movimiento lineal rapido a los réctangulos, como en la caso de la
           // gamba del artículo anterior, si añadimos x ejemplo
           rec.y--;
          //Provocaremos que los rectángulos que aparezcan vayan hacia abajo.
          // También podemos añadir colisiones, para actuar en caso de que un rectángulo toque otro.
            if(rec.overlaps(otroRectangulo)){
                //Esto sirve para eliminar el rectángulo que a chocado como ejemplo.
                mobsMalotes.removeValue(rec, true);
               //Un sonido, etc
                game.assets.hit.play();
            }
        }
 
Iterator

Si creamos un Iterator también nos hará la misma función. Es una utilidad del java, importarla de Java.Utils.

//El mismo caso que en el for each.
Iterator<Rectangulo> iter = mobsMalotes.iterator();
            while(iter.hasNext()){
                Rectangulo rec = iter.next();
                batch.draw(game.assets.textura,rec.x,rec.y,rec.height,rec.width);              
                if(rec.getBounds().overlaps(otroRectangulo)){      
                        iter.remove();               
                }

Con ésto ya tenemos dibujo y lógica de cualquier cosa que spawneemos, me falta explicar como hacer un temporizador cíclico para que podaís probar a spawnear cosas cada segundo, controlar los disparos, o simplemente contar tiempo.

TimeUtils. Manejando el tiempo.

TimeUtils.millis()  Devuelve el tiempo de ejecución en milisegundos.
TimeUtils.nanos() Devuelve el tiempo de ejecución en nanosegundos.


Contador de segundos:

Creo un long como atributo y en el método create o show le asigno el tiempo actual de ejecución.

private long time;

public void create(){
time = TimeUtils.millis();
}


Luego en el método render, añado éste código:

public void render(){
 //Si el tiempo en ejecucion, que no a parado se le resta el tiempo que hemos almacenado nos dará el //tiempo en millis que ha pasado, en caso de ser mayor que 1000 es que habrá llegado al segundo
//Al entrar en el condicional, volvemos  a asignar el tiempo actual de ejecucion a la variable time
//provocando lo mismo una y otra vez.
 if(TimeUtils.millis() - time > 1000){
//Esta parte se ejecutará cada segundo.
time= TimeUtils.millis();
}
}

 Finalmente añado mi clase para que veaís un ejemplo hecho, se spawnean cada 2 segundos. rocas en la parte superior y van hacia abajo, si chocan con el pez se eliminan y suena un sonido.

 //Parte de la pantalla creada en el tutorial anterior.
private Rectangle recP,recG;
    private boolean debug,move;
    //Array
    private Array<Rectangle> rocas;
    //TIEMPO
    private long time;
    private LibgdxSpain game;
    public PlayScreen(LibgdxSpain game){
    this.game = game;

    }
   
    @Override
    public void render(float delta) {

        //TEMPORIZADOR
        if(TimeUtils.millis() - time > 2000){
            spawnRoca();
            time = TimeUtils.millis();
        }
       
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

        batch.setProjectionMatrix(camera.combined);

        if(Gdx.input.isTouched()){
           
            float sY = game.h -Gdx.input.getY();
            if(Gdx.input.getX() > recP.x & Gdx.input.getX() < recP.x + recP.width & sY > recP.y & sY < recP.y + recP.height){                       
                move = true;
            }
       
        }
   
        if(!Gdx.input.isTouched()){
            move = false;
        }
       
        if(move){
        recP.x = Gdx.input.getX() - (recP.height/2);   
        recP.y = game.h - Gdx.input.getY() - (recP.width/2);       
        }
       
        if(recP.x < 0){
            recP.x = 0;
        }
       
        if(recP.x + recP.width > game.w){
            recP.x = game.w - recP.width;
        }
       
        if(recP.y < 0){
            recP.y = 0;
        }
       
        if(recP.y + recP.height > game.h){
            recP.y = game.h - recP.height;
        }
           
        recG.x = recG.x - 0.5f;
       
        if(recG.x < -60){
            recG.x = 800f;
        }
       
       
        if(recP.overlaps(recG)){
            recP.x = 400;
            recP.y = 400;
            move = false;
        }
       
       
        batch.begin();
        batch.draw(game.assets.background,0,0);
        batch.draw(game.assets.pez,recP.x,recP.y);
        batch.draw(game.assets.gamba,recG.x,recG.y);
       
        //Iteracion
        Iterator<Rectangle> iter = rocas.iterator();
        while(iter.hasNext()){
            Rectangle rec = iter.next();
            batch.draw(game.assets.roca,rec.x,rec.y,rec.height,rec.width);  
            rec.y--;
            if(rec.overlaps(recP)){    
                    iter.remove();    
                    game.assets.hit.play();
            }
        }
       
       
        batch.end();
       
        if(debug){
           
        shrend.begin(ShapeType.Rectangle);
        shrend.setColor(Color.BLUE);
        shrend.rect(recP.x, recP.y, recP.width, recP.height);
       
        shrend.setColor(Color.RED);
        shrend.rect(recG.x, recG.y, recG.width, recG.height);
        shrend.end();
        }
       
       
    }

    @Override
    public void resize(int width, int height) {
   
       
    }

    @Override
    public void show() {
        game.assets.cargarAssets();
        time = TimeUtils.millis();
        shrend = new ShapeRenderer();
        rocas = new Array<Rectangle>();
        recP= new Rectangle();
        recP.height = 64;
        recP.width = 64;
        recP.x = 400;
        recP.y = 400;
       
        recG = new Rectangle(800,0,60,60);
       
       
        debug = true;
   
   
        camera = new OrthographicCamera();
        camera.setToOrtho(false,game.w,game.h);

        batch = new SpriteBatch();   
        //Más de lo mismo.
        game.assets.musica.setLooping(true);
        game.assets.musica.play();
       
    }

    public void spawnRoca(){
        Rectangle rec = new Rectangle();
        rec.height = 128;
        rec.width = 128;
        float randomX = MathUtils.random(0 ,(game.w - rec.width));
        rec.x = randomX;
        rec.y = game.h + rec.height;
        rocas.add(rec);
        }

Y eso es todo por ésta semana , la siguiente semana explicaré el uso de Preferencias, que son los datos permanentes que se guardan con la aplicación, para almacenar records, volumen, etc.

Gracias a tod@s y cualquier duda preguntar en los comentarios! Suerte ;)

12 comentarios:

  1. Excelente, muchas gracias, mi pregunta es, porque se deben cargar los assets en las dos clases, no se podrian cargar en el constructor de PlayScreen?

    ResponderEliminar
    Respuestas
    1. Era un error, con que las cargues una vez es suficiente, mientras las cargues antes de usarlas ya nos sirve, en éste caso como son assets generales del juego lo cargo al iniciarlo, aunque puedo hacer diferentes métodos para que me cargue diferentes assets en cada pantalla. Gracias por darte cuenta! :D

      Eliminar
  2. Excelente trabajo Sergio, sigue así.

    ResponderEliminar
  3. Muy buen trabajo, pero podrias poner como quedo la clase AssetsManager? que no se que recurso usaste en "rock" y que es "hit" los dos de la clase AssetsManager

    ResponderEliminar
    Respuestas
    1. Hola Rocket, son simple textura y sonido.

      roca = new Texture(Gdx.files.internal("data/Images/roca.png"));
      hit = Gdx.audio.newSound(Gdx.files.internal("data/Music/hit.wav"));

      Intentaré añadir todas las classes usadas en los siguientes tutoriales, Muchas gracias por tu critica constructiva^^!

      Eliminar
  4. Estoy intentando que se vea alguna textura y no hay manera.

    ResponderEliminar
    Respuestas
    1. Que error te lanza? Asegurate que las medidas tienen que ser potencia de dos!

      Eliminar
  5. Hola. Muy bueno todo pero no puedo conseguir que se vea algo en la pantalla. Ya probe de las mil maneras. No me tira ningun error. Solo se ve toda la pantalla en negro. (hasta se escucha la musica de fondo). Probe creando otra clase bien sencilla donde solo dibujo el fondo y nada.

    ResponderEliminar
    Respuestas
    1. Ya encontre el error. En la clase que extiende de Game hay que agregar la linea :

      super.render();

      Eliminar
  6. Hola Muy buenos tutos la verdad me han ayudado mucho y es lo mas actual ya que otros tutoriales son viejos y no funcionan con lo nuevo de libgdx, solo tengo una duda yo lo compilo en mi dispositivo pero aparece horizontal el juego como puedo hacer que solo sea vertical? y por cierto algún correo para mandar algunas dudas ?

    ResponderEliminar
    Respuestas
    1. Mirate la orientacion dentro del manifest.xml del proyecto android tiene que ser "portrait" y un correo para dudas mandamelo a firedarknes@gmail.com puedo resolver cosas puntuales ya que problemas gordos ya tengo suficientes xd

      Eliminar
  7. Que tal amigo, muy buenos tutoriales viendo estos (y algunos otros XD), aun tengo una duda acerca de como implementar una pantalla de pausa de manera correcta, mas específicamente que esta tenga varias opciones y que me muestre estadisticas y demás, estilo por ejemplo castlevania o megaman donde no solo se pausa el juego sino que se relizan acciones, incluso definiendo comportamientos y variables como la música o añadirme un arma nueva, comprar algún objeto o algo así, entonces volviendo a mi duda no se como implementarlo correctamente, generalmente de los ejemplos y tutoriales que he visto son muy básicos y generalmente solo muestran que hay una variable para determinar si el juego esta en pausa o no, en algunos casos determinando si se actualiza el juego normal o se actualiza los datos de pausa, pero no se es si para realizar una pantalla como te digo debo crear una clase que extienda screen y asi cuando se pause cambiar mediante setScreen y de alguna manera guardar la informacion del juego para que luego de salir de pausa retomarla luego de nuevamente usar el metodo setScreen, o si definitivamente lo adecuado seria tener una funcion que dibuje el juego o el menu pausa de acuerdo al estado actual del juego, o si existe una mejor manera de hacerlo, te agradezco de antemano si me pudieras colaborar.

    ResponderEliminar