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

ca.blarg.gdx.tilemap3d.TileContainer Maven / Gradle / Ivy

Go to download

Library to handle management and rendering of a game world composed of 3D "tiles" arranged in a uniform 3D grid, via libGDX.

The newest version!
package ca.blarg.gdx.tilemap3d;

import ca.blarg.gdx.math.IntersectionTester;
import ca.blarg.gdx.math.MathHelpers;
import ca.blarg.gdx.tilemap3d.tilemesh.TileMesh;
import ca.blarg.gdx.tilemap3d.tilemesh.TileMeshCollection;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.BoundingBox;
import com.badlogic.gdx.math.collision.Ray;

public abstract class TileContainer {
	static final Vector3 tmp1 = new Vector3();
	static final Vector3 tmpTMax = new Vector3();
	static final Vector3 tmpTDelta = new Vector3();
	static final TileCoord tmpCoords = new TileCoord();

	public abstract int getWidth();
	public abstract int getHeight();
	public abstract int getDepth();
	public abstract int getMinX();
	public abstract int getMinY();
	public abstract int getMinZ();
	public abstract int getMaxX();
	public abstract int getMaxY();
	public abstract int getMaxZ();

	public abstract Tile get(int x, int y, int z);
	public abstract Tile getSafe(int x, int y, int z);

	public abstract Vector3 getPosition();
	public abstract BoundingBox getBounds();

	public boolean isWithinBounds(int x, int y, int z) {
		if (x < getMinX() || x > getMaxX())
			return false;
		else if (y < getMinY() || y > getMaxY())
			return false;
		else if (z < getMinZ() || z > getMaxZ())
			return false;
		else
			return true;
	}

	public boolean isWithinLocalBounds(int x, int y, int z) {
		if (x < 0 || x >= getWidth())
			return false;
		else if (y < 0 || y >= getHeight())
			return false;
		else if (z < 0 || z >= getDepth())
			return false;
		else
			return true;
	}

	public void getBoundingBoxFor(int x, int y, int z, BoundingBox result) {
		// local "TileContainer space"
		result.min.set(x, y, z);
		result.max.set(x + 1.0f, y + 1.0f, z + 1.0f);   // 1.0f = tile width

		// move to "world/tilemap space"
		result.min.add(getBounds().min);
		result.max.add(getBounds().min);
	}

	public boolean getOverlappedTiles(BoundingBox box, TileCoord min, TileCoord max) {
		// make sure the given box actually intersects with this TileContainer in the first place
		if (!IntersectionTester.test(getBounds(), box))
			return false;

		// convert to tile coords (these will be in "world/tilemap space")
		// HACK: ceil() calls and "-1"'s keep us from picking up too many/too few
		// tiles. these were arrived at through observation
		int minX = (int)box.min.x;
		int minY = (int)box.min.y;
		int minZ = (int)box.min.z;
		int maxX = MathUtils.ceil(box.max.x);
		int maxY = MathUtils.ceil(box.max.y - 1.0f);
		int maxZ = MathUtils.ceil(box.max.z);

		// trim off the excess bounds so that we end up with a min-to-max area
		// that is completely within the bounds of this TileContainer
		// HACK: "+1"'s ensure we pick up just the right amount of tiles. these were arrived
		// at through observation
		minX = MathUtils.clamp(minX, getMinX(), getMaxX() + 1);
		minY = MathUtils.clamp(minY, getMinY(), getMaxY());
		minZ = MathUtils.clamp(minZ, getMinZ(), getMaxZ() + 1);
		maxX = MathUtils.clamp(maxX, getMinX(), getMaxX() + 1);
		maxY = MathUtils.clamp(maxY, getMinY(), getMaxY());
		maxZ = MathUtils.clamp(maxZ, getMinZ(), getMaxZ() + 1);

		// return the leftover area, converted to the local coordinate space of the TileContainer
		min.x = minX - getMinX();
		min.y = minY - getMinY();
		min.z = minZ - getMinZ();
		max.x = maxX - getMinX();
		max.y = maxY - getMinY();
		max.z = maxZ - getMinZ();

		return true;
	}

