// Assets/_Project/Scripts/Gameplay/SelectionRingVisual.cs using UnityEngine; namespace TD.Gameplay { /// /// Local-only visual indicator for builder selection. Sits as a child of the /// builder prefab. Subscribes to /// and toggles the visibility of its own renderers (and any descendant /// renderers) when the parent builder becomes selected/unselected. /// /// /// Pure local visualization. No NetworkBehaviour. Selection is a /// UI concept — every client renders selection state for its own player only. /// This component has no networked state. /// /// Why a separate component. 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." /// /// Why renderer-toggle, not GameObject.SetActive. 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. /// /// Prefab setup. 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. /// 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(); if (parentBuilder == null) { Debug.LogError("[SelectionRingVisual] No Builder component found on " + "self or any parent. Disabling."); enabled = false; return; } cachedRenderers = GetComponentsInChildren(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; } } } }