PixelCollision.cs
namespace Sandbox;
/// <summary>
/// Tracks a <see cref="PixelSpriteComponent"/> alongside the world-space position
/// that the game code owns. Updated each frame by the caller before collision queries.
/// </summary>
public class CollisionEntry
{
public PixelSpriteComponent Sprite { get; set; }
public int X { get; set; }
public int Y { get; set; }
public CollisionEntry( PixelSpriteComponent sprite, int x = 0, int y = 0 )
{
Sprite = sprite;
X = x;
Y = y;
}
}
/// <summary>
/// Static scene-level collision system for <see cref="PixelSpriteComponent"/>.
///
/// Game code registers each collideable sprite via <see cref="Register"/> and
/// keeps the entry's position up to date. Collision queries scan the registry
/// using AABB overlap on the flip-aware hitboxes.
///
/// Ported from the Unity pixelshitter framework's Scene.Collide / Entity.Collide
/// pattern, flattened into a static utility because the s&box port doesn't have
/// a Scene/Entity hierarchy.
/// </summary>
public static class PixelCollision
{
private static readonly List<CollisionEntry> _entries = new();
// ── Registration ─────────────────────────────────────────────────────
/// <summary>
/// Register a sprite for collision queries. Returns the <see cref="CollisionEntry"/>
/// so the caller can update its position each frame.
/// </summary>
public static CollisionEntry Register( PixelSpriteComponent sprite, int x = 0, int y = 0 )
{
CollisionEntry entry = new( sprite, x, y );
_entries.Add( entry );
return entry;
}
/// <summary>Remove a previously registered entry.</summary>
public static void Unregister( CollisionEntry entry )
{
_entries.Remove( entry );
}
/// <summary>Remove all entries whose sprite matches.</summary>
public static void Unregister( PixelSpriteComponent sprite )
{
_entries.RemoveAll( e => e.Sprite == sprite );
}
/// <summary>Remove all registered entries.</summary>
public static void Clear()
{
_entries.Clear();
}
// ── Queries ──────────────────────────────────────────────────────────
/// <summary>
/// Find the first registered sprite with <paramref name="tag"/> whose hitbox
/// overlaps <paramref name="source"/> at the hypothetical position
/// (<paramref name="x"/>, <paramref name="y"/>).
/// </summary>
/// <returns>The matching entry, or null if no overlap.</returns>
public static CollisionEntry Collide( PixelSpriteComponent source, string tag, int x, int y )
{
PixelRect sourceBox = source.GetOffsetHitbox( x, y );
foreach ( CollisionEntry entry in _entries )
{
if ( entry.Sprite == source ) continue;
if ( !entry.Sprite.Collideable ) continue;
if ( !entry.Sprite.Tags.Contains( tag ) ) continue;
PixelRect otherBox = entry.Sprite.GetOffsetHitbox( entry.X, entry.Y );
if ( sourceBox.Overlaps( otherBox ) )
return entry;
}
return null;
}
/// <summary>
/// Find all registered sprites with <paramref name="tag"/> whose hitboxes
/// overlap <paramref name="source"/> at the hypothetical position.
/// </summary>
public static List<CollisionEntry> CollideWithAll( PixelSpriteComponent source, string tag, int x, int y )
{
List<CollisionEntry> results = new();
PixelRect sourceBox = source.GetOffsetHitbox( x, y );
foreach ( CollisionEntry entry in _entries )
{
if ( entry.Sprite == source ) continue;
if ( !entry.Sprite.Collideable ) continue;
if ( !entry.Sprite.Tags.Contains( tag ) ) continue;
PixelRect otherBox = entry.Sprite.GetOffsetHitbox( entry.X, entry.Y );
if ( sourceBox.Overlaps( otherBox ) )
results.Add( entry );
}
return results;
}
/// <summary>
/// Test whether an arbitrary <paramref name="rect"/> overlaps any registered
/// sprite with <paramref name="tag"/> (excluding <paramref name="source"/>).
/// </summary>
public static CollisionEntry Collide( PixelSpriteComponent source, string tag, PixelRect rect )
{
foreach ( CollisionEntry entry in _entries )
{
if ( entry.Sprite == source ) continue;
if ( !entry.Sprite.Collideable ) continue;
if ( !entry.Sprite.Tags.Contains( tag ) ) continue;
PixelRect otherBox = entry.Sprite.GetOffsetHitbox( entry.X, entry.Y );
if ( rect.Overlaps( otherBox ) )
return entry;
}
return null;
}
/// <summary>
/// Check whether <paramref name="source"/>'s hitbox at the given position
/// fits entirely within the <see cref="PixelScreen"/> pixel buffer.
/// </summary>
public static bool IsInBounds( PixelSpriteComponent source, int x, int y )
{
PixelScreen screen = PixelScreen.Instance;
if ( screen is null )
return false;
PixelRect box = source.GetOffsetHitbox( x, y );
return box.XMin >= 0
&& box.YMin >= 0
&& box.XMax <= screen.PixelWidth
&& box.YMax <= screen.PixelHeight;
}
}