Tilemap/TileMapCollider.cs
using Sandbox;
using Sandbox.Helper;
using System.Collections.Generic;
namespace Saandy.Tilemapper;
public sealed class TileMapCollider : Collider, ITilemapSceneEvent
{
[Property, Order( -3 )] public TileMap Tilemap { get; set; }
[Property, Order( -1 )] public bool DebugChanges { get; set; } = false;
[Change][Property, Order( -2 )] public float Depth { get; set; } = 1;
//
// CreatePhysicsShapes can be called repeatedly while editing. Reuse the
// collision mask instead of creating a LINQ Select().ToList() allocation
// for every layer/rebuild.
//
private readonly List<int> CollisionMap = new();
void OnDepthChanged( float oldValue, float newValue )
{
RebuildImmediately();
}
protected override IEnumerable<PhysicsShape> CreatePhysicsShapes( PhysicsBody targetBody, Transform local )
{
//
// Always clear stale shapes first. If the tilemap disappeared, returning
// before ClearShapes() would leave the old tilemap collision on this body.
//
targetBody.ClearShapes();
if ( Tilemap == null || !Tilemap.IsValid() )
{
return targetBody.Shapes;
}
for ( int layerIndex = 0; layerIndex < Tilemap.LayerCount; layerIndex++ )
{
var layer = Tilemap.GetLayer( layerIndex );
if ( layer == null || !layer.IsVisible || !layer.CollisionsEnabled ) { continue; }
if ( layer.Tiles == null || layer.Tiles.Count == 0 || layer.Tiles.Count != layer.Width * layer.Height ) { continue; }
//
// Important:
// Do not use TilesetId directly here.
// A tile can be mask-present while still being a visual empty/non-colliding
// placeholder for a brush, for example Bitmask2x2Edge tile 15.
//
CollisionMap.Clear();
if ( CollisionMap.Capacity < layer.Tiles.Count )
{
CollisionMap.Capacity = layer.Tiles.Count;
}
for ( int i = 0; i < layer.Tiles.Count; i++ )
{
CollisionMap.Add( Tilemap.IsCollisionTile( layer.Tiles[i] ) ? 1 : 0 );
}
foreach ( BBox box in Histogram.GetOptimalQuadding( CollisionMap, layer.Width, layer.Height ) )
{
// Histogram boxes are in local layer array coordinates. Add the layer origin
// to get map coordinates, then let TileMap convert those coordinates onto
// the selected world plane. Collision shapes are not layer-offset because
// these layers are for 2D gameplay.
float mapX = (box.Center.x + layer.Origin.x) * Tilemap.TileSize;
float mapY = (box.Center.y + layer.Origin.y) * Tilemap.TileSize;
Vector3 pos = Tilemap.MapToWorld( mapX, mapY );
Vector3 size = Tilemap.MapSizeToWorld(
box.Size.x * Tilemap.TileSize,
box.Size.y * Tilemap.TileSize,
1
);
#if DEBUG
#endif
targetBody.AddBoxShape( pos, Rotation.Identity, size * 0.5f, rebuildMass: false );
}
}
targetBody.BodyType = PhysicsBodyType.Static;
targetBody.Surface = Surface;
return targetBody.Shapes;
}
void ITilemapSceneEvent.OnTilemapStable()
{
if ( Game.IsPlaying ) { return; }
RebuildImmediately();
}
}