En este tutorial vamos a ver cómo usar cámaras (y en el siguiente, ventanas gráficas) en LibGDX. Lo admito, llegué un poco tarde al cubrir este tema, ya que debería haberlo cubierto mucho antes en la serie. De hecho, en algunos tutoriales anteriores utilicé cámaras con poca discusión previa. Más vale tarde que nunca, ¿no?
La primera pregunta inmediata que viene a la mente es probablemente «¿Qué es una cámara, qué es una ventana gráfica y en qué se diferencian?».
Bueno, básicamente una cámara es responsable de ser el «ojo» de los jugadores en el mundo del juego. Es una analogía con la forma en que funcionan las cámaras de video en el mundo real. Una ventana gráfica representa cómo se muestra al espectador lo que ve la cámara. Cualquier manera fácil de pensar en esto es pensar en su caja de cable o satélite HD y su televisor HD. La señal de video entra en su caja (esta es la cámara), esta es la imagen que se va a mostrar. Luego, sus dispositivos de TV muestran cómo mostrar la señal que ingresa. Por ejemplo, la caja puede enviarle una imagen de 480i o una imagen de 1080p, y es responsabilidad de su televisor decidir cómo se muestra. Esto es lo que hace una ventana gráfica… toma una imagen entrante y la adapta para que funcione mejor en el dispositivo al que se le envía. En algún momento, esto significa estirar la imagen, mostrar barras negras o posiblemente no hacer nada.
Así que, simple descripción resumida…
- Cámara: ojo en la escena, determina lo que el jugador puede ver, utilizado por LibGDX para renderizar la escena.
- Viewport: controla cómo se muestran al usuario los resultados de renderizado de la cámara, ya sea con barras negras, estirados o sin hacer nada.
En LibGDX hay dos tipos de cámaras, la PerspectivaCámara y el Cámara Ortográfica. Ambas son palabras muy grandes y algo aterradoras, pero ninguna tiene por qué serlo. En primer lugar, si está trabajando en un juego en 2D, hay un cambio del 99,9 % en que desea una cámara ortográfica, mientras que si está trabajando en 3D, lo más probable (pero no siempre) es que desee utilizar una cámara en perspectiva.
Ahora, la diferencia entre ellos. Una cámara en perspectiva intenta imitar la forma en que el ojo humano ve el mundo (en lugar de cómo funciona realmente el mundo). Para el ojo humano, cuanto más se aleja algo, más pequeño parece. Una de las formas más fáciles de ilustrar el efecto es encender Blender y ver un Cubo en ambas perspectivas:
Perspectiva renderizada:
Representación ortográfica:

Cuando se trata de 3D, una cámara Perspectiva se parece mucho más a lo que esperamos en el mundo real. Sin embargo, cuando trabaja en 2D, en realidad todavía está en 3D, pero en su mayor parte está ignorando la profundidad (excepto por el orden de los sprites). En un juego 2D, no querrás que los objetos cambien de tamaño cuanto más «dentro de la pantalla» estén.
Entonces, versión TL; DR, si estás haciendo un juego 2D, probablemente quieras Orthographic. Si no lo eres, probablemente no lo hagas.
Ok, suficiente charla, tiempo de CÓDIGO.
Vamos a implementar una cámara ortográfica simple que se desplaza alrededor de una sola imagen que representa nuestro mundo de juego. Para esta demostración, voy a usar esta imagen de 2048 × 1024 (haga clic en ella para obtener la resolución completa, la versión no comprimida o cree la suya propia):

¡Habilidades de pintura en su máxima expresión! Ahora veamos cómo renderizar esto usando una cámara:
package com.gamefromscratch; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; public class CameraDemo extends ApplicationAdapter { SpriteBatch batch; Texture img; OrthographicCamera camera; @Override public void create () { batch = new SpriteBatch(); img = new Texture("TheWorld.png"); camera = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics.getHeight()); } @Override public void render () { Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); camera.update(); batch.setProjectionMatrix(camera.combined); batch.begin(); batch.draw(img, 0, 0); batch.end(); } }
El proceso es bastante simple. Simplemente creamos una cámara, luego, en nuestro bucle de renderizado, llamamos a su método update() y luego lo configuramos como la matriz de proyección de nuestro SpriteBatch. Si es nuevo en 3D (2D falso), ProjectionMatrix junto con View Matrix son multiplicaciones basadas en matrices responsables de transformar datos 3D en espacio de pantalla 2D. Camera.combined devuelve la vista de la cámara y las matrices de perspectiva multiplicadas. Esencialmente, este proceso es lo que posiciona todo, desde la escena hasta la pantalla.
Ahora, si seguimos adelante y ejecutamos este código, vemos:

Hmmmm… puede que no sea lo que esperabas. Entonces, ¿qué pasó exactamente aquí?
Bueno, la cámara está ubicada en la posición 0,0. Sin embargo, la lente de la cámara está en realidad en su centro. El rojo que ves en la imagen de arriba son las partes de la escena que no tienen nada. Entonces, en el ejemplo anterior, si desea comenzar en la parte inferior izquierda de su mundo, debe tener en cuenta las dimensiones de la cámara. Al igual que:
camera = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics.getHeight()); camera.translate(camera.viewportWidth/2,camera.viewportHeight/2);
Ahora, cuando lo ejecuta, los resultados probablemente sean más parecidos a los que esperaba:

En los ejemplos anteriores, en realidad hice algo que realmente no quieres hacer en la vida real:
camera = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics.getHeight());
Estoy configurando la ventana gráfica de la cámara para usar la resolución de los dispositivos, luego estoy traduciendo usando píxeles. Si está trabajando en varios dispositivos, es casi seguro que este no es el enfoque que desea adoptar, ya que muchos dispositivos diferentes tienen resoluciones diferentes. En cambio, lo que normalmente desea hacer es trabajar en unidades mundiales de alguna forma. Esto es especialmente cierto si está trabajando con un motor de física como Box2D.
Entonces, ¿qué es una unidad mundial? La respuesta corta es, ¡lo que quieras que sea! Lo bueno es que, independientemente de las unidades que elija, se comportará igual en todos los dispositivos con la misma relación de aspecto! La relación de aspecto es el factor más importante aquí. La relación de aspecto es la relación de píxeles horizontales a verticales.
Tomemos el código anterior y modifíquelo ligeramente para que ya no use coordenadas de píxeles. En cambio, definiremos nuestro mundo como 50 unidades de ancho por 25 de alto. Luego vamos a configurar nuestra cámara para que esté a la altura del mundo y centrada. Finalmente, lo conectaremos para que pueda controlar la cámara usando las teclas de flecha. Veamos el código:
package com.gamefromscratch; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.graphics.g2d.SpriteBatch; public class CameraDemo2 extends ApplicationAdapter implements InputProcessor { SpriteBatch batch; Sprite theWorld; OrthographicCamera camera; final float WORLD_WIDTH = 50; final float WORLD_HEIGHT = 25; @Override public void create () { batch = new SpriteBatch(); theWorld = new Sprite(new Texture(Gdx.files.internal("TheWorld.png"))); theWorld.setPosition(0,0); theWorld.setSize(50,25); float aspectRatio = (float)Gdx.graphics.getHeight()/(float)Gdx.graphics.getWidth(); camera = new OrthographicCamera(25 * aspectRatio ,25); camera.position.set(WORLD_WIDTH/2,WORLD_HEIGHT/2,0); Gdx.input.setInputProcessor(this); } @Override public void render () { Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); camera.update(); batch.setProjectionMatrix(camera.combined); batch.begin(); theWorld.draw(batch); batch.end(); } @Override public boolean keyUp(int keycode) { return false; } @Override public boolean keyDown(int keycode) { if(keycode == Input.Keys.RIGHT) camera.translate(1f,0f); if(keycode == Input.Keys.LEFT) camera.translate(-1f,0f); if(keycode == Input.Keys.UP) camera.translate(0f,1f); if(keycode == Input.Keys.DOWN) camera.translate(0f,-1f); return false; } @Override public boolean keyTyped(char character) { return false; } @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { return false; } @Override public boolean touchUp(int screenX, int screenY, int pointer, int button) { return false; } @Override public boolean touchDragged(int screenX, int screenY, int pointer) { return false; } @Override public boolean mouseMoved(int screenX, int screenY) { return false; } @Override public boolean scrolled(int amount) { return false; } }
Ahora cuando ejecutas este código:

Ahora veamos qué sucede cuando cambiamos la resolución de nuestra aplicación. Como ya no usamos píxeles, los resultados deberían ser bastante fluidos.
En caso de que lo haya olvidado, establece la resolución en la clase de aplicación específica de la plataforma. Para iOS y Android, no puede configurar la resolución. Para HTML y Desktop puede hacerlo. Establecer la resolución en el escritorio es cuestión de editar DesktopLauncher, así:
public class DesktopLauncher { public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); config.width = 920; config.height = 480; new LwjglApplication(new CameraDemo2(), config); } }
Aquí está el código que se ejecuta en 920 × 480

Y aquí está 1280×400:

Como puede ver, el código se actualiza para que los resultados se muestren de forma independiente de los píxeles.
Sin embargo, y como puede ver arriba, si sus relaciones de aspecto no se mantienen iguales, los resultados se ven muy diferentes. En el ejemplo anterior, puede ver en el renderizado de 1280 × 400, los resultados se ven aplastados y el mundo contiene mucho menos contenido. Obviamente, los valores que usé para ilustrar este punto son bastante extremos. En su lugar, usemos las dos relaciones de aspecto diferentes más comunes, 16: 9 (la mayoría de los dispositivos Android, muchas consolas que funcionan a 1080p) y 4: 3 (las señales iPad y SD NTSC):
16:9 resultados:

4:3 resultados:

Si bien es mucho el cierre, los resultados aún se verán bastante horribles. Hay dos formas de lidiar con esto, ambas tienen sus méritos.
Primero, es crear una versión para cada relación de aspecto principal. Esto en realidad tiene mucho sentido, aunque puede ser un dolor en el trasero. Los resultados finales son los mejores, pero duplica la carga de los activos artísticos. Hablaremos sobre la gestión de activos de arte de resolución múltiple en un tutorial diferente en una fecha posterior.
En segundo lugar, elige una relación de aspecto nativa para que se ejecute su juego, luego usa una ventana gráfica para administrar las diferentes relaciones de aspecto, al igual que usa el botón de aspecto en su televisor. Dado que este tutorial se está haciendo bastante largo, cubriremos las ventanas gráficas en la siguiente sección, ¡así que estad atentos!