Decals, ghost textures, placement functionality, builder stub ins, a new camera system, and more.
121 lines
No EOL
5.4 KiB
C#
121 lines
No EOL
5.4 KiB
C#
// Assets/_Project/Scripts/Gameplay/BuildRangeIndicator.cs
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering.Universal;
|
|
|
|
namespace TD.Gameplay
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>Sits as a child of the <see cref="Builder"/> GameObject. The
|
|
/// <see cref="DecalProjector"/> renders a circular texture onto whatever ground
|
|
/// geometry is below — flat plane or sloped terrain alike, no special handling needed.</para>
|
|
///
|
|
/// <para><b>Owner-only.</b> 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.</para>
|
|
///
|
|
/// <para><b>Toggling.</b> The indicator is only visible when the local player is in
|
|
/// placement mode. It checks <c>TowerPlacementController.IsPlacing</c> each frame
|
|
/// and toggles the projector accordingly. When sized correctly the projector size
|
|
/// matches <c>buildRange * 2</c> (diameter).</para>
|
|
/// </remarks>
|
|
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<DecalProjector>();
|
|
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<Builder>();
|
|
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<TowerPlacementController>();
|
|
if (cachedPlacementController == null) return false;
|
|
}
|
|
return cachedPlacementController.IsPlacing;
|
|
}
|
|
}
|
|
} |