Tilemap/TileMapCollider.cs

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.

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