A component that manages purely cosmetic game feedback for Breakout: center banner text, damage flash sequencing, camera shake, debris/wall-hit/confetti prefabs, and a clear-sting sound. It exposes banner and damage state for the HUD and spawns particle/gameobject effects.
using System;
using Sandbox;
namespace Breakout;
/// <summary>
/// Owns purely-cosmetic game juice: the center banner, the damage flash, camera shake, brick
/// debris, wall-hit sparks, confetti and the board-clear sting. BreakoutGame delegates here and
/// re-exposes the banner/damage state for the HUD.
/// </summary>
[Title( "Game Feedback" ), Category( "Breakout" ), Icon( "auto_awesome" )]
public sealed class GameFeedback : Component
{
[Property] public GameObject DebrisPrefab { get; set; }
[Property] public GameObject WallHitPrefab { get; set; }
[Property] public GameObject ConfettiPrefab { get; set; }
[Property] public float LifeLostShake { get; set; } = 0.65f;
/// <summary>
/// Big center banner text, like "LEVEL 1" or "BOARD CLEAR!".
/// </summary>
public string BannerText { get; private set; }
/// <summary>
/// Smaller line shown beneath the banner (may be null).
/// </summary>
public string BannerSub { get; private set; }
/// <summary>
/// Ticks up each time a new banner is shown, so the HUD can restart its animation.
/// </summary>
public int BannerSeq { get; private set; }
/// <summary>
/// True while the banner should currently be visible.
/// </summary>
public bool BannerActive => !string.IsNullOrEmpty( BannerText ) && bannerSince < bannerHold;
/// <summary>
/// Ticks up each time the player takes damage, so the HUD can restart the red flash.
/// </summary>
public int DamageSeq { get; private set; }
/// <summary>
/// True while the red damage flash should currently be showing.
/// </summary>
public bool DamageActive => DamageSeq > 0 && damageSince < DamageFlashHold;
private RealTimeSince bannerSince;
private float bannerHold;
private RealTimeSince damageSince;
private const float DamageFlashHold = 0.05f;
/// <summary>
/// Shows the center banner with optional sub-text for hold seconds.
/// </summary>
public void ShowBanner( string text, string sub = null, float hold = 1.1f )
{
BannerText = text;
BannerSub = sub;
bannerSince = 0f;
bannerHold = hold;
BannerSeq++;
}
/// <summary>
/// Kicks off the red screen flash and a small camera shake when the player loses a life.
/// </summary>
public void TriggerDamageFeedback()
{
damageSince = 0f;
DamageSeq++;
ShakeCamera( LifeLostShake );
}
/// <summary>
/// Shakes the camera. Bigger amount means a stronger, longer shake.
/// </summary>
public void ShakeCamera( float amount )
{
if ( amount <= 0f )
return;
Scene.Camera.AddShake( amount * 20, amount * 10f, 0.5f );
}
/// <summary>
/// Spawns the shatter particles for a broken brick, tinted to match it.
/// </summary>
public void SpawnDebris( Block block )
{
if ( !block.IsValid() || DebrisPrefab is null )
return;
var go = DebrisPrefab.Clone( new CloneConfig( new Transform( block.WorldPosition ), startEnabled: false ) );
foreach ( var fx in go.Components.GetAll<ParticleEffect>( FindMode.EverythingInSelfAndDescendants ) )
fx.Tint = block.HealthyTint;
go.Enabled = true;
}
/// <summary>
/// Spawns a spark burst facing inward where the ball hit a wall or the paddle.
/// </summary>
public void SpawnWallHit( Vector3 worldPos, Vector3 inwardNormal )
{
if ( WallHitPrefab is null )
return;
var n = inwardNormal.Normal;
var up = MathF.Abs( n.z ) > 0.99f ? Vector3.Forward : Vector3.Up;
var rot = Rotation.LookAt( n, up );
WallHitPrefab.Clone( new CloneConfig( new Transform( worldPos, rot ), name: "wall_hit" ) );
}
/// <summary>
/// Spawns the celebratory confetti burst used on a board clear.
/// </summary>
public void SpawnConfetti( Vector3 worldPos ) => ConfettiPrefab?.Clone( worldPos );
/// <summary>
/// Plays the rising three-note chime used when a board is cleared.
/// </summary>
public void PlayClearSting()
{
float[] pitches = { 1.15f, 1.5f, 1.9f };
foreach ( var pitch in pitches )
{
var note = Sound.Play( "brick_break_01" );
if ( note is not null )
note.Pitch = pitch;
}
}
}