package com.fdangelo.circleworld.universeengine.tilemap; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; import com.fdangelo.circleworld.universeengine.utils.DataPools; import com.fdangelo.circleworld.utils.Mathf; import com.fdangelo.circleworld.utils.Vector2I; public abstract class TilemapCircle { public static final float TILE_SIZE = 0.5f; public static final float TILE_SIZE_INV = 1.0f / TILE_SIZE; protected int seed; protected int height; protected int width; protected byte[] tiles; protected Vector2[] circleNormals; protected float[] circleHeights; // Used when finding tileY positions! private float height0; private float k; private float logk; protected ITilemapCircleListener listener; protected float positionX; protected float positionY; protected float rotation; static private TileHitInfo tmpHitInfo = new TileHitInfo(); static private Vector2 tmpv1 = new Vector2(); public final int getHeight() { return height; } public final int getWidth() { return width; } public final int getSeed() { return seed; } public final float getPositionX() { return positionX; } public final float getPositionY() { return positionY; } public final void setPosition(final float x, final float y) { positionX = x; positionY = y; } public final float getRotation() { return rotation; } public final void setRotation(final float value) { rotation = value; } public final Vector2[] getCircleNormals() { return circleNormals; } public float[] getCircleHeights() { return circleHeights; } public final ITilemapCircleListener getListener() { return listener; } public final void setListener(final ITilemapCircleListener value) { listener = value; } public final void init(final int seed, int height) { if (height < 5) { height = 5; } this.seed = seed; this.height = height; initData(); updateTiles(); } protected abstract void updateTiles(); private final void initData() { width = (((int) (height * MathUtils.PI * 2.0f)) / 4) * 4; circleNormals = DataPools.poolVector2.getArray(width); circleHeights = DataPools.poolFloat.getArray(height + 1); tiles = DataPools.poolByte.getArray(width * height); final float angleStep = ((2.0f * MathUtils.PI) / width); for (int i = 0; i < width; i++) { final float angle = i * angleStep; circleNormals[i] = new Vector2(Mathf.sin(angle), Mathf.cos(angle)); } height0 = (height - 1) * TILE_SIZE; k = -((width / (MathUtils.PI * 2.0f))) / (1 - (width / (MathUtils.PI * 2.0f))); logk = (float) Math.log(k); circleHeights[0] = height0; for (int i = 1; i <= height; i++) { final float r1 = circleHeights[i - 1]; // float r2 = ((-r1 * width) / (Mathf.PI * 2.0f)) / (1 - (width / // (Mathf.PI * 2.0f))); final float r2 = r1 * k; circleHeights[i] = r2; } } public final byte getTile(final int tileX, final int tileY) { return tiles[tileX + tileY * width]; } public final void getTile(final int tileX, final int tileY, final byte tile) { if (tiles[tileX + tileY * width] != tile) { tiles[tileX + tileY * width] = tile; if (listener != null) { listener.onTilemapTileChanged(tileX, tileY); } } } public final int getTileYFromDistance(final float distance) { // This was taken from wolfram-alpha, by solving the radius relationship // function // Original function: // http://www.wolframalpha.com/input/?i=g%280%29%3Dk%2C+g%28n%2B1%29%3Dl+*+g%28n%29 // Solution: // http://www.wolframalpha.com/input/?i=y+%3D+k+*+l%CB%86x+find+x (we // use the solution over reals with y > 0) // int tileY = (int) (Mathf.Log (distance / height0) / Mathf.Log (k)); final int tileY = (int) (Math.log(distance / height0) / logk); return tileY; } public final float getDistanceFromTileY(final int tileY) { return (float) (height0 * Math.pow(k, tileY)); } public final float getDistanceFromPosition(final float positionX, final float positionY) { final float dx = positionX - this.positionX; final float dy = positionY - this.positionY; return (float) Math.sqrt(dx * dx + dy * dy); } public final int getTileXFromAngle(final float angle) { int tileX = MathUtils.floor((angle / (MathUtils.PI * 2.0f)) * width); tileX = tileX % width; if (tileX < 0) { tileX += width; } return tileX; } public final Vector2 getPositionFromTileCoordinate(final int tileX, final int tileY) { // return position + GetNormalFromTileX(tileX) * // GetDistanceFromTileY(tileY); return getNormalFromTileX(tileX).scl(getDistanceFromTileY(tileY)).add(positionX, positionY); } public final Vector2 getPositionFromDistanceAndAngle(final float distance, final float angle) { // return position + GetNormalFromAngle(angle) * distance; final Vector2 normal = getNormalFromAngle(angle); normal.scl(distance); normal.add(positionX, positionY); return normal; } public final boolean getTileCoordinatesFromPosition(final float positionX, final float positionY, final Vector2I tileCoordinate) { final float dx = positionX - this.positionX; final float dy = positionY - this.positionY; final float distance = (float) Math.sqrt(dx * dx + dy * dy); final float angle = -Mathf.atan2(dy, dx) + MathUtils.PI * 0.5f; tileCoordinate.y = getTileYFromDistance(distance); tileCoordinate.x = getTileXFromAngle(angle); if (tileCoordinate.y >= height || tileCoordinate.y < 0) { return false; } return true; } public final float getScaleFromPosition(final float positionX, final float positionY) { final float dx = positionX - this.positionX; final float dy = positionY - this.positionY; final float distance = (float) Math.sqrt(dx * dx + dy * dy); final float scale = MathUtils.clamp((distance * 2.0f * MathUtils.PI) / width, (circleHeights[0] * 2.0f * MathUtils.PI) / width, (circleHeights[circleHeights.length - 1] * 2.0f * MathUtils.PI) / width) * TILE_SIZE_INV; return scale; } public final Vector2 getNormalFromPosition(final float positionX, final float positionY) { final float dx = positionX - this.positionX; final float dy = positionY - this.positionY; final float distance = (float) Math.sqrt(dx * dx + dy * dy); return tmpv1.set(dx / distance, dy / distance); } public final Vector2 getNormalFromAngle(final float angle) { return tmpv1.set(Mathf.sin(angle), Mathf.cos(angle)); } public final float getAngleFromPosition(final float positionX, final float positionY) { final float dx = positionX - this.positionX; final float dy = positionY - this.positionY; final float angle = -Mathf.atan2(dy, dx) + MathUtils.PI * 0.5f; return angle; } public final Vector2 getNormalFromTileX(int tileX) { tileX = tileX % width; return tmpv1.set(circleNormals[tileX]); } public final Vector2 getTangentFromPosition(final float positionX, final float positionY) { final Vector2 normal = getNormalFromPosition(positionX, positionY); return tmpv1.set(normal.y, -normal.x); } public final Vector2 getTangentFromTileCoordinate(final int tileX, final int tileY) { final Vector2 normal = getNormalFromTileX(tileX); return tmpv1.set(normal.y, -normal.x); } public final boolean raycastSquare(final float originX, final float originY, float size, final TileDirection direction, final float len, final TileHitInfo hitInfo) { size *= 0.95f; final int iterations = Math.max(MathUtils.ceil(size / TILE_SIZE), 1); // Vector2 from = origin - GetTanget(origin, direction) * (size * 0.5f); Vector2 from = getTanget(originX, originY, direction); from.scl(size * 0.5f); from.add(originX, originY); float fromX = from.x; float fromY = from.y; from = null; // Vector2 step = GetTanget(origin, direction) * (size / iterations); Vector2 step = getTanget(originX, originY, direction); step.scl(size / iterations); final float stepX = step.x; final float stepY = step.y; step = null; boolean hitAny = false; final TileHitInfo localHitInfo = tmpHitInfo; for (int i = 0; i <= iterations; i++) { if (raycast(fromX, fromY, direction, len, localHitInfo)) { if (!hitAny) { hitAny = true; hitInfo.set(localHitInfo); } else if (localHitInfo.hitDistance < hitInfo.hitDistance) { hitInfo.set(localHitInfo); } } fromX += stepX; fromY += stepY; } return hitAny; } public final Vector2 getDirection(final float originX, final float originY, final TileDirection direction) { switch (direction) { case Down: return getNormalFromPosition(originX, originY).scl(-1); case Up: return getNormalFromPosition(originX, originY); case Right: return getTangentFromPosition(originX, originY); case Left: return getTangentFromPosition(originX, originY).scl(-1); default: return tmpv1.set(0, 0); } } public final Vector2 getTanget(final float originX, final float originY, final TileDirection direction) { switch (direction) { case Down: return getTangentFromPosition(originX, originY); case Up: return getTangentFromPosition(originX, originY); case Right: return getNormalFromPosition(originX, originY); case Left: return getNormalFromPosition(originX, originY); default: return tmpv1.set(0, 0); } } public final boolean raycast(final float originX, final float originY, final TileDirection direction, float len, final TileHitInfo hitInfo) { final float dx = originX - positionX; final float dy = originY - positionY; float originDistance = (float) Math.sqrt(dx * dx + dy * dy); float targetX; float targetY; float targetdx; float targetdy; float targetDistance; float tangentDistance; float segmentSize; if (originDistance < 0.001f) { originDistance = 0.001f; } float originMapAngle = -Mathf.atan2(dy, dx) + MathUtils.PI * 0.5f; while (originMapAngle > MathUtils.PI2) { originMapAngle -= MathUtils.PI2; } while (originMapAngle < 0.0f) { originMapAngle += MathUtils.PI2; } final float originNormalX = dx / originDistance; final float originNormalY = dy / originDistance; final float originTangentX = originNormalY; final float originTangentY = -originNormalX; if (direction == TileDirection.Right) { targetX = originX + originTangentX * len; targetY = originY + originTangentY * len; targetdx = targetX - positionX; targetdy = targetY - positionY; targetDistance = (float) Math.sqrt(targetdx * targetdx + targetdy * targetdy); if (originDistance > circleHeights[circleHeights.length - 1]) { // Origin point outside, not hit! return false; } for (int i = 1; i < circleHeights.length; i++) { if (originDistance < circleHeights[i]) { hitInfo.hitTileY = i - 1; break; } } segmentSize = (circleHeights[hitInfo.hitTileY] * 2.0f * MathUtils.PI) / width; tangentDistance = ((originMapAngle / (MathUtils.PI2)) * width); hitInfo.hitTileX = (int) tangentDistance; hitInfo.hitTileX = (hitInfo.hitTileX + 1) % width; len -= segmentSize * (MathUtils.ceil(tangentDistance) - tangentDistance); while (hitInfo.hitTileX < width && len >= 0) { if (getTile(hitInfo.hitTileX, hitInfo.hitTileY) != 0) { final Vector2 tanget = getTangentFromTileCoordinate(hitInfo.hitTileX, hitInfo.hitTileY); hitInfo.hitNormalX = -tanget.x; hitInfo.hitNormalY = -tanget.y; hitInfo.hitPositionX = positionX + circleNormals[hitInfo.hitTileX].x * originDistance; hitInfo.hitPositionY = positionY + circleNormals[hitInfo.hitTileX].y * originDistance; hitInfo.hitDistance = Mathf.len(originX - hitInfo.hitPositionX, originY - hitInfo.hitPositionY); return true; } len -= segmentSize; hitInfo.hitTileX++; } } else if (direction == TileDirection.Left) { targetX = originX + originTangentX * len; targetY = originY + originTangentY * len; targetdx = targetX - positionX; targetdy = targetY - positionY; targetDistance = (float) Math.sqrt(targetdx * targetdx + targetdy * targetdy); if (originDistance > circleHeights[circleHeights.length - 1]) { // Origin point outside, not hit! return false; } for (int i = 1; i < circleHeights.length; i++) { if (originDistance < circleHeights[i]) { hitInfo.hitTileY = i - 1; break; } } segmentSize = (circleHeights[hitInfo.hitTileY] * 2.0f * MathUtils.PI) / width; tangentDistance = ((originMapAngle / (MathUtils.PI2)) * width); hitInfo.hitTileX = (int) tangentDistance; hitInfo.hitTileX = (hitInfo.hitTileX - 1) % width; if (hitInfo.hitTileX < 0) { hitInfo.hitTileX += width; } len -= segmentSize * (tangentDistance - MathUtils.floor(tangentDistance)); while (hitInfo.hitTileX >= 0 && len >= 0) { if (getTile(hitInfo.hitTileX, hitInfo.hitTileY) != 0) { final Vector2 tangent = getTangentFromTileCoordinate(hitInfo.hitTileX + 1, hitInfo.hitTileY); hitInfo.hitNormalX = tangent.x; hitInfo.hitNormalY = tangent.y; hitInfo.hitPositionX = positionX + circleNormals[(hitInfo.hitTileX + 1) % width].x * originDistance; hitInfo.hitPositionX = positionY + circleNormals[(hitInfo.hitTileX + 1) % width].y * originDistance; hitInfo.hitDistance = Mathf.len(originX - hitInfo.hitPositionX, originY - hitInfo.hitPositionY); return true; } len -= segmentSize; hitInfo.hitTileX--; } } else if (direction == TileDirection.Up) { targetX = originX + originNormalX * len; targetY = originY + originNormalY * len; targetdx = targetX - positionX; targetdy = targetY - positionY; targetDistance = (float) Math.sqrt(targetdx * targetdx + targetdy * targetdy); if (originDistance > circleHeights[circleHeights.length - 1]) { // Origin point outside, not hit! return false; } hitInfo.hitTileX = (int) ((originMapAngle / (MathUtils.PI * 2.0f)) * width); hitInfo.hitTileX = hitInfo.hitTileX % width; for (int i = 1; i < circleHeights.length; i++) { if (originDistance < circleHeights[i]) { hitInfo.hitTileY = i; len -= circleHeights[i] - originDistance; break; } } while (hitInfo.hitTileY < height && len >= 0) { if (getTile(hitInfo.hitTileX, hitInfo.hitTileY) != 0) { hitInfo.hitNormalX = -originNormalX; hitInfo.hitNormalY = -originNormalY; hitInfo.hitPositionX = positionX + originNormalX * circleHeights[hitInfo.hitTileY]; hitInfo.hitPositionY = positionY + originNormalY * circleHeights[hitInfo.hitTileY]; hitInfo.hitDistance = Mathf.len(originX - hitInfo.hitPositionX, originY - hitInfo.hitPositionY); return true; } if (hitInfo.hitTileY < height - 1) { len -= (circleHeights[hitInfo.hitTileY + 1] - circleHeights[hitInfo.hitTileY]); } hitInfo.hitTileY++; } } else if (direction == TileDirection.Down) { targetX = originX - originNormalX * len; targetY = originY - originNormalY * len; targetdx = targetX - positionX; targetdy = targetY - positionY; targetDistance = (float) Math.sqrt(targetdx * targetdx + targetdy * targetdy); if (/* originDistance > circleHeights[circleHeights.Length - 1] && */ targetDistance > circleHeights[circleHeights.length - 1]) { // Target outside, no hit! return false; } else if (targetDistance < circleHeights[0]) { // Target inside core, core hit! hitInfo.hitTileY = 0; hitInfo.hitNormalX = originNormalX; hitInfo.hitNormalY = originNormalY; hitInfo.hitPositionX = positionX + originNormalX * circleHeights[hitInfo.hitTileY + 1]; hitInfo.hitPositionY = positionY + originNormalY * circleHeights[hitInfo.hitTileY + 1]; hitInfo.hitDistance = Mathf.len(originX - hitInfo.hitPositionX, originY - hitInfo.hitPositionY); return true; } hitInfo.hitTileX = (int) ((originMapAngle / (MathUtils.PI * 2.0f)) * width); hitInfo.hitTileX = hitInfo.hitTileX % width; for (int i = circleHeights.length - 1; i >= 1; i--) { if (originDistance > circleHeights[i]) { hitInfo.hitTileY = i - 1; len -= originDistance - circleHeights[i]; break; } } while (hitInfo.hitTileY >= 0 && len > 0) { if (getTile(hitInfo.hitTileX, hitInfo.hitTileY) != 0) { hitInfo.hitNormalX = originNormalX; hitInfo.hitNormalY = originNormalY; hitInfo.hitPositionX = positionX + originNormalX * circleHeights[hitInfo.hitTileY + 1]; hitInfo.hitPositionY = positionY + originNormalY * circleHeights[hitInfo.hitTileY + 1]; hitInfo.hitDistance = Mathf.len(originX - hitInfo.hitPositionX, originY - hitInfo.hitPositionY); return true; } if (hitInfo.hitTileY > 0) { len -= (circleHeights[hitInfo.hitTileY] - circleHeights[hitInfo.hitTileY - 1]); } hitInfo.hitTileY--; } } return false; } public void recycle() { DataPools.poolVector2.returnArray(circleNormals); circleNormals = null; DataPools.poolFloat.returnArray(circleHeights); circleHeights = null; DataPools.poolByte.returnArray(tiles); tiles = null; listener = null; } }