A collider component that generates physics box shapes from a TileMap component. It scans visible, collision-enabled layers, builds a collision bitmap, computes optimal bounding boxes, converts them to world coordinates and adds static box shapes to the PhysicsBody.
using Sandbox;
using Sandbox.Helper;
using System.Collections.Generic;
using System.Linq;
namespace Saandy.Tilemapper;
public sealed class TileMapCollider : Collider, ITilemapSceneEvent
{
[RequireComponent] public TileMap Tilemap { get; set; }
protected override IEnumerable<PhysicsShape> CreatePhysicsShapes( PhysicsBody targetBody, Transform local )
{
Tilemap = GameObject.GetComponent<TileMap>();
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,
0.0f
);
targetBody.AddBoxShape( pos, Rotation.Identity, size * 0.5f, rebuildMass: false );
}
}
targetBody.BodyType = PhysicsBodyType.Static;
return targetBody.Shapes;
}
void ITilemapSceneEvent.OnTilemapStable()
{
Reset();
}
}