Rendering/PlayerAmmoOverlay.cs

A rendering component that draws a semi-transparent, bottom-up fill overlay over the player's body sprite to indicate ammo (or similar). It builds a quad mesh based on the player's sprite size, texture UVs and a Fill fraction, and renders it each frame via a SceneCustomObject.

Native Interop
using System.Collections.Generic;
using Sandbox;

namespace BlockParty;

/// <summary>Draws a bottom-up, alpha-masked fill over the player's current body frame.</summary>
public sealed class PlayerAmmoOverlay : Component
{
	const float OVERLAY_ALPHA = 0.25f;
	const float DEPTH_OFFSET = SpriteLayer.LAYER_Z_STEP;

	readonly RenderAttributes _attributes = new();
	readonly List<Vertex> _vertices = new( 6 );
	SceneCustomObject _sceneObject;
	Material _material;
	Player _player;
	float _fill;

	public void Setup( Player player, float fill )
	{
		_player = player;
		Fill = fill;
	}

	public float Fill
	{
		get => _fill;
		set => _fill = Math.Clamp( value, 0f, 1f );
	}

	protected override void OnEnabled()
	{
		_material = Material.FromShader( "shaders/player_ammo_overlay.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 (_fill <= 0f || 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 );

		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 ) );
		float left = center.x - width * 0.5f;
		float right = center.x + width * 0.5f;
		float bottom = center.y - height * 0.5f;
		float top = bottom + height * _fill;
		float z = center.z + DEPTH_OFFSET;

		float leftU = body.FlipHorizontal ? 1f : 0f;
		float rightU = body.FlipHorizontal ? 0f : 1f;
		float bottomV = body.FlipVertical ? 0f : 1f;
		float topV = body.FlipVertical ? _fill : 1f - _fill;
		var color = Color.White.WithAlpha( OVERLAY_ALPHA );

		Vertex V( float x, float y, float u, float v )
			=> new( new Vector3( x, y, z ), new Vector4( u, v, 0f, 0f ), color );

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

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