====== Desarrollo de videojuegos con libGDX ====== {{ libgdx-logo.png?200 }} ===== ¿Qué es libGDX ===== **libGDX** es lo que se conoce actualmente como motor de videojuegos (//game engine//). Desde el punto de vista de un programador, libGDX proporciona un framework para el desarrollo de videojuegos en lenguaje Java. Entre sus principales características destacan las siguientes: * Es **multiplataforma** por lo que el juego desarrollado se puede ejecutar en PC, Android, iOS, Web y BlackBerry * Se integra con algunos de los **IDEs más conocidos** (IntelliJ, Eclipse y NetBeans) * Dispone de APIs de alto nivel tanto para **2D** como para **3D** * Incopora dos **motores de físicas** muy extendidos (Box2D para 2D y Bullet para 3D) * Dispone de librería para trabajar con niveles diseñados como **TiledMaps** (//.tmx//) * Dispone de un **API para gestionar el almacenamiento** proporcionando un nivel de abstracción para el programador en el acceso a recursos, preferencias, . . . en las diferentes plataformas * Dispone de un **API para la gestión del input de usuario** (teclado, ratón, pantalla táctil, acelerómetro, . . .) que proporciona un nivel de abstracción para las diferentes plataformas * Al tratarse de software libre dispone de una amplía **comunidad de desarrolladores** (muy en auge actualmente) * Su desarrollo sigue **activo** en la actualidad {{ youtube>hA2e3xIuNlk }} \\ Actualmente incluso cuenta con una [[https://libgdx.com/wiki/|Wiki muy completa con muchísima documentación]] ==== Algunos conceptos de videojuegos ==== === Frame === Un frame es cada uno de los fotogramas que se visualizan en pantalla en un videojuego. Si echamos un vistazo al [[apuntes:libgdx#ciclo_de_vida_de_un_videojuego|ciclo de vida de un videojuego]] vemos que éste se encuentra continuamente dentro de un bucle que se ejecuta ininterrupidamente mientras dure su ejecución. Así, en cada iteración del bucle se pintará un frame en la pantalla. Dependerá de cuantos bucles sea capaz de pintar el ordenador por segundo para determinar los FPS (frames por segundo).
{{frame1.png?300 |Frame 1}} {{ frame2.png?300|Frame 2}} {{frame3.png?300 |Frame 3}} {{ frame4.png?300|Frame 4}} Frames de un juego
=== Renderizar === Es la acción por la cual se pinta (//se renderiza//) cada uno de los frames en la pantalla.
{{ gameloop.png?200 |Bucle de un juego}} Bucle de un juego
=== FPS (Frames Per Second) === Es una medida que indica cuántos **F**rames **P**or **S**egundo es capaz de renderizar el ordenador que ejecuta el juego. Cuántos mas FPS sea capaz de procesar, más fluido se moverá. Hay que tener en cuenta que un número alto de FPS (60, por ejemplo) significa que el ordenador es capaz de procesar y renderizar cada frame en muy poco tiempo (//delta time//) y eso hará que los movimientos del juego sean más suaves al no pasar mucho tiempo entre frames. Si por el contrario, al ordenador le cuesta mucho tiempo procesar y renderizar cada frame, entre dos frames diferentes habrán podido pasar muchas cosas y será molesto para el jugador, puesto que todo parece que se mueve //a golpes// ocurriendo demasiadas cosas entre frames ante las cuales no es posible reaccionar. Actualmente se considera que a partir de 30 FPS se proporciona una jugabilidad fluida. === Textura === Una textura es cada una de las imágenes 2D que el ordenador tiene que cargar en memoria para poderlas renderizar en la pantalla durante la ejecución del videojuego. Hay que tener en cuenta que el videojuego necesita tener almacenada en memoria cualquier textura que haya que renderizar durante la partida. Por eso, en muchos juegos hay un proceso de carga al inicio de su ejecución, donde se cargan todas las texturas (y demás recursos) de forma que luego sólo haya que renderizarlos y pueda hacerse en el menor tiempo posible. Si el espacio que ocupan las texturas es demasiado grande, habrá que cargar las texturas de cada nivel al inicio del mismo o incluso dividir los niveles de forma que a mitad se establezca de alguna manera un momento para cargar las necesarias en cada parte.
{{ texture.png?75 |Textura}} Textura de un personaje
=== Sprite === Representa a una textura con las coordenadas x e y para determinar su posición en la pantalla. Desde el punto de vista del programador permite que en un solo objeto tengamos la textura y las coordenadas x e y de la misma. === Animación (2D) === Es un conjunto de texturas 2D que representa la secuencia de movimientos de algún elemento del videojuego (personaje, item, enemigo, . . .). Así, cada elemento animado tendrá una animación diferente para cada movimiento que estará compuesto de un númeo variable de texturas. Cuando todas esas texturas se muevan formarán la animación.
{{ explosion.png |Animación}} Frames que forman una animación
=== TextureAtlas === Es un conjunto de texturas almacenadas en un mismo fichero. La idea es reducir el tiempo de carga de los recursos puesto que es mucho más rápido cargar un fichero con un número determinado de texturas que cargar ese mismo número de texturas en ficheros separados. Una vez cargado el atlas, utilizando la API de libGDX podremos acceder a cada una de las diferentes texturas por separado o bien por animaciones por lo que además de reducir el tiempo de carga nos hace más fácil el acceso a estos recursos. Si el videojuego no tienen un número muy alto de texturas podremos crear un solo //TextureAtlas// y cargarlo al inicio del videojuego. De esa manera no tendremos que realizar accesos a disco durante la partida y se reducirá en gran medida el tiempo que el ordenador tarda en procesar y renderizar un frame, mejorando los FPS lo que afectará al rendimiento general del juego. Si por el contrario, son demasiadas las texturas, siempre podremos crear varios atlas (uno por nivel, uno por mundo, . . .) y realizar la carga del mismo al inicio de cada nivel o mundo. En ningún caso acceder a disco mientras el usuario se encuentra jugando.
{{ textureatlas2.png?400 |TextureAtlas}} TextureAtlas
=== Delta Time (dt) === Es el tiempo que tarda el ordenador en procesar y renderizar un frame del videojuego. En libGDX se mide en segundos por lo que habrá que tener en cuenta que son números muy cercanos a cero.
{{ deltatime.png |Independencia de FPS}} Relación entre dt y FPS en un juego
Gráfico de BNG (Build New Games)((http://buildnewgames.com/real-time-multiplayer/)) Este parámetro nos servirá para ajustar la velocidad del juego al equipo que lo ejecuta. Si no contamos con el //delta time// y un personaje se mueve 10px por la acción del usuario, hay que tener en cuenta que el personaje se moverá más rápido dependiendo del ordenador. Toda la lógica del juego se ejecuta dentro de un bucle (ver [[http://multimedia.codeandcoke.com/apuntes:libgdx#ciclo_de_vida_de_un_videojuego|ciclo de vida de un juego)]] por lo que un ordenador potente tardará menos tiempo en hacerlo y lo ejecutará más veces, moviendo así más rápido al jugador. . . . // El usuario mueve al personaje jugador.posicion += 10; . . . En caso de que utilicemos //delta time// como coeficiente a multiplicar hará que se igualen las velocidades. Para ordenador potentes //dt// tendrá un valor bajo y hará que el jugador se mueva más lento. En ordenadores menos potentes //dt// tendrá un valor más alto y hará que se mueva más rápido. En definitiva, se equilibrarán las velocidades haciendo el juego independiente de la máquina donde se lance (siempre y cuando la máquina cumpla los requisitos mínimos) . . . // El usuario mueve al personaje jugador.posicion += 10 * dt; . . . === HUD (Head-up display) === El HUD hace referencia a la información del juego que se muestra al usuario en todo momento. Normalmente serán las vidas restantes, la puntuación, algunos objetos que posea el personaje en un momento dado, . . .
{{ hud1.png}}{{hud2.png }} Head-up display
===== Estructura del framework libGDX ===== ==== Estructura de módulos ====
{{ modules_overview.png |Estructura de módulos en libGDX}} Estructura de módulos en libGDX
==== Ciclo de vida de un videojuego ==== A continuación se muestra el ciclo de vida por el que todo videojuego desarrollado en libGDX pasa. Básicamente el juego se desarrolla dentro del método ''render()'' que se ejecuta por el framework de forma continua en forma de bucle. Antes de llegar a ese método se ejecutan los métodos ''create()'' y ''resize()'' y cuando la aplicación termina, se ejecutan los métodos ''pause()'' y ''dispose()'' antes de hacerlo completamente. También se puede observar que, durante la ejecución del bucle principal con el método ''render()'', el juego puede ser pausado para luego continuar su ejecución, pasando por la ejecución de los métodos ''pause()'' y ''resume()''. El paso a este estado de pausa y la ejecución de estos métodos no dependerá de nosotros sino que tendremos que dejar allí el código necesario para cuando se produzca el evento correspondiente. Por ejemplo, supongamos que estamos jugando a un juego en el móvil y nos llaman por teléfono, será libGDX el encargado de invocar al método ''pause()'' en ese preciso instante para que la partida se pause mientras hablamos. De forma similar, al colgar, será libGDX quién invocará al método ''resume()'' para continuar jugando donde lo hayamos dejado. De forma parecida, seremos nosotros quienes tendremos que dejar el código necesario en los métodos ''create()'' y ''resize()'' para realizar la carga de recursos del juego antes de comenzar a ejecutarse el bucle principal en ''render()'' y también tendremos que liberar los recursos utilizados cuando la aplicación vaya a terminar pasando por los métodos ''pause()'' y ''dispose()''.
{{ libgdx_lifecycle.png |Ciclo de vida de un videojuego en libGDX}} Ciclo de vida de un videojuego en libGDX
===== Componentes libGDX ===== ==== API 2D ==== === Sistema de coordenadas === Para representar los elementos del juego 2D en pantalla, libGDX utilizar el sistema de coordenadas cartesiano que todos conocemos, utilizando dos parámetros x e y según los ejes de dicho sistema.
{{ xy.png }} Sistema ortogonal (cartesiano)
Hay que tener en cuenta que para cualquier recurso representado en el juego, sus coordenadas (x,y) marcarán la posición de su esquina inferior izquierda por lo que siempre habrá que tener en cuenta su altura y anchura para realizar cálculos como su colisión con otros elementos. === SpriteBatch === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/SpriteBatch.html|SpriteBatch]] es la encargada de dibujar en pantalla las texturas, sprites o frames de una animación. También se utilizar para pintar texto en pantalla (HUD, Head-Up Display). . . . spriteBatch.begin(); // Pinta en pantalla los elementos que sean necesarios spriteBatch.draw(personaje.getTextura() , personaje.getX(), personaje.getY()); for (Enemigo enemigo : enemigos) spriteBatch.draw(enemigo.getTextura() , enemigo.getX(), enemigo.getY()); spriteBatch.end(); . . . Para pintar texto en pantalla tendremos que hacer uso de una fuente con la clase ''BitmapFont'' . . . // Carga la fuente BitmapFont font = new BitmapFont(Gdx.files.internal("ui/default.fnt")); . . . spriteBatch.begin(); font.draw(spriteBatch, "Esto es un texto", xPosition, yPosition); spriteBatch.end(); . . . === Texture === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/Texture.html|Texture]] representa una imagen en memoria. Es la forma de representarla en el código para luego, por ejemplo, pintarla en la pantalla. Solo almacena la imagen, no la posición que ésta debería ocupar en la pantalla . . . // Carga una textura en memoria Texture texture = new Texture(Gdx.files.internal("items/gun.png")); // Pinta la textura en una posicion (x, y) determinada de la pantalla spriteBatch.draw(texture , posicionX , posicionY); . . . === TextureRegion === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/TextureRegion.html|TextureRegion]] representa una región delimitada de una textura . . . Texture texture = new Texture(Gdx.files.internal("items/weapons.png")); // "Recorta" una region de la textura indicando x, y, anchura, altura TextureRegion textureRegion = new TextureRegion(texture, 20, 20, 50, 50); // Pinta esa zona en pantalla en una posicion (x, y) determinada de la pantalla spriteBatch.draw(textureRegion , posicionX , posicionY); . . . === Sprite === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/Sprite.html|Sprite]] representa una textura junto con su posición (x, y) en la pantalla . . . Texture texture = new Texture(Gdx.files.internal("items/weapons.png")); // Crea un sprite indicando la region de la textura asignada y su anchura y altura Sprite sprite = new Sprite(texture); // Asigna posición x, y en la pantalla sprite.setPosition(10, 30); // Es posible asignar una rotación al sprite sprite.setRotation(90f); // Pinta el sprite en pantalla en su posición sprite.draw(spriteBatch); . . . === Array === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/utils/Array.html|Array]] se utiliza como sustitución de clases como ''ArrayList'' en //libGDX// al estar optimizada para su uso en el desarrollo de videojuegos . . . Array enemigos = new Array<>(); . . . // Permite eliminar elementos del array mientras se recorre con un foreach for (Enemigo enemigo : enemigos) { if (. . .) enemigos.removeValue(enemigo, true); } . . . === TextureAtlas === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/TextureAtlas.html|TextureAtlas]] representa en memoria un mapa de texturas. ([[http://multimedia.codeandcoke.com/apuntes:libgdx#atlas_de_texturas_textureatlas|Más información]]) . . . // Carga el TextureAtlas en memoria TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("characters/characters. pack")); // Accede a una textura del atlas por su nombre Texture texturePlayer = atlas.findRegion("player").getTexture(); // Accede a un grupo de texturas por su nombre Animation enemyAnimation = new Animation(0.25f, atlas.findRegions("enemy")); . . . === Animation === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/Animation.html|Animation]] permite almacenar un conjunto de frames (texturas) como una animación y permite así añadir movimiento de una forma sencilla a un conjunto de texturas relacionadas. . . . // Cargamos un mapa de texturas en el objeto atlas Animation enemyAnimation = new Animation(0.25f, atlas.findRegions("enemy")); . . . // Lo ejecutamos dentro del bucle que genera el método render // stateTime contiene el tiempo de juego TextureRegion currentFrame = enemyAnimation.getKeyFrame(stateTime, true); spriteBatch.draw(curentFrame, position.x, position.y); . . . === Vector2 === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/math/Vector2.html|Vector2]] representa un objeto compuesto por dos valores ''x'' e ''y'' para almacenar la posición de algún elemento en pantalla. También se puede utilizar para almacenar otros parámetros como pueden ser la velocidad de algún elemento en ambos ejes de la pantalla. Algunos métodos útiles son: * ''scl(float)'' Permite escalar los valores x e y con respecto a un valor (Muy útil para hacerlo con //delta time//) * ''add(Vector2)'' Permite sumar dos vectores . . . Vector2 position = new Vector2(100, 200); . . . // Es posible acceder directamente a los valores x e y spriteBatch.draw(texture, position.x, position.y); . . . === Rectangle === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/math/Rectangle.html|Rectangle]] permite almacenar los parámetros de la representación de un rectángulo para utilizarlo como rectángulo de colisión de un elemento del juego. Se almacena su posición (x,y) y su altura y anchura.
{{ collisions1.png }} Rectángulos de colisión
public class Personaje { . . . Rectangle rect = new Rectangle(x, y, width, height); . . . } Así, con el método ''overlaps(Rectangle)'' se puede calcular si dos rectángulos colisionan . . . if (personaje.rect.overlaps(enemigo.rect)) { // Personaje y enemigo han colisionado . . . } . . . // También podemos calcular el valor de su área y perímetro float area = personaje.rect.area(); float perimeter = personaje.rect.perimeter(); . . . === Circle === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/math/Circle.html|Circle]], de forma similar a lo que hace la clase ''Rectangle'', también podemos trabajar con círculos de colisión para los casos en los que esta figura geométrica se aproxime más a la forma del personaje que representa.
{{ collisions2.png }} Círculo de colisión
public class Personaje { . . . Circle circle = new Circle(x, y, radius); . . . } Así, con el método ''overlaps(Circle)'' se puede calcular si dos círculos colisionan . . . if (personaje.circle.overlaps(enemigo.circle)) { // Personaje y enemigo han colisionado . . . } . . . // También podemos calcular su área y longitud de la circunferencia float area = personaje.circle.area(); float circumference = personaje.circle.circumference(); . . . > Conviene tener en cuenta que tanto ''Rectangle'' como ''Circle'', ambos con el método ''overlaps'', sólo son capaces de comprobar colisiones con elementos de su mismo tipo. Si queremos comprobar la colisión entre figuras geométricas diferentes tendremos que usar alguno de los métodos que ofrece la clase ''Intersector'' que se comenta a continuación. === Intersector === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/math/Intersector.html|Intersector]] es una clase que ofrece numerosos métodos estáticos para detectar colisiones entre cualquier tipo de figura geométrica. Por ejemplo, ofrece métodos para detectar colisiones entre objetos de tipo ''Rectangle'', ''Circle'' y entre un ''Rectangle'' y un ''Circle'', entre otros: * ''static float distanceLinePoint(startX, startY, endX, endY, pointX, pointY)'': Calcula la distancia entre una recta y un punto * ''static boolean overlaps(Rectangle, Rectangle)'' Comprueba si dos rectángulos colisionan * ''static boolean overlaps(Rectangle, Circle)'' Comprueba si un rectángulo y un círculo colisionan * ''static overlaps(Circle, Circle)'' Comprueba si dos círculos colisionan . . . Rectangle rect = new Rectangle(. . .); Circle circle = new Circle(. . .); . . . if (Intersector.overlaps(rect, circle)) { // Rectángulo y círculo han colisionado . . . } . . . === TimeUtils === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/utils/TimeUtils.html|TimeUtils]] proporciona una serie de métodos de utilidad para gestionar el tiempo durante el juego: * ''static long millis()'': Devuelve la diferencia de tiempo en milisegundos entre el instante actual y el 1/1/1970 * ''static long timeSinceMillis(long previousTime)'': Devuelve la diferencia de tiempo en milisegundos entre el instante actual y el que se le pasa como parámetro Así, si queremos que se ejecute cierto código cada cierto tiempo, podemos hacer lo siguiente: . . . // Ejecuta el método doSomeThing cada 10 segundos if (TimeUtils.timeSinceMillis(previousTime) > 10000) { doSomething(); previousTime = TimeUtils.millis(); } . . . === MathUtils === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/math/MathUtils.html|MathUtils]] proporciona métodos matemáticos de uso muy habitual. Estos son algunos: * ''static value clamp(value, min, max)'' Controla que un valor entre dos valores mínimo y máximo * ''static value random(start, end)'' Devuelve un valor aleatorio entre dos valores dados * ''static float random()'' Devuelve un número aleatorio entre 0 y 1 * ''static boolean randomBoolean()'' Devuelve un valor booleano aleatorio * ''static int randomSign()'' Devuelve -1 ó 1 de forma aleatoria === Timer === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/utils/Timer.html|Timer]] permite que se ejecuten tareas futuras en el bucle de juego. Por ejemplo, es posible programar el lanzamiento de una tarea indicando el retardo en segundos: . . . // Ejecuta el método doSomething() pasados 3 segundos Timer.schedule(new Timer.Task() { public void run() { doSomething(); } }, 3); . . . === Logger === La clase [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/utils/Logger.html|Logger]] permite crear registros para mejorar la salida de mensajes durante la ejecución de la aplicación para motivos de depuración. . . . Logger logger = new Logger("tag"); /* * Hay 4 niveles que pueden ser modificados en tiempo de ejecución * Logger.ERROR hará que se muestren por pantalla sólo los mensajes de error * Logger.INFO hará que se muestren por pantalla los mensajes que no sean de depuracion (debug) * Logger.DEBUG permite que se muestren por pantalla todos los mensajes * Logger.NONE no muestra ningún mensaje por pantalla */ logger.setLevel(Logger.DEBUG); . . . logger.debug("Esto es un mensaje"); . . . ==== Gestión del input de usuario ==== Para la gestión del input de usuario, ya sea mediante el teclado, ratón o gamepad, libGDX proporciona una API completa a través del paquete ''Gdx.input'' === Teclado === * Comprobar si el usuario está pulsando una tecla (al estar en el bucle principal de juego la acción se ejecuta de forma continuada mientras el jugador mantiene pulsada la tecla) . . . if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { // Mover al jugador a la derecha player.velocity.x = PLAYER_SPEED; player.state = Player.State.RIGHT; } . . . * Comprobar si el usuario acaba de pulsar una tecla . . . if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE)) { // El jugador salta player.jump(); } . . . === Ratón === * Comprobar si el usuario está pulsando algún botón del ratón . . . if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) { // Hacer algo cuando el usuario pulse el botón izquierdo del ratón } . . . * Comprobar si el usuario pulsa en la pantalla (o bien hace click en alguna parte de la misma) . . . if (Gdx.input.isTouched()) { // Hacer algo } . . . === Gamepad === * Inicializar y gestionar el control de un gamepad . . . public class SpriteManager implements ControllerListener { . . . public SpriteManager() { . . . Controllers.addListener(this); . . . } . . . @Override public void connected(Controller controller) { // Hacer algo cuando el usuario conecta un gamepad } @Override public void disconnected(Controller controller) { // Hacer algo cuando el usuario desconecta un gamepad } @Override public boolean buttonDown(Controller controller, int buttonCode) { // Hacer algo cuando el usuario pulsa un botón // buttonCode = 0 si el botón pulsado es X // buttonCode = 1 si el botón pulsado es A // buttonCode = 2 si el botón pulsado es B // buttonCode = 3 si el botón pulsado es Y return false; } @Override public boolean buttonUp(Controller controller, int buttonCode) { // Hacer algo cuando el usuario suelta un botón return false; } @Override public boolean axisMoved(Controller controller, int axisCode, float value) { // Hacer algo cuando el usuario pulsa el control pad // Hay que tener en cuenta que el evento salta cuando se pulsa o se suelta, no // de forma continua mientras lo tiene pulsado // axisCode = 0 si el usuario se mueve en el eje X // axisCode = 1 si el usuario se mueve en el eje Y // value = 1.0 si el usuario se mueve hacia la derecha/arriba // value = -1.0 si el usuario se mueve hacia la izquierda/abajo // value != 1.0/-1.0 cuando el usuario suelta el control pad return false; } @Override public boolean povMoved(Controller controller, int povCode, PovDirection value) { return false; } @Override public boolean xSliderMoved(Controller controller, int sliderCode, boolean value) { return false; } @Override public boolean ySliderMoved(Controller controller, int sliderCode, boolean value) { return false; } @Override public boolean accelerometerMoved(Controller controller, int accelerometerCode, Vector3 value) { return false; } } . . . ==== Gestión del modo de video ==== A través del API de //libGDX// también es posible acceder a diferentes aspectos del modo de video o relativo a los gráficos durante el juego, en tiempo de ejecución === Obtener el delta time === Como ya vimos, //delta time// es el tiempo que tarda el juego en pintar un frame, es decir, en ejecutar una iteración completa del bucle principal del juego (método ''render()''). . . . float deltaTime = Gdx.graphics.getDeltaTime(); . . . === Cambiar el modo de video === También podemos cambiar el modo de video o la resolución en tiempo de ejecución, de forma que podremos hacer que el usuario pueda configurarlo desde el propio menú de configuración antes de empezar a jugar o bien desde un menú durante la partida. * Por ejemplo, podemos cambiar el modo de video a pantalla completa: . . . Graphics.DisplayMode mode = Gdx.graphics.getDisplayMode(); Gdx.graphics.setFullscreenMode(mode); . . . * Y también simplemente a modo ventana fijando una resolución determinada . . . Gdx.graphics.setWindowedMode(1024, 768); . . . === Cambiar el cursor === También es posible cambiar la apariencia del cursor del ratón en el juego . . . Pixmap cursorPixmap = new Pixmap(Gdx.files.internal("assets/tests/cross_arrow_cursor_mini.png")); . . . Gdx.graphics.setCursor(Gdx.graphics.newCursor(pixmap, 0, 0)); . . . ==== Gestión de ficheros ==== //libGDX// también proporciona una API para la gestión de ficheros de forma más cómoda que como se hace con la propia API de Java * ''Gdx.files.internal(String)'' Obtiene la referencia a un fichero especificado como una cadena de texto * ''Gdx.files.getExternalStorage()'' Obtiene la ruta al almacenamiento externo. En Android será la tarjeta de memoria y en un PC la carpeta personal del usuario (carpeta home) * ''Gdx.files.getInternalStorage()'' Obtiene la ruta al almacenamiento interno. En Android será el directorio privado y en un PC la carpeta donde se encuentre el .jar Y algunos ejemplos de la clase ''FileHandle'' que añade alguna funcionalidad sobre la clase ''File'' que proporciona el API de Java: . . . // Obtiene la referencia a un fichero FileHandle file = Gdx.files.internal("un_fichero.loquesea"); // Lee el contenido de un fichero como un String String content = file.readString(); // Escribe la cadena de texto en el fichero // (append:true o false, según se quiera añadir o sobrescribir) file.writeString(aString, append); // Comprueba si el fichero existe if (file.exists()) { . . . } // Elimina el fichero file.delete(); // Obtiene el nombre del fichero sin la extensión String fileName = file.nameWithoutExtension(); . . . ==== Sonido y música ==== Para la gestión de recursos de sonido en //libGDX// existen las //interfaces// [[http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/audio/Sound.html|Sound]] y [[https://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/audio/Music.html|Music]]. Ambas permiten la reproducción de sonido para efectos de audio y música respectivamente. La clase ''Sound'' está específicamente diseñada para reproducción de sonidos cortos como efectos de sonido mientras que la clase ''Music'' lo está para reproducir pistas de audio de larga duración como pueden ser las bandas sonoras de los juegos o la música de cada una de las pantallas que suena de fondo durante toda la parte (en forma de bucle, por ejemplo). Hay que tener en cuenta que //libGDX// cargará todo el fichero completo en el caso de un objeto ''Sound'' mientras que lo reproducirá mediante streaming en el caso de un objeto de tipo ''Music''. Ambas interfaces soportan los formatos de audio .wav, .mp3 y .ogg. Además, conviene tener en cuenta que al tratarse de //interfaces// no pueden ser instanciadas directamente. Es por ello que un clip de sonido o audio debe ser cargado utilizando unos métodos específicos que incluye la API de //libGDX// . . . Sound aSound = Gdx.audio.newSound(Gdx.files.internal("hit.mp3")); . . . Music aMusic = Gdx.audio.newMusic(Gdx.files.internal("bso.mp3")); . . . Ambas clases tienen métodos similares para realizar las mismas operaciones sobre el fichero de audio que tengan asociado. A continuación se listan algunos de los métodos de los que dispone la //interface// ''Sound'': * ''void dispose()'' Libera los recursos utilizados por la pista de audio * ''long loop()'' Reproduce una pista de audio en formato bucle y devuelve el ''id'' de dicha pista, o -1 en caso de fallo * ''void pause()'' Pausa la reproducción de una pista de audio * ''long play()'' Reproduce una pista de audio y devuelve el ''id'' de dicha pista, o -1 en caso de fallo * ''long play(float volume)'' Reproduce una pista de audio con un volumen determinado (entre 0 y 1) y devuelve el ''id'' de dicha pista, o -1 en caso de fallo * ''void resume()'' Continua la reproducción de una pista de audio que haya sido pausada * ''void setVolume(long soundId, float volume)'' Fija el volumen de una pista de audio * ''void stop()'' Detiene la reproducción de una pista de Audio En el caso de la //interface// ''Music'' disponemos de algunos métodos como los que siguen: * ''void dispose()'' Libera los recursos utilizados por la pista de audio * ''float getPosition()'' Devuelve la posición de la reproducción en segundos * ''void pause()'' Pausa la reproducción de una pista de audio * ''void play()'' Reproduce una pista de audio * ''void setLooping(boolean isLooping)'' Fija (o no) la reproducción en modo bucle * ''void setPosition(float position)'' Fija la posición de reproducción en segundos * ''void setVolume(float volume)'' Fija el volumen de una pista de audio * ''void stop()'' Detiene la reproducción de una pista de Audio ==== Animaciones ==== El siguiente ejemplo muestra cómo cargar la animación de un personaje a partir de un texture atlas . . . // Primero se obtienen los frames con los que queremos crear la animación TextureAtlas myAtlas = assetManager.get("my_texture_atlas.pack"); Array allFrames = myAtlas.findRegions("personaje"); . . . // El primer parámetro indica el tiempo entre frames de la animación, en segundos animation = new Animation(0.15f, allFrames); . . . // Estas lineas se ejecutaran en los metodos update() y/o render() stateTime += deltaTime; TextureRegion currentFrame = animation.getKeyFrame(stateTime , true); spriteBatch.draw(currentFrame, position.x, position.y); . . . Así, se puede observar en el código que lo que ocurre es que almacenamos la animación completa (con todos los frames que tenga) y en el método donde haya que pintar ese personaje o animación simplemente obtenemos en cada momento el siguiente frame que procede pintar en función del tiempo de juego y del tiempo por frame que hayamos asignado a dicha animación. Hay que tener en cuenta que en cada momento sólo estaremos pintando un frame de la animación. En el proyecto [[https://bitbucket.org/sfaci/libgdx/src/595a4c467c28/animaciones/?at=master|animaciones]] se puede consultar el código de un pequeño ejemplo donde se muestra como poner en marcha las animaciones para un personaje y un enemigo cargando sus texturas desde un ''TextureAtlas''. {{ youtube>qk5qQrgXy4k }} \\ ==== Componentes UI ==== Los componentes para //UI// (User Interface, interfaz de usuario) son aquellos componentes que permiten diseñar e implementar menús para las diferentes partes del juego, tanto para iniciar una partida como para interactuar durante la misma (//ingame menu//).
{{ uimenu.png }} Menú de juego
//libGDX// proporciona una serie de componentes para el diseño de UI (User Interface) a través de la librería [[https://github.com/libgdx/libgdx/wiki/Scene2d|Scene2d]] con el que es posible diseñar e implementar menús de juego muy completos. {{ youtube>znrIfjg6vmQ }} \\ Además, recientemente ha aparecido un proyecto muy interesante que surge como una mejora sobre estos componentes, llamado [[https://github.com/kotcrab/vis-editor/wiki/VisUI|VisUI]] que reimplementa algunos de estos componentes y añade otros que no existen, por lo que resulta una librería mucho más apropiada y completa para trabajar. Para usarlo, simplemente tenemos que añadirlo como extensión en el ''build.gradle'' de nuestro proyecto y añadir una dependencia para descargar la librería (Más información en el enlace a la librería). . . . ext { . . . visuiVersion = '1.3.0-SNAPSHOT' . . . } . . . project(":core") { apply plugin: "java" . . . dependencies { . . . compile "com.kotcrab.vis:vis-ui:$visuiVersion" } } . . . El siguiente fragmento de código muestra cómo construir una //UI// en la que se pinta solamente un botón. En la [[https://github.com/kotcrab/vis-editor/wiki/VisUI|documentación de VisUI]] se puede encontrar una relación de todos los componentes de esta librería con ejemplos y muestras de código de como trabajar con los mismos. public class MainMenuScreen implements Screen { . . . Stage stage; @Override public void show() { . . . if (!VisUI.isLoaded()) VisUI.load(); stage = new Stage(); VisTable table = new VisTable(true); table.setFillParent(true); stage.addActor(table); VisTextButton playButton = new VisTextButton("PLAY"); playButton.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { // Ir a jugar dispose(); } }); VisTextButton configButton = new VisTextButton("CONFIGURATION"); configButton.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { // Ir a la pantalla de configuración dispose(); } }); VisTextButton quitButton = new VisTextButton("QUIT"); quitButton.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { VisUI.dispose(); // Salir del juego } }); VisLabel aboutLabel = new VisLabel("Demo libGDX\n(c) Santiago Faci 2017"); // Añade filas a la tabla y añade los componentes table.row(); table.add(playButton).center().width(200).height(100).pad(5); table.row(); table.add(configButton).center().width(200).height(50).pad(5); table.row(); table.add(quitButton).center().width(200).height(50).pad(5); table.row(); table.add(aboutLabel).left().width(200).height(20).pad(5); Gdx.input.setInputProcessor(stage); } @Override public void render(float dt) { Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); // Pinta la UI en la pantalla stage.act(dt); stage.draw(); } @Override public void resize(int width, int height) { // Redimensiona la escena al redimensionar la ventana del juego stage.getViewport().update(width, height); } @Override public void dispose() { // Libera los recursos de la escena stage.dispose(); } . . . } Y a continuación se muestra el set completo del que dispone la librería //VisUI//, que puede consultarse directamente en su [[http://vis.kotcrab.com/demo/ui|demo online]]
{{ visui.png }} Componentes VisUI
===== Hello World! ===== Antes de comenzar, necesitaremos lanzar el //starter// que hay disponible en la web de libGDX donde además se explica cómo configurarlo en su guía [[https://libgdx.com/wiki/start/setup|Set up a project]]. Con eso conseguiremos crear la semilla de lo que será nuestro proyecto y a partir de lo cual comenzaremos a trabajar. En [[https://libgdx.com/wiki/|la Wiki oficial de libGDX]] existe una guía muy interesante sobre cómo realizar un primer videojuego con este framework, se llama [[https://libgdx.com/wiki/start/a-simple-game|A simple game]] y es una sencilla aplicación que muestra muy bien cómo empezar a trabajar con este motor. Conviene echarle un vistazo puesto que sienta las bases de cómo trabajar con los aspectos más básicos de libGDX. Para este curso, sobre el ejemplo básico he preparado 3 versiones más de ese primer juego utilizando estructuras más complejas para mostrar algunos conceptos más avanzados que se pueden utilizar en videojuegos más complejos: * Versión oficial de "A simple game" [[https://github.com/codeandcoke/libgdx/tree/master/DropGame_v1|ver código]]
{{ drop1.png }} Estructura proyecto v1
* Versión ampliada utilizando Screens [[https://github.com/codeandcoke/libgdx/tree/master/DropGame_v2|ver código]]
{{ drop2.png }} Estructura proyecto v2
* Versión ampliada con managers [[https://github.com/codeandcoke/libgdx/tree/master/DropGame_v3|ver código]]
{{ drop3.png }} Estructura proyecto v3
* Versión utilizando POO (Programación Orientada a Objetos) [[https://github.com/codeandcoke/libgdx/tree/master/DropGame_POO|ver código]]
{{ drop_poo.png }} Estructura proyecto con POO
{{ drop.png }} A simple game
===== Estructura de un videojuego ===== En cuanto a la organización dentro del IDE de programación que utilicemos y su estructura de paquetes, podemos ver a continuación un ejemplo para un videojuego:
{{ project_structure.png?350 |Estructura de un videojuego}} Estructura de un videojuego
En la estructura de arriba se pueden distinguir dos módulos, el principal y el proyecto lanzadera que sólo utilizaremos para ejecutar el juego en una plataforma determinada. Así, podemos distinguir las siguientes partes en un proyecto ==== Módulo principal (core) ==== El módulo principal o **core** contiene todo el desarrollo del juego exceptuando las clases //lanzadoras// (donde se encontrará el método ''main'' que lanza la ejecución del juego) y los //assets// o recursos del videjuego (texturas, imágenes, clips de audio, . . .). No hay una estructura definida en el framework de //libGDX// por lo que a continuación se detallará una estructura habitual y conveniente que permita organizar el código de las diferentes partes de un juego desarrollado con este (y otros) frameworks. === Paquete characters ===
{{ diagrama_characters.png }} Diagrama de clases del paquete characters
Donde podemos organizar las clases que representen las estructuras de los caracteres del juego (personajes, enemigos, items, . . .). Es conveniente diseñar bien las clases para aprovechar al máximo la POO ya que un buen diseño, aunque a priori nos pueda costar, más adelante nos ahorrará infinidad de líneas de código y trabajo * La clase ''Character'' puede contener todos los métodos y atributos comunes a cualquier caracter del videojuego. La idea es que sirva de clase base para todos los personajes o enemigos animados que vayamos a implementar en adelante por lo que normalmente se definirá como abstracta public abstract class Character { public Vector2 velocity; public Vector2 position; public float stateTime; public TextureRegion currentFrame; public Rectangle rect; public int lives; protected boolean dead; . . . public Character(. . .) { . . . } public void render(Batch batch) { batch.draw(currentFrame, position.x, position.y); } public abstract void update(float dt); public abstract void fire(); public abstract void die(); public boolean isDead() { return dead; } public void doSomething() { . . . . . . } . . . } Así, personajes como nuestro jugador heredarán de la clase abstracta ''Character'' para aprovechar todos los métodos ya implementados allí o la estructura ya definida en el caso de los que se definieron como abstractos. public class Player extends Character { public int score; . . . public Player(. . .) { super(. . .); . . . } public void update(float dt) { . . . } . . . } En el caso de otros personajes animados del juego como los enemigos, también podrán heredar de la clase ''Character'' e implementar //a su manera// todas aquellas acciones (métodos) definidas como abstractas puesto que es de esperar que, aunque realicen las mismas acciones que un personaje principal, las hagan de forma diferente (moverse, disparar, morir, . . .) public class Enemy extends Character { public EnemyType type; public boolean invicible; public Enemy(. . .) { super(. . .); . . . } public void update(float dt) { . . . } . . . } === Paquete managers ===
{{ diagrama_managers.png }} Diagrama de clases del paquete managers
En el paquete **managers** podemos encontrar las clases encargadas de gestionar cada parte del videojuego: los recursos, la lógica, el renderizado, los niveles, la configuración y la cámara. No todos son obligatorios ni tampoco tienen una estructura fija, sino que se trata de una forma de organizar el código por funcionalidades, de forma que éste sea mucho más abordable a medida que va aumentando su tamaño. === ResourceManager === La clase ''ResourceManager'' contiene el código que permite realizar la carga y el acceso a todos los recursos (''assets'') public class ResourceManager { public static AssetManager assets = new AssetManager(); public void loadAllResources() { . . . } public static boolean update() { return assets.update(); } public void loadSounds() { . . . } public static TextureRegion getRegion(String name) { . . . } public static Array getRegions(String name) { . . . } public static Sound getSound(String name) { . . . } } === SpriteManager === La clase ''SpriteManager'' contiene todos los métodos que se encargan de gestionar la lógica del videojuego public class SpriteManager { Player player; Array enemies; Array items; . . . public void handleInput() { . . . } private void updateEnemies(float dt) { . . . } private void updateItems(float dt) { . . . } private void updatePlayer(float dt) { . . . } public void update(float dt) { updateEnemies(dt); updateItems(dt); updatePlayer(dt); . . . } . . . } === RenderManager === La clase ''RenderManager'' contiene el código que permite el renderizado (o pintado) de todos los elementos del juego en la pantalla. Es habitual que no contenga mucho código puesto que los cálculos de dónde pintar a cada elemento del juego los realiza el ''SpriteManager'' y esta clase sólo tiene que pintar cada uno de ellos utilizando el método que cada uno tiene implementado public class RenderManager { SpriteBatch batch; BitmapFont font; SpriteManager spriteManager; . . . public void drawFrame() { batch.begin(); drawHud(); spriteManager.player.render(batch); for (Enemy enemy : spriteManager.enemies) enemy.render(batch); for (Item item : spriteManager.items) item.render(batch); batch.end(); } private void drawHud() { . . . } . . . } === LevelManager === La clase ''LevelManager'' se encarga de la carga de niveles (pantallas jugables) y de todos los elementos de los mismos, como pueden ser el mapa, objetos, enemigos y cualquier otro elemento que deba aparecer en la misma. Una vez que éstos son cargados en memoria, será el ''SpriteManager'' el encargado de hacer que se muevan. El ''RenderManager'' sólo los carga desde el fichero donde esté la información del mapa correspondiente. También puede contener los métodos necesarios para realizar el cambio de nivel (el jugador ha llegado al final de uno), reiniciarlo (cuando muere el jugador, por ejemplo) o cualquier otra acción sobre el mismo. public class LevelManager { TiledMap map; int currentLevel; . . . public void loadCurrentLevel() { . . . } public void restartCurrentLevel() { . . . } public void loadEnemies() { . . . } public void loadItems() { . . . } . . . } == ConfigurationManager === La clase ''ConfigurationManager'' es la encargada de almacenar y acceder a las preferencias del usuario. El usuario dispondrá de una pantalla (''Screen'') de personalización del juego para fijar sus valores de forma que el juego en todo momento pueda consultarlas para comportarse como corresponda (desactivar el audio, configurar teclas, dificultad del juego, . . .) public class ConfigurationManager { private static Preferences prefs = Gdx.app.getPreferences(Constants.APP_NAME); public static boolean isSoundEnabled() { . . . } public static int getDifficultyLevel() { . . . } . . . } === CameraManager === La clase ''CameraManager'' se encarga de gestionar la cámara, para que señale siempre en el lugar que corresponda según el jugador avance por el nivel. Normalmente intentará mantener al jugador siempre centrado en la pantalla.
{{ camera1.png}}{{camera2.png }} Cámara
public class CameraManager { OrthographicCamera camera; public void init() { . . . } public void handleCamera() { . . . } . . . } === Paquete screens === En el paquete ''screens'' implementaremos todas las clases que representan a cada uno de los estados de nuestro videojuego. Todas ellas implementan el interface ''Screen''. El interface ''Screen'' es parte del API de //libGDX// y tiene los siguientes métodos, que deberán ser codificados en las clases que la implementen: * ''dispose()'' Invocado cuando se liberan todos los recursos * ''hide()'' Invocado cuando esta ''Screen'' ya no es la actual * ''pause()'' Invocado cuando la ''Screen'' pasa a segundo plano * ''render()'' Invocado como un bucle para renderizar la ''Screen'' actual * ''resize()'' Invocado cuando se redimensiona la pantalla * ''resume()'' Invocado cuando la ''Screen''pasa a primer plano * ''show()'' Invocado en el momento en que esta ''Screen'' pasa a ser la actual Teniendo en cuenta que el método ''dispose()'' no se invoca automáticamente Una estructura clásica para los estados de un juego implementado con //Screens// sería el siguiente: * ''SplashScreen'': Pantalla de carga del juego (y de los //assets//) * ''MainMenuScreen'': Pantalla con el menú principal * ''GameScreen'': Pantalla donde el usuario juega la partida * ''ConfigurationScreen'': Pantalla donde el usuario configura el juego * ''GameOverScreen'': Pantalla donde se indica al usuario que ha terminado la partida Se puede ver a continuación el diseño de clases que quedaría para el ejemplo anterior:
{{ diagrama_screens.png }} Diagrama de clases del paquete screens
Así, la estructura básica de cada ''Screen'' se muestra a continuación: public class MyScreen implements Screen { // Si es un menú private Stage stage; public MyScreen() { . . . } /* * Este método se ejecuta en el momento en que se cambia a esta Screen * Si esta Screen carga un menú es el momento de crearlo * Si es una pantalla de juego, es el momento de inicializar lo que no * se ha inicializado en el constructor */ @Override public void show() { . . . } /* * Es el bucle principal de la Screen, donde se pinta el menú o donde ocurre * la partida, según el tipo de Screen */ @Override public void render(float dt) { . . . // Cambia de pantalla (cuando ocurra algún evento) ((Game) Gdx.app.getApplicationListener).setScreen(new AnotherScreen()); dispose(); . . . // Si tiene que mostrar un menú stage.act(dt); stage.draw(); . . . // Si es la pantalla de juego, se invocará aquí a la lógica, // renderizado y demás partes del juego checkInput(); renderFrame(); checkCollisions(); . . . // O bien actualizar los diferentes managers si se ha estructurado // asi el juego spriteManager.update(dt); renderManager.render(dt); . . . } @Override public void resize(int width, int height) { . . . } @Override public void hide() { . . . } @Override public void pause() { . . . } @Override public void resume() { . . . } @Override public void dispose() { // Si es un menú stage.dispose(); . . . } } === Paquete util === En el paquete ''util'' implementaremos, al menos, dos clases: * La clase ''Constants'' que contendrá todas las constantes que vayamos a utilizar en todo el proyecto del videojuego public class Constants { public static final String APP_NAME = "minijumper"; public static final int SCREEN_WIDTH = 1024; public static final int SCREEN_HEIGHT = 768; public static final int TILE_WIDTH = 32; public static final int TILE_HEIGHT = 32; . . . public static final int TILES_IN_CAMERA = 16; public static final int CAMERA_WIDTH = TILES_IN_CAMERA * TILE_WIDTH; public static final int CAMERA_HEIGHT = TILES_IN_CAMERA * TILE_HEIGHT; . . . } * La clase ''Util'' donde implementaremos todos los métodos transversales al proyecto que puedan ser de utilidad en cualquier parte del mismo y no tengan cabida en ninguna de las clases en las que estructuramos nuestro videojuego, normalmente como método estáticos public class Util { . . . } === La clase principal === La clase principal del **core** contiene muy poco código. Es la clase que se ejecutará desde los proyectos lanzadera desde las diferentes plataformas desde donde el juego sea ejecutado. En nuestro caso simplemente cargará la pantalla de ''SplashScreen'' que será la encargada de cargar el juego para acabar mostrando el menú principal al jugador, pudiendo así comenzar a interactuar con el mismo (según el [[http://multimedia.codeandcoke.com/apuntes:libgdx#estados_de_un_videojuego|diagrama de estados]] que hayamos definido). public class MiniJumper extends Game { @Override public void create() { . . . ((Game) Gdx.app.getApplicationListener).setScreen(new SplashScreen()); } @Override public void render() { super.render(); } @Override public void dispose() { . . . } } ==== Módulo lanzadera (desktop) ==== Módulo lanzadera, en este caso para PC (deskto), con una sola clase que será la encargada de lanzar la ejecución del juego === La clase DesktopMiniJumper === public class DesktopMiniJumper { public static void main(String[] args) { LwjglApplicationConfiguration configuration = new LwjglApplicationConfiguration(); configuration.title = "MiniJumper"; configuration.width = Constants.SCREEN_WIDTH; configuration.height = Constants.SCREEN_HEIGHT; configuration.fullscreen = true; new LwjglApplication(new MiniJumper(), configuration); } } Además, como se puede ver en la estructura del proyecto, este módulo Lanzadera contendrá una carpeta donde se almacenarán todos los recursos (o assets) del videojuego, de los que hablaremos más adelante en [[http://multimedia.codeandcoke.com/apuntes:libgdx#gestion_de_recursos|Gestión de Recursos]] ==== Diagrama de clases de un proyecto de videojuego ==== Para terminar, a continuación se muestra un diagrama de clases de un proyecto estándar de videojuego donde se puede ver cómo se relacionan éstas.
{{ uml_game.png |Estructura de clases}} Estructura de clases básica
===== Estados de un videojuego ===== El diagrama de estados de un videojuego define los diferentes estados en los que se puede encontrar el jugador durante la ejecución del mismo. En el caso de //libGDX// esto viene predefinido mediante la interface ''Screen'' de la que debemos implementar para definir cada uno de nuestros estados o ''Screens'' (ver [[|Paquete screens]] A continuación se muestra el diagrama básico que sigue cualquier juego desde que se carga (//Loading//) pasando por mostrar el logo utilizando, por ejemplo, una //SplashScreen// para llegar a un estado //Main menu// donde el jugador podrá escoger entre acceder al estado //Credits// para ver información sobre los autores del juego o bien pasar a //Level selection// para escoger un nivel y comenzar una partida si accede al estado //Game//. Igualmente se puede ver como es posible regresar a algunos estados anteriores, por ejemplo, desde el estado //Game// es posible volver atrás hasta //Main menu// para escoger otra acción o bien repetir. En cualquier caso será decisión nuestra definir qué estados tendrá nuestro juego, puesto que a los que hemos visto ahora podríamos añadir otros como //Game over//, //Preferences// para mostrar la pantalla de fin de partida o bien configurar la partida, entre otros.
{{ game_screens.png |Estados de un videojuego}} Estados de un videojuego
Y desde el punto de vista más técnico utilizando //libGDX//, la dependencia entre los diferentes estados utilizando el interface ''Screen'' que //libGDX// nos proporciona, quedaría como sigue
{{ screen-management.png |Gestión de pantallas o escenas}} Gestión de pantallas o escenas ((gráfico de [[http://www.pixnbgames.com/blog/|PixNB]]))
{{ youtube>P7fImmqd2_E }} \\ ===== Gestión de recursos ===== La gestión de recursos de un videojuego incluye la gestión de todo aquello que forma parte del mismo que no sea código. Podríamos hablar de clips de audio, texturas de los personajes, ficheros de los niveles diseñados con alguna aplicación de terceros ([[http://www.mapeditor.org|Tiled]], por ejemplo), texturas para el diseño de los menús de juego y cualquier otro fichero de este tipo.
{{assets.png|Gestión de recursos}} Gestión de recursos
Lo que haremos será crear una clase que contenga todo lo necesario para gestionar los recursos de nuestro juego, desde la carga de los mismos durante la ejecución hasta los métodos que nos permitan acceder a los mismos en cualquier instante. Todo eso lo haremos a través de lo que llamaremos el ''ResourceManager'': En el caso de que almacenemos nuestras texturas como un ''TextureAtlas'' (tal y como se hace para el ejemplo) conviene echar un vistazo a la parte donde se habla de [[http://multimedia.codeandcoke.com/apuntes:libgdx#atlas_de_texturas_textureatlas|los atlas de texturas]]. public class ResourceManager { // Clase de libGDX que permite la gestión de assets public static AssetManager assets = new AssetManager(); /** * Carga todos los recursos del juego */ public static void loadAllResources() { assets.load("my_texture_atlas.pack", TextureAtlas.class); loadSounds(); loadMusics(); } /** Actualiza la carga de recursos */ public static boolean update() { return assets.update(); } /** * Carga los sonidos */ public static void loadSounds() { assets.load("sounds" + File.separator + "game_begin.wav", Sound.class); . . . } /** * Carga las músicas */ public static void loadMusics() { assets.load("musics" + File.separator + "bso.mp3", Music.class); . . . } /** * Obtiene una región de textura o la primera de una animación * @param name * @return */ public static TextureRegion getRegion(String name) { return assets.get(TEXTURE_ATLAS, TextureAtlas.class).findRegion(name); } /** * Obtiene una región de textura determinada de las que forman una animación */ public static TextureRegion getRegion(String name, int position) { return assets.get(TEXTURE_ATLAS, TextureAtlas.class).findRegion(name, position); } /** * Obtiene todas las regiones de textura que forman una misma animación * @param name * @return */ public static Array getRegions(String name) { return assets.get(TEXTURE_ATLAS, TextureAtlas.class).findRegions(name); } /** * Obtiene un sonido determinado */ public static Sound getSound(String name) { return assets.get(name, Sound.class); } /** * Obtiene una música determinada */ public static Music getMusic(String name) { return assets.get(name, Music.class); } } Así, teniendo nuestro propio gestor de recursos, lo más conveniente será realizar la carga de los mismos durante la carga del juego en lo que se suele conocer como ''SplashScreen''. De esa manera podremos mostrar al usuario información sobre el juego mientras éste se carga junto con todos los assets del mismo: public class SplashScreen implements Screen { private Texture splashTexture; private Image splashImage; private Stage stage; private boolean splashDone = false; public SplashScreen() { splashTexture = new Texture(Gdx.files.internal("ui/splash.png")); splashImage = new Image(splashTexture); } @Override public void show() { stage = new Stage(); Table table = new Table(); table.setFillParent(true); table.center(); // Muestra la imagen de SplashScreen como una animación splashImage.addAction(Actions.sequence(Actions.alpha(0), Actions.fadeIn(1f), Actions.delay(1.5f), Actions.run(new Runnable() { @Override public void run() { splashDone = true; } }) )); table.row().height(splashTexture.getHeight()); table.add(splashImage).center(); stage.addActor(table); // Lanza la carga de recursos ResourceManager.loadAllResources(); } @Override public void render(float dt) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_CLEAR_VALUE); stage.act(dt); stage.draw(); // Comprueba si se han cargado todos los recursos if (ResourceManager.update()) { // Si la animación ha terminado se muestra ya el menú principal if (splashDone) { ((Game) Gdx.app.getApplicationListener).setScreen(new MainMenuScreen()); } } } @Override public void resize(int width, int height) { stage.setViewport(width, height); } . . . @Override public void dispose() { splashTexture.dispose(); stage.dispose(); } } ==== Atlas de texturas (TextureAtlas) ==== El atlas de texturas consiste en empaquetar en una sola imagen todas las texturas que se necesiten para el juego. De esa manera, desde nuestro ''ResourceManager'' podemos cargar todas ellas minimizando el acceso a disco y podremos acceder además de una manera más directa puesto que //libGDX// proporciona una API para extraer del atlas las texturas por nombre y también como un ''Array'' de todas aquellas que pertenezcan a la misma animación. Hay que tener en cuenta que al fichero .png que se genera con todas las texturas le acompañará un fichero de texto que contiene toda la información de cómo están distribuidas dichas texturas en el empaquetado. Asi, podremos obtener cualquier textura por su nombre o bien toda la colección de texturas que formen una misma animación. E incluso algún frame suelto de aquellos que forman una animación con otros.
{{ minijumper.png }} Atlas de texturas para un juego de plataformas. Debajo su fichero de empaquetado
minijumper.png format: RGBA8888 filter: Linear,Linear repeat: none big_stone_broken rotate: false xy: 1, 53 size: 75, 74 orig: 75, 74 offset: 0, 0 index: -1 big_stone rotate: false xy: 78, 53 size: 74, 74 orig: 74, 74 offset: 0, 0 index: 1 big_stone rotate: false xy: 154, 53 size: 74, 74 orig: 74, 74 offset: 0, 0 index: 2 . . . Además, //libGDX// a través del [[https://github.com/libgdx/libgdx/wiki/Texture-packer|Texture packer]] proporciona una API para realizar el empaquetado de una serie de texturas de manera procedimental, con unas pocas líneas de código. De esa manera, lo recomendable será tener preparadas todas las texturas para empaquetar en una misma carpeta y lanzar la ejecución del siguiente fragmento de código, que cogerá todas las texturas que haya en la carpeta ''core/assets/textures'' y las empaquetará formando un ''TextureAtlas'' que dejará en la carpeta ''core/assets'' al que llamará ''atlas.pack'' (fichero de empaquetado) y al que acompañar la imagen con todas las texturas con el nombre ''atlas.png'' public class Packer { public static void main(String[] args) { TexturePacker2.Settings settings = new TexturePacker2.Settings(); settings.maxWidth = 1024; settings.maxHeight = 1024; settings.filterMag = Texture.TextureFilter.Linear; settings.filterMin = Texture.TextureFilter.Linear; TexturePacker2.process(settings, "core/assets/textures", "core/assets", "atlas.pack"); } } Y también es posible crear estos //TextureAtlas// utilizando una aplicación llamada [[https://github.com/crashinvaders/gdx-texture-packer-gui|gdx-texturepacker-gui]] que permite realizar el empaquetado de las imágenes de nuestro videojuego desde un sencillo interfaz de usuario.
{{ gdx-texturepacker-gui.png }} gdx-texturepacker-gui
===== Gestión de preferencias ===== public class ConfigurationManager { private static Preferences prefs = Gdx.app.getPreferences(Constants.APP_NAME);; /** * Comprueba si el sonido está o no activado durante el juego * @return */ public static boolean isSoundEnabled() { return prefs.getBoolean("sound"); } } . . . if (ConfigurationManager.isSoundEnabled()) ResourceManager.getSound(SOUND + "blop.mp3").play(); . . . ===== Colisiones =====
{{ collisions1.png }} Colisiones por rectángulos
{{ collisions2.png }} Colisiones por círculos
{{ collisions3.png }} Escoger la forma más apropiada
{{ collisions4.png }} Múltiples dominios de colisión
{{ youtube>tXHQVIB2w4g }} \\ ===== Creación y gestión de niveles ===== ==== TiledMaps ==== Los //TiledMaps// son niveles diseñados como mapas donde el área se divide en //tiles// o //baldosas// (también llamados //patrones//) que formarán tanto el terreno como todos los elementos que puedan aparecer en el juego. Además, existe la posibilidad de añadir esos patrones como objetos de forma que se puede trabajar con ellos con mayor libertad y también incluso añadir formas geométricas como rectángulos, líneas o círculos para definir formas más complejas con las que se pueda interactuar durante la partida. En este caso el mapa se diseña con una aplicación de terceros (en nuestro caso [[http://www.mapeditor.org|Tiled]] y desde el código, utilizando la API que //libGDX// nos proporciona, podremos pintar el mapa y acceder a toda la información que en él hayamos diseñado: * Podemos diseñar el mapa en varias capas por lo que podremos decidir cuáles de ellas queremos que se pinten en cada momento * Las capas permiten decidir qué elementos están por delante/por detrás de los demás * Utilizaremos las capas de patrones normalmente para definir el terreno y fondo del mapa. Lo habitual será que sean las capas que pintaremos siempre en cada frame (a no ser que queramos ocultar alguna en algún momento) * Utilizaremos las capas de objetos para diseñar elementos animados (enemigos, por ejemplo) y objetos del juego. Estos elementos los cargaremos al principio del nivel para animarlos como objetos de nuestro proyecto (en nuestro código, como clases y objetos que habremos implementado para ellos) * Cada patrón u objeto podrá tener una serie de características a las que podremos acceder desde el proyecto para parametrizar dichos elementos Asi, a continuación se muestra una captura de pantalla de la aplicación [[http://www.mapeditor.org|Tiled]] para el diseño de //TiledMaps// donde se pueden ver: * El diseño del mapa * Las capas utilizadas * Los conjuntos de patrones (tilesets) de donde se cogen las baldosas para pintar el mapa * La sección donde se añaden los atributos (características) a cada uno de los patrones u objetos del mapa
{{ tiled2.png?800 }} TiledMap editado con la aplicación Tiled
El siguiente fragmento de código permitiría, desde una ''Screen'', cargar el mapa e instanciar los objetos necesarios para moverse sobre él (la cámara) y el objeto encargado de renderizar tanto el mapa como los objetos que aparezcan durante la partida (objeto ''Batch'' que obtenemos del ''mapRenderer''). La cámara sólo será necesario en los casos en los que el mapa que diseñemos no vaya a ocupar toda la pantalla. Si todo el mapa ocupa justo una pantalla no será necesaria. En el caso de que las dimensiones del mapa sobrepasen las de la pantalla y el personaje pueda avanzar a lo largo del mismo, tendremos que enfocarle a lo largo de la partida haciendo uso de la cámara, que moveremos en función de los movimientos de dicho personaje (derecha/izquierda y/o arriba/abajo, según proceda). . . . Batch batch; OrthographicCamera camera; TiledMap map; OrthogonalTiledMapRenderer mapRenderer; . . . @Override public void show() { . . . camera = new OrthographicCamera(); // Fija la anchura y altura de la camara en base al número de tiles que se mostrarán camera.setToOrtho(false, TILES_IN_CAMERA_WIDTH * TILE_WIDTH, TILES_IN_CAMERA_HEIGHT * TILE_WIDTH); camera.update(); map = new TmxMapLoader().load("levels/level1.tmx"); mapRenderer = new OrthogonalTiledMapRenderer(map); batch = mapRenderer.getBatch(); mapRenderer.setView(camera); . . . } Así, para hacer el renderizado del mapa en la pantalla, podemos hacerlo directamente con el objeto ''mapRenderer'' indicándole qué capas queremos dibujar. En este caso solamente pintamos las capas 0 y 1 (background y terrain según la captura anterior) que son las capas que dibujan el nivel. El resto de elementos del juego los instanciaremos como objetos de nuestro proyecto ya que algunos son móviles y otros pueden desaparecer de forma individual. Si tuvieramos que pintar algún otro elemento del juego como nuestro personaje, utilizaríamos para ello el objeto ''Batch'' que hemos obtenido del objeto ''mapRenderer'' durante la carga del mapa. Recordad que habrá que declarar e instanciar al personaje en la carga del ''Screen'' junto con el mapa y la cámara. . . . @Override public void render(float dt) { . . . mapRenderer.render(new int[]{0, 1}); batch.begin(); batch.draw(player.imagen, player.posicion.x, player.posicion.y); batch.end(); . . . } . . . Ahora sólo nos quedará permitir que la cámara (en este caso las dimensiones del mapa no caben en la pantalla) que hemos utilizado para visualizar el mapa se mueva en función de los movimientos de nuestro jugador. . . . @Override public void render(float dt) { . . . handleCamera(); . . . } private void handleCamera() { if (player.position.x < TILES_IN_CAMERA_WIDTH * TILE_WIDTH / 2) camera.position.set(TILES_IN_CAMERA_WIDTH * TILE_WIDTH / 2, TILES_IN_CAMERA_HEIGHT * TILE_WIDTH / 2 - TILE_WIDTH, 0); else camera.position.set(player.position.x, TILES_IN_CAMERA_HEIGHT * TILE_WIDTH / 2 - TILE_WIDTH, 0); camera.update(); mapRenderer.setView(camera); } . . . Para probar el ejemplo completo faltaría añadir una mínima gestión del input del usuario para que el personaje se pueda al menos mover de izquierda a derecha por la pantalla. . . . @Override public void render(float dt) { . . . handleInput(); . . . } private void handleInput() { if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) player.move(new Vector2(-10, 0)); else if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) player.move(new Vector2(10, 0)); } . . . Hay que tener en cuenta que todavía no hemos añadido colisiones ni al resto de elementos del juego (los diamantes y los enemigos). Simplemente tenemos a un personaje capaz de moverse por el mapa y la cámara siguiendo al mismo.
{{ camara.gif }} Movimiento de cámara sobre un TiledMap
{{ youtube>J9jndvIP1nM }} \\ ==== Gestión de objetos de TiledMaps ==== Ya hemos visto como crear el mapa con //Tiled//, cómo pintar las capas de patrones en la pantalla y cómo mover el personaje a lo largo del mismo utilizando una cámara. En el siguiente fragmento de código se muestra cómo recorrer todos los objetos de una capa del mapa y cuáles son las operaciones que podemos hacer con ellos según queramos instanciarlos para incorporarlos a la lógica de nuestro programa o bien comprobar simplemente si el personaje colisiona con él, por ejemplo en el caso de que se trate de rectángulos que delimiten terreno. . . . // Obtiene todos los objetos de la capa 'colision' MapLayer collisionsLayer = map.getLayers().get("colision"); for (MapObject object : collisionsLayer.getObjects()) { RectangleMapObject rectangleObject = (RectangleMapObject) object; // Caso 1: Comprueba si el objeto contiene una propiedad if (rectangleObject.getProperties().contains("enemy")) { . . . } // Caso 2: Obtiene el valor de una propiedad int score = Integer.parseInt(rectangleObject.getProperties().get("score")); . . . // Caso 3: Obtiene el rectangulo ocupado por el objeto Rectangle rect = rectangleObject.getRectangle(); if (player.rect.overlaps(rect)) { . . . } } . . . * **Caso 1**: Queremos comprobar el tipo de objeto. Suponemos que lo hemos 'marcado' en el mapa asignándole una propiedad sin valor
{{ object_properties_2.png }} Objeto marcado con una propiedad
* **Caso 2**: Queremos extraer el valor de una propiedad asignada a un objeto del mapa
{{ object_properties.png }} Objeto y sus propiedades
* **Caso 3**: Queremos obtener el rectángulo que ocupa el objeto para comprobar si el jugador ha colisionado con él
{{ rectangle_collision.png }} Rectángulos como objetos para delimitar zonas de colisión
===== Acceso a Bases de Datos: SQLite ===== Para el acceso a Bases de Datos, puesto que programamos en Java, podríamos utilizar, técnicamente, cualquier motor que pudiera usarse con este lenguaje. Pero, dadas las características de los videojuegos que normalmente se desarrollan, donde la necesidad de almacenamiento es algo secundario, podemos utilizar un motor ligero y sencillo de utilizar como es [[http://sqlite.org|SQLite]]. Si el diseño o la mecánica de nuestro juego se centrara en una gran Base de Datos siempre podríamos pensar en otros SGBDs más grandes, pero si queremos almacenar puntuaciones, inventarios, partidas, . . . lo más interesante es utilizar un motor ligero y que además no requiere instalación puesto que apenas ocupa 1 MB === Almacenar información === try { Class.forName("org.sqlite.JDBC"); Connection connection = null; connection = DriverManager.getConnection("jdbc:sqlite:" + Gdx.files.internal("scores.db")); String sql = "CREATE TABLE IF NOT EXISTS scores (id integer primary key autoincrement, name text, score int)"; PreparedStatement statement = connection.prepareStatement(sql); statement.executeUpdate(); sql = "INSERT INTO scores (name, score) VALUES (?, ?)"; statement = connection.prepareStatement(sql); statement.setString(1, name); statement.setInt(2, score); statement.executeUpdate(); if (statement != null) statement.close(); if (connection != null) connection.close(); } catch (ClassNotFoundException cnfe) { cnfe.printStackTrace(); } catch (SQLException sqle) { sqle.printStackTrace(); } === Consultar información === try { Class.forName("org.sqlite.JDBC"); Connection connection = null; connection = DriverManager.getConnection("jdbc:sqlite:" + Gdx.files.internal("scores.db")); String sql = "SELECT name, score FROM scores ORDER BY score DESC LIMIT 10"; PreparedStatement statement = connection.prepareStatement(sql); ResultSet result = statement.executeQuery(); List scores = new ArrayList(); Score score; while (result.next()) { score = new Score(); score.name = result.getString("name"); score.score = result.getInt("score"); scores.add(score); } if (statement != null) statement.close(); if (result != null) result.close(); if (connection != null) connection.close(); return scores; } catch (ClassNotFoundException cnfe) { cnfe.printStackTrace(); } catch (SQLException sqle) { sqle.printStackTrace(); } return new ArrayList(); Se puede encontrar un driver para SQLite y Java en la web del proyecto [[https://github.com/xerial/sqlite-jdbc|Xerial-SQLite]]. ===== Empaquetar el videojuego ===== Actualmente, puesto que los proyectos de libGDX se construyen de forma más o menos automática, lo más cómodo es [[https://libgdx.com/wiki/deployment/deploying-your-application|empaquetar el videojuego utilizando Gradle]] ---- ===== Proyectos de ejemplo ===== * [[https://github.com/codeandcoke/libgdx/tree/master/DropGame_v1|DropGame I]] Juego sencillo con colisiones. Es la implementación del juego de ejemplo de //libGDX// * [[https://github.com/codeandcoke/libgdx/tree/master/DropGame_v2|DropGame II]] Versión ampliada utilizando estados utilizando la interfaz ''Screen'' * [[https://github.com/codeandcoke/libgdx/tree/master/DropGame_v3|DropGame III]] Versión ampliada utilizando Managers * [[https://github.com/codeandcoke/libgdx/tree/master/DropGame_POO|DropGame IV (POO)]] Versión mejorada utilizando Programación Orientada a Objetos * [[https://github.com/codeandcoke/libgdx/tree/master/animaciones|Animaciones]] Proyecto de ejemplo para el uso de animaciones * [[https://github.com/codeandcoke/libgdx/tree/master/robin2dx|Robin2dx (juego tipo RPG)]] Proyecto de ejemplo para el uso de TiledMaps Puede encontrarse más proyectos de ejemplo en el repositorio [[https://github.com/codeandcoke/libgdx|libgdx]] y unos pocos más en el repositorio [[https://github.com/codeandcoke/multimedia-ejercicios|multimedia-ejercicios]] donde están los proyectos que vamos haciendo en clase. ==== Juegos de ejemplo ==== * [[https://github.com/codeandcoke/jumper2dx|Jumper2dx (juego plataformas)]]
{{jumper2dx.png?700 }} Jumper2dx, juego de plataformas
* [[https://github.com/codeandcoke/jfighter2dx|Jfighter2dx (juego de naves)]]
{{jfighter2dx.png?700 }} JFighter2dx, juego de naves
* [[https://github.com/codeandcoke/jbombermanx|JBombermanx (juego de mapas)]]
{{jbombermanx.png?700 }} JBombermanx, juego de mapas
* [[https://github.com/codeandcoke/arkanoidx|Arkanoidx (juego tipo puzzle)]]
{{arkanoidx.png?700 }} Arkanoidx, juego de puzzles
* [[https://github.com/codeandcoke/minijumper2dx|MiniJumper2dx (juego plataformas)]]
{{minijumper2dx.png?700 }} Minijumper2dx, juego de plataformas
---- (c) 2016-2023 Santiago Faci