/* * Copyright 2010 David Fraska (dfraska@gmail.com) * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ package com.badlogic.gdx.graphics.g2d.tiled; import java.util.StringTokenizer; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.GL11; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.g2d.SpriteCache; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.IntArray; /** * A renderer for Tiled maps backed with a Sprite Cache. * * @author David Fraska */ public class TileMapRenderer implements Disposable { private SpriteCache cache; private int normalCacheId[][][], blendedCacheId[][][]; private TileAtlas atlas; private TiledMap map; private int mapHeightUnits, mapWidthUnits; private int tileWidth, tileHeight; private float unitsPerTileX, unitsPerTileY; private int tilesPerBlockX, tilesPerBlockY; private float unitsPerBlockX, unitsPerBlockY; private int[] allLayers; private boolean isSimpleTileAtlas = false; private IntArray blendedTiles; /** * A renderer for static tile maps backed with a Sprite Cache. * * This constructor is for convenience when loading TiledMaps. The normal Tiled coordinate system is used when * placing tiles. * * A default shader is used if OpenGL ES 2.0 is enabled. * * The tilesPerBlockX and tilesPerBlockY parameters will need to be adjusted for best performance. Smaller values * will cull more precisely, but result in longer loading times. Larger values result in shorter loading times, but * will cull less precisely. * * @param map * A tile map's tile numbers, in the order [layer][row][column] * @param atlas * The tile atlas to be used when drawing the map * @param tilesPerBlockX * The width of each block to be drawn, in number of tiles * @param tilesPerBlockY * The height of each block to be drawn, in number of tiles */ public TileMapRenderer(TiledMap map, TileAtlas atlas, int tilesPerBlockX, int tilesPerBlockY) { this(map, atlas, tilesPerBlockX, tilesPerBlockY, map.tileWidth, map.tileHeight); } /** * A renderer for static tile maps backed with a Sprite Cache. * * This constructor is for convenience when loading TiledMaps. * * A default shader is used if OpenGL ES 2.0 is enabled. * * The tilesPerBlockX and tilesPerBlockY parameters will need to be adjusted for best performance. Smaller values * will cull more precisely, but result in longer loading times. Larger values result in shorter loading times, but * will cull less precisely. * * @param map * A tile map's tile numbers, in the order [layer][row][column] * @param atlas * The tile atlas to be used when drawing the map * @param tilesPerBlockX * The width of each block to be drawn, in number of tiles * @param tilesPerBlockY * The height of each block to be drawn, in number of tiles * @param unitsPerTileX * The number of units per tile in the x direction * @param unitsPerTileY * The number of units per tile in the y direction */ public TileMapRenderer(TiledMap map, TileAtlas atlas, int tilesPerBlockX, int tilesPerBlockY, float unitsPerTileX, float unitsPerTileY) { this(map, atlas, tilesPerBlockX, tilesPerBlockY, unitsPerTileX, unitsPerTileY, null); } /** * A renderer for static tile maps backed with a Sprite Cache. * * This constructor is for convenience when loading TiledMaps. The normal Tiled coordinate system is used when * placing tiles. * * The tilesPerBlockX and tilesPerBlockY parameters will need to be adjusted for best performance. Smaller values * will cull more precisely, but result in longer loading times. Larger values result in shorter loading times, but * will cull less precisely. * * @param map * A tile map's tile numbers, in the order [layer][row][column] * @param atlas * The tile atlas to be used when drawing the map * @param tilesPerBlockX * The width of each block to be drawn, in number of tiles * @param tilesPerBlockY * The height of each block to be drawn, in number of tiles * @param shader * Shader to use for OpenGL ES 2.0, null uses a default shader. Ignored if using OpenGL ES 1.0. */ public TileMapRenderer(TiledMap map, TileAtlas atlas, int tilesPerBlockX, int tilesPerBlockY, ShaderProgram shader) { this(map, atlas, tilesPerBlockX, tilesPerBlockY, map.tileWidth, map.tileHeight, shader); } public TileMapRenderer(TiledMap map, TileAtlas atlas, int tilesPerBlockX, int tilesPerBlockY, float unitsPerTileX, float unitsPerTileY, ShaderProgram shader) { int[][][] tileMap = new int[map.layers.size()][][]; for (int i = 0; i < map.layers.size(); i++) { tileMap[i] = map.layers.get(i).tiles; } for (int i = 0; i < map.tileSets.size(); i++) { if (map.tileSets.get(i).tileHeight - map.tileHeight > overdrawY * unitsPerTileY) overdrawY = (map.tileSets.get(i).tileHeight - map.tileHeight) / unitsPerTileY; if (map.tileSets.get(i).tileWidth - map.tileWidth > overdrawX * unitsPerTileX) overdrawX = (map.tileSets.get(i).tileWidth - map.tileWidth) / unitsPerTileX; } String blendedTiles = map.properties.get("blended tiles"); IntArray blendedTilesArray; if (blendedTiles != null) { blendedTilesArray = createFromCSV(blendedTiles); } else { blendedTilesArray = new IntArray(0); } init(tileMap, atlas, map.tileWidth, map.tileHeight, unitsPerTileX, unitsPerTileY, blendedTilesArray, tilesPerBlockX, tilesPerBlockY, shader); this.map = map; } /** * A renderer for static tile maps backed with a Sprite Cache. * * The tilesPerBlockX and tilesPerBlockY parameters will need to be adjusted for best performance. Smaller values * will cull more precisely, but result in longer loading times. Larger values result in shorter loading times, but * will cull less precisely. * * A default shader is used if OpenGL ES 2.0 is enabled. * * @param map * A tile map's tile numbers, in the order [layer][row][column] * @param atlas * The tile atlas to be used when drawing the map * @param tileWidth * The width of the tiles, in pixels * @param tileHeight * The height of the tiles, in pixels * @param unitsPerTileX * The number of units per tile in the x direction * @param unitsPerTileY * The number of units per tile in the y direction * @param blendedTiles * Array containing tile numbers that require blending * @param tilesPerBlockX * The width of each block to be drawn, in number of tiles * @param tilesPerBlockY * The height of each block to be drawn, in number of tiles */ public TileMapRenderer(int[][][] map, TileAtlas atlas, int tileWidth, int tileHeight, float unitsPerTileX, float unitsPerTileY, IntArray blendedTiles, int tilesPerBlockX, int tilesPerBlockY) { init(map, atlas, tileWidth, tileHeight, unitsPerTileX, unitsPerTileY, blendedTiles, tilesPerBlockX, tilesPerBlockY, null); } /** * A renderer for static tile maps backed with a Sprite Cache. * * The tilesPerBlockX and tilesPerBlockY parameters will need to be adjusted for best performance. Smaller values * will cull more precisely, but result in longer loading times. Larger values result in shorter loading times, but * will cull less precisely. * * @param map * A tile map's tile numbers, in the order [layer][row][column] * @param atlas * The tile atlas to be used when drawing the map * @param tileWidth * The width of the tiles, in pixels * @param tileHeight * The height of the tiles, in pixels * @param unitsPerTileX * The number of units per tile in the x direction * @param unitsPerTileY * The number of units per tile in the y direction * @param blendedTiles * Array containing tile numbers that require blending * @param tilesPerBlockX * The width of each block to be drawn, in number of tiles * @param tilesPerBlockY * The height of each block to be drawn, in number of tiles * @param shader * Shader to use for OpenGL ES 2.0, null uses a default shader. Ignored if using OpenGL ES 1.0. */ public TileMapRenderer(int[][][] map, TileAtlas atlas, int tileWidth, int tileHeight, float unitsPerTileX, float unitsPerTileY, IntArray blendedTiles, int tilesPerBlockX, int tilesPerBlockY, ShaderProgram shader) { init(map, atlas, tileWidth, tileHeight, unitsPerTileX, unitsPerTileY, blendedTiles, tilesPerBlockX, tilesPerBlockY, shader); } /** * Initializer, used to avoid a "Constructor call must be the first statement in a constructor" syntax error when * creating a map from a TiledMap */ private void init(int[][][] map, TileAtlas atlas, int tileWidth, int tileHeight, float unitsPerTileX, float unitsPerTileY, IntArray blendedTiles, int tilesPerBlockX, int tilesPerBlockY, ShaderProgram shader) { this.atlas = atlas; this.tileWidth = tileWidth; this.tileHeight = tileHeight; this.unitsPerTileX = unitsPerTileX; this.unitsPerTileY = unitsPerTileY; this.blendedTiles = blendedTiles; this.tilesPerBlockX = tilesPerBlockX; this.tilesPerBlockY = tilesPerBlockY; unitsPerBlockX = unitsPerTileX * tilesPerBlockX; unitsPerBlockY = unitsPerTileY * tilesPerBlockY; isSimpleTileAtlas = atlas instanceof SimpleTileAtlas; int layer, row, col; allLayers = new int[map.length]; // Calculate maximum cache size and map height in pixels, fill allLayers array int maxCacheSize = 0; int maxHeight = 0; int maxWidth = 0; for (layer = 0; layer < map.length; layer++) { allLayers[layer] = layer; if (map[layer].length > maxHeight) maxHeight = map[layer].length; for (row = 0; row < map[layer].length; row++) { if (map[layer][row].length > maxWidth) maxWidth = map[layer][row].length; for (col = 0; col < map[layer][row].length; col++) if (map[layer][row][col] != 0) maxCacheSize++; } } mapHeightUnits = (int) (maxHeight * unitsPerTileY); mapWidthUnits = (int) (maxWidth * unitsPerTileX); if (shader == null) cache = new SpriteCache(maxCacheSize, false); else cache = new SpriteCache(maxCacheSize, shader, false); normalCacheId = new int[map.length][][]; blendedCacheId = new int[map.length][][]; for (layer = 0; layer < map.length; layer++) { normalCacheId[layer] = new int[(int) MathUtils.ceil((float) map[layer].length / tilesPerBlockY)][]; blendedCacheId[layer] = new int[(int) MathUtils.ceil((float) map[layer].length / tilesPerBlockY)][]; for (row = 0; row < normalCacheId[layer].length; row++) { normalCacheId[layer][row] = new int[(int) MathUtils.ceil((float) map[layer][row].length / tilesPerBlockX)]; blendedCacheId[layer][row] = new int[(int) MathUtils.ceil((float) map[layer][row].length / tilesPerBlockX)]; for (col = 0; col < normalCacheId[layer][row].length; col++) { if (isSimpleTileAtlas) { // Everything considered blended blendedCacheId[layer][row][col] = addBlock(map[layer], row, col, false); } else { normalCacheId[layer][row][col] = addBlock(map[layer], row, col, false); blendedCacheId[layer][row][col] = addBlock(map[layer], row, col, true); } } } } } private static final int FLAG_FLIP_X = 0x80000000; private static final int FLAG_FLIP_Y = 0x40000000; private static final int FLAG_ROTATE = 0x20000000; private static final int MASK_CLEAR = 0xE0000000; private int addBlock(int[][] layer, int blockRow, int blockCol, boolean blended) { cache.beginCache(); int firstCol = blockCol * tilesPerBlockX; int firstRow = blockRow * tilesPerBlockY; int lastCol = firstCol + tilesPerBlockX; int lastRow = firstRow + tilesPerBlockY; float offsetX = ((tileWidth - unitsPerTileX) / 2); float offsetY = ((tileHeight - unitsPerTileY) / 2); for (int row = firstRow; row < lastRow && row < layer.length; row++) { for (int col = firstCol; col < lastCol && col < layer[row].length; col++) { int tile = layer[row][col]; boolean flipX = ((tile & FLAG_FLIP_X) != 0); boolean flipY = ((tile & FLAG_FLIP_Y) != 0); boolean rotate = ((tile & FLAG_ROTATE) != 0); tile = tile & ~MASK_CLEAR; if (tile != 0) { if (blended == blendedTiles.contains(tile)) { TextureRegion reg = atlas.getRegion(tile); if (reg != null) { float x = col * unitsPerTileX - offsetX; float y = (layer.length - row - 1) * unitsPerTileY - offsetY; float width = reg.getRegionWidth(); float height = reg.getRegionHeight(); float originX = width * 0.5f; float originY = height * 0.5f; float scaleX = unitsPerTileX / tileWidth; float scaleY = unitsPerTileY / tileHeight; float rotation = 0; int sourceX = reg.getRegionX(); int sourceY = reg.getRegionY(); int sourceWidth = reg.getRegionWidth(); int sourceHeight = reg.getRegionHeight(); if (rotate) { if (flipX && flipY) { rotation = -90; sourceX += sourceWidth; sourceWidth = -sourceWidth; } else if (flipX && !flipY) { rotation = -90; } else if (flipY && !flipX) { rotation = +90; } else if (!flipY && !flipX) { rotation = -90; sourceY += sourceHeight; sourceHeight = -sourceHeight; } } else { if (flipX) { sourceX += sourceWidth; sourceWidth = -sourceWidth; } if (flipY) { sourceY += sourceHeight; sourceHeight = -sourceHeight; } } cache.add(reg.getTexture(), x, y, originX, originY, width, height, scaleX, scaleY, rotation, sourceX, sourceY, sourceWidth, sourceHeight, false, false); } } } } } return cache.endCache(); } /** * Renders the entire map. Use this function only on very small maps or for debugging purposes. The size of the map * is based on the first layer and the first row's size. */ public void render() { render(0, 0, (int) getMapWidthUnits(), (int) (getMapHeightUnits())); } /** * Renders all layers between the given bounding box in map units. This is the same as calling * {@link TileMapRenderer#render(float, float, float, float, int[])} with all layers in the layers list. */ public void render(float x, float y, float width, float height) { render(x, y, width, height, allLayers); } /** * Renders specific layers in the given a camera * * @param cam * The camera to use */ public void render(OrthographicCamera cam) { render(cam, allLayers); } Vector3 tmp = new Vector3(); /** * Renders specific layers in the given a camera. * * @param cam * The camera to use * @param layers * The list of layers to draw, 0 being the lowest layer. You will get an IndexOutOfBoundsException if a * layer number is too high. */ public void render(OrthographicCamera cam, int[] layers) { getProjectionMatrix().set(cam.combined); tmp.set(0, 0, 0); cam.unproject(tmp); render(tmp.x, tmp.y, cam.viewportWidth * cam.zoom, cam.viewportHeight * cam.zoom, layers); } /** * Sets the amount of overdraw in the X direction (in units). Use this if an actual tile width is greater than the * tileWidth specified in the constructor. Use the value actual_tile_width - tileWidth (from the constructor). */ public float overdrawX; /** * Sets the amount of overdraw in the Y direction (in units). Use this if an actual tile height is greater than the * tileHeight specified in the constructor. Use the value actual_tile_height - tileHeight (from the constructor). */ public float overdrawY; private int initialRow, initialCol, currentRow, currentCol, lastRow, lastCol, currentLayer; /** * Renders specific layers between the given bounding box in map units. * * @param x * The x coordinate to start drawing * @param y * the y coordinate to start drawing * @param width * the width of the tiles to draw * @param height * the width of the tiles to draw * @param layers * The list of layers to draw, 0 being the lowest layer. You will get an IndexOutOfBoundsException if a * layer number is too high. */ public void render(float x, float y, float width, float height, int[] layers) { lastRow = (int) ((mapHeightUnits - (y - height + overdrawY)) / (unitsPerBlockY)); initialRow = (int) ((mapHeightUnits - (y - overdrawY)) / (unitsPerBlockY)); initialRow = (initialRow > 0) ? initialRow : 0; // Clamp initial Row > 0 lastCol = (int) ((x + width + overdrawX) / (unitsPerBlockX)); initialCol = (int) ((x - overdrawX) / (unitsPerBlockX)); initialCol = (initialCol > 0) ? initialCol : 0; // Clamp initial Col > 0 Gdx.gl.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); cache.begin(); if (isSimpleTileAtlas) { // Without this special case the top left corner doesn't work properly on mutilayered maps Gdx.gl.glEnable(GL10.GL_BLEND); for (currentLayer = 0; currentLayer < layers.length; currentLayer++) { for (currentRow = initialRow; currentRow <= lastRow && currentRow < getLayerHeightInBlocks(currentLayer); currentRow++) { for (currentCol = initialCol; currentCol <= lastCol && currentCol < getLayerWidthInBlocks(currentLayer, currentRow); currentCol++) { cache.draw(blendedCacheId[layers[currentLayer]][currentRow][currentCol]); } } } } else { for (currentLayer = 0; currentLayer < layers.length; currentLayer++) { for (currentRow = initialRow; currentRow <= lastRow && currentRow < getLayerHeightInBlocks(currentLayer); currentRow++) { for (currentCol = initialCol; currentCol <= lastCol && currentCol < getLayerWidthInBlocks(currentLayer, currentRow); currentCol++) { Gdx.gl.glDisable(GL10.GL_BLEND); cache.draw(normalCacheId[layers[currentLayer]][currentRow][currentCol]); Gdx.gl.glEnable(GL10.GL_BLEND); cache.draw(blendedCacheId[layers[currentLayer]][currentRow][currentCol]); } } } } cache.end(); Gdx.gl.glDisable(GL10.GL_BLEND); } private int getLayerWidthInBlocks(int layer, int row) { int normalCacheWidth = 0; if (normalCacheId != null && normalCacheId[layer] != null && normalCacheId[layer][row] != null) { normalCacheWidth = normalCacheId[layer][row].length; } int blendedCacheWidth = 0; if (blendedCacheId != null && blendedCacheId[layer] != null && blendedCacheId[layer][row] != null) { blendedCacheWidth = blendedCacheId[layer][row].length; } return Math.max(normalCacheWidth, blendedCacheWidth); } private int getLayerHeightInBlocks(int layer) { int normalCacheHeight = 0; if (normalCacheId != null && normalCacheId[layer] != null) { normalCacheHeight = normalCacheId[layer].length; } int blendedCacheHeight = 0; if (blendedCacheId != null && blendedCacheId[layer] != null) { blendedCacheHeight = blendedCacheId[layer].length; } return Math.max(normalCacheHeight, blendedCacheHeight); } public Matrix4 getProjectionMatrix() { return cache.getProjectionMatrix(); } public Matrix4 getTransformMatrix() { return cache.getTransformMatrix(); } /** * Computes the Tiled Map row given a Y coordinate in units * * @param worldY * the Y coordinate in units */ public int getRow(int worldY) { return (int) (worldY / unitsPerTileY); } /** * Computes the Tiled Map column given an X coordinate in units * * @param worldX * the X coordinate in units */ public int getCol(int worldX) { return (int) (worldX / unitsPerTileX); } /** * Returns the initial drawn block row, for debugging purposes. Use this along with * {@link TileMapRenderer#getLastRow()} to compute the number of rows drawn in the last call to * {@link TileMapRenderer#render(float, float, float, float, int[])}. */ public int getInitialRow() { return initialRow; } /** * Returns the initial drawn block column, for debugging purposes. Use this along with * {@link TileMapRenderer#getLastCol()} to compute the number of columns drawn in the last call to * {@link TileMapRenderer#render(float, float, float, float, int[])}. */ public int getInitialCol() { return initialCol; } /** * Returns the final drawn block row, for debugging purposes. Use this along with * {@link TileMapRenderer#getInitialRow()} to compute the number of rows drawn in the last call to * {@link TileMapRenderer#render(float, float, float, float, int[])}. */ public int getLastRow() { return lastRow; } /** * Returns the final drawn block column, for debugging purposes. Use this along with * {@link TileMapRenderer#getInitialCol()} to compute the number of columns drawn in the last call to * {@link TileMapRenderer#render(float, float, float, float, int[])}. */ public int getLastCol() { return lastCol; } public float getUnitsPerTileX() { return unitsPerTileX; } public float getUnitsPerTileY() { return unitsPerTileY; } public int getMapHeightUnits() { return mapHeightUnits; } public int getMapWidthUnits() { return mapWidthUnits; } private static int parseIntWithDefault(String string, int defaultValue) { if (string == null) return defaultValue; try { return Integer.parseInt(string); } catch (NumberFormatException e) { return defaultValue; } } /** Releases all resources held by this TiledMapRenderer. */ @Override public void dispose() { cache.dispose(); } public TiledMap getMap() { return map; } public TileAtlas getAtlas() { return atlas; } private static IntArray createFromCSV(String values) { IntArray list = new IntArray(false, (values.length() + 1) / 2); StringTokenizer st = new StringTokenizer(values, ","); while (st.hasMoreTokens()) { list.add(Integer.parseInt(st.nextToken())); } list.shrink(); return list; } }