A collider component for a TileMap that builds physics box shapes from tile collision data. It scans visible, collision-enabled layers, creates a binary collision map, computes optimal bounding boxes, converts them to world positions/sizes, adds box shapes to the PhysicsBody, and marks the body static.
using Sandbox;
using Sandbox.Helper;
using System.Collections.Generic;
using System.Linq;
namespace Saandy.Tilemapper;
public sealed class TileMapCollider : Collider, ITilemapSceneEvent
{
[Property, Order(-1)] public TileMap Tilemap { get; set; }
protected override IEnumerable<PhysicsShape> CreatePhysicsShapes( PhysicsBody targetBody, Transform local )
{
if(!Tilemap.IsValid()) { return targetBody.Shapes; }
Log.Info( targetBody.GameObject.Name );
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 "painted / mask-present" while still being a visual empty
// placeholder for a specific brush, for example Bitmask2x2Edge tile 15.
List<int> collisionMap = layer.Tiles
.Select( tile => Tilemap.IsCollisionTile( tile ) ? 1 : 0 )
.ToList();
List<BBox> colBBoxes = Histogram.GetOptimalQuadding(
collisionMap,
layer.Width,
layer.Height
).ToList();
for ( int i = 0; i < colBBoxes.Count; i++ )
{
BBox box = colBBoxes[i];
// 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,
16
);
DebugOverlay.Box( pos, size, Color.Red, 1 );
PhysicsShape shape = targetBody.AddBoxShape( pos, Rotation.Identity, size * 0.5f, rebuildMass: false );
shape.Surface = Surface;
}
}
targetBody.BodyType = PhysicsBodyType.Static;
targetBody.Surface = Surface;
return targetBody.Shapes;
}
void ITilemapSceneEvent.OnTilemapStable()
{
if(Game.IsPlaying) { return; }
RebuildImmediately();
}
}