SpriteFlipbook.cs

Component that animates a 2D sprite by swapping materials on a SkinnedModelRenderer. It holds lists of materials for idle, run and attack, chooses which list based on the model's current animation sequence, advances frames by Fps and sets MaterialOverride on the renderer.

Native Interop
namespace SBH;

using Sandbox;
using System.Collections.Generic;

/// <summary>
/// [SBHBase] Animación de un sprite 2D por cambio de material: listas de
/// materiales (un .vmat por frame, generados por el Nextbot 2D Maker desde
/// un GIF) para idle / correr / atacar, y va poniendo MaterialOverride en el
/// SkinnedModelRenderer al ritmo de Fps. Qué lista usar lo decide la
/// secuencia que esté sonando en el modelo compartido (el Brain pone
/// walk/run/attack por nombre, así que no hay que enterarse de la IA).
/// Con una sola imagen por lista queda fija (sin costo).
/// </summary>
public sealed class SpriteFlipbook : Component
{
	[Property, Description( "Frames en reposo / caminando (obligatorio, al menos 1)" )]
	public List<Material> Idle { get; set; } = new();

	[Property, Description( "Frames al correr / perseguir (vacío = usa Idle)" )]
	public List<Material> Correr { get; set; } = new();

	[Property, Description( "Frames al atacar (vacío = usa Correr o Idle)" )]
	public List<Material> Atacar { get; set; } = new();

	[Property, Description( "Frames por segundo de la animación" )]
	public float Fps { get; set; } = 8f;

	SkinnedModelRenderer _renderer;
	List<Material> _lista;
	int _indice;
	float _acumulado;
	Material _puesto;

	protected override void OnEnabled()
	{
		_renderer = Components.Get<SkinnedModelRenderer>();
		_lista = null;
		_indice = 0;
		_acumulado = 0f;
		Aplicar( true );
	}

	protected override void OnUpdate()
	{
		if ( !_renderer.IsValid() ) return;

		var lista = Elegir();
		if ( lista != _lista )
		{
			_lista = lista;
			_indice = 0;
			_acumulado = 0f;
			Aplicar( true );
			return;
		}
		if ( lista == null || lista.Count <= 1 ) return;

		_acumulado += Time.Delta * System.MathF.Max( 0.1f, Fps );
		if ( _acumulado >= 1f )
		{
			int pasos = (int)_acumulado;
			_acumulado -= pasos;
			_indice = (_indice + pasos) % lista.Count;
			Aplicar( false );
		}
	}

	List<Material> Elegir()
	{
		var seq = _renderer.SceneModel?.CurrentSequence.Name ?? "";
		if ( seq.StartsWith( "attack" ) && Atacar != null && Atacar.Count > 0 ) return Atacar;
		if ( (seq == "run" || seq == "walk" || seq.StartsWith( "attack" )) && Correr != null && Correr.Count > 0 ) return Correr;
		return Idle != null && Idle.Count > 0 ? Idle : null;
	}

	void Aplicar( bool forzar )
	{
		var lista = _lista ?? Elegir();
		if ( lista == null || lista.Count == 0 || !_renderer.IsValid() ) return;
		var mat = lista[_indice % lista.Count];
		if ( mat == null ) return;
		if ( !forzar && mat == _puesto ) return;
		_renderer.MaterialOverride = mat;
		_puesto = mat;
	}
}