// Assets/_Project/Scripts/UI/FloatingTextSpawner.cs using UnityEngine; namespace TD.UI { /// /// Scene singleton that spawns instances above /// world positions for gold-reward and life-loss feedback. /// /// /// Why a singleton: the spawner is shared by every caller (kills, leaks, /// future status pop-ups) and holds inspector-tunable colors and a prefab /// reference. Plain MonoBehaviour — visual-only, no networking required. /// /// Who calls this: invokes /// and via ClientRpc /// so every peer shows the popup locally. /// /// Inspector setup: drop this on any scene GameObject (e.g. a /// FloatingTextSpawner empty), assign the floating-text prefab, tune /// colors to match the HUD. /// public class FloatingTextSpawner : MonoBehaviour { // ----- Singleton -------------------------------------------------- public static FloatingTextSpawner Instance { get; private set; } // ----- Inspector -------------------------------------------------- [Tooltip("Prefab to instantiate for each pop-up. Must have a FloatingText " + "component and a TextMeshPro renderer.")] [SerializeField] private FloatingText prefab; [Tooltip("Color used for kill-reward gold pop-ups. Tune to match the HUD's " + "gold label color.")] [SerializeField] private Color goldColor = new Color(1f, 0.84f, 0.2f); [Tooltip("Color used for life-loss pop-ups (enemies reaching the goal).")] [SerializeField] private Color livesColor = new Color(0.95f, 0.25f, 0.25f); [Tooltip("Default vertical offset above the source position. Tunes how high " + "above the enemy the text starts.")] [SerializeField] private float verticalOffset = 2.0f; [Tooltip("Maximum random horizontal jitter applied to the spawn position so " + "consecutive pop-ups don't stack visually.")] [SerializeField] private float positionJitter = 0.4f; [Tooltip("Maximum random vertical jitter applied on top of verticalOffset.")] [SerializeField] private float verticalJitter = 0.2f; // ----- Lifecycle -------------------------------------------------- private void Awake() { if (Instance != null && Instance != this) { Debug.LogWarning("[FloatingTextSpawner] Duplicate instance detected. " + "Only one should exist per scene."); return; } Instance = this; } private void OnDestroy() { if (Instance == this) Instance = null; } // ----- Public API ------------------------------------------------- /// Spawns a gold-reward popup (e.g. "+10") at the given world position. public void SpawnGoldReward(Vector3 worldPos, int amount) => SpawnInternal(worldPos, $"+{amount}", goldColor); /// Spawns a life-loss popup (e.g. "-1") at the given world position. public void SpawnLifeLoss(Vector3 worldPos, int amount) => SpawnInternal(worldPos, $"-{amount}", livesColor); /// Lower-level overload: arbitrary content and color. public void SpawnCustom(Vector3 worldPos, string text, Color color) => SpawnInternal(worldPos, text, color); // ----- Internal --------------------------------------------------- private void SpawnInternal(Vector3 worldPos, string text, Color color) { if (prefab == null) { Debug.LogWarning("[FloatingTextSpawner] No prefab assigned. " + "Pop-up will not be shown."); return; } Vector3 jitter = new Vector3( Random.Range(-positionJitter, positionJitter), Random.Range(0f, verticalJitter), Random.Range(-positionJitter, positionJitter)); Vector3 spawnPos = worldPos + Vector3.up * verticalOffset + jitter; var instance = Instantiate(prefab, spawnPos, Quaternion.identity); instance.Init(text, color); } } }