Player/PlayerDamageIndicators.cs

A player component that shows damage feedback for the local player. It adds a red vignette on recent hurt and spawns radial directional indicators pointing toward the attacker world positions, drawing textures on the HUD until their lifetime expires.

File Access
public sealed class PlayerDamageIndicators : Component, IPlayerEvent
{
	[RequireComponent] Player Player { get; set; }

	float RadialDistanceFromCenter => 128f;
	float RadialIndicatorLifetime => 2f;

	TimeSince timeSinceHurt = 10f;
	Vignette damageVignette;
	List<(Vector3 WorldPos, TimeSince Lifetime)> radialIndicators = new();

	protected override void OnDisabled()
	{
		damageVignette?.Destroy();
	}

	protected override void OnDestroy()
	{
		damageVignette?.Destroy();
	}

	protected override void OnPreRender()
	{
		if ( !Player.IsLocalPlayer ) return;
		if ( Scene.Camera is null ) return;

		UpdateVignette();
		UpdateRadialIndicators();
	}

	void UpdateVignette()
	{
		if ( damageVignette is null )
		{
			damageVignette = Scene.Camera.AddComponent<Vignette>();
			damageVignette.Flags |= ComponentFlags.NotNetworked;
			damageVignette.Color = Color.Red;
			damageVignette.Intensity = 0;
		}

		if ( timeSinceHurt < 1f )
		{
			damageVignette.Intensity = (1f - timeSinceHurt) / 10f;
		}
		else
		{
			damageVignette.Intensity = 0;
		}
	}

	Texture texture = Texture.Load( "ui/damage_radial.png" );

	void UpdateRadialIndicators()
	{
		var hud = Scene.Camera.Hud;
		var playerPos = Player.EyeTransform.Position;
		var playerRot = Player.EyeTransform.Rotation;
		var center = Screen.Size / 2f;

		// rough approx of where the crosshair is in worldspace, makes close-up directions more easily parsable/accurate
		var focalPoint = playerPos + playerRot.Forward * 16;

		for ( int i = radialIndicators.Count - 1; i >= 0; i-- )
		{
			var entry = radialIndicators[i];
			if ( entry.Lifetime >= RadialIndicatorLifetime )
			{
				radialIndicators.RemoveAt( i );
				continue;
			}

			var dir = (entry.WorldPos - focalPoint).Normal;
			var angle = -MathF.Atan2( dir.y, dir.x ) + playerRot.Angles().yaw.DegreeToRadian() - (MathF.PI / 2f);

			Matrix matrix = Matrix.CreateRotation( Rotation.From( 0, angle.RadianToDegree(), 0 ) );
			matrix *= Matrix.CreateTranslation( center );
			hud.SetMatrix( matrix );

			var size = new Vector2( 256, 512 ) * Hud.Scale;
			var rect = new Rect( new Vector2( RadialDistanceFromCenter * Hud.Scale, -size.y / 2 ), size );

			// scale alpha based on damage dealt or something?
			hud.DrawTexture( texture, rect, Color.Red.WithAlpha( 1f - (entry.Lifetime / RadialIndicatorLifetime) ) );
		}

		hud.SetMatrix( Matrix.Identity );
	}

	void IPlayerEvent.OnDamage( IPlayerEvent.DamageParams args )
	{
		timeSinceHurt = 0f;

		if ( args.Attacker.IsValid() )
		{
			radialIndicators.Add( (args.Attacker.WorldPosition, 0f) );
		}
	}
}