Tileset/TilesetCollider.cs
using Sandbox;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
namespace SpriteTools;
[Hide]
public partial class TilesetCollider : Collider, Component.ExecuteInEditor
{
internal TilesetComponent Tileset { get; set; }
internal bool IsDirty = true;
private Model CollisionMesh { get; set; }
private List<Vector3> CollisionVertices { get; set; } = new();
private List<int[]> CollisionFaces { get; set; } = new();
protected override void OnUpdate()
{
base.OnUpdate();
if ( IsDirty )
{
IsDirty = false;
RebuildMesh();
}
}
protected override void DrawGizmos()
{
base.DrawGizmos();
if ( !Gizmo.IsSelected ) return;
if ( CollisionMesh is not null )
{
using ( Gizmo.Scope( "tile_collisions" ) )
{
Gizmo.Draw.Color = Color.Green;
Gizmo.Draw.LineThickness = 2f;
foreach ( var face in CollisionFaces )
{
for ( int i = 0; i < face.Length; i++ )
{
var a = CollisionVertices[face[i]];
var b = CollisionVertices[face[(i + 1) % face.Length]];
Gizmo.Draw.Line( a, b );
}
}
}
}
}
internal void RebuildMesh()
{
if ( CollisionMesh is not null )
{
CollisionMesh = null;
CollisionVertices.Clear();
CollisionFaces.Clear();
}
CollisionBoxes.Clear();
if ( !(Tileset?.HasCollider ?? false) ) return;
if ( Tileset.Layers is null ) return;
var collisionLayer = Tileset.Layers.FirstOrDefault( x => x.IsCollisionLayer );
if ( collisionLayer is null ) collisionLayer = Tileset.Layers.FirstOrDefault();
if ( collisionLayer is null ) return;
var tilePositions = new Dictionary<Vector2Int, bool>();
foreach ( var tile in collisionLayer.Tiles )
{
tilePositions[tile.Key] = true;
}
if ( tilePositions.Count == 0 ) return;
var minPosition = tilePositions.Keys.Aggregate( ( min, next ) => Vector2Int.Min( min, next ) );
var maxPosition = tilePositions.Keys.Aggregate( ( max, next ) => Vector2Int.Max( max, next ) );
var totalSize = maxPosition - minPosition + Vector2Int.One;
bool[,] tiles = new bool[totalSize.x, totalSize.y];
foreach ( var tile in tilePositions )
{
var pos = tile.Key - minPosition;
tiles[pos.x, pos.y] = true;
}
// Generate mesh from tiles
var firstResource = Tileset.Layers[0].TilesetResource;
var tileSize = (Vector2)firstResource.GetTileSize();
var mesh = new PolygonMesh();
CollisionVertices = new List<Vector3>();
CollisionFaces = new List<int[]>();
var min3d = new Vector3( minPosition.x * tileSize.x, minPosition.y * tileSize.y, 0 );
bool[,] visited = new bool[totalSize.x, totalSize.y];
for ( int x = 0; x < totalSize.x; x++ )
{
for ( int y = 0; y < totalSize.y; y++ )
{
if ( tiles[x, y] && !visited[x, y] )
{
int width = 1;
int height = 1;
// Check width
while ( x + width < totalSize.x && tiles[x + width, y] && !visited[x + width, y] )
{
width++;
}
// Check height
while ( y + height < totalSize.y && IsRectangle( tiles, visited, x, y, width, height ) )
{
height++;
}
// Mark the cells of this rectangle as visited
for ( int i = 0; i < width; i++ )
{
for ( int j = 0; j < height; j++ )
{
visited[x + i, y + j] = true;
}
}
AddRectangle( CollisionVertices, CollisionFaces, tiles, x, y, width, height, tileSize, Tileset.ColliderWidth, minPosition, CollisionBoxes );
}
}
}
var hVertices = mesh.AddVertices( CollisionVertices.ToArray() );
var faceMat = Material.Load( "materials/dev/reflectivity_30.vmat" );
foreach ( var face in CollisionFaces )
{
var faceIndex = mesh.AddFace( face.Select( x => hVertices[x] ).ToArray() );
mesh.SetFaceMaterial( faceIndex, faceMat );
}
mesh.Transform = Transform.World;
CollisionMesh = mesh.Rebuild();
RebuildImmediately();
}
List<BBox> CollisionBoxes = new();
protected override IEnumerable<PhysicsShape> CreatePhysicsShapes( PhysicsBody targetBody )
{
if ( !(Tileset?.HasCollider ?? false) ) yield break;
if ( CollisionBoxes.Count > 0 )
{
foreach ( var box in CollisionBoxes )
{
var shape = targetBody.AddBoxShape( box, Rotation.Identity, true );
yield return shape;
}
}
else
{
if ( CollisionMesh is null ) yield break;
if ( CollisionMesh.Physics is null ) yield break;
var bodyTransform = targetBody.Transform.ToLocal( Transform.World );
foreach ( var part in CollisionMesh.Physics.Parts )
{
var bx = bodyTransform.ToWorld( part.Transform );
foreach ( var mesh in part.Meshes )
{
var shape = targetBody.AddShape( mesh, bx, false, true );
shape.Surface = mesh.Surface;
shape.Surfaces = mesh.Surfaces;
yield return shape;
}
}
}
}
static bool IsRectangle( bool[,] grid, bool[,] visited, int x, int y, int width, int height )
{
for ( int i = 0; i < width; i++ )
{
if ( !grid[x + i, y + height] || visited[x + i, y + height] )
{
return false;
}
}
return true;
}
static void AddRectangle( List<Vector3> vertices, List<int[]> faces, bool[,] grid, int x, int y, int width, int height, Vector2 tileSize, float depth, Vector2Int minPosition, List<BBox> boxes )
{
int startIndex = vertices.Count;
float currentDepth = MathF.Abs( depth );
float z = currentDepth / 2f;
// Top Face
var v0 = new Vector3( (minPosition.x + x) * tileSize.x, (minPosition.y + y) * tileSize.x, z );
var v1 = new Vector3( (minPosition.x + x + width) * tileSize.x, (minPosition.y + y) * tileSize.y, z );
var v2 = new Vector3( (minPosition.x + x + width) * tileSize.x, (minPosition.y + y + height) * tileSize.y, z );
var v3 = new Vector3( (minPosition.x + x) * tileSize.x, (minPosition.y + y + height) * tileSize.y, z );
AddFace( vertices, faces, v0, v1, v2, v3 );
if ( depth == 0 ) return;
// Bottom Face
z -= currentDepth;
var v4 = new Vector3( (minPosition.x + x) * tileSize.x, (minPosition.y + y) * tileSize.y, z );
var v5 = new Vector3( (minPosition.x + x + width) * tileSize.x, (minPosition.y + y) * tileSize.y, z );
var v6 = new Vector3( (minPosition.x + x + width) * tileSize.x, (minPosition.y + y + height) * tileSize.y, z );
var v7 = new Vector3( (minPosition.x + x) * tileSize.x, (minPosition.y + y + height) * tileSize.y, z );
AddFace( vertices, faces, v4, v5, v6, v7 );
boxes.Add( new BBox( v0, v6 ) );
// Add indices for the sides if not inner
if ( IsExposedFace( grid, x, y, 1, height, -1, 0 ) ) // Left
{
AddFace( vertices, faces, v0, v3, v7, v4 );
}
if ( IsExposedFace( grid, x + width - 1, y, 1, height, 1, 0 ) ) // Right
{
AddFace( vertices, faces, v2, v1, v5, v6 );
}
if ( IsExposedFace( grid, x, y, width, 1, 0, -1 ) ) // Front
{
AddFace( vertices, faces, v1, v0, v4, v5 );
}
if ( IsExposedFace( grid, x, y + height - 1, width, 1, 0, 1 ) ) // Back
{
AddFace( vertices, faces, v3, v2, v6, v7 );
}
}
static void AddFace( List<Vector3> vertices, List<int[]> faces, Vector3 a, Vector3 b, Vector3 c, Vector3 d )
{
var startIndex = vertices.Count;
vertices.AddRange( new Vector3[] { a, b, c, d } );
faces.Add( new int[] { startIndex, startIndex + 1, startIndex + 2 } );
faces.Add( new int[] { startIndex + 2, startIndex + 3, startIndex + 0 } );
}
static bool IsExposedFace( bool[,] grid, int x, int y, int width, int height, int dx, int dy )
{
int rows = grid.GetLength( 0 );
int cols = grid.GetLength( 1 );
for ( int i = 0; i < width; i++ )
{
for ( int j = 0; j < height; j++ )
{
int nx = x + i + dx;
int ny = y + j + dy;
if ( nx < 0 || nx >= rows || ny < 0 || ny >= cols || !grid[nx, ny] )
{
return true;
}
}
}
return false;
}
protected override IEnumerable<PhysicsShape> CreatePhysicsShapes( PhysicsBody targetBody, Transform local )
{
throw new NotImplementedException();
}
}