using UnityEngine; namespace Ashwild.Building { /// /// Placement validity for a build ghost. Each frame BuildManager calls Evaluate with the /// obstruction mask it owns (configured in one place, not per ghost prefab), which box-overlaps /// the ghost's footprint against those layers (other structures, props, players — never the /// ground it rests on) and shows the result by adding a valid or invalid overlay material on top /// of each renderer's base materials (not by tinting them). BuildManager reads /// to allow or block the build. The footprint collider is only a bounds source: the overlap test /// reads its dimensions directly rather than relying on physics contacts. /// [DisallowMultipleComponent] public class BuildGhost : MonoBehaviour { #region Serialized Fields [Header("Footprint")] [Tooltip("Box whose bounds define the volume tested for obstructions. Sized in the prefab.")] [SerializeField] private BoxCollider footprint; [Header("Overlay Materials")] [Tooltip("Renderers the overlay is added onto. Auto-filled from children if left empty.")] [SerializeField] private Renderer[] renderers; [Tooltip("Material added on top of the base ones when the spot is buildable (green).")] [SerializeField] private Material validOverlay; [Tooltip("Material added on top of the base ones when the spot is blocked (red).")] [SerializeField] private Material invalidOverlay; #endregion #region State /// /// Whether the ghost is currently on a buildable spot (no obstruction overlap). /// public bool IsValid { get; private set; } /// /// Each renderer's original materials, so the overlay can be appended without losing them. /// private Material[][] baseMaterials; /// /// The validity reflected by the currently applied overlay, so we only swap on a real change. /// private bool lastValid; private bool overlayApplied; private readonly Collider[] overlapResults = new Collider[8]; #endregion #region Unity Lifecycle /// /// Auto-collects renderers when none were assigned and caches their base materials. /// private void Awake() { if (renderers == null || renderers.Length == 0) renderers = GetComponentsInChildren(true); baseMaterials = new Material[renderers.Length][]; for (int i = 0; i < renderers.Length; i++) baseMaterials[i] = renderers[i] != null ? renderers[i].sharedMaterials : new Material[0]; } #endregion #region Public API /// /// Re-tests the footprint against the obstruction layers (passed in by BuildManager, which /// owns the mask so it is configured in one place), swaps the overlay material when validity /// changed, and returns the result. A ghost with no footprint is always considered valid. /// public bool Evaluate(LayerMask obstructionMask) { IsValid = !IsObstructed(obstructionMask); if (!overlayApplied || IsValid != lastValid) { ApplyOverlay(IsValid ? validOverlay : invalidOverlay); lastValid = IsValid; overlayApplied = true; } return IsValid; } #endregion #region Internal /// /// Box-overlaps the footprint against the obstruction layers, ignoring triggers. /// private bool IsObstructed(LayerMask obstructionMask) { if (footprint == null) return false; Transform t = footprint.transform; Vector3 center = t.TransformPoint(footprint.center); Vector3 halfExtents = Vector3.Scale(footprint.size, t.lossyScale) * 0.5f; int count = Physics.OverlapBoxNonAlloc(center, halfExtents, overlapResults, t.rotation, obstructionMask, QueryTriggerInteraction.Ignore); return count > 0; } /// /// Rebuilds each renderer's material list as "base materials + overlay", so the overlay /// draws on top of the model instead of replacing it. Skips when no overlay is assigned. /// private void ApplyOverlay(Material overlay) { if (overlay == null) return; for (int i = 0; i < renderers.Length; i++) { Renderer r = renderers[i]; if (r == null) continue; Material[] baseMats = baseMaterials[i]; Material[] combined = new Material[baseMats.Length + 1]; for (int j = 0; j < baseMats.Length; j++) combined[j] = baseMats[j]; combined[baseMats.Length] = overlay; r.materials = combined; } } #endregion } }