UnityTowerDefense/Assets/_Project/Scripts/Gameplay/SelectionRingVisual.cs

126 lines
4.8 KiB
C#

// Assets/_Project/Scripts/Gameplay/SelectionRingVisual.cs
using UnityEngine;
namespace TD.Gameplay
{
/// <summary>
/// Local-only visual indicator for builder selection. Sits as a child of the
/// builder prefab. Subscribes to <see cref="SelectionState.OnSelectionChanged"/>
/// and toggles the visibility of its own renderers (and any descendant
/// renderers) when the parent builder becomes selected/unselected.
/// </summary>
/// <remarks>
/// <para><b>Pure local visualization.</b> No NetworkBehaviour. Selection is a
/// UI concept — every client renders selection state for its own player only.
/// This component has no networked state.</para>
///
/// <para><b>Why a separate component.</b> Keeps Builder focused on gameplay
/// state. The visual prefab structure can change independently
/// (disc → ring → decal projector → animated effect) without touching gameplay
/// code. The contract is just "renderers visible when selected."</para>
///
/// <para><b>Why renderer-toggle, not GameObject.SetActive.</b> Disabling our
/// own GameObject would prevent OnEnable/Update from running, which would
/// break the SelectionState subscription lifecycle (we'd never receive the
/// "you're selected again" event). Toggling the renderers' enabled state
/// achieves the same visual effect without breaking the event flow.</para>
///
/// <para><b>Prefab setup.</b> Attach this component to a child GameObject of
/// the Builder prefab. The child carries (or has descendants carrying) a
/// flattened cylinder mesh sitting just above the ground plane, with an
/// unlit transparent green material. The component handles initial visibility
/// — leave the renderer enabled in the prefab; we'll turn it off in Awake
/// until selection fires.</para>
/// </remarks>
public class SelectionRingVisual : MonoBehaviour
{
// Cached parent builder, resolved in Awake.
private Builder parentBuilder;
// Cached renderers (this object plus all descendants). Captured once in
// Awake to avoid per-event GetComponentsInChildren allocations.
private Renderer[] cachedRenderers;
// Tracks subscription state so OnDisable / OnDestroy unsubscribe correctly,
// and so Update can retry subscription if SelectionState wasn't ready at OnEnable.
private bool subscribed;
private void Awake()
{
parentBuilder = GetComponentInParent<Builder>();
if (parentBuilder == null)
{
Debug.LogError("[SelectionRingVisual] No Builder component found on " +
"self or any parent. Disabling.");
enabled = false;
return;
}
cachedRenderers = GetComponentsInChildren<Renderer>(includeInactive: true);
// Start hidden. If selection fires later (including the auto-select
// in Builder.OnNetworkSpawn), HandleSelectionChanged will turn the
// renderers back on.
SetRenderersVisible(false);
}
private void OnEnable()
{
TrySubscribe();
}
private void OnDisable()
{
Unsubscribe();
}
private void OnDestroy()
{
Unsubscribe();
}
// Per-frame fallback: if SelectionState wasn't available at OnEnable
// (scene load ordering), keep trying. Cost is one null-check per frame
// until subscription succeeds, then nothing.
private void Update()
{
if (!subscribed) TrySubscribe();
}
private void TrySubscribe()
{
if (subscribed) return;
var sel = SelectionState.Instance;
if (sel == null) return;
sel.OnSelectionChanged += HandleSelectionChanged;
subscribed = true;
// Sync to current state — selection may have happened before we subscribed
// (e.g., Builder.OnNetworkSpawn auto-selecting before this Awake runs).
HandleSelectionChanged(sel.SelectedBuilder);
}
private void Unsubscribe()
{
if (!subscribed) return;
var sel = SelectionState.Instance;
if (sel != null) sel.OnSelectionChanged -= HandleSelectionChanged;
subscribed = false;
}
private void HandleSelectionChanged(Builder newSelection)
{
SetRenderersVisible(newSelection == parentBuilder);
}
private void SetRenderersVisible(bool visible)
{
if (cachedRenderers == null) return;
foreach (var rend in cachedRenderers)
{
if (rend != null) rend.enabled = visible;
}
}
}
}