com.altportalgames.colorrain.utils.TiledMapHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.altportalgames.colorrain.utils.TiledMapHelper.java

Source

package com.altportalgames.colorrain.utils;

/**
 *   Copyright 2011 David Kirchner dpk@dpk.net
 *
 *   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.
 *   
 * TiledMapHelper can simplify your game's tiled map operations. You can find
 * some sample code using this class at my blog:
 * 
 * http://dpk.net/2011/05/08/libgdx-box2d-tiled-maps-full-working-example-part-2/
 * 
 * Note: This code does have some limitations. It only supports single-layered
 * maps.
 * 
 * This code is based on TiledMapTest.java found at:
 * http://code.google.com/p/libgdx/
 */

import java.util.ArrayList;
import java.util.HashMap;

import com.altportalgames.colorrain.model.Assets;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.tiled.TileAtlas;
import com.badlogic.gdx.graphics.g2d.tiled.TiledLoader;
import com.badlogic.gdx.graphics.g2d.tiled.TiledMap;
import com.badlogic.gdx.graphics.g2d.tiled.TileMapRenderer;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;

public class TiledMapHelper {
    private static final int[] layersList = { 0, 1 };
    private static Body groundBody;

    /**
     * Renders the part of the map that should be visible to the user.
     */
    public void render() {
        tiledMapRenderer.getProjectionMatrix().set(camera.combined);

        Vector3 tmp = new Vector3();
        tmp.set(0, 0, 0);
        camera.unproject(tmp);

        /*tiledMapRenderer.render((int) tmp.x,
        tiledMapRenderer.getMapHeightUnits() - (int) tmp.y,
        Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), layersList);*/
        tiledMapRenderer.render(tmp.x, tmp.y, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), layersList);
    }

    /**
     * Get the height of the map in pixels
     * 
     * @return y
     */
    public int getHeight() {
        return tiledMapRenderer.getMapHeightUnits();
    }

    /**
     * Get the width of the map in pixels
     * 
     * @return x
     */
    public int getWidth() {
        return tiledMapRenderer.getMapWidthUnits();
    }

    /**
     * Get the map, useful for iterating over the set of tiles found within
     * 
     * @return TiledMap
     */
    public TiledMap getMap() {
        return map;
    }

    /**
     * Calls dispose on all disposable resources held by this object.
     */
    public void dispose() {
        tileAtlas.dispose();
        tiledMapRenderer.dispose();
    }

    /**
     * Sets the pack file that will be used for a subsequent loadMap call.
     * 
     * @param packFile
     */
    public void setCommonPackFile(String packFile) {
        packFileHandle = Gdx.files.internal(packFile);
    }

    /**
     * Sets the base directory that holds the image file(s) found inside the
     * pack file.
     * 
     * @param baseDir
     */
    public void setBaseDir(String baseDir) {
        baseDirHandle = Gdx.files.internal(baseDir);
    }

    /**
     * Loads the requested tmx map file in to the object, sets up the camera and
     * controller.
     * 
     * @param tmxFile
     */
    public void loadMap() {
        final String path = "data/tramp/output/";
        final String mapname = "tramp gzip";

        FileHandle mapHandle = Gdx.files.internal(path + mapname + ".tmx");
        FileHandle baseDir = Gdx.files.internal(path);

        //map = TiledLoader.createMap(Gdx.files.internal(tmxFile));
        map = TiledLoader.createMap(mapHandle);
        int blockWidth = 10;
        int blockHeight = 15;
        tileAtlas = new TileAtlas(map, baseDir);

        tiledMapRenderer = new TileMapRenderer(map, tileAtlas, blockWidth, blockWidth);

        camera = new OrthographicCamera(320, 480);

        camera.position.set(map.width * map.tileWidth / 2, map.height * map.tileHeight / 2, 0);
    }

    /**
     * Reads a file describing the collision boundaries that should be set
     * per-tile and adds static bodies to the boxd world.
     * 
     * @param collisionsFile
     * @param world
     * @param pixelsPerMeter
     *            the pixels per meter scale used for this world
     */
    public void loadCollisions(String collisionsFile, World world, float pixelsPerMeter) {
        System.out.println("tilewidth " + getMap().height);
        /**
         * Detect the tiles and dynamically create a representation of the map
         * layout, for collision detection. Each tile has its own collision
         * rules stored in an associated file.
         * 
         * The file contains lines in this format (one line per type of tile):
         * tileNumber XxY,XxY XxY,XxY
         * 
         * Ex:
         * 
         * 3 0x0,31x0 ... 4 0x0,29x0 29x0,29x31
         * 
         * For a 32x32 tileset, the above describes one line segment for tile #3
         * and two for tile #4. Tile #3 has a line segment across the top. Tile
         * #1 has a line segment across most of the top and a line segment from
         * the top to the bottom, 30 pixels in.
         */

        FileHandle fh = Gdx.files.internal(collisionsFile);
        String collisionFile = fh.readString();
        String lines[] = collisionFile.split("\\r?\\n");

        HashMap<Integer, ArrayList<LineSegment>> tileCollisionJoints = new HashMap<Integer, ArrayList<LineSegment>>();

        /**
         * Some locations on the map (perhaps most locations) are "undefined",
         * empty space, and will have the tile type 0. This code adds an empty
         * list of line segments for this "default" tile.
         */
        tileCollisionJoints.put(Integer.valueOf(0), new ArrayList<LineSegment>());

        for (int n = 0; n < lines.length; n++) {
            String cols[] = lines[n].split(" ");
            int tileNo = Integer.parseInt(cols[0]);

            ArrayList<LineSegment> tmp = new ArrayList<LineSegment>();

            for (int m = 1; m < cols.length; m++) {
                String coords[] = cols[m].split(",");

                String start[] = coords[0].split("x");
                String end[] = coords[1].split("x");

                tmp.add(new LineSegment(Integer.parseInt(start[0]), Integer.parseInt(start[1]),
                        Integer.parseInt(end[0]), Integer.parseInt(end[1])));
            }

            tileCollisionJoints.put(Integer.valueOf(tileNo), tmp);
        }

        ArrayList<LineSegment> collisionLineSegments = new ArrayList<LineSegment>();

        for (int y = 0; y < getMap().height; y++) {
            for (int x = 0; x < getMap().width; x++) {
                int tileType = getMap().layers.get(1).tiles[(getMap().height - 1) - y][x];

                for (int n = 0; n < tileCollisionJoints.get(Integer.valueOf(tileType)).size(); n++) {
                    LineSegment lineSeg = tileCollisionJoints.get(Integer.valueOf(tileType)).get(n);

                    addOrExtendCollisionLineSegment(x * Assets.tileWidth + lineSeg.start().x,
                            y * Assets.tileHeight - lineSeg.start().y + Assets.tileHeight,
                            x * Assets.tileWidth + lineSeg.end().x,
                            y * Assets.tileHeight - lineSeg.end().y + Assets.tileHeight, collisionLineSegments);
                }
            }
        }

        BodyDef groundBodyDef = new BodyDef();
        groundBodyDef.type = BodyDef.BodyType.StaticBody;
        groundBody = world.createBody(groundBodyDef);
        for (LineSegment lineSegment : collisionLineSegments) {
            PolygonShape environmentShape = new PolygonShape();
            environmentShape.setAsEdge(lineSegment.start().mul(1 / Assets.PIXELS_PER_METER_X),
                    lineSegment.end().mul(1 / Assets.PIXELS_PER_METER_X));
            groundBody.createFixture(environmentShape, 0);
            environmentShape.dispose();
        }

        /**
         * Drawing a boundary around the entire map. We can't use a box because
         * then the world objects would be inside and the physics engine would
         * try to push them out.
         */

        PolygonShape mapBounds = new PolygonShape();
        mapBounds.setAsEdge(new Vector2(0.0f, 0.0f), new Vector2(getWidth() / Assets.PIXELS_PER_METER_X, 0.0f));
        groundBody.createFixture(mapBounds, 0);

        mapBounds.setAsEdge(new Vector2(0.0f, getHeight() / Assets.PIXELS_PER_METER_Y),
                new Vector2(getWidth() / Assets.PIXELS_PER_METER_X, getHeight() / Assets.PIXELS_PER_METER_Y));
        groundBody.createFixture(mapBounds, 0);

        mapBounds.setAsEdge(new Vector2(0.0f, 0.0f), new Vector2(0.0f, getHeight() / Assets.PIXELS_PER_METER_Y));
        groundBody.createFixture(mapBounds, 0);

        mapBounds.setAsEdge(new Vector2(getWidth() / Assets.PIXELS_PER_METER_X, 0.0f),
                new Vector2(getWidth() / Assets.PIXELS_PER_METER_X, getHeight() / Assets.PIXELS_PER_METER_Y));
        groundBody.createFixture(mapBounds, 0);

        mapBounds.dispose();
    }

    public static Body getGroundBody() {
        return groundBody;
    }

    /**
     * This is a helper function that makes calls that will attempt to extend
     * one of the line segments already tracked by TiledMapHelper, if possible.
     * The goal is to have as few line segments as possible.
     * 
     * Ex: If you have a line segment in the system that is from 1x1 to 3x3 and
     * this function is called for a line that is 4x4 to 9x9, rather than add a
     * whole new line segment to the list, the 1x1,3x3 line will be extended to
     * 1x1,9x9. See also: LineSegment.extendIfPossible.
     * 
     * @param lsx1
     *            starting x of the new line segment
     * @param lsy1
     *            starting y of the new line segment
     * @param lsx2
     *            ending x of the new line segment
     * @param lsy2
     *            ending y of the new line segment
     * @param collisionLineSegments
     *            the current list of line segments
     */
    private void addOrExtendCollisionLineSegment(float lsx1, float lsy1, float lsx2, float lsy2,
            ArrayList<LineSegment> collisionLineSegments) {
        LineSegment line = new LineSegment(lsx1, lsy1, lsx2, lsy2);

        boolean didextend = false;

        for (LineSegment test : collisionLineSegments) {
            if (test.extendIfPossible(line)) {
                didextend = true;
                break;
            }
        }

        if (!didextend) {
            collisionLineSegments.add(line);
        }
    }

    /**
     * Returns the camera object created for viewing the loaded map.
     * 
     * @return OrthographicCamera
     */
    public OrthographicCamera getCamera() {
        if (camera == null) {
            throw new IllegalStateException("getCamera() called out of sequence");
        }
        return camera;
    }

    /**
     * Describes the start and end points of a line segment and contains a
     * helper method useful for extending line segments.
     */
    private class LineSegment {
        private Vector2 start = new Vector2();
        private Vector2 end = new Vector2();

        /**
         * Construct a new LineSegment with the specified coordinates.
         * 
         * @param x1
         * @param y1
         * @param x2
         * @param y2
         */
        public LineSegment(float x1, float y1, float x2, float y2) {
            start = new Vector2(x1, y1);
            end = new Vector2(x2, y2);
        }

        /**
         * The "start" of the line. Start and end are misnomers, this is just
         * one end of the line.
         * 
         * @return Vector2
         */
        public Vector2 start() {
            return start;
        }

        /**
         * The "end" of the line. Start and end are misnomers, this is just one
         * end of the line.
         * 
         * @return Vector2
         */
        public Vector2 end() {
            return end;
        }

        /**
         * Determine if the requested line could be tacked on to the end of this
         * line with no kinks or gaps. If it can, the current LineSegment will
         * be extended by the length of the passed LineSegment.
         * 
         * @param lineSegment
         * @return boolean true if line was extended, false if not.
         */
        public boolean extendIfPossible(LineSegment lineSegment) {
            /**
             * First, let's see if the slopes of the two segments are the same.
             */
            double slope1 = Math.atan2(end.y - start.y, end.x - start.x);
            double slope2 = Math.atan2(lineSegment.end.y - lineSegment.start.y,
                    lineSegment.end.x - lineSegment.start.x);

            if (Math.abs(slope1 - slope2) > 1e-9) {
                return false;
            }

            /**
             * Second, check if either end of this line segment is adjacent to
             * the requested line segment. So, 1 pixel away up through sqrt(2)
             * away.
             * 
             * Whichever two points are within the right range will be "merged"
             * so that the two outer points will describe the line segment.
             */
            if (start.dst(lineSegment.start) <= Math.sqrt(2) + 1e-9) {
                start.set(lineSegment.end);
                return true;
            } else if (end.dst(lineSegment.start) <= Math.sqrt(2) + 1e-9) {
                end.set(lineSegment.end);
                return true;
            } else if (end.dst(lineSegment.end) <= Math.sqrt(2) + 1e-9) {
                end.set(lineSegment.start);
                return true;
            } else if (start.dst(lineSegment.end) <= Math.sqrt(2) + 1e-9) {
                start.set(lineSegment.start);
                return true;
            }

            return false;
        }

        /**
         * Returns a pretty description of the LineSegment.
         * 
         * @return String
         */
        @Override
        public String toString() {
            return "[" + start.x + "x" + start.y + "] -> [" + end.x + "x" + end.y + "]";
        }
    }

    private FileHandle packFileHandle;
    private FileHandle baseDirHandle;

    private OrthographicCamera camera;

    private TileAtlas tileAtlas;
    private TileMapRenderer tiledMapRenderer;

    private TiledMap map;
}