towers fall from the sky - neat!
This commit is contained in:
parent
d14305d804
commit
362b76ae75
10 changed files with 396 additions and 219 deletions
|
|
@ -58,6 +58,12 @@ namespace TD.Combat
|
|||
// is in range, the tower fires and the timer resets to 1 / AttacksPerSecond.
|
||||
private float attackCooldown;
|
||||
|
||||
// Counts down from towerInstance.LandingDuration, set in OnNetworkSpawn. Combat is
|
||||
// fully inert until it reaches zero, so a tower can't target/fire while its mesh is
|
||||
// still visually falling (TowerLandingVisual). Server-only; independent of whatever
|
||||
// the client-side fall visual is doing to the (possibly shared, on a host) transform.
|
||||
private float landingCooldown;
|
||||
|
||||
// Cached on OnNetworkSpawn — avoids GetComponent every Update.
|
||||
private TowerInstance towerInstance;
|
||||
|
||||
|
|
@ -102,6 +108,7 @@ namespace TD.Combat
|
|||
public override void OnNetworkSpawn()
|
||||
{
|
||||
towerInstance = GetComponent<TowerInstance>();
|
||||
landingCooldown = towerInstance.LandingDuration;
|
||||
replicatedTarget.OnValueChanged += HandleReplicatedTargetChanged;
|
||||
|
||||
// Late-joining clients receive the NV's current value in the initial
|
||||
|
|
@ -128,6 +135,12 @@ namespace TD.Combat
|
|||
{
|
||||
if (!IsServer) return;
|
||||
|
||||
if (landingCooldown > 0f)
|
||||
{
|
||||
landingCooldown -= Time.deltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
TowerDefinition def = towerInstance?.Definition;
|
||||
if (def == null || def.Range <= 0f || def.AttacksPerSecond <= 0f) return;
|
||||
|
||||
|
|
|
|||
41
Assets/_Project/Scripts/Combat/TowerLandedSound.cs
Normal file
41
Assets/_Project/Scripts/Combat/TowerLandedSound.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Assets/_Project/Scripts/Combat/TowerLandedSound.cs
|
||||
using TD.Audio;
|
||||
using TD.Gameplay;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Local-only audio for the tower's landing impact. Subscribes to
|
||||
/// <see cref="TowerInstance.OnLandedClient"/> and plays a sound on every peer when the
|
||||
/// tower's fall animation (<see cref="TowerLandingVisual"/>) finishes.
|
||||
/// Attach to any tower prefab and assign its clip in the Inspector.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(TowerInstance))]
|
||||
public class TowerLandedSound : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private SoundConfig landedSound;
|
||||
|
||||
private TowerInstance towerInstance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
towerInstance = GetComponent<TowerInstance>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (towerInstance != null) towerInstance.OnLandedClient += HandleLanded;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (towerInstance != null) towerInstance.OnLandedClient -= HandleLanded;
|
||||
}
|
||||
|
||||
private void HandleLanded()
|
||||
{
|
||||
AudioManager.Instance?.Play(landedSound.clip, AudioCategory.UI, landedSound.RandomPitch(), landedSound.volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/Combat/TowerLandedSound.cs.meta
Normal file
2
Assets/_Project/Scripts/Combat/TowerLandedSound.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 22d67b2aa780d786a93acfe06d1909b0
|
||||
104
Assets/_Project/Scripts/Combat/TowerLandingVisual.cs
Normal file
104
Assets/_Project/Scripts/Combat/TowerLandingVisual.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Assets/_Project/Scripts/Combat/TowerLandingVisual.cs
|
||||
using System.Collections;
|
||||
using TD.Gameplay;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TD.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Local-only "falls from the sky and lands" spawn visual. Subscribes to
|
||||
/// <see cref="TowerInstance.OnBuiltClient"/> and, on every client (never on a
|
||||
/// dedicated server, which has no camera to show it to), animates the tower's
|
||||
/// transform down from <see cref="TowerInstance.DropHeight"/> above its already-correct
|
||||
/// resting position over <see cref="TowerInstance.LandingDuration"/> seconds.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>No NetworkTransform involved.</b> Tower prefabs have no NetworkTransform —
|
||||
/// position is a one-shot snapshot sent at spawn. This animation is purely local per
|
||||
/// peer (same pattern as <see cref="TowerBuiltSound"/>'s audio cue); each peer computes
|
||||
/// an identical animation independently from the same replicated landing position and
|
||||
/// the same authored <see cref="TowerInstance.DropHeight"/>/<see cref="TowerInstance.LandingDuration"/>
|
||||
/// values, so no networking is required.</para>
|
||||
///
|
||||
/// <para><b>Safe on a host despite sharing one Transform with server combat logic.</b>
|
||||
/// <see cref="TowerCombat"/> withholds all targeting/attack logic for
|
||||
/// <see cref="TowerInstance.LandingDuration"/> seconds after spawn (see its
|
||||
/// <c>landingCooldown</c> field), so it never reads <c>transform.position</c> while this
|
||||
/// animation is moving it. The two timers start from the same value in the same call
|
||||
/// (<see cref="TowerInstance.OnBuiltClient"/> fires from <c>OnNetworkSpawn</c>), so they
|
||||
/// finish within a frame of each other.</para>
|
||||
///
|
||||
/// <para><b>Root transform, not a child wrapper.</b> Tower prefabs render their mesh
|
||||
/// directly on the root GameObject (no separate "Model" child exists to offset
|
||||
/// instead), so this animates <c>transform.position</c> directly. Footprint/pathing is
|
||||
/// unaffected — <see cref="TowerInstance"/> stamps walkability from its replicated
|
||||
/// anchor tile (grid data), not from <c>transform.position</c>.</para>
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(TowerInstance))]
|
||||
public class TowerLandingVisual : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Eases the fall over LandingDuration. X = normalized time, Y = normalized " +
|
||||
"fraction of DropHeight already descended (0 = still at drop height, 1 = landed).")]
|
||||
[SerializeField] private AnimationCurve fallCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
|
||||
|
||||
[Tooltip("Optional impact VFX (dust burst, etc.) instantiated at the landing point when " +
|
||||
"the fall finishes. Leave empty for no VFX.")]
|
||||
[SerializeField] private GameObject impactVfxPrefab;
|
||||
|
||||
[Tooltip("Seconds before the instantiated impact VFX is destroyed. Ignored if " +
|
||||
"impactVfxPrefab is unassigned.")]
|
||||
[SerializeField] private float impactVfxLifetime = 3f;
|
||||
|
||||
private TowerInstance towerInstance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
towerInstance = GetComponent<TowerInstance>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (towerInstance != null) towerInstance.OnBuiltClient += HandleBuilt;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (towerInstance != null) towerInstance.OnBuiltClient -= HandleBuilt;
|
||||
}
|
||||
|
||||
private void HandleBuilt()
|
||||
{
|
||||
// Dedicated (non-host) servers have no camera/visuals and must never move the
|
||||
// shared transform. TowerCombat's landing gate is fully independent of this
|
||||
// animation, so skipping it here on a pure server is a harmless no-op.
|
||||
if (!towerInstance.IsClient) return;
|
||||
|
||||
StartCoroutine(AnimateDrop());
|
||||
}
|
||||
|
||||
private IEnumerator AnimateDrop()
|
||||
{
|
||||
Vector3 landedPosition = transform.position; // already correct (SeatOnGround, pre-spawn)
|
||||
float dropHeight = towerInstance.DropHeight;
|
||||
float duration = towerInstance.LandingDuration;
|
||||
|
||||
transform.position = landedPosition + Vector3.up * dropHeight;
|
||||
|
||||
float t = 0f;
|
||||
while (t < duration)
|
||||
{
|
||||
t += Time.deltaTime;
|
||||
float u = fallCurve.Evaluate(Mathf.Clamp01(t / duration));
|
||||
transform.position = landedPosition + Vector3.up * dropHeight * (1f - u);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
transform.position = landedPosition; // snap exactly, avoid float drift
|
||||
|
||||
if (impactVfxPrefab != null)
|
||||
Destroy(Instantiate(impactVfxPrefab, landedPosition, Quaternion.identity), impactVfxLifetime);
|
||||
|
||||
towerInstance.ClientNotifyLanded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bf337521317d9e297a652e97c666376f
|
||||
Loading…
Add table
Add a link
Reference in a new issue