All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.badlogic.gdx.graphics.g2d.tiled.TileMapRenderer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2010 David Fraska ([email protected])
 * 
 * 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.TextureAtlas.AtlasRegion;
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 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;

		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];
				if (tile != 0) {
					if (blended == blendedTiles.contains(tile)) {
						TextureRegion reg = atlas.getRegion(tile);
						if (reg != null) {
							if (!isSimpleTileAtlas) {
								AtlasRegion region = (AtlasRegion)reg;
								cache.add(region, col * unitsPerTileX, (layer.length - row - 1) * unitsPerTileY, (float)region.offsetX
									* unitsPerTileX / tileWidth, (float)(region.offsetY) * unitsPerTileY / (float)tileHeight,
									region.packedWidth, region.packedHeight, unitsPerTileX / (float)tileWidth, unitsPerTileY
										/ (float)tileHeight, (region.rotate) ? 90 : 0);
							} else {
								cache.add(reg, col * unitsPerTileX, (layer.length - row - 1) * unitsPerTileY, 0, 0, reg.getRegionWidth(),
									reg.getRegionHeight(), unitsPerTileX / tileWidth, unitsPerTileY / tileHeight, 0);
							}
						}
					}
				}
			}
		}

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy