203 lines
9.2 KiB
C#
203 lines
9.2 KiB
C#
using FishNet.Connection;
|
|
using FishNet.Object;
|
|
using UnityEngine;
|
|
|
|
namespace Ashwild.Building
|
|
{
|
|
/// <summary>
|
|
/// The networked side of building: it commits placements and owns the live ghost preview.
|
|
///
|
|
/// Every committed structure — a wall as much as a chest or a campfire — is spawned as a real
|
|
/// NetworkObject owned by the server. That is the whole design: FishNet already replicates spawns,
|
|
/// catches late joiners up, and tears objects down on despawn, so there is no build list to keep,
|
|
/// no index to align, and no rebuild path to write. Each build carries its own state on its own
|
|
/// <see cref="BuiltStructure"/>, which is what lets a stateful structure (chest, cooking station)
|
|
/// be placed by exactly the same code as a plain wall instead of needing a parallel mechanism.
|
|
///
|
|
/// This replaced an earlier data-driven registry (a SyncList of pose records that every client
|
|
/// instantiated locally). That saved FishNet's per-object bookkeeping but not the GameObjects —
|
|
/// they were instantiated locally anyway — while forcing a second, incompatible path for anything
|
|
/// with real state. One mechanism is worth the bookkeeping.
|
|
///
|
|
/// The ghost is likewise a NetworkObject, but owned by the placer so FishNet's NetworkTransform
|
|
/// replicates its motion smoothly; it is transient and one per player.
|
|
///
|
|
/// BuildManager (the local build UX) drives the owned ghost; on confirm this registry spawns the
|
|
/// structure and despawns the ghost. One instance per scene, a scene NetworkObject like the others.
|
|
/// </summary>
|
|
[RequireComponent(typeof(NetworkObject))]
|
|
public class BuildRegistry : NetworkBehaviour
|
|
{
|
|
#region State
|
|
|
|
/// <summary>
|
|
/// The single scene instance, so BuildManager can request ghost spawns/despawns and build commits.
|
|
/// </summary>
|
|
public static BuildRegistry Instance { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Network Lifecycle
|
|
|
|
/// <summary>
|
|
/// Claims the singleton on every machine.
|
|
/// </summary>
|
|
public override void OnStartNetwork()
|
|
{
|
|
base.OnStartNetwork();
|
|
Instance = this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases the singleton when the session ends. Committed structures are NetworkObjects, so
|
|
/// FishNet despawns them itself — there is nothing to tear down here.
|
|
/// </summary>
|
|
public override void OnStopNetwork()
|
|
{
|
|
base.OnStopNetwork();
|
|
if (Instance == this) Instance = null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Commit Build
|
|
|
|
/// <summary>
|
|
/// Called by BuildManager when the local player confirms a placement: commit this buildable at
|
|
/// this pose so the real structure appears for every player.
|
|
/// </summary>
|
|
public void RequestBuild(ushort buildableId, Vector3 position, Quaternion rotation)
|
|
=> RequestBuildServerRpc(buildableId, position, rotation);
|
|
|
|
/// <summary>
|
|
/// Server-side: validates the id, instantiates the built prefab, seeds its identity and health,
|
|
/// then spawns it for everyone. The appear pop is sent separately to current observers only, so a
|
|
/// late joiner streaming in an existing base does not watch all of it pop at once.
|
|
///
|
|
/// The structure is spawned server-owned (no connection passed): builds are shared world state
|
|
/// and several players interact with the same chest or fire, so handing ownership to whoever
|
|
/// placed it would tie everyone's access to that player's connection.
|
|
/// </summary>
|
|
[ServerRpc(RequireOwnership = false)]
|
|
private void RequestBuildServerRpc(ushort buildableId, Vector3 position, Quaternion rotation, NetworkConnection conn = null)
|
|
{
|
|
BuildableData data = BuildableDatabase.Instance != null ? BuildableDatabase.Instance.GetBuildable(buildableId) : null;
|
|
if (data == null || data.BuiltPrefab == null)
|
|
{
|
|
Debug.LogWarning($"[BuildRegistry] Buildable id {buildableId} is unknown or has no BuiltPrefab — nothing spawned.", this);
|
|
return;
|
|
}
|
|
|
|
// TODO: validate + consume the requester's resources (conn → PlayerInventory) before committing.
|
|
GameObject go = Instantiate(data.BuiltPrefab, position, rotation);
|
|
|
|
BuiltStructure structure = go.GetComponent<BuiltStructure>();
|
|
if (structure == null)
|
|
{
|
|
Debug.LogError($"[BuildRegistry] BuiltPrefab '{data.BuiltPrefab.name}' has no BuiltStructure — add one (it carries the build's identity, health and demolition).", this);
|
|
Destroy(go);
|
|
return;
|
|
}
|
|
|
|
structure.InitialiseOnServer(buildableId, data.MaxHealth);
|
|
base.ServerManager.Spawn(go);
|
|
structure.PlaySpawnBumpForObservers();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Networked Ghost Preview
|
|
|
|
/// <summary>
|
|
/// Client → asks the server to spawn a ghost of this buildable, owned by us. The spawned
|
|
/// object is handed back to BuildManager through <see cref="TargetAttachGhost"/>, carrying
|
|
/// the request token so a superseded spawn can be discarded.
|
|
/// </summary>
|
|
public void RequestSpawnGhost(ushort buildableId, int token) => RequestSpawnGhostServerRpc(buildableId, token);
|
|
|
|
/// <summary>
|
|
/// Client → asks the server to despawn a ghost we own (on confirm, cancel or switch).
|
|
/// </summary>
|
|
public void RequestDespawnGhost(NetworkObject ghost)
|
|
{
|
|
if (ghost != null) RequestDespawnGhostServerRpc(ghost);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-side: spawns the buildable's ghost prefab owned by the requester, then tells that
|
|
/// client which object to drive. The prefab must be a NetworkObject registered in
|
|
/// DefaultPrefabObjects with a client-authoritative NetworkTransform.
|
|
/// </summary>
|
|
[ServerRpc(RequireOwnership = false)]
|
|
private void RequestSpawnGhostServerRpc(ushort buildableId, int token, NetworkConnection conn = null)
|
|
{
|
|
if (conn == null) return;
|
|
|
|
BuildableData data = BuildableDatabase.Instance != null ? BuildableDatabase.Instance.GetBuildable(buildableId) : null;
|
|
if (data == null || data.GhostPrefab == null) return;
|
|
|
|
GameObject go = Instantiate(data.GhostPrefab);
|
|
base.ServerManager.Spawn(go, conn);
|
|
TargetAttachGhost(conn, go.GetComponent<NetworkObject>(), token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owner-side: hands the freshly spawned ghost (and its request token) to BuildManager to drive.
|
|
/// </summary>
|
|
[TargetRpc]
|
|
private void TargetAttachGhost(NetworkConnection conn, NetworkObject ghost, int token)
|
|
{
|
|
if (ghost == null || BuildManager.Instance == null) return;
|
|
BuildManager.Instance.AttachGhost(ghost.gameObject, token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-side: destroys a ghost (never pools it — a ghost is single-use). Ownership guards
|
|
/// that a client only despawns its own.
|
|
/// </summary>
|
|
[ServerRpc(RequireOwnership = false)]
|
|
private void RequestDespawnGhostServerRpc(NetworkObject ghost, NetworkConnection conn = null)
|
|
{
|
|
if (ghost == null) return;
|
|
if (ghost.Owner != conn) return;
|
|
base.ServerManager.Despawn(ghost, DespawnType.Destroy);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owner → replicates the ghost's current spot validity to every other client so teammates see
|
|
/// the same green (clear) / red (blocked) preview. Only the owner drives the evaluation locally,
|
|
/// so without this a remote ghost stays on its default look; BuildManager calls this only when the
|
|
/// value actually changes, so the traffic is a rare per-ghost blip, not a per-frame stream.
|
|
/// </summary>
|
|
public void SetGhostValidity(NetworkObject ghost, bool valid)
|
|
{
|
|
if (ghost != null) SetGhostValidityServerRpc(ghost, valid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-side relay: forwards the placer's validity to all observers.
|
|
/// </summary>
|
|
[ServerRpc(RequireOwnership = false)]
|
|
private void SetGhostValidityServerRpc(NetworkObject ghost, bool valid, NetworkConnection conn = null)
|
|
{
|
|
if (ghost == null) return;
|
|
ObserversSetGhostValidity(ghost, valid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the replicated validity on every client except the placer, whose own local evaluation
|
|
/// already drives the richer verdict (and knows about affordability, which is never sent). A
|
|
/// missing ghost is ignored — it may have been despawned mid-flight.
|
|
/// </summary>
|
|
[ObserversRpc]
|
|
private void ObserversSetGhostValidity(NetworkObject ghost, bool valid)
|
|
{
|
|
if (ghost == null || ghost.IsOwner) return;
|
|
BuildGhost bg = ghost.GetComponent<BuildGhost>();
|
|
if (bg != null) bg.SetRemoteValidity(valid);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|