140 lines
5.1 KiB
C#
140 lines
5.1 KiB
C#
using UnityEngine;
|
|
|
|
namespace Ashwild.Building
|
|
{
|
|
/// <summary>
|
|
/// 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 <see cref="IsValid"/>
|
|
/// 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.
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <summary>
|
|
/// Whether the ghost is currently on a buildable spot (no obstruction overlap).
|
|
/// </summary>
|
|
public bool IsValid { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Each renderer's original materials, so the overlay can be appended without losing them.
|
|
/// </summary>
|
|
private Material[][] baseMaterials;
|
|
|
|
/// <summary>
|
|
/// The validity reflected by the currently applied overlay, so we only swap on a real change.
|
|
/// </summary>
|
|
private bool lastValid;
|
|
private bool overlayApplied;
|
|
|
|
private readonly Collider[] overlapResults = new Collider[8];
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Auto-collects renderers when none were assigned and caches their base materials.
|
|
/// </summary>
|
|
private void Awake()
|
|
{
|
|
if (renderers == null || renderers.Length == 0)
|
|
renderers = GetComponentsInChildren<Renderer>(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
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Box-overlaps the footprint against the obstruction layers, ignoring triggers.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|