	public boolean checkForCollision(Ray ray, TileCoord collisionCoords) {
		// make sure that the ray and this TileContainer can actually collide in the first place
		tmp1.set(Vector3.Zero);
		if (!IntersectionTester.test(ray, getBounds(), tmp1))
			return false;

		// convert initial collision point to tile coords (this is in "world/tilemap space")
		int currentX = (int)tmp1.x;
		int currentY = (int)tmp1.y;
		int currentZ = (int)tmp1.z;

		// make sure the coords are inrange of this container. due to some floating
		// point errors / decimal truncating from the above conversion we could
		// end up with one or more that are very slightly out of bounds.
		// this is still in "world/tilemap space"
		currentX = MathUtils.clamp(currentX, getMinX(), getMaxX());
		currentY = MathUtils.clamp(currentY, getMinY(), getMaxY());
		currentZ = MathUtils.clamp(currentZ, getMinZ(), getMaxZ());

		// convert to the local space of this TileContainer
		currentX -= getMinX();
		currentY -= getMinY();
		currentZ -= getMinZ();

		// is the start position colliding with a solid tile?
		Tile startTile = get(currentX, currentY, currentZ);
		if (startTile.isCollideable())
		{
			// collision found, set the tile coords of the collision
			if (collisionCoords != null) {
				collisionCoords.x = currentX;
				collisionCoords.y = currentY;
				collisionCoords.z = currentZ;
			}

			// and we're done
			return true;
		}

		// no collision initially, continue on with the rest ...

		// step increments in "TileContainer tile" units
		int stepX = (int)MathHelpers.sign(ray.direction.x);
		int stepY = (int)MathHelpers.sign(ray.direction.y);
		int stepZ = (int)MathHelpers.sign(ray.direction.z);

		// tile boundary (needs to be in "world/tilemap space")
		int tileBoundaryX = getMinX() + currentX + (stepX > 0 ? 1 : 0);
		int tileBoundaryY = getMinY() + currentY + (stepY > 0 ? 1 : 0);
		int tileBoundaryZ = getMinZ() + currentZ + (stepZ > 0 ? 1 : 0);

		// HACK: for the tMax and tDelta initial calculations below, if any of the
		//       components of ray.direction are zero, it will result in "inf"
		//       components in tMax or tDelta. This is fine, but it has to be
		//       *positive* "inf", not negative. What I found was that sometimes
		//       they would be negative, sometimes positive. So, we force them to be
		//       positive below. Additionally, "nan" components (which will happen
		//       if both sides of the divisions are zero) are bad, and we need to
		//       change that up for "inf" as well.

		// determine how far we can travel along the ray before we hit a tile boundary
		tmpTMax.set(
				(tileBoundaryX - ray.origin.x) / ray.direction.x,
				(tileBoundaryY - ray.origin.y) / ray.direction.y,
				(tileBoundaryZ - ray.origin.z) / ray.direction.z
		);
		if (tmpTMax.x == Float.NEGATIVE_INFINITY)
			tmpTMax.x = Float.POSITIVE_INFINITY;
		if (tmpTMax.y == Float.NEGATIVE_INFINITY)
			tmpTMax.y = Float.POSITIVE_INFINITY;
		if (tmpTMax.z == Float.NEGATIVE_INFINITY)
			tmpTMax.z = Float.POSITIVE_INFINITY;
		if (Float.isNaN(tmpTMax.x))
			tmpTMax.x = Float.POSITIVE_INFINITY;
		if (Float.isNaN(tmpTMax.y))
			tmpTMax.y = Float.POSITIVE_INFINITY;
		if (Float.isNaN(tmpTMax.z))
			tmpTMax.z = Float.POSITIVE_INFINITY;

		// determine how far we must travel along the ray before we cross a grid cell
		tmpTDelta.set(
				stepX / ray.direction.x,
				stepY / ray.direction.y,
				stepZ / ray.direction.z
		);
		if (tmpTDelta.x == Float.NEGATIVE_INFINITY)
			tmpTDelta.x = Float.POSITIVE_INFINITY;
		if (tmpTDelta.y == Float.NEGATIVE_INFINITY)
			tmpTDelta.y = Float.POSITIVE_INFINITY;
		if (tmpTDelta.z == Float.NEGATIVE_INFINITY)
			tmpTDelta.z = Float.POSITIVE_INFINITY;
		if (Float.isNaN(tmpTDelta.x))
			tmpTDelta.x = Float.POSITIVE_INFINITY;
		if (Float.isNaN(tmpTDelta.y))
			tmpTDelta.y = Float.POSITIVE_INFINITY;
		if (Float.isNaN(tmpTDelta.z))
			tmpTDelta.z = Float.POSITIVE_INFINITY;

		boolean collided = false;
		boolean outOfContainer = false;
		while (!outOfContainer)
		{
			// step up to the next tile using the lowest step value
			// (in other words, we figure out on which axis, X, Y, or Z, the next
			// tile that lies on the ray is closest, and use that axis step increment
			// to move us up to get to the next tile location)
			if (tmpTMax.x < tmpTMax.y && tmpTMax.x < tmpTMax.z)
			{
				// tMax.x is lowest, the YZ tile boundary plane is closest
				currentX += stepX;
				tmpTMax.x += tmpTDelta.x;
			}
			else if (tmpTMax.y < tmpTMax.z)
			{
				// tMax.y is lowest, the XZ tile boundary plane is closest
				currentY += stepY;
				tmpTMax.y += tmpTDelta.y;
			}
			else
			{
				// tMax.z is lowest, the XY tile boundary plane is closest
				currentZ += stepZ;
				tmpTMax.z += tmpTDelta.z;
			}

			// need to figure out if this new position is still inside the bounds of
			// the container before we can attempt to determine if the current tile is
			// solid
			// (remember, currentX/Y/Z is in the local "TileContainer space"
			if (
					currentX < 0 || currentX >= getWidth() ||
					currentY < 0 || currentY >= getHeight() ||
					currentZ < 0 || currentZ >= getDepth()
				)
				outOfContainer = true;
			else
			{
				// still inside and at the next position, test for a solid tile
				Tile tile = get(currentX, currentY, currentZ);
				if (tile.isCollideable())
				{
					collided = true;

					// set the tile coords of the collision
					if (collisionCoords != null) {
						collisionCoords.x = currentX;
						collisionCoords.y = currentY;
						collisionCoords.z = currentZ;
					}

					break;
				}
			}
		}

		return collided;
	}

