En el tutorial anterior obtuvimos un mapa de mosaico simple de arriba hacia abajo funcionando. Ahora queremos agregarle algo de carácter, literalmente. Dependiendo de sus necesidades, esto puede ser ridículamente simple o algo complejo. Echemos un vistazo a un ejemplo básico. Esto se basa en nuestro ejemplo de código anterior y si ha trabajado en los tutoriales anteriores de esta serie, no hay nada nuevo aquí todavía.
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; import com.badlogic.gdx.maps.tiled.TiledMap; import com.badlogic.gdx.maps.tiled.TiledMapRenderer; import com.badlogic.gdx.maps.tiled.TmxMapLoader; import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer; public class TiledTest extends ApplicationAdapter implements InputProcessor { Texture img; TiledMap tiledMap; OrthographicCamera camera; TiledMapRenderer tiledMapRenderer; SpriteBatch sb; Texture texture; Sprite sprite; @Override public void create () { float w = Gdx.graphics.getWidth(); float h = Gdx.graphics.getHeight(); camera = new OrthographicCamera(); camera.setToOrtho(false,w,h); camera.update(); tiledMap = new TmxMapLoader().load("MyCrappyMap.tmx"); tiledMapRenderer = new OrthogonalTiledMapRenderer(tiledMap); Gdx.input.setInputProcessor(this); sb = new SpriteBatch(); texture = new Texture(Gdx.files.internal("pik.png")); sprite = new Sprite(texture); } @Override public void render () { Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); camera.update(); tiledMapRenderer.setView(camera); tiledMapRenderer.render(); sb.begin(); sprite.draw(sb); sb.end(); } @Override public boolean keyDown(int keycode) { return false; } @Override public boolean keyUp(int keycode) { if(keycode == Input.Keys.LEFT) camera.translate(-32,0); if(keycode == Input.Keys.RIGHT) camera.translate(32,0); if(keycode == Input.Keys.UP) camera.translate(0,-32); if(keycode == Input.Keys.DOWN) camera.translate(0,32); if(keycode == Input.Keys.NUM_1) tiledMap.getLayers().get(0).setVisible(!tiledMap.getLayers().get(0).isVisible()); if(keycode == Input.Keys.NUM_2) tiledMap.getLayers().get(1).setVisible(!tiledMap.getLayers().get(1).isVisible()); 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; } }
Entonces, ¿qué hicimos exactamente? Bueno, en primer lugar, agregamos este sprite al proyecto en la carpeta android/asset.
Puede parecer algo familiar…
En create() asignamos un lote de sprites, cargamos nuestra textura y creamos un sprite con ella. Finalmente, en render() después de dibujar el mapa, simplemente dibujamos nuestro sprite en el lote. El orden es de importancia crítica. Otra cosa que probablemente debería señalar… No dispongo () de nada, por lo que este código se filtra como un loco… Ahora, si ejecuta el código, verá:

¡Hasta ahora, todo bien!
El primer problema que tenemos aquí es, ¿cómo posicionamos nuestro sprite dentro del mundo? En este caso, simplemente estoy dibujando en el origen, pero ¿cómo posicionaríamos el sprite en el mosaico en el que hace clic el usuario? ¡Vamos a seguir adelante y ver!
Agregue lo siguiente al controlador touchDown:
public boolean touchDown(int screenX, int screenY, int pointer, int button) { Vector3 clickCoordinates = new Vector3(screenX,screenY,0); Vector3 position = camera.unproject(clickCoordinates); sprite.setPosition(position.x, position.y); return true; }
Ahora necesitamos hacer un pequeño cambio en nuestro lote de sprites. Necesitamos aplicarle nuestras transformaciones de cámara, de modo que cuando desplazamos la pantalla con las teclas de flecha, nuestro sprite se queda quieto. Esto se logra fácilmente en render():
public void render () { Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); camera.update(); tiledMapRenderer.setView(camera); tiledMapRenderer.render(); sb.setProjectionMatrix(camera.combined); sb.begin(); sprite.draw(sb); sb.end(); }
Ahora, cuando lo ejecutes, el sprite dibujará donde hagas clic en el mundo:

