135 lines
5.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|