mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-02 19:38:24 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
414 lines
12 KiB
C#
414 lines
12 KiB
C#
using Sandbox.Rendering;
|
|
|
|
namespace Sandbox;
|
|
|
|
/// <summary>
|
|
/// Renders a sprite in the world
|
|
/// </summary>
|
|
[Expose]
|
|
[Title( "Sprite Renderer" )]
|
|
[Category( "Rendering" )]
|
|
[Icon( "favorite" )]
|
|
public sealed partial class SpriteRenderer : Renderer, Component.ExecuteInEditor, ISpriteRenderGroup
|
|
{
|
|
[Flags]
|
|
public enum FlipFlags
|
|
{
|
|
None = 0,
|
|
|
|
[Icon( "align_horizontal_center" )]
|
|
[Title( "Horizontal Flip" )]
|
|
[Description( "Flip the sprite horizontally around the origin." )]
|
|
FlipX = 2,
|
|
[Icon( "align_vertical_center" )]
|
|
[Title( "Vertical Flip" )]
|
|
[Description( "Flip the sprite vertically around the origin." )]
|
|
FlipY = 4
|
|
}
|
|
|
|
public enum BillboardMode
|
|
{
|
|
Always,
|
|
YOnly,
|
|
Particle,
|
|
None
|
|
}
|
|
|
|
/// <summary>
|
|
/// The sprite resource to render. This can be completely static or contain animation(s).
|
|
/// </summary>
|
|
[Property]
|
|
public Sprite Sprite
|
|
{
|
|
get => _sprite;
|
|
set
|
|
{
|
|
if ( _sprite == value ) return;
|
|
_sprite = value;
|
|
_currentAnimationIndex = 0;
|
|
_animationState.ResetState();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The animation that this sprite should start playing when the scene starts.
|
|
/// </summary>
|
|
[Property, Title( "Current Animation" ), Editor( "sprite_animation_name" )]
|
|
[ShowIf( nameof( IsAnimated ), true )]
|
|
public string StartingAnimationName
|
|
{
|
|
get => CurrentAnimation?.Name ?? (_sprite?.Animations?.FirstOrDefault()?.Name ?? "");
|
|
set
|
|
{
|
|
if ( _sprite == null ) return;
|
|
PlayAnimation( value );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The playback speed of the animation. 0 is paused, and negative values will play the animation in reverse.
|
|
/// </summary>
|
|
[Property]
|
|
[ShowIf( nameof( IsAnimated ), true )]
|
|
public float PlaybackSpeed
|
|
{
|
|
get => _animationState.PlaybackSpeed;
|
|
set => _animationState.PlaybackSpeed = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The width and height of the sprite in world units.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public Vector2 Size { get; set; } = 10.0f;
|
|
|
|
/// <summary>
|
|
/// The color of the sprite. This is multiplied with the texture color.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public Color Color { get; set; } = Color.White;
|
|
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public Color OverlayColor { get; set; } = Color.White.WithAlpha( 0 );
|
|
|
|
/// <summary>
|
|
/// Whether or not the sprite should be rendered additively.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public bool Additive { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether or not the sprite should cast shadows.
|
|
/// </summary>
|
|
[Property, Title( "Cast Shadows" ), Category( "Visuals" ), Order( -200 )]
|
|
public bool Shadows { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether or not the sprite should be rendered opaque. If true, any semi-transparent pixels will be dithered.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public bool Opaque { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether or not the sprite should be lit by the scene's lighting system. Otherwise it will be unlit/fullbright.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public bool Lighting { get; set; }
|
|
|
|
/// <summary>
|
|
/// Amount of feathering applied to the depth, softening its intersection with geometry.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public float DepthFeather { get; set; }
|
|
|
|
/// <summary>
|
|
/// The strength of the fog effect applied to the sprite. This determines how much the sprite blends with any fog in the scene.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public float FogStrength { get; set; } = 1.0f;
|
|
|
|
/// <summary>
|
|
/// Whether or not the sprite should be flipped horizontally.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public bool FlipHorizontal { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether or not the sprite should be flipped vertically.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public bool FlipVertical { get; set; }
|
|
|
|
/// <summary>
|
|
/// The texture filtering mode used when rendering the sprite. For pixelated sprites, use <see cref="Sandbox.UI.ImageRendering.Point"/>.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public FilterMode TextureFilter { get; set; } = FilterMode.Bilinear;
|
|
|
|
/// <summary>
|
|
/// Alignment mode for the sprite's billboard behavior.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public BillboardMode Billboard { get; set; } = BillboardMode.Always;
|
|
|
|
/// <summary>
|
|
/// Whether or not the sprite should be sorted by depth. If the sprite is opaque, this can be turned off for a performance boost if not needed.
|
|
/// </summary>
|
|
[Property, Category( "Visuals" ), Order( -200 )]
|
|
public bool IsSorted { get; set; }
|
|
|
|
/// <summary>
|
|
/// This action is invoked when an animation starts playing. The string parameter is the name of the animation that started.
|
|
/// </summary>
|
|
[Property, Category( "Actions" )]
|
|
public Action<string> OnAnimationStart { get; set; }
|
|
|
|
/// <summary>
|
|
/// This action is invoked when an animation finishes playing or has looped. The string parameter is the name of the animation.
|
|
/// </summary>
|
|
[Property, Category( "Actions" )]
|
|
public Action<string> OnAnimationEnd { get; set; }
|
|
|
|
/// <summary>
|
|
/// This action is invoked when advancing to a new frame that has broadcast messages. The string parameter is the message being broadcast.
|
|
/// </summary>
|
|
[Property, Category( "Actions" )]
|
|
public Action<string> OnBroadcastMessage { get; set; }
|
|
|
|
/// <summary>
|
|
/// The animation that is currently being played. Returns null if no sprite is set or the sprite has no animations.
|
|
/// </summary>
|
|
public Sprite.Animation CurrentAnimation => _sprite?.GetAnimation( _currentAnimationIndex );
|
|
|
|
/// <summary>
|
|
/// The index of the current frame being displayed. This will change over time if the sprite is animated, and can be set to go to a specific frame even during playback.
|
|
/// </summary>
|
|
public int CurrentFrameIndex
|
|
{
|
|
get => _animationState.CurrentFrameIndex;
|
|
set
|
|
{
|
|
_animationState.CurrentFrameIndex = value;
|
|
_animationState.TimeSinceLastFrame = 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether or not the sprite is animated. This is true if the sprite has more than one animation or if the current animation has more than one frame.
|
|
/// </summary>
|
|
public bool IsAnimated => (_sprite?.Animations?.Count ?? 0) > 1;
|
|
|
|
/// <summary>
|
|
/// The texture of the current frame being displayed. Returns a transparent texture when no valid frame is available.
|
|
/// </summary>
|
|
public Texture Texture
|
|
{
|
|
get
|
|
{
|
|
var _anim = CurrentAnimation;
|
|
if ( _anim is null )
|
|
return Texture.Transparent;
|
|
if ( CurrentFrameIndex < 0 || CurrentFrameIndex >= _anim.Frames.Count )
|
|
return Texture.Transparent;
|
|
return _anim.Frames[CurrentFrameIndex]?.Texture;
|
|
}
|
|
[Obsolete]
|
|
set { }
|
|
}
|
|
|
|
internal Vector2 Pivot
|
|
{
|
|
get
|
|
{
|
|
var _anim = CurrentAnimation;
|
|
if ( _anim is null )
|
|
return new Vector2( 0.5f, 0.5f );
|
|
return _anim.Origin;
|
|
}
|
|
}
|
|
|
|
Sprite.AnimationState _animationState = new();
|
|
HashSet<(MessageType Type, Sprite.BroadcastEvent Content)> _messageQueue = new();
|
|
int _currentAnimationIndex = 0;
|
|
Sprite _sprite;
|
|
|
|
protected override void DrawGizmos()
|
|
{
|
|
base.DrawGizmos();
|
|
if ( Game.IsPlaying ) return;
|
|
|
|
Gizmo.Transform = Transform.World;
|
|
|
|
bool isBillboard = Billboard == BillboardMode.Always || Billboard == BillboardMode.YOnly;
|
|
if ( isBillboard )
|
|
Gizmo.Transform = Gizmo.Transform.WithRotation( new Rotation() );
|
|
|
|
Vector3 scale = new( Transform.World.Scale.x, Transform.World.Scale.x, Transform.World.Scale.z );
|
|
Gizmo.Transform = Gizmo.Transform.WithScale( scale );
|
|
|
|
Vector2 pivotScale = (Pivot - 0.5f) * Size;
|
|
Vector2 spriteSize = new( Size.x, Size.y );
|
|
|
|
if ( isBillboard )
|
|
{
|
|
spriteSize += Vector2.Abs( pivotScale * 2 );
|
|
|
|
// Calculate the AABB of the rotated sprite around its pivot
|
|
float angle = MathX.DegreeToRadian( Transform.World.Rotation.Roll() );
|
|
float cos = MathF.Cos( angle );
|
|
float sin = MathF.Sin( angle );
|
|
|
|
spriteSize = new Vector2(
|
|
MathF.Abs( spriteSize.x * cos ) + MathF.Abs( spriteSize.y * sin ),
|
|
MathF.Abs( spriteSize.x * sin ) + MathF.Abs( spriteSize.y * cos )
|
|
);
|
|
}
|
|
|
|
// Flatten it if not a billboard
|
|
Vector3 bboxSize = new( isBillboard ? spriteSize.x : 0.5f, spriteSize.x, spriteSize.y );
|
|
Vector3 bboxPos = isBillboard ? Vector3.Zero : new Vector3( 0, pivotScale.x, pivotScale.y );
|
|
var bbox = BBox.FromPositionAndSize( bboxPos, bboxSize );
|
|
|
|
Gizmo.Hitbox.BBox( bbox );
|
|
|
|
if ( Gizmo.IsHovered || Gizmo.IsSelected )
|
|
{
|
|
Gizmo.Draw.Color = Gizmo.IsSelected ? Color.White : Color.Orange;
|
|
Gizmo.Draw.LineBBox( bbox );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Play an animation by index (the first animation is index 0).
|
|
/// </summary>
|
|
public void PlayAnimation( int index )
|
|
{
|
|
if ( _sprite is null )
|
|
return;
|
|
if ( index < 0 || index >= (_sprite.Animations?.Count ?? 0) )
|
|
{
|
|
Log.Warning( $"Sprite '{_sprite.ResourceName}' does not have an animation at index {index}." );
|
|
return;
|
|
}
|
|
if ( _currentAnimationIndex == index )
|
|
return;
|
|
|
|
_currentAnimationIndex = index;
|
|
_animationState.ResetState();
|
|
OnAnimationStart?.Invoke( CurrentAnimation?.Name );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Play an animation by name.
|
|
/// </summary>
|
|
public void PlayAnimation( string name )
|
|
{
|
|
if ( _sprite is null ) return;
|
|
int index = _sprite.GetAnimationIndex( name );
|
|
if ( index < 0 )
|
|
{
|
|
Log.Warning( $"Sprite '{_sprite.ResourceName}' does not have an animation named '{name}'." );
|
|
return;
|
|
}
|
|
|
|
PlayAnimation( index );
|
|
}
|
|
|
|
protected override void OnUpdate()
|
|
{
|
|
base.OnUpdate();
|
|
|
|
ProcessMessageQueue();
|
|
}
|
|
|
|
internal void AdvanceFrame()
|
|
{
|
|
var result = _animationState.TryAdvanceFrame( CurrentAnimation, Game.IsPlaying ? Time.Delta : RealTime.Delta );
|
|
if ( !result )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( _animationState.JustFinished )
|
|
{
|
|
QueueMessage( MessageType.AnimationEnd, new Sprite.BroadcastEvent() { Message = CurrentAnimation?.Name ?? "" } );
|
|
}
|
|
|
|
var newFrameIndex = _animationState.CurrentFrameIndex;
|
|
var frame = CurrentAnimation?.Frames?[newFrameIndex];
|
|
if ( frame is not null )
|
|
{
|
|
foreach ( var message in frame.BroadcastMessages )
|
|
{
|
|
QueueMessage( MessageType.BroadcastMessage, message );
|
|
}
|
|
}
|
|
}
|
|
|
|
void QueueMessage( MessageType messageType, Sprite.BroadcastEvent message )
|
|
{
|
|
_messageQueue.Add( (messageType, message) );
|
|
}
|
|
|
|
// Process any actions that were queued up during frame advancement
|
|
void ProcessMessageQueue()
|
|
{
|
|
// Do this so the actions end up getting invoked on the main thread
|
|
if ( _messageQueue.Count == 0 )
|
|
return;
|
|
|
|
foreach ( var ev in _messageQueue )
|
|
{
|
|
switch ( ev.Type )
|
|
{
|
|
case MessageType.BroadcastMessage:
|
|
RunBroadcastEvent( ev.Content );
|
|
break;
|
|
case MessageType.AnimationStart:
|
|
OnAnimationStart?.Invoke( ev.Content.Message );
|
|
break;
|
|
case MessageType.AnimationEnd:
|
|
OnAnimationEnd?.Invoke( ev.Content.Message );
|
|
break;
|
|
}
|
|
}
|
|
_messageQueue.Clear();
|
|
}
|
|
|
|
// Run any user-defined broadcast events
|
|
void RunBroadcastEvent( Sprite.BroadcastEvent broadcastEvent )
|
|
{
|
|
var isEditorOnly = Scene.IsEditor && GameObject.Flags.HasFlag( GameObjectFlags.EditorOnly );
|
|
var shouldBroadcast = Game.IsPlaying || isEditorOnly;
|
|
switch ( broadcastEvent.Type )
|
|
{
|
|
case Sprite.BroadcastEventType.CustomMessage:
|
|
if ( shouldBroadcast )
|
|
{
|
|
OnBroadcastMessage?.Invoke( broadcastEvent.Message );
|
|
}
|
|
break;
|
|
case Sprite.BroadcastEventType.PlaySound:
|
|
if ( shouldBroadcast && broadcastEvent.Sound is not null )
|
|
{
|
|
Sound.Play( broadcastEvent.Sound, WorldPosition );
|
|
}
|
|
break;
|
|
case Sprite.BroadcastEventType.SpawnPrefab:
|
|
// Only spawn prefabs during gameplay, not in editor
|
|
if ( Game.IsPlaying )
|
|
{
|
|
broadcastEvent.Prefab?.Clone( WorldPosition );
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
enum MessageType
|
|
{
|
|
BroadcastMessage,
|
|
AnimationStart,
|
|
AnimationEnd
|
|
}
|
|
}
|