A rendering component for the Swapper character that finds the staff pixel in sprite frames, builds a tinted quad matching the character sprite bounds, and renders it as an overlay for hover/transition effects. It maps animations and frames to marker pixel coordinates and exposes a method to get the marker world position.
using System.Collections.Generic;
using Sandbox;
namespace BlockParty;
/// <summary>Recolors Swapper's unique blue staff pixel and locates it for hover transition effects.</summary>
public sealed class SwapperStaffIndicator : Component
{
const float DEPTH_OFFSET = SpriteLayer.LAYER_Z_STEP;
readonly RenderAttributes _attributes = new();
readonly List<Vertex> _vertices = new( 6 );
SceneCustomObject _sceneObject;
Material _material;
Player _player;
string _animation = "idle";
Color _tint = Color.White;
public Color Tint
{
get => _tint;
set => _tint = value;
}
public void Setup( Player player )
{
_player = player;
}
public void SetAnimation( string animation )
{
_animation = animation;
}
protected override void OnEnabled()
{
_material = Material.FromShader( "shaders/swapper_staff_indicator.shader" );
_sceneObject = new SceneCustomObject( Scene.SceneWorld )
{
Transform = global::Transform.Zero,
RenderLayer = SceneRenderLayer.OverlayWithDepth,
RenderOverride = OnRender,
};
_sceneObject.Flags.IsOpaque = false;
_sceneObject.Flags.IsTranslucent = true;
_sceneObject.Flags.CastShadows = false;
}
protected override void OnDisabled()
{
_sceneObject?.Delete();
_sceneObject = null;
}
protected override void OnUpdate()
{
RebuildGeometry();
}
void RebuildGeometry()
{
_vertices.Clear();
SpriteRenderer body = _player?.BodySpriteRenderer;
if ( body is null || !body.Enabled || !body.GameObject.Active ) return;
Texture texture = body.Texture;
if ( texture is null || texture == Texture.Transparent ) return;
texture.MarkUsed( ushort.MaxValue );
_attributes.Set( "MainTexture", texture );
GetFrameBounds( body, texture, out float left, out float right, out float bottom, out float top );
float leftU = body.FlipHorizontal ? 1f : 0f;
float rightU = body.FlipHorizontal ? 0f : 1f;
float bottomV = body.FlipVertical ? 0f : 1f;
float topV = body.FlipVertical ? 1f : 0f;
float z = body.GameObject.WorldPosition.z + DEPTH_OFFSET;
Vertex V( float x, float y, float u, float v )
=> new( new Vector3( x, y, z ), new Vector4( u, v, 0f, 0f ), _tint );
_vertices.Add( V( left, bottom, leftU, bottomV ) );
_vertices.Add( V( right, bottom, rightU, bottomV ) );
_vertices.Add( V( right, top, rightU, topV ) );
_vertices.Add( V( left, bottom, leftU, bottomV ) );
_vertices.Add( V( right, top, rightU, topV ) );
_vertices.Add( V( left, top, leftU, topV ) );
}
public bool TryGetMarkerWorldPosition( out Vector2 position )
{
position = default;
SpriteRenderer body = _player?.BodySpriteRenderer;
Texture texture = body?.Texture;
if ( body is null || texture is null || texture == Texture.Transparent
|| !TryGetMarkerPixel( body.CurrentFrameIndex, out Vector2 pixel ) )
return false;
GetFrameBounds( body, texture, out float left, out float right, out float bottom, out float top );
float pixelWidth = (right - left) / Math.Max( 1, texture.Width );
float pixelHeight = (top - bottom) / Math.Max( 1, texture.Height );
float column = body.FlipHorizontal ? texture.Width - 1f - pixel.x : pixel.x;
float row = body.FlipVertical ? texture.Height - 1f - pixel.y : pixel.y;
position = new Vector2(
left + (column + 0.5f) * pixelWidth,
top - (row + 0.5f) * pixelHeight );
return true;
}
bool TryGetMarkerPixel( int frame, out Vector2 pixel )
{
Vector2? result = _animation switch
{
"idle" or "air_up" or "air_down" or "air_fall_fast" or "wall_up" or "wall_down" => new Vector2( 10, 5 ),
"crouch" => new Vector2( 11, 8 ),
"air_up_active" => new Vector2( 9, 6 ),
"air_down_active" => new Vector2( 11, 5 ),
"walk" => frame switch
{
0 or 1 => new Vector2( 6, 4 ),
2 or 5 => new Vector2( 10, 5 ),
3 or 4 => new Vector2( 11, 4 ),
_ => null,
},
"squish_h" => frame switch
{
0 => new Vector2( 10, 5 ),
1 => new Vector2( 7, 5 ),
2 => new Vector2( 6, 5 ),
_ => null,
},
"squish_v" => frame switch
{
0 => new Vector2( 10, 5 ),
1 => new Vector2( 10, 6 ),
2 => new Vector2( 10, 7 ),
3 => new Vector2( 10, 8 ),
4 => new Vector2( 9, 8 ),
_ => null,
},
"explode" => frame switch
{
0 => new Vector2( 10, 5 ),
1 => new Vector2( 11, 5 ),
2 => new Vector2( 12, 5 ),
_ => null,
},
_ => null,
};
pixel = result ?? default;
return result.HasValue;
}
static void GetFrameBounds( SpriteRenderer body, Texture texture, out float left, out float right,
out float bottom, out float top )
{
Vector3 center = body.GameObject.WorldPosition;
Vector3 scale = body.GameObject.WorldScale;
float aspect = texture.Width / (float)Math.Max( 1, texture.Height );
float width = MathF.Abs( body.Size.x * scale.x * MathF.Min( aspect, 1f ) );
float height = MathF.Abs( body.Size.y * scale.y / MathF.Max( aspect, 1f ) );
left = center.x - width * 0.5f;
right = center.x + width * 0.5f;
bottom = center.y - height * 0.5f;
top = center.y + height * 0.5f;
}
void OnRender( SceneObject sceneObject )
{
SpriteRenderer body = _player?.BodySpriteRenderer;
if ( _vertices.Count == 0 || _material is null || body is null || !body.Enabled ) return;
Graphics.Draw( _vertices, _vertices.Count, _material, _attributes, Graphics.PrimitiveType.Triangles );
}
}