add TileContainer base class

This commit is contained in:
Gered 2013-08-25 17:37:03 -04:00
parent 3b146aa99f
commit 8ffa17555b
2 changed files with 312 additions and 0 deletions

View file

@ -200,6 +200,7 @@
<Compile Include="TileMap\Meshes\TileMesh.cs" />
<Compile Include="TileMap\Meshes\CubeTileMesh.cs" />
<Compile Include="TileMap\Meshes\TileMeshCollection.cs" />
<Compile Include="TileMap\TileContainer.cs" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<ItemGroup>

View file

@ -0,0 +1,311 @@
using System;
using Blarg.GameFramework.TileMap.Meshes;
namespace Blarg.GameFramework.TileMap
{
public abstract class TileContainer
{
public abstract int Width { get; }
public abstract int Height { get; }
public abstract int Depth { get; }
public abstract int MinX { get; }
public abstract int MinY { get; }
public abstract int MinZ { get; }
public abstract int MaxX { get; }
public abstract int MaxY { get; }
public abstract int MaxZ { get; }
public abstract Vector3 Position { get; }
public abstract BoundingBox Bounds { get; }
public abstract Tile Get(int x, int y, int z);
public abstract Tile GetSafe(int x, int y, int z);
public bool IsWithinBounds(int x, int y, int z)
{
if (x < MinX || x > MaxX)
return false;
else if (y < MinY || y > MaxY)
return false;
else if (z < MinZ || z > MaxZ)
return false;
else
return true;
}
public bool IsWithinLocalBounds(int x, int y, int z)
{
if (x < 0 || x >= Width)
return false;
else if (y < 0 || y >= Height)
return false;
else if (z < 0 || z >= Depth)
return false;
else
return true;
}
public void GetBoundingBoxFor(int x, int y, int z, ref 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 += Bounds.Min;
result.Max += Bounds.Min;
}
public bool GetOverlappedTiles(BoundingBox box, Point3 min, Point3 max)
{
// make sure the given box actually intersects with this TileContainer in the first place
var bounds = Bounds;
if (!IntersectionTester.Test(ref bounds, ref 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 = (int)Math.Ceiling(box.Max.X);
int maxY = (int)Math.Ceiling(box.Max.Y - 1.0f);
int maxZ = (int)Math.Ceiling(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 = MathHelpers.Clamp(minX, MinX, MaxX + 1);
minY = MathHelpers.Clamp(minY, MinY, MaxY);
minZ = MathHelpers.Clamp(minZ, MinZ, MaxZ + 1);
maxX = MathHelpers.Clamp(maxX, MinX, MaxX + 1);
maxY = MathHelpers.Clamp(maxY, MinY, MaxY);
maxZ = MathHelpers.Clamp(maxZ, MinZ, MaxZ + 1);
// return the leftover area, converted to the local coordinate space of the TileContainer
min.X = minX - MinX;
min.Y = minY - MinY;
min.Z = minZ - MinZ;
max.X = maxX - MinX;
max.Y = maxY - MinY;
max.Z = maxZ - MinZ;
return true;
}
public bool CheckForCollision(Ray ray, ref Point3 collisionCoords)
{
// make sure that the ray and this TileContainer can actually collide in the first place
var bounds = Bounds;
var position = Vector3.Zero;
if (!IntersectionTester.Test(ref ray, ref bounds, ref position))
return false;
// convert initial collision point to tile coords (this is in "world/tilemap space")
int currentX = (int)position.X;
int currentY = (int)position.Y;
int currentZ = (int)position.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 = MathHelpers.Clamp(currentX, MinX, MaxX);
currentY = MathHelpers.Clamp(currentY, MinY, MaxY);
currentZ = MathHelpers.Clamp(currentZ, MinZ, MaxZ);
// convert to the local space of this TileContainer
currentX -= MinX;
currentY -= MinY;
currentZ -= MinZ;
// is the start position colliding with a solid tile?
var startTile = Get(currentX, currentY, currentZ);
if (startTile.IsCollideable)
{
// collision found, set the tile coords of the collision
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 = Math.Sign(ray.Direction.X);
int stepY = Math.Sign(ray.Direction.Y);
int stepZ = Math.Sign(ray.Direction.Z);
// tile boundary (needs to be in "world/tilemap space")
int tileBoundaryX = MinX + currentX + (stepX > 0 ? 1 : 0);
int tileBoundaryY = MinY + currentY + (stepY > 0 ? 1 : 0);
int tileBoundaryZ = MinZ + 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
Vector3 tMax;
tMax.X = (tileBoundaryX - ray.Position.X) / ray.Direction.X;
tMax.Y = (tileBoundaryY - ray.Position.Y) / ray.Direction.Y;
tMax.Z = (tileBoundaryZ - ray.Position.Z) / ray.Direction.Z;
if (tMax.X == Single.NegativeInfinity)
tMax.X = Single.PositiveInfinity;
if (tMax.Y == Single.NegativeInfinity)
tMax.Y = Single.PositiveInfinity;
if (tMax.Z == Single.NegativeInfinity)
tMax.Z = Single.PositiveInfinity;
if (Single.IsNaN(tMax.X))
tMax.X = Single.PositiveInfinity;
if (Single.IsNaN(tMax.Y))
tMax.Y = Single.PositiveInfinity;
if (Single.IsNaN(tMax.Z))
tMax.Z = Single.PositiveInfinity;
// determine how far we must travel along the ray before we cross a grid cell
Vector3 tDelta;
tDelta.X = stepX / ray.Direction.X;
tDelta.Y = stepY / ray.Direction.Y;
tDelta.Z = stepZ / ray.Direction.Z;
if (tDelta.X == Single.NegativeInfinity)
tDelta.X = Single.PositiveInfinity;
if (tDelta.Y == Single.NegativeInfinity)
tDelta.Y = Single.PositiveInfinity;
if (tDelta.Z == Single.NegativeInfinity)
tDelta.Z = Single.PositiveInfinity;
if (Single.IsNaN(tDelta.X))
tDelta.X = Single.PositiveInfinity;
if (Single.IsNaN(tDelta.Y))
tDelta.Y = Single.PositiveInfinity;
if (Single.IsNaN(tDelta.Z))
tDelta.Z = Single.PositiveInfinity;
bool collided = false;
bool 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 (tMax.X < tMax.Y && tMax.X < tMax.Z)
{
// tMax.x is lowest, the YZ tile boundary plane is closest
currentX += stepX;
tMax.X += tDelta.X;
}
else if (tMax.Y < tMax.Z)
{
// tMax.y is lowest, the XZ tile boundary plane is closest
currentY += stepY;
tMax.Y += tDelta.Y;
}
else
{
// tMax.z is lowest, the XY tile boundary plane is closest
currentZ += stepZ;
tMax.Z += tDelta.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 >= Width ||
currentY < 0 || currentY >= Height ||
currentZ < 0 || currentZ >= Depth
)
outOfContainer = true;
else
{
// still inside and at the next position, test for a solid tile
var tile = Get(currentX, currentY, currentZ);
if (tile.IsCollideable)
{
collided = true;
// set the tile coords of the collision
collisionCoords.X = currentX;
collisionCoords.Y = currentY;
collisionCoords.Z = currentZ;
break;
}
}
}
return collided;
}
public bool CheckForCollision(Ray ray, ref Point3 collisionCoords, TileMeshCollection tileMeshes, ref 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...
if (!CheckForCollision(ray, ref collisionCoords))
return false;
// 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,
collisionCoords.X, collisionCoords.Y, collisionCoords.Z,
tileMeshes,
ref tileMeshCollisionPoint
);
}
public bool CheckForCollisionWithTileMesh(Ray ray, int x, int y, int z, TileMeshCollection tileMeshes, ref Vector3 outCollisionPoint)
{
var tile = Get(x, y, z);
var mesh = tileMeshes.Get(tile);
var vertices = mesh.CollisionVertices;
// world position of this tile, will be used to move each
// mesh triangle into world space
var tileWorldPosition = new Vector3((float)x, (float)y, (float)z);
float closestSquaredDistance = Single.PositiveInfinity;
bool collided = false;
var collisionPoint = 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)
var a = vertices[i] + tileWorldPosition;
var b = vertices[i + 1] + tileWorldPosition;
var c = vertices[i + 2] + tileWorldPosition;
if (IntersectionTester.Test(ref ray, ref a, ref b, ref c, ref collisionPoint))
{
collided = true;
// if this is the closest collision yet, then keep the distance
// and point of collision
float squaredDistance = (collisionPoint - ray.Position).LengthSquared;
if (squaredDistance < closestSquaredDistance)
{
closestSquaredDistance = squaredDistance;
outCollisionPoint = collisionPoint;
}
}
}
return collided;
}
}
}