	public boolean checkForCollision(Ray ray, TileCoord collisionCoords, TileMeshCollection tileMeshes, Vector3 tileMeshCollisionPoint) {
		// if the ray doesn't collide with any solid tiles in the first place, then
		// we can skip this more expensive triangle collision check...
		tmpCoords.set(Vector3.Zero);
		if (!checkForCollision(ray, tmpCoords))
			return false;

		if (collisionCoords != null)
			collisionCoords.set(tmpCoords);

		// now perform the per-triangle collision check against the tile position
		// where the ray ended up at the end of the above checkForCollision() call
		return checkForCollisionWithTileMesh(
				ray,
				tmpCoords.x, tmpCoords.y, tmpCoords.z,
				tileMeshes,
				tileMeshCollisionPoint
		);
	}

	static final Vector3 tileWorldPosition = new Vector3();
	static final Vector3 collisionPoint = new Vector3();
	static final Vector3 tmpA = new Vector3();
	static final Vector3 tmpB = new Vector3();
	static final Vector3 tmpC = new Vector3();
	public boolean checkForCollisionWithTileMesh(Ray ray, int x, int y, int z, TileMeshCollection tileMeshes, Vector3 outCollisionPoint) {
		Tile tile = get(x, y, z);
		TileMesh mesh = tileMeshes.get(tile);

		Vector3[] vertices = mesh.getCollisionVertices();

		// world position of this tile, will be used to move each
		// mesh triangle into world space
		tileWorldPosition.set((float)x, (float)y, (float)z);

		float closestSquaredDistance = Float.POSITIVE_INFINITY;
		boolean collided = false;
		collisionPoint.set(Vector3.Zero);

		for (int i = 0; i < vertices.length; i += 3) {
			// get the vertices making up this triangle (and move the vertices into world space)
			tmpA.set(vertices[i]).add(tileWorldPosition);
			tmpB.set(vertices[i + 1]).add(tileWorldPosition);
			tmpC.set(vertices[i + 2]).add(tileWorldPosition);

			if (IntersectionTester.test(ray, tmpA, tmpB, tmpC, collisionPoint)) {
				collided = true;

				// if this is the closest collision yet, then keep the distance
				// and point of collision
				float squaredDistance = tmp1.set(collisionPoint).sub(ray.origin).len2();
				if (squaredDistance < closestSquaredDistance) {
					closestSquaredDistance = squaredDistance;
					outCollisionPoint.set(collisionPoint);
				}
			}
		}

		return collided;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy