// Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs using UnityEngine; using UnityEngine.Rendering.Universal; namespace TD.Gameplay { /// /// Visualizes a builder's build range as a decal projector circle on the ground. /// Visible only to the owning client, and only while placement mode is active. /// /// /// Sits as a child of the GameObject. The /// renders a circular texture onto whatever ground /// geometry is below — flat plane or sloped terrain alike, no special handling needed. /// /// Owner-only. Non-owning clients should not see other players' build /// range indicators (would be visual clutter). The decal projector is force-disabled /// for non-owners on spawn. /// /// Toggling. The indicator is only visible when the local player is in /// placement mode. It checks TowerPlacementController.IsPlacing each frame /// and toggles the projector accordingly. When sized correctly the projector size /// matches buildRange * 2 (diameter). /// public class BuildRangeIndicator : MonoBehaviour { [Tooltip("DecalProjector child component to drive. Auto-found in Awake if empty.")] [SerializeField] private DecalProjector projector; [Tooltip("Vertical thickness of the decal projector's projection volume. Should " + "exceed your map's vertical range so the decal projects onto terrain at any height.")] [SerializeField] private float projectionDepth = 50f; // Cached references resolved lazily. private Builder cachedBuilder; private TowerPlacementController cachedPlacementController; // ----- Lifecycle -------------------------------------------------- private void Awake() { if (projector == null) projector = GetComponentInChildren(); if (projector == null) { Debug.LogError("[BuildRangeIndicator] No DecalProjector found. Add one as a " + "child of this GameObject."); enabled = false; return; } // Start hidden; visibility is updated each frame. projector.enabled = false; } private void Start() { cachedBuilder = GetComponentInParent(); if (cachedBuilder == null) { Debug.LogError("[BuildRangeIndicator] No Builder found in parents. " + "Disabling indicator."); enabled = false; return; } // Hide for non-owners — other players don't see your range indicator. if (!cachedBuilder.IsOwner) { enabled = false; if (projector != null) projector.enabled = false; return; } // Size the projector to match the builder's range (diameter = 2 * range). // Modern URP DecalProjector exposes width/height/pivot as separate properties // rather than a single Vector3 size. Width and Height are the ground-plane extents // of the projection; pivot.z is the volume's depth offset (0 = volume centered // on the projector position, projecting equally above and below). float diameter = cachedBuilder.BuildRange * 2f; // The DecalProjector exposes a `size` Vector3 on its API even though the // inspector splits it into Width/Height/ProjectionDepth — assignment is still // valid in current URP. We use it here to set all three at once. projector.size = new Vector3(diameter, diameter, projectionDepth); projector.pivot = new Vector3(0f, 0f, 0f); // Center the projection volume on the builder. The decal projector projects // along its local +Z axis by default; rotate to project downward (look down). // // CRITICAL: rotate the PROJECTOR's transform, not this component's transform. // BuildRangeIndicator lives on the Builder root (so it can find Builder via // GetComponentInParent), but `transform` here is the Builder's transform — // rotating it tips the cylinder onto its side. The projector lives on a child // GameObject; that's the one that needs the 90° X rotation. projector.transform.localRotation = Quaternion.Euler(90f, 0f, 0f); projector.transform.localPosition = Vector3.zero; } private void Update() { if (cachedBuilder == null) return; bool shouldShow = IsLocalPlayerPlacing(); if (projector.enabled != shouldShow) projector.enabled = shouldShow; } // ----- Helpers ---------------------------------------------------- private bool IsLocalPlayerPlacing() { if (cachedPlacementController == null) { cachedPlacementController = UnityEngine.Object.FindAnyObjectByType(); if (cachedPlacementController == null) return false; } return cachedPlacementController.IsPlacing; } } }