Cozy Wather + buld systeme
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
using System.Collections.Generic;
|
||||
using FishNet.Connection;
|
||||
using FishNet.Object;
|
||||
using FishNet.Object.Synchronizing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.Building
|
||||
{
|
||||
/// <summary>
|
||||
/// Owns the whole networked side of building, grouped here by cohesion rather than split into
|
||||
/// two tiny scripts:
|
||||
///
|
||||
/// 1. Committed structures — a SyncList of BuildRecords (id + pose). They accumulate to high
|
||||
/// counts, so they are data-driven, not NetworkObjects (§3): every machine (late joiners
|
||||
/// included) instantiates the matching BuiltPrefab locally from the synced list.
|
||||
/// 2. The live ghost preview — spawned as a real NetworkObject owned by the placer, so FishNet's
|
||||
/// NetworkTransform replicates its motion to everyone smoothly. A ghost is one-per-player and
|
||||
/// transient, the case where a NetworkObject beats a data registry — the deliberate exception
|
||||
/// to §3's "no NetworkObjects for world state" (which targets high-count scatter, not this).
|
||||
///
|
||||
/// BuildManager (the local build UX) drives the owned ghost; on confirm this registry commits the
|
||||
/// record 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; }
|
||||
|
||||
/// <summary>
|
||||
/// Every committed structure, server-written and replicated to all clients.
|
||||
/// </summary>
|
||||
private readonly SyncList<BuildRecord> builds = new SyncList<BuildRecord>();
|
||||
|
||||
/// <summary>
|
||||
/// The local instances rendered from <see cref="builds"/>, kept index-aligned with it.
|
||||
/// </summary>
|
||||
private readonly List<GameObject> spawned = new List<GameObject>();
|
||||
|
||||
/// <summary>
|
||||
/// How close two sockets must be to count as connected — they snap to the exact same point,
|
||||
/// so a tiny threshold is enough and no layer/radius has to be configured.
|
||||
/// </summary>
|
||||
private const float OccupancyThresholdSqr = 0.05f * 0.05f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Network Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Starts listening to the replicated build list on every machine.
|
||||
/// </summary>
|
||||
public override void OnStartNetwork()
|
||||
{
|
||||
base.OnStartNetwork();
|
||||
Instance = this;
|
||||
builds.OnChange += OnBuildsChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes and tears down the local instances when the session ends.
|
||||
/// </summary>
|
||||
public override void OnStopNetwork()
|
||||
{
|
||||
base.OnStopNetwork();
|
||||
if (Instance == this) Instance = null;
|
||||
builds.OnChange -= OnBuildsChanged;
|
||||
ClearAll();
|
||||
}
|
||||
|
||||
#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 and appends the record, which replicates to all clients.
|
||||
/// </summary>
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
private void RequestBuildServerRpc(ushort buildableId, Vector3 position, Quaternion rotation, NetworkConnection conn = null)
|
||||
{
|
||||
if (BuildableDatabase.Instance == null || BuildableDatabase.Instance.GetBuildable(buildableId) == null)
|
||||
return;
|
||||
|
||||
// TODO: validate + consume the requester's resources (conn → PlayerInventory) before committing.
|
||||
builds.Add(new BuildRecord { buildableId = buildableId, position = position, rotation = rotation });
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sync → Local Instances
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the replicated build list into local instances. The host processes only the
|
||||
/// server callback (asServer) so a build is never instantiated twice on the same machine.
|
||||
/// </summary>
|
||||
private void OnBuildsChanged(SyncListOperation op, int index, BuildRecord oldItem, BuildRecord newItem, bool asServer)
|
||||
{
|
||||
if (!asServer && base.IsServerInitialized) return;
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case SyncListOperation.Add:
|
||||
case SyncListOperation.Insert:
|
||||
{
|
||||
GameObject go = CreateInstance(newItem);
|
||||
spawned.Insert(index, go);
|
||||
MarkOccupancy(go);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyncListOperation.Set:
|
||||
DestroyAt(index);
|
||||
spawned[index] = CreateInstance(newItem);
|
||||
break;
|
||||
|
||||
case SyncListOperation.RemoveAt:
|
||||
DestroyAt(index);
|
||||
spawned.RemoveAt(index);
|
||||
break;
|
||||
|
||||
case SyncListOperation.Clear:
|
||||
ClearAll();
|
||||
break;
|
||||
|
||||
case SyncListOperation.Complete:
|
||||
RebuildAll();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a record's BuiltPrefab at its pose, or null when the id or prefab is missing.
|
||||
/// </summary>
|
||||
private GameObject CreateInstance(BuildRecord record)
|
||||
{
|
||||
BuildableData data = BuildableDatabase.Instance != null ? BuildableDatabase.Instance.GetBuildable(record.buildableId) : null;
|
||||
if (data == null || data.BuiltPrefab == null)
|
||||
{
|
||||
Debug.LogWarning($"[BuildRegistry] Buildable id {record.buildableId} introuvable ou sans BuiltPrefab — rien de spawné.", this);
|
||||
return null;
|
||||
}
|
||||
return Instantiate(data.BuiltPrefab, record.position, record.rotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the local instance at an index without touching the list bookkeeping.
|
||||
/// </summary>
|
||||
private void DestroyAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= spawned.Count) return;
|
||||
if (spawned[index] != null) Destroy(spawned[index]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys every local instance and clears the list.
|
||||
/// </summary>
|
||||
private void ClearAll()
|
||||
{
|
||||
foreach (GameObject go in spawned)
|
||||
if (go != null) Destroy(go);
|
||||
spawned.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds all local instances from the current list — used for the late-join catch-up —
|
||||
/// then recomputes which sockets are connected so late joiners see the same occupancy.
|
||||
/// </summary>
|
||||
private void RebuildAll()
|
||||
{
|
||||
ClearAll();
|
||||
for (int i = 0; i < builds.Count; i++)
|
||||
spawned.Add(CreateInstance(builds[i]));
|
||||
|
||||
foreach (GameObject go in spawned)
|
||||
MarkOccupancy(go);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the sockets a piece shares a position with — and the matching sockets on the pieces
|
||||
/// it touches — as occupied, so placement snapping skips them. Occupancy is derived purely
|
||||
/// from geometry (two connected sockets sit on the exact same point), so it stays consistent
|
||||
/// on every client without anything extra crossing the wire.
|
||||
/// </summary>
|
||||
private void MarkOccupancy(GameObject instance)
|
||||
{
|
||||
if (instance == null) return;
|
||||
|
||||
BuildSnapPoint[] newSockets = instance.GetComponentsInChildren<BuildSnapPoint>(true);
|
||||
if (newSockets.Length == 0) return;
|
||||
|
||||
foreach (GameObject other in spawned)
|
||||
{
|
||||
if (other == null || other == instance) continue;
|
||||
|
||||
foreach (BuildSnapPoint os in other.GetComponentsInChildren<BuildSnapPoint>(true))
|
||||
{
|
||||
foreach (BuildSnapPoint ns in newSockets)
|
||||
{
|
||||
if (ns.Category != os.Category) continue;
|
||||
if ((ns.transform.position - os.transform.position).sqrMagnitude > OccupancyThresholdSqr) continue;
|
||||
|
||||
ns.SetOccupied(true);
|
||||
os.SetOccupied(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user