Aquí hay un pequeño problema posible… ¿y si quisieras que tu sprite estuviera DETRÁS de esas rocas u objetos en las capas de nivel superior?
Hay un par de maneras de hacer esto. Primero, podrías crear una capa para los sprites de los jugadores. Simplemente puede crear una capa en Tiled que mantuvo vacía y agregarle su sprite. Aquí, en cambio, lo haré mediante programación. Permítanme dejar algo perfectamente claro, esto ciertamente *NO* es cómo lo haría, pero brinda una idea interesante de cómo se componen los mosaicos y las capas.
public void create () { float w = Gdx.graphics.getWidth(); float h = Gdx.graphics.getHeight(); camera = new OrthographicCamera(); camera.setToOrtho(false,w,h); camera.update(); tiledMap = new TmxMapLoader().load("MyCrappyMap.tmx"); tiledMapRenderer = new OrthogonalTiledMapRenderer(tiledMap); Gdx.input.setInputProcessor(this); sb = new SpriteBatch(); texture = new Texture(Gdx.files.internal("pik.png")); sprite = new Sprite(texture); // Get the width and height of our maps // Then halve it, as our sprites are 64x64 not 32x32 that our map is made of int mapWidth = tiledMap.getProperties().get("width",Integer.class)/2; int mapHeight = tiledMap.getProperties().get("height",Integer.class)/2; // Create a new map layer TiledMapTileLayer tileLayer = new TiledMapTileLayer(mapWidth,mapHeight,64,64); // Create a cell(tile) to add to the layer TiledMapTileLayer.Cell cell = new TiledMapTileLayer.Cell(); // The sprite/tilesheet behind our new layer is a single image (our sprite) // Create a TextureRegion that is the entire size of our texture TextureRegion textureRegion = new TextureRegion(texture,64,64); // Now set the graphic for our cell to our newly created region cell.setTile(new StaticTiledMapTile(textureRegion)); // Now set the cell at position 4,10 ( 8,20 in map coordinates ). This is the position of a tree // Relative to 0,0 in our map which is the bottom left corner tileLayer.setCell(4,10,cell); // Ok, I admit, this part is a gross hack. // Get the current top most layer from the map and store it MapLayer tempLayer = tiledMap.getLayers().get(tiledMap.getLayers().getCount()-1); // Now remove it tiledMap.getLayers().remove(tiledMap.getLayers().getCount()-1); // Now add our newly created layer tiledMap.getLayers().add(tileLayer); // Now add it back, now our new layer is not the top most one. tiledMap.getLayers().add(tempLayer); }
¡Y mira, estamos detrás de un árbol!

Los comentarios lo explican todo realmente. Básicamente, estamos creando mediante programación una nueva capa de mosaico dentro del mapa y colocándola debajo de la capa superior. Nunca lo harías de esta manera para un sprite por varias razones. Primero, solo podrá moverse celda por celda (es decir, 64 píxeles a la vez). En ciertos mapas eso tiene sentido, como los juegos de rol tipo Ultima 4 de la vieja escuela. A continuación, para mover, tendrá que eliminar el mosaico de su ubicación actual y agregarlo a la nueva. Finalmente, es probable que sea bastante lento para arrancar. Sin embargo, si desea poblar dinámicamente una capa, digamos con potenciadores o cosas que crea mediante programación en lugar de usar el editor de niveles, así es como puede hacerlo.
¿Qué pasa cuando se trata del sprite de los jugadores y se mueve solo unos pocos píxeles a la vez? Bueno, eso es un poco más complicado.
Una opción que tiene, especialmente si desea usar la clase Sprite, es extender la clase de representación del mapa, en este caso, OrthogonalTiledMapRenderer y luego anular el método de representación y representar su(s) sprite(s). Aquí hay un ejemplo simple:
package com.gamefromscratch; import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.maps.MapLayer; import com.badlogic.gdx.maps.MapObject; import com.badlogic.gdx.maps.tiled.TiledMap; import com.badlogic.gdx.maps.tiled.TiledMapTileLayer; import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer; import java.util.ArrayList; import java.util.List; public class OrthogonalTiledMapRendererWithSprites extends OrthogonalTiledMapRenderer { private Sprite sprite; private List<Sprite> sprites; private int drawSpritesAfterLayer = 1; public OrthogonalTiledMapRendererWithSprites(TiledMap map) { super(map); sprites = new ArrayList<Sprite>(); } public void addSprite(Sprite sprite){ sprites.add(sprite); } @Override public void render() { beginRender(); int currentLayer = 0; for (MapLayer layer : map.getLayers()) { if (layer.isVisible()) { if (layer instanceof TiledMapTileLayer) { renderTileLayer((TiledMapTileLayer)layer); currentLayer++; if(currentLayer == drawSpritesAfterLayer){ for(Sprite sprite : sprites) sprite.draw(this.getSpriteBatch()); } } else { for (MapObject object : layer.getObjects()) { renderObject(object); } } } } endRender(); } }
Ahora simplemente lo usamos en lugar del otro TileMapRenderer. No ahora que ya no necesitamos un SpriteBatch, ya que estamos usando el renderizador de mapas. Aquí está el código actualizado:
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; import com.badlogic.gdx.maps.tiled.TiledMap; import com.badlogic.gdx.maps.tiled.TmxMapLoader; import com.badlogic.gdx.math.Vector3; public class TiledTest extends ApplicationAdapter implements InputProcessor { Texture img; TiledMap tiledMap; OrthographicCamera camera; OrthogonalTiledMapRendererWithSprites tiledMapRenderer; Texture texture; Sprite sprite; @Override public void create () { float w = Gdx.graphics.getWidth(); float h = Gdx.graphics.getHeight(); camera = new OrthographicCamera(); camera.setToOrtho(false,w,h); camera.update(); texture = new Texture(Gdx.files.internal("pik.png")); sprite = new Sprite(texture); tiledMap = new TmxMapLoader().load("MyCrappyMap.tmx"); tiledMapRenderer = new OrthogonalTiledMapRendererWithSprites(tiledMap); tiledMapRenderer.addSprite(sprite); Gdx.input.setInputProcessor(this); } @Override public void render () { Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); camera.update(); tiledMapRenderer.setView(camera); tiledMapRenderer.render(); } @Override public boolean keyDown(int keycode) { return false; } @Override public boolean keyUp(int keycode) { if(keycode == Input.Keys.LEFT) camera.translate(-32,0); if(keycode == Input.Keys.RIGHT) camera.translate(32,0); if(keycode == Input.Keys.UP) camera.translate(0,-32); if(keycode == Input.Keys.DOWN) camera.translate(0,32); if(keycode == Input.Keys.NUM_1) tiledMap.getLayers().get(0).setVisible(!tiledMap.getLayers().get(0).isVisible()); if(keycode == Input.Keys.NUM_2) tiledMap.getLayers().get(1).setVisible(!tiledMap.getLayers().get(1).isVisible()); return false; } @Override public boolean keyTyped(char character) { return false; } @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { Vector3 clickCoordinates = new Vector3(screenX,screenY,0); Vector3 position = camera.unproject(clickCoordinates); sprite.setPosition(position.x, position.y); return true; } @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 lo ejecute, donde haga clic, el sprite del jugador se colocará correctamente y detrás de los objetos:

.
Sin embargo, bajo este sistema, solo tratas a tu sprite normalmente. Actualice su posición y se actualizará en el mundo del juego.
Hay otra forma posible de admitir esta funcionalidad, que es un poco un híbrido de ambos enfoques. Al igual que en el ejemplo anterior, agregamos una nueva capa al mapa, en este caso lo haremos en Mosaico en lugar de mediante programación.
En Tiled, cree una nueva capa de objeto y colóquela así:

Ahora vamos a acceder a él usando la clase MapObject, específicamente el TextureMapObject (una clase con un nombre algo confuso…). Una vez más, esto requiere una implementación personalizada de TileRenderer, esta vez anulando la clase renderObject, así:
package com.gamefromscratch; import com.badlogic.gdx.maps.MapObject; import com.badlogic.gdx.maps.objects.TextureMapObject; import com.badlogic.gdx.maps.tiled.TiledMap; import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer; public class OrthogonalTiledMapRendererWithSprites extends OrthogonalTiledMapRenderer { public OrthogonalTiledMapRendererWithSprites(TiledMap map) { super(map); } @Override public void renderObject(MapObject object) { if(object instanceof TextureMapObject) { TextureMapObject textureObj = (TextureMapObject) object; spriteBatch.draw(textureObj.getTextureRegion(), textureObj.getX(), textureObj.getY()); } } }
renderObject() se llama para renderizar los diversos objetos MapObjects en el mapa de mosaicos. La implementación predeterminada está vacía, por lo que debemos proporcionar una. Aquí simplemente verificamos si nuestro objeto es un TextureMapObject y si lo es, lo dibujamos usando spriteBatch, como antes.
Un efecto secundario es que ya no usamos la clase Sprite para el posicionamiento. En su lugar, creamos un TextureRegion nuevamente y controlamos la posición a través de los métodos TextureMapObject setX/setY. Echemos un vistazo al resultado en nuestra aplicación principal:
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; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.maps.MapLayer; import com.badlogic.gdx.maps.objects.TextureMapObject; import com.badlogic.gdx.maps.tiled.TiledMap; import com.badlogic.gdx.maps.tiled.TiledMapRenderer; import com.badlogic.gdx.maps.tiled.TmxMapLoader; import com.badlogic.gdx.math.Vector3; public class TiledTest extends ApplicationAdapter implements InputProcessor { Texture img; TiledMap tiledMap; OrthographicCamera camera; TiledMapRenderer tiledMapRenderer; SpriteBatch sb; Texture texture; Sprite sprite; MapLayer objectLayer; TextureRegion textureRegion; @Override public void create () { float w = Gdx.graphics.getWidth(); float h = Gdx.graphics.getHeight(); camera = new OrthographicCamera(); camera.setToOrtho(false,w,h); camera.update(); tiledMap = new TmxMapLoader().load("MyCrappyMap.tmx"); tiledMapRenderer = new OrthogonalTiledMapRendererWithSprites(tiledMap); Gdx.input.setInputProcessor(this); texture = new Texture(Gdx.files.internal("pik.png")); objectLayer = tiledMap.getLayers().get("objects"); textureRegion = new TextureRegion(texture,64,64); TextureMapObject tmo = new TextureMapObject(textureRegion); tmo.setX(0); tmo.setY(0); objectLayer.getObjects().add(tmo); } @Override public void render () { Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); camera.update(); tiledMapRenderer.setView(camera); tiledMapRenderer.render(); } @Override public boolean keyDown(int keycode) { return false; } @Override public boolean keyUp(int keycode) { if(keycode == Input.Keys.LEFT) camera.translate(-32,0); if(keycode == Input.Keys.RIGHT) camera.translate(32,0); if(keycode == Input.Keys.UP) camera.translate(0,-32); if(keycode == Input.Keys.DOWN) camera.translate(0,32); if(keycode == Input.Keys.NUM_1) tiledMap.getLayers().get(0).setVisible(!tiledMap.getLayers().get(0).isVisible()); if(keycode == Input.Keys.NUM_2) tiledMap.getLayers().get(1).setVisible(!tiledMap.getLayers().get(1).isVisible()); return false; } @Override public boolean keyTyped(char character) { return false; } @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { Vector3 clickCoordinates = new Vector3(screenX,screenY,0); Vector3 position = camera.unproject(clickCoordinates); TextureMapObject character = (TextureMapObject)tiledMap.getLayers().get("objects").getObjects().get(0); character.setX((float)position.x); character.setY((float)position.y); return true; } @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; } }
Es casi lo mismo, excepto, por supuesto, el renderizador diferente, el hecho de que ya no usamos un objeto Sprite y en touchDown() ubicamos el TextureMapObject en la capa de «objetos» y actualizamos sus posiciones.
El método que utilice depende de usted, de sus preferencias personales y de los requisitos de su juego. Yo mismo, creo que el segundo es probablemente el más limpio. Por supuesto, si su sprite no necesita preocuparse por la profundidad del mosaico, puede ignorar todo esto y simplemente usar SpriteBatch encima y ahorrarse muchos dolores de cabeza.