Feature: per-tower construction-phase build bisuals
Adding the correct pipeline for creating the different building phases for towers. Adding a default set with primitive cubes.
This commit is contained in:
parent
a87b221d87
commit
7b93137216
16 changed files with 594 additions and 36 deletions
|
|
@ -1,5 +1,4 @@
|
|||
// Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
|
||||
using Unity.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
|
|
@ -63,27 +62,25 @@ namespace TD.Gameplay
|
|||
"Suggested: muted/grey-tinted variant of the constructing material.")]
|
||||
[SerializeField] private Material pausedMaterial;
|
||||
|
||||
[Header("Construction stages")]
|
||||
[Tooltip("Number of discrete growth stages while constructing. 4 matches the " +
|
||||
"design doc (1/4 → 2/4 → 3/4 → 4/4 height).")]
|
||||
[Header("Construction phases")]
|
||||
[Tooltip("Project-default construction-stage visuals used when the tower being built " +
|
||||
"doesn't define its own ConstructionPhases. When this (or the tower's set) " +
|
||||
"has phases, the build site swaps between those phase prefabs as it builds. " +
|
||||
"Leave empty to fall back to the legacy cube Y-scale growth below.")]
|
||||
[SerializeField] private ConstructionPhaseSet defaultPhaseSet;
|
||||
|
||||
[Header("Construction stages (cube fallback)")]
|
||||
[Tooltip("FALLBACK ONLY (no phase set assigned): number of discrete growth stages " +
|
||||
"while constructing. 4 = 1/4 → 2/4 → 3/4 → 4/4 height.")]
|
||||
[SerializeField] private int stageCount = 4;
|
||||
|
||||
[Tooltip("Y-scale applied to scaleTarget when Stage == Queued. Visually " +
|
||||
"distinct from any constructing height so the queued ghost reads as " +
|
||||
"'intent, not progress'.")]
|
||||
[Tooltip("FALLBACK ONLY: Y-scale applied to scaleTarget when Stage == Queued. " +
|
||||
"Visually distinct from any constructing height so the queued ghost reads " +
|
||||
"as 'intent, not progress'.")]
|
||||
[SerializeField] private float queuedYScale = 0.15f;
|
||||
|
||||
// ----- Networked state --------------------------------------------
|
||||
|
||||
// Replicated definition name so clients can resolve the source TowerDefinition
|
||||
// (matches TowerInstance's pattern). Used for footprint size only — the visual
|
||||
// prefab is the same regardless of tower type for now.
|
||||
private readonly NetworkVariable<FixedString64Bytes> definitionName =
|
||||
new NetworkVariable<FixedString64Bytes>(
|
||||
default,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// Replicated owner slot for color tinting. Mirrors TowerInstance.
|
||||
private readonly NetworkVariable<PlayerSlot> ownerSlot =
|
||||
new NetworkVariable<PlayerSlot>(
|
||||
|
|
@ -232,7 +229,6 @@ namespace TD.Gameplay
|
|||
|
||||
// ----- Pre-spawn init data (server) -------------------------------
|
||||
|
||||
private string pendingDefName;
|
||||
private PlayerSlot pendingOwner = PlayerSlot.None;
|
||||
private float pendingBuildTime;
|
||||
private Vector2Int pendingAnchor;
|
||||
|
|
@ -240,6 +236,17 @@ namespace TD.Gameplay
|
|||
private int pendingGoldSpent;
|
||||
private bool hasPendingInit;
|
||||
|
||||
// ----- Construction-phase runtime (local, all peers) --------------
|
||||
|
||||
// Resolved on spawn from the tower's ConstructionPhases or the defaultPhaseSet.
|
||||
// When non-null with PhaseCount > 0, the build site swaps between these
|
||||
// instantiated phase prefabs instead of Y-scaling the fallback cube.
|
||||
private ConstructionPhaseSet effectivePhaseSet;
|
||||
private GameObject[] phaseInstances; // one instantiated child per phase
|
||||
private MeshRenderer[][] phaseRenderers; // cached renderers per phase, for tinting
|
||||
private int activePhaseIndex = -1;
|
||||
private bool usePhases;
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -261,7 +268,6 @@ namespace TD.Gameplay
|
|||
return;
|
||||
}
|
||||
|
||||
pendingDefName = def != null ? def.name : string.Empty;
|
||||
pendingOwner = owner;
|
||||
pendingBuildTime = def != null ? def.BuildTime : 0f;
|
||||
pendingAnchor = anchorTile;
|
||||
|
|
@ -281,7 +287,6 @@ namespace TD.Gameplay
|
|||
// message so clients see correct values on their first OnNetworkSpawn.
|
||||
if (IsServer && hasPendingInit)
|
||||
{
|
||||
definitionName.Value = new FixedString64Bytes(pendingDefName ?? string.Empty);
|
||||
ownerSlot.Value = pendingOwner;
|
||||
buildTime.Value = pendingBuildTime;
|
||||
anchor.Value = pendingAnchor;
|
||||
|
|
@ -290,6 +295,10 @@ namespace TD.Gameplay
|
|||
hasPendingInit = false;
|
||||
}
|
||||
|
||||
// Resolve and instantiate the construction-phase visuals (or fall back to the
|
||||
// cube). Depends on towerTypeId, which is replicated by now on every peer.
|
||||
ResolvePhaseSet();
|
||||
|
||||
// Subscribe to value changes so visual updates are reactive.
|
||||
currentStage.OnValueChanged += HandleStageChanged;
|
||||
|
||||
|
|
@ -344,14 +353,25 @@ namespace TD.Gameplay
|
|||
|
||||
private void Update()
|
||||
{
|
||||
// While constructing, smoothly interpolate Y-scale through the stages
|
||||
// based on server time. This runs on every peer (server + clients) so
|
||||
// visuals stay synchronized regardless of who's looking.
|
||||
// Paused stage does NOT update — Y-scale is frozen at the pause point.
|
||||
// While constructing, advance the visual based on server time. Runs on every
|
||||
// peer so visuals stay synchronized. Paused does NOT update — the visual is
|
||||
// frozen at the pause point.
|
||||
if (currentStage.Value != BuildStage.Constructing) return;
|
||||
|
||||
float yScale = ComputeConstructingYScale();
|
||||
ApplyYScale(yScale);
|
||||
if (usePhases)
|
||||
{
|
||||
int index = PhaseIndexFromProgress();
|
||||
if (index != activePhaseIndex)
|
||||
{
|
||||
ShowPhase(index);
|
||||
// Renderers changed with the swap — re-apply material + owner tint.
|
||||
ApplyStageMaterialAndTint(BuildStage.Constructing);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: smoothly grow the cube's Y-scale through the stages.
|
||||
ApplyYScale(ComputeConstructingYScale());
|
||||
}
|
||||
|
||||
// ----- Server API -------------------------------------------------
|
||||
|
|
@ -498,33 +518,117 @@ namespace TD.Gameplay
|
|||
}
|
||||
|
||||
private void ApplyStageVisual(BuildStage stage)
|
||||
{
|
||||
if (usePhases)
|
||||
{
|
||||
// Queued shows the first phase; constructing/paused show the phase
|
||||
// matching current progress. ShowPhase repoints tintedRenderers at the
|
||||
// active phase instance, so material + tint are applied AFTER it.
|
||||
int index = stage == BuildStage.Queued ? 0 : PhaseIndexFromProgress();
|
||||
ShowPhase(index);
|
||||
ApplyStageMaterialAndTint(stage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: legacy cube Y-scale growth (no phase set wired).
|
||||
ApplyStageMaterialAndTint(stage);
|
||||
switch (stage)
|
||||
{
|
||||
case BuildStage.Queued: ApplyYScale(queuedYScale); break;
|
||||
case BuildStage.Constructing: ApplyYScale(ComputeConstructingYScale()); break;
|
||||
// ComputePausedYScale uses accumulatedConstructionTime alone when
|
||||
// constructionStartServerTime is -1 (the pause sentinel).
|
||||
case BuildStage.Paused: ApplyYScale(ComputePausedYScale()); break;
|
||||
}
|
||||
}
|
||||
|
||||
// Applies the stage's material + owner tint to whatever renderers are currently
|
||||
// active — the cube fallback, or the active phase instance's renderers. Paused
|
||||
// falls back to the constructing material if no paused material is assigned.
|
||||
private void ApplyStageMaterialAndTint(BuildStage stage)
|
||||
{
|
||||
switch (stage)
|
||||
{
|
||||
case BuildStage.Queued:
|
||||
SwapMaterial(queuedMaterial);
|
||||
ApplyYScale(queuedYScale);
|
||||
SwapMaterial(queuedMaterial); // green ghost — no owner tint
|
||||
break;
|
||||
|
||||
case BuildStage.Constructing:
|
||||
SwapMaterial(constructingMaterial);
|
||||
ApplyOwnerTint();
|
||||
ApplyYScale(ComputeConstructingYScale());
|
||||
break;
|
||||
|
||||
case BuildStage.Paused:
|
||||
// Use paused material if assigned; fall back to constructing
|
||||
// material if not (still readable, just less distinct).
|
||||
SwapMaterial(pausedMaterial != null ? pausedMaterial : constructingMaterial);
|
||||
ApplyOwnerTint();
|
||||
// Freeze Y-scale at whatever the accumulated progress represents.
|
||||
// ComputeConstructingYScale uses accumulatedConstructionTime alone
|
||||
// when constructionStartServerTime is -1 (the pause sentinel).
|
||||
ApplyYScale(ComputePausedYScale());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Construction phases ----------------------------------------
|
||||
|
||||
// Resolves the effective phase set (tower's own, else the project default) and
|
||||
// instantiates one inactive child per phase. Sets usePhases=false (cube fallback)
|
||||
// if neither set has any phases. Runs on every peer in OnNetworkSpawn.
|
||||
private void ResolvePhaseSet()
|
||||
{
|
||||
var def = TowerPlacementManager.GetDefinition(towerTypeId.Value);
|
||||
effectivePhaseSet =
|
||||
(def != null && def.ConstructionPhases != null && def.ConstructionPhases.PhaseCount > 0)
|
||||
? def.ConstructionPhases
|
||||
: defaultPhaseSet;
|
||||
|
||||
int n = effectivePhaseSet != null ? effectivePhaseSet.PhaseCount : 0;
|
||||
if (n <= 0)
|
||||
{
|
||||
usePhases = false;
|
||||
return;
|
||||
}
|
||||
|
||||
usePhases = true;
|
||||
phaseInstances = new GameObject[n];
|
||||
phaseRenderers = new MeshRenderer[n][];
|
||||
|
||||
// Hide the legacy fallback cube — the instantiated phase prefabs replace it.
|
||||
if (scaleTarget != null) scaleTarget.gameObject.SetActive(false);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var prefab = effectivePhaseSet.GetPhase(i);
|
||||
if (prefab == null) continue;
|
||||
|
||||
var go = Instantiate(prefab, transform);
|
||||
go.transform.localPosition = Vector3.zero;
|
||||
go.transform.localRotation = Quaternion.identity;
|
||||
go.SetActive(false);
|
||||
|
||||
phaseInstances[i] = go;
|
||||
phaseRenderers[i] = go.GetComponentsInChildren<MeshRenderer>(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Activates only phase <paramref name="index"/> and points tintedRenderers at its
|
||||
// renderers so the existing material/tint helpers drive the active phase.
|
||||
private void ShowPhase(int index)
|
||||
{
|
||||
if (phaseInstances == null) return;
|
||||
index = Mathf.Clamp(index, 0, phaseInstances.Length - 1);
|
||||
|
||||
for (int i = 0; i < phaseInstances.Length; i++)
|
||||
if (phaseInstances[i] != null)
|
||||
phaseInstances[i].SetActive(i == index);
|
||||
|
||||
activePhaseIndex = index;
|
||||
tintedRenderers = phaseRenderers[index];
|
||||
}
|
||||
|
||||
// Maps current normalized progress to a phase index (0..PhaseCount-1).
|
||||
private int PhaseIndexFromProgress()
|
||||
{
|
||||
int n = effectivePhaseSet != null ? effectivePhaseSet.PhaseCount : 1;
|
||||
if (n <= 1) return 0;
|
||||
float p = ComputeProgressNormalized();
|
||||
return Mathf.Clamp(Mathf.FloorToInt(p * n), 0, n - 1);
|
||||
}
|
||||
|
||||
// Stage index 0..stageCount-1 based on elapsed server time PLUS any accumulated
|
||||
// time from previous Constructing runs (resume support).
|
||||
// Returned Y-scale is (stageIndex + 1) / stageCount, so stage 0 = 1/4,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue