Code/Tilemap/TileMapCollider.cs

A Collider component for a TileMap in a 2D tilemapper. It builds physics box shapes from tile collision data by converting tile collision cells into bounding boxes and adding box shapes to the target PhysicsBody, and resets when the tilemap signals stability.

Native Interop
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();
	}
}