UnityTowerDefense/Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs

135 lines
5.3 KiB
C#

// Assets/_Project/Scripts/Combat/TowerRangeIndicator.cs
using UnityEngine;
using UnityEngine.Rendering.Universal;
using TD.Gameplay;
namespace TD.Combat
{
/// <summary>
/// Displays a translucent decal circle representing this tower's attack range
/// when the local player selects the tower.
/// </summary>
/// <remarks>
/// <b>Visual only.</b> No networking — selection is a local UI concept.
/// All clients independently show the range indicator for whatever tower
/// they have selected.
///
/// <b>Prefab setup:</b>
/// <list type="number">
/// <item>Add this component to the tower prefab root (alongside TowerInstance).</item>
/// <item>Add a child GameObject named "RangeIndicator".</item>
/// <item>Add a <c>DecalProjector</c> to that child and assign it to
/// <see cref="rangeProjector"/> (or leave it unassigned — auto-found
/// via <c>GetComponentInChildren</c> in Start).</item>
/// <item>Assign a translucent range-circle material to the DecalProjector.</item>
/// </list>
///
/// <b>Sizing:</b> The projector diameter is set once in <c>Start</c> from
/// <c>TowerDefinition.Range</c>. Towers are static, so no per-frame resize is needed.
///
/// <b>Subscription timing:</b> Follows the same deferred-subscribe pattern as
/// <see cref="SelectionVisualizer"/> — retries until <see cref="SelectionState"/>
/// is available, then stops polling.
/// </remarks>
public class TowerRangeIndicator : MonoBehaviour
{
[Tooltip("DecalProjector used to render the range circle on the ground. " +
"Auto-found via GetComponentInChildren if left empty.")]
[SerializeField] private DecalProjector rangeProjector;
[Tooltip("Vertical extent of the decal projection volume in world units. " +
"Must be tall enough to project onto terrain at any height in your map.")]
[SerializeField] private float projectionDepth = 50f;
private TowerInstance towerInstance;
private bool subscribed;
// ----- Lifecycle ---------------------------------------------------
private void Start()
{
towerInstance = GetComponent<TowerInstance>();
if (towerInstance == null)
{
Debug.LogError("[TowerRangeIndicator] No TowerInstance found on this " +
"GameObject. TowerRangeIndicator must sit on the same " +
"prefab root as TowerInstance.");
enabled = false;
return;
}
if (rangeProjector == null)
rangeProjector = GetComponentInChildren<DecalProjector>();
if (rangeProjector == null)
{
Debug.LogError("[TowerRangeIndicator] No DecalProjector found. " +
"Add one as a child of this GameObject and assign it " +
"to the rangeProjector field.");
enabled = false;
return;
}
// TowerInstance resolves its Definition in OnNetworkSpawn, which runs
// before Start, so Definition is guaranteed available here.
float range = towerInstance.Definition != null ? towerInstance.Definition.Range : 0f;
float diameter = range * 2f;
rangeProjector.size = new Vector3(diameter, diameter, projectionDepth);
rangeProjector.pivot = Vector3.zero;
// DecalProjector projects along its local +Z axis; rotate 90° around X
// to project downward onto the ground plane.
rangeProjector.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
rangeProjector.transform.localPosition = Vector3.zero;
rangeProjector.enabled = false;
TrySubscribe();
}
private void OnDestroy()
{
Unsubscribe();
}
private void Update()
{
// Retry subscription each frame until SelectionState is ready.
// Once subscribed the branch short-circuits immediately.
if (!subscribed) TrySubscribe();
}
// ----- Subscription -----------------------------------------------
private void TrySubscribe()
{
if (subscribed) return;
var sel = SelectionState.Instance;
if (sel == null) return;
sel.OnSelectionChanged += HandleSelectionChanged;
subscribed = true;
// Catch whatever is already selected before we subscribed.
HandleSelectionChanged(sel.SelectedObject);
}
private void Unsubscribe()
{
if (!subscribed) return;
var sel = SelectionState.Instance;
if (sel != null) sel.OnSelectionChanged -= HandleSelectionChanged;
subscribed = false;
}
// ----- Selection handler ------------------------------------------
private void HandleSelectionChanged(ISelectable newSelection)
{
if (rangeProjector == null) return;
// Show only when THIS tower's TowerInstance is the selected object.
rangeProjector.enabled = (object)newSelection == (object)towerInstance;
}
}
}