A Component that wraps the engine SpriteRenderer and Sprite resource to provide a 2D sprite layer replacement for older SpriteTools code. It creates a hidden child GameObject with a SpriteRenderer, exposes properties like Tint, FlashTint, PlaybackSpeed, SpriteFlags, shadow settings and animation control, and forwards renderer events back to assigned actions.
using Sandbox;
using System;
using System.Linq;
using System.Text.Json.Serialization;
/// <summary>
/// 2D sprite component for this project. It wraps the engine's native <see cref="SpriteRenderer"/>
/// and <see cref="Sprite"/> resource, replicating the behaviour previously provided by SpriteTools'
/// SpriteRendererLayer so the project no longer depends on that library.
///
/// Like the old layer, it spawns a hidden child GameObject holding a native SpriteRenderer configured
/// for a top-down 2D view (Billboard = None plus a rotation offset derived from <see cref="UpDirection"/>)
/// and exposes the same property surface the game code and prefabs already use (Tint, FlashTint,
/// SpriteFlags, etc.). This keeps the exact visual/behavioural result while dropping the dependency.
/// </summary>
[Category( "2D" )]
[Icon( "emoji_emotions" )]
[Title( "2D Sprite" )]
public sealed class Sprite2D : Component, Component.ExecuteInEditor
{
/// <summary>Which world axis the sprite treats as "up" when orienting itself for the 2D camera.</summary>
public enum Axis { XPositive, XNegative, YPositive, YNegative, ZPositive, ZNegative }
/// <summary>How the sprite participates in shadow rendering.</summary>
public enum ShadowRenderType { On, Off, ShadowsOnly }
/// <summary>
/// The sprite resource that this component renders.
/// </summary>
[Property]
public Sprite Sprite
{
get => _sprite;
set { _sprite = value; ApplySprite(); }
}
Sprite _sprite;
/// <summary>
/// Which axis is treated as "up". Controls the rotation offset applied to the underlying renderer so
/// the sprite faces the 2D camera correctly.
/// </summary>
[Property]
public Axis UpDirection
{
get => _upDirection;
set
{
_upDirection = value;
switch ( value )
{
case Axis.XPositive: _rotationOffset = Rotation.From( 0, 180, 0 ); break;
case Axis.XNegative: _rotationOffset = Rotation.From( 0, 0, 0 ); break;
case Axis.YPositive: _rotationOffset = Rotation.From( 0, -90, 0 ); break;
case Axis.YNegative: _rotationOffset = Rotation.From( 0, 90, 0 ); break;
case Axis.ZPositive: _rotationOffset = Rotation.From( 90, 0, 0 ); break;
case Axis.ZNegative: _rotationOffset = Rotation.From( -90, 0, 0 ); break;
}
ApplyRotation();
}
}
Axis _upDirection = Axis.YPositive;
Rotation _rotationOffset = Rotation.From( 0, -90, 0 );
/// <summary>The colour tint of the sprite (multiplied with the texture). Maps to SpriteRenderer.Color.</summary>
[Property, Category( "Visuals" )]
public Color Tint
{
get => _tint;
set { if ( _tint == value ) return; _tint = value; ApplyColor(); }
}
Color _tint = Color.White;
/// <summary>The flash/overlay colour of the sprite. Maps to SpriteRenderer.OverlayColor.</summary>
[Property, Category( "Visuals" )]
public Color FlashTint
{
get => _flashTint;
set { if ( _flashTint == value ) return; _flashTint = value; ApplyColor(); }
}
Color _flashTint = Color.White.WithAlpha( 0 );
/// <summary>Playback speed of the animation. 0 pauses, negative plays in reverse.</summary>
[Property]
public float PlaybackSpeed
{
get => _playbackSpeed;
set { _playbackSpeed = value; ApplyPlaybackSpeed(); }
}
float _playbackSpeed = 1.0f;
[Property] public bool UsePixelScale { get; set; } = false;
/// <summary>When enabled, keeps the depth (Z) scale locked to the X scale in play mode.</summary>
[Property] public bool AutoScale { get; set; } = true;
[Property, Category( "Visuals" )]
public ShadowRenderType CastShadows
{
get => _castShadows;
set { _castShadows = value; ApplyShadows(); }
}
ShadowRenderType _castShadows = ShadowRenderType.On;
[Property, Category( "Visuals" )]
public SpriteFlags SpriteFlags
{
get => _spriteFlags;
set { if ( _spriteFlags == value ) return; _spriteFlags = value; ApplySpriteFlags(); }
}
SpriteFlags _spriteFlags = SpriteFlags.None;
/// <summary>The animation currently playing (or the queued one if the renderer isn't created yet).</summary>
[JsonIgnore]
public Sprite.Animation CurrentAnimation
{
get => _spriteRenderer?.CurrentAnimation ?? Sprite?.GetAnimation( _queuedAnimationName );
set { PlayAnimation( value.Name ); }
}
[Property, Category( "Sprite" ), Title( "Current Animation" ), Editor( "sprite_animation_name" )]
string StartingAnimationName
{
get => CurrentAnimation?.Name ?? (_sprite?.Animations?.FirstOrDefault()?.Name ?? "");
set
{
if ( Sprite == null ) return;
var animation = Sprite.Animations.Find( a => a.Name.ToLowerInvariant() == value.ToLowerInvariant() );
if ( animation == null ) { _queuedAnimationName = value; return; }
PlayAnimation( animation.Name );
}
}
string _queuedAnimationName = "";
[Property, Category( "Actions" )]
public Action<string> OnAnimationComplete { get; set; }
[Property, Category( "Actions" )]
public Action<string> OnBroadcastMessage { get; set; }
public BBox Bounds
{
get
{
var size = new Vector2( 50, 50 );
BBox bbox = new BBox( new Vector3( -size.x, -size.y, -0.1f ), new Vector3( size.x, size.y, 0.1f ) );
var origin = (CurrentAnimation?.Origin ?? new Vector2( 0.5f, 0.5f )) - new Vector2( 0.5f, 0.5f );
bbox = bbox.Translate( new Vector3( origin.y, origin.x, 0 ) * new Vector3( -size.x * 2f, -size.y * 2f, 1f ) );
return bbox;
}
}
/// <summary>The index of the frame currently being displayed.</summary>
public int CurrentFrameIndex
{
get => _spriteRenderer?.CurrentFrameIndex ?? 0;
set { if ( _spriteRenderer.IsValid() ) _spriteRenderer.CurrentFrameIndex = value; }
}
/// <summary>The native SpriteRenderer doing the actual rendering (hosted on a hidden child object).</summary>
public SpriteRenderer Renderer => _spriteRenderer;
SpriteRenderer _spriteRenderer;
protected override void OnAwake()
{
base.OnAwake();
if ( !_spriteRenderer.IsValid() )
CreateSpriteRenderer();
}
protected override void OnEnabled()
{
base.OnEnabled();
if ( _spriteRenderer.IsValid() )
{
_spriteRenderer.Enabled = true;
_spriteRenderer.AlphaCutoff = 0f;
_spriteRenderer.Shadows = false;
}
}
protected override void OnDisabled()
{
base.OnDisabled();
if ( _spriteRenderer.IsValid() )
_spriteRenderer.Enabled = false;
}
protected override void OnDestroy()
{
base.OnDestroy();
if ( _spriteRenderer.IsValid() )
{
_spriteRenderer.GameObject?.Destroy();
_spriteRenderer = null;
}
}
protected override void OnPreRender()
{
base.OnPreRender();
if ( !Game.IsPlaying ) return;
// Keep the sprite's depth (Z) scale in sync with its planar scale so uniform 2D scaling behaves
// as it did with the old SpriteTools layer.
if ( LocalScale.z == 1 && LocalScale.y != 1 )
LocalScale = new Vector3( LocalScale.x, LocalScale.y, LocalScale.y );
else if ( LocalScale.x == 1 && LocalScale.z == 1 && LocalScale.y != 1 )
LocalScale = new Vector3( LocalScale.y, LocalScale.y, LocalScale.y );
else if ( AutoScale )
LocalScale = new Vector3( LocalScale.x, LocalScale.y, LocalScale.x );
}
protected override void DrawGizmos()
{
base.DrawGizmos();
if ( Sprite is null ) return;
Gizmo.Transform = Gizmo.Transform.WithRotation( WorldRotation * _rotationOffset );
var bbox = Bounds;
Gizmo.Hitbox.BBox( bbox );
if ( Gizmo.IsHovered || Gizmo.IsSelected )
{
bbox.Mins.z = 0;
bbox.Maxs.z = 0.0f;
Gizmo.Draw.Color = Gizmo.IsSelected ? Color.White : Color.Orange;
Gizmo.Draw.LineBBox( bbox );
}
}
void ApplySprite()
{
if ( !_spriteRenderer.IsValid() ) return;
_spriteRenderer.Sprite = _sprite;
if ( !string.IsNullOrEmpty( _queuedAnimationName ) )
{
_spriteRenderer.StartingAnimationName = _queuedAnimationName;
_queuedAnimationName = null;
}
}
void ApplyColor()
{
if ( !_spriteRenderer.IsValid() ) return;
_spriteRenderer.Color = _tint;
_spriteRenderer.OverlayColor = _flashTint;
}
void ApplySpriteFlags()
{
if ( !_spriteRenderer.IsValid() ) return;
_spriteRenderer.FlipHorizontal = _spriteFlags.HasFlag( SpriteFlags.HorizontalFlip );
_spriteRenderer.FlipVertical = _spriteFlags.HasFlag( SpriteFlags.VerticalFlip );
}
void ApplyRotation()
{
if ( !_spriteRenderer.IsValid() ) return;
_spriteRenderer.LocalRotation = _rotationOffset * new Angles( -90, 0, 0 );
}
void ApplyShadows()
{
if ( !_spriteRenderer.IsValid() ) return;
_spriteRenderer.Shadows = _castShadows != ShadowRenderType.Off;
}
void ApplyPlaybackSpeed()
{
if ( !_spriteRenderer.IsValid() ) return;
_spriteRenderer.PlaybackSpeed = _playbackSpeed;
}
void CreateSpriteRenderer()
{
var childObject = new GameObject();
childObject.Parent = GameObject;
childObject.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.Hidden;
_spriteRenderer = childObject.AddComponent<SpriteRenderer>();
_spriteRenderer.Billboard = SpriteRenderer.BillboardMode.None;
_spriteRenderer.TextureFilter = Sandbox.Rendering.FilterMode.Point;
_spriteRenderer.Enabled = false;
_spriteRenderer.Size = 100;
_spriteRenderer.IsSorted = true;
_spriteRenderer.Shadows = false;
// Forward through lambdas so reassigning the actions at runtime is respected (the delegate isn't
// captured by value at creation time).
_spriteRenderer.OnAnimationEnd += ( name ) => OnAnimationComplete?.Invoke( name );
_spriteRenderer.OnBroadcastMessage += ( msg ) => OnBroadcastMessage?.Invoke( msg );
ApplySprite();
ApplyColor();
ApplySpriteFlags();
ApplyRotation();
ApplyShadows();
ApplyPlaybackSpeed();
}
/// <summary>
/// Plays an animation from the current Sprite by name.
/// </summary>
/// <param name="animationName">The name of the animation.</param>
/// <param name="force">Unused; retained for source compatibility with the old SpriteTools layer API.</param>
public void PlayAnimation( string animationName, bool force = false )
{
if ( _spriteRenderer?.CurrentAnimation?.Name == animationName )
return;
if ( _spriteRenderer.IsValid() )
_spriteRenderer.PlayAnimation( animationName );
else
_queuedAnimationName = animationName;
}
}
/// <summary>
/// Flags controlling how a <see cref="Sprite2D"/> is drawn. Ported from SpriteTools so existing game
/// code (and serialized prefab values) keep working after removing that library.
/// </summary>
[Flags]
public enum SpriteFlags
{
None = 0,
[Icon( "align_horizontal_center" )]
[Title( "Horizontal Flip" )]
[Description( "Flip the sprite horizontally" )]
HorizontalFlip = 1 << 2,
[Icon( "align_vertical_center" )]
[Title( "Vertical Flip" )]
[Description( "Flip the sprite vertically" )]
VerticalFlip = 1 << 3,
[Icon( "flip_to_back" )]
[Title( "Draw Backface" )]
[Description( "Draw the backface of the sprite" )]
DrawBackface = 1 << 4
}