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 ;)