Adding tons of new functionality
Decals, ghost textures, placement functionality, builder stub ins, a new camera system, and more.
This commit is contained in:
parent
56dc775c68
commit
a63cce53e2
54 changed files with 4817 additions and 238 deletions
273
Assets/_Project/Scripts/Gameplay/TowerInstance.cs
Normal file
273
Assets/_Project/Scripts/Gameplay/TowerInstance.cs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
// Assets/_Project/Scripts/Gameplay/TowerInstance.cs
|
||||
using Unity.Collections;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Core;
|
||||
using TD.Towers;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-tower runtime component. Lives on the tower's NetworkObject prefab root.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// <list type="bullet">
|
||||
/// <item>Hold the network-replicated identity of this tower: which
|
||||
/// <see cref="TowerDefinition"/> it is and which <see cref="PlayerSlot"/> owns it.</item>
|
||||
/// <item>On <see cref="OnNetworkSpawn"/>, stamp the tower's footprint into
|
||||
/// <see cref="LevelLoader"/> on every client so local grids stay in sync
|
||||
/// with the server-authoritative state.</item>
|
||||
/// <item>Apply the owner's player color to the tower mesh, so towers are
|
||||
/// visually distinct by zone during testing.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// <para><b>Grid stamping split.</b> The server stamps the footprint in
|
||||
/// <c>TowerPlacementManager.ProcessRequest</c> (before <c>NetworkObject.Spawn</c>)
|
||||
/// so the path-validity check in the same frame sees the updated grid. Non-host
|
||||
/// clients stamp in <see cref="OnNetworkSpawn"/> when NGO replicates the
|
||||
/// NetworkObject to them. The server's <see cref="OnNetworkSpawn"/> also runs,
|
||||
/// but by then the footprint is already stamped — <see cref="SetWalkable"/> and
|
||||
/// <see cref="SetOccupied"/> are idempotent writes, so double-stamping is safe.</para>
|
||||
///
|
||||
/// <para><b>Definition reference replication.</b> TowerDefinition assets live in the
|
||||
/// project on all clients. We replicate the asset by name via a
|
||||
/// <see cref="NetworkVariable{T}"/> holding a <c>FixedString64Bytes</c>, then look
|
||||
/// up the asset locally. This avoids serializing the full ScriptableObject over the
|
||||
/// network. The lookup uses a <see cref="TowerRegistry"/> singleton that must be
|
||||
/// present in the scene. (Temporary: will be driven by RaceDefinition in Path E.)</para>
|
||||
///
|
||||
/// <para><b>Combat.</b> No combat logic here yet. Combat fields live stubbed on
|
||||
/// <see cref="TowerDefinition"/>; they will be consumed by a future
|
||||
/// <c>TowerCombat</c> component added to the same prefab.</para>
|
||||
/// </remarks>
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public class TowerInstance : NetworkBehaviour
|
||||
{
|
||||
// ----- Networked state ------------------------------------------------
|
||||
|
||||
// The name of the TowerDefinition asset for this tower. Replicated so all
|
||||
// clients can look up the full definition locally without sending the whole
|
||||
// ScriptableObject over the wire.
|
||||
private readonly NetworkVariable<FixedString64Bytes> definitionName =
|
||||
new NetworkVariable<FixedString64Bytes>(
|
||||
default,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// The footprint anchor (SW corner, world-tile coords). Replicated so
|
||||
// clients can stamp the correct tiles in OnNetworkSpawn.
|
||||
private readonly NetworkVariable<Vector2Int> anchorTile =
|
||||
new NetworkVariable<Vector2Int>(
|
||||
default,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// The PlayerSlot that placed this tower. Replicated for the HUD context
|
||||
// panel and for view-selection by non-owning clients.
|
||||
private readonly NetworkVariable<PlayerSlot> ownerSlot =
|
||||
new NetworkVariable<PlayerSlot>(
|
||||
PlayerSlot.None,
|
||||
readPerm: NetworkVariableReadPermission.Everyone,
|
||||
writePerm: NetworkVariableWritePermission.Server);
|
||||
|
||||
// ----- Local resolved state -------------------------------------------
|
||||
|
||||
// Resolved on every client in OnNetworkSpawn from definitionName.
|
||||
// Null if the lookup fails (missing TowerRegistry or unknown name).
|
||||
private TowerDefinition resolvedDefinition;
|
||||
|
||||
// ----- Pre-spawn initialization data ----------------------------------
|
||||
//
|
||||
// Set by InitializeServer (called by TowerPlacementManager BEFORE Spawn).
|
||||
// Read by the server's OnNetworkSpawn to populate the NetworkVariables.
|
||||
//
|
||||
// Why this two-step dance: NGO 2.x disallows writing NetworkVariables
|
||||
// before NetworkObject.Spawn() — those writes produce warnings and may
|
||||
// not replicate reliably. The supported pattern is to set NVs inside
|
||||
// OnNetworkSpawn on the server; NGO captures those writes and includes
|
||||
// them in the initial sync message sent to clients, so every client
|
||||
// sees correct values on its very first OnNetworkSpawn callback.
|
||||
|
||||
private TowerDefinition pendingDefinition;
|
||||
private Vector2Int pendingAnchor;
|
||||
private PlayerSlot pendingOwner = PlayerSlot.None;
|
||||
private bool hasPendingInit;
|
||||
|
||||
// ----- Public accessors -----------------------------------------------
|
||||
|
||||
/// <summary>The TowerDefinition for this tower, resolved locally. Null until
|
||||
/// <see cref="OnNetworkSpawn"/> runs and the definition lookup succeeds.</summary>
|
||||
public TowerDefinition Definition => resolvedDefinition;
|
||||
|
||||
/// <summary>The PlayerSlot that placed this tower.</summary>
|
||||
public PlayerSlot Owner => ownerSlot.Value;
|
||||
|
||||
/// <summary>The footprint anchor tile (SW corner, world-tile coords).</summary>
|
||||
public Vector2Int AnchorTile => anchorTile.Value;
|
||||
|
||||
// ----- Server-only initialization -------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Called by <c>TowerPlacementManager</c> on the server immediately after
|
||||
/// instantiation and before <c>NetworkObject.Spawn</c>. Stores the data that
|
||||
/// the server's <see cref="OnNetworkSpawn"/> will copy into the
|
||||
/// NetworkVariables. NetworkVariables themselves are NOT written here —
|
||||
/// see the comment on the pending-init fields above for why.
|
||||
/// </summary>
|
||||
public void InitializeServer(TowerDefinition def, Vector2Int anchor, PlayerSlot owner)
|
||||
{
|
||||
var nm = NetworkManager.Singleton;
|
||||
if (nm == null || !nm.IsServer)
|
||||
{
|
||||
Debug.LogError("[TowerInstance] InitializeServer called when not running " +
|
||||
"as a server. This must only be called by " +
|
||||
"TowerPlacementManager on the server.");
|
||||
return;
|
||||
}
|
||||
|
||||
pendingDefinition = def;
|
||||
pendingAnchor = anchor;
|
||||
pendingOwner = owner;
|
||||
hasPendingInit = true;
|
||||
|
||||
// Cache the resolved definition on the server immediately — clients
|
||||
// will resolve via the registry lookup once definitionName arrives.
|
||||
resolvedDefinition = def;
|
||||
}
|
||||
|
||||
// ----- NGO lifecycle --------------------------------------------------
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// Server-only step: now that the NetworkObject is fully spawned,
|
||||
// write the pending init values to the NetworkVariables. These writes
|
||||
// will be captured in the initial sync message sent to clients, so
|
||||
// every client sees correct values on its very first OnNetworkSpawn.
|
||||
if (IsServer && hasPendingInit)
|
||||
{
|
||||
definitionName.Value = new FixedString64Bytes(pendingDefinition.name);
|
||||
anchorTile.Value = pendingAnchor;
|
||||
ownerSlot.Value = pendingOwner;
|
||||
|
||||
// Clear the pending data — it's now committed to NetworkVariables.
|
||||
hasPendingInit = false;
|
||||
}
|
||||
|
||||
// Resolve the TowerDefinition from the (now-available) replicated name.
|
||||
// On the server this is already set by InitializeServer; the lookup is
|
||||
// redundant but harmless and keeps the code path uniform.
|
||||
ResolveDefinition();
|
||||
|
||||
// Stamp the footprint into the local LevelLoader grids.
|
||||
// The server already stamped in TowerPlacementManager before Spawn(),
|
||||
// but SetWalkable/SetOccupied are idempotent — double-stamping is safe.
|
||||
StampFootprint(walkable: false, occupied: true);
|
||||
|
||||
// Apply owner color to the mesh renderer.
|
||||
ApplyOwnerColor();
|
||||
|
||||
if (resolvedDefinition != null)
|
||||
{
|
||||
Debug.Log($"[TowerInstance] Spawned '{resolvedDefinition.DisplayName}' " +
|
||||
$"for {ownerSlot.Value} at anchor {anchorTile.Value}. " +
|
||||
$"IsServer={IsServer}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
// Un-stamp the footprint when the tower is destroyed (sold, wave end, etc.)
|
||||
// so the tiles become walkable and buildable again.
|
||||
StampFootprint(walkable: true, occupied: false);
|
||||
}
|
||||
|
||||
// ----- Private helpers ------------------------------------------------
|
||||
|
||||
private void ResolveDefinition()
|
||||
{
|
||||
// Already resolved (server path via InitializeServer).
|
||||
if (resolvedDefinition != null) return;
|
||||
|
||||
string defName = definitionName.Value.ToString();
|
||||
if (string.IsNullOrEmpty(defName))
|
||||
{
|
||||
Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
|
||||
$"definitionName is empty. Cannot resolve TowerDefinition.");
|
||||
return;
|
||||
}
|
||||
|
||||
var registry = TowerRegistry.Instance;
|
||||
if (registry == null)
|
||||
{
|
||||
Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
|
||||
$"No TowerRegistry found in scene. Cannot resolve '{defName}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
resolvedDefinition = registry.Get(defName);
|
||||
if (resolvedDefinition == null)
|
||||
{
|
||||
Debug.LogError($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
|
||||
$"TowerRegistry does not contain a definition named '{defName}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void StampFootprint(bool walkable, bool occupied)
|
||||
{
|
||||
var loader = LevelLoader.Instance;
|
||||
if (loader == null || !loader.IsLoaded)
|
||||
{
|
||||
Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
|
||||
$"LevelLoader not available during footprint stamp. " +
|
||||
$"Grids may be out of sync.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine footprint size from the resolved definition, falling back to
|
||||
// 2×2 if the definition hasn't resolved yet (shouldn't happen, but defensive).
|
||||
Vector2Int footprintSize = resolvedDefinition != null
|
||||
? resolvedDefinition.FootprintSize
|
||||
: new Vector2Int(2, 2);
|
||||
|
||||
foreach (var tile in GridCoordinates.GetFootprintTiles(anchorTile.Value, footprintSize))
|
||||
{
|
||||
loader.SetWalkable(tile, walkable);
|
||||
loader.SetOccupied(tile, occupied);
|
||||
}
|
||||
}
|
||||
|
||||
// Reused per-instance across color updates to avoid per-call GC allocation.
|
||||
// MaterialPropertyBlock is not thread-safe but all rendering runs on the
|
||||
// main thread, so a single instance per TowerInstance is fine.
|
||||
private MaterialPropertyBlock colorPropertyBlock;
|
||||
private static readonly int ColorPropertyId = Shader.PropertyToID("_Color");
|
||||
// URP Lit uses _BaseColor, not _Color. Writing both ensures the tint applies
|
||||
// regardless of which shader the prefab uses; unknown property writes are
|
||||
// silently ignored.
|
||||
private static readonly int BaseColorPropertyId = Shader.PropertyToID("_BaseColor");
|
||||
|
||||
private void ApplyOwnerColor()
|
||||
{
|
||||
Color ownerColor = PlayerColors.Get(ownerSlot.Value);
|
||||
ownerColor.a = 1f;
|
||||
|
||||
// MaterialPropertyBlock sets per-renderer properties without allocating
|
||||
// a new Material object. Safe to reuse across calls on the same instance.
|
||||
// All Unity standard/URP shaders expose _Color or _BaseColor, so no shader changes needed.
|
||||
colorPropertyBlock ??= new MaterialPropertyBlock();
|
||||
colorPropertyBlock.SetColor(ColorPropertyId, ownerColor);
|
||||
colorPropertyBlock.SetColor(BaseColorPropertyId, ownerColor);
|
||||
|
||||
var renderers = GetComponentsInChildren<MeshRenderer>();
|
||||
foreach (var rend in renderers)
|
||||
rend.SetPropertyBlock(colorPropertyBlock);
|
||||
|
||||
if (renderers.Length == 0)
|
||||
{
|
||||
Debug.LogWarning($"[TowerInstance] NetworkObject {NetworkObjectId}: " +
|
||||
$"No MeshRenderers found for owner color tinting.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue