147 lines
No EOL
6 KiB
C#
147 lines
No EOL
6 KiB
C#
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
namespace TD.Gameplay
|
|
{
|
|
/// <summary>
|
|
/// GoldManager — canonical server-authoritative template for this project.
|
|
///
|
|
/// Every gameplay system (towers, enemies, waves, damage) should follow
|
|
/// the same three-beat pattern demonstrated here:
|
|
/// 1. State lives in NetworkVariables, which only the server can write.
|
|
/// 2. Clients REQUEST changes via [Rpc(SendTo.Server, ...)] methods.
|
|
/// They never change state directly.
|
|
/// 3. The server VALIDATES the request before applying it.
|
|
/// Never trust the client.
|
|
///
|
|
/// Cosmetic-only reactions (sounds, VFX, UI popups) can use
|
|
/// [Rpc(SendTo.ClientsAndHost)] or [Rpc(SendTo.NotServer)] to broadcast.
|
|
/// </summary>
|
|
public class GoldManager : NetworkBehaviour
|
|
{
|
|
// --- Tunables (editable in Inspector) -----------------------------
|
|
|
|
[Tooltip("How much gold every player starts with when the game begins.")]
|
|
[SerializeField] private int startingGold = 100;
|
|
|
|
// --- Networked state ----------------------------------------------
|
|
|
|
// A NetworkVariable<T> automatically syncs from server to clients.
|
|
// readPerm = Everyone: all clients can read the current value.
|
|
// writePerm = Server: only the server can change it.
|
|
private readonly NetworkVariable<int> currentGold = new NetworkVariable<int>(
|
|
value: 0,
|
|
readPerm: NetworkVariableReadPermission.Everyone,
|
|
writePerm: NetworkVariableWritePermission.Server
|
|
);
|
|
|
|
// Public read-only accessor for other scripts (UI, tower placement).
|
|
public int CurrentGold => currentGold.Value;
|
|
|
|
// --- Lifecycle ----------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// OnNetworkSpawn runs on every peer (server + all clients) when this
|
|
/// NetworkBehaviour becomes active on the network. Replaces Start()
|
|
/// for networked setup.
|
|
/// </summary>
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
Debug.Log($"[GoldManager] OnNetworkSpawn ran. IsServer={IsServer}, IsClient={IsClient}, IsHost={IsHost}");
|
|
|
|
currentGold.OnValueChanged += HandleGoldChanged;
|
|
Debug.Log($"[GoldManager] Subscribed to OnValueChanged. Current value before init: {currentGold.Value}");
|
|
|
|
if (IsServer)
|
|
{
|
|
currentGold.Value = startingGold;
|
|
Debug.Log($"[GoldManager] Server initialized gold. Current value after set: {currentGold.Value}");
|
|
}
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
// Always unsubscribe to avoid callback leaks.
|
|
currentGold.OnValueChanged -= HandleGoldChanged;
|
|
}
|
|
|
|
private void HandleGoldChanged(int previous, int current)
|
|
{
|
|
// Fires on every peer whenever the value syncs. Use Log here so
|
|
// you can see syncing in the Console during development.
|
|
Debug.Log($"[GoldManager] Gold changed: {previous} -> {current}");
|
|
}
|
|
|
|
// --- Public API (called by client-side code) ----------------------
|
|
|
|
/// <summary>
|
|
/// Client-side entry point for spending gold. Called by gameplay code
|
|
/// like TowerPlacement when the local player clicks "build tower."
|
|
///
|
|
/// The actual spending happens on the server via the Rpc.
|
|
/// </summary>
|
|
public void RequestSpendGold(int amount)
|
|
{
|
|
SpendGoldRpc(amount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-side entry point for awarding gold (wave clear, enemy kill).
|
|
/// Not Rpc-wrapped — this is called directly by server game logic in
|
|
/// response to server-authoritative events.
|
|
/// </summary>
|
|
public void AwardGold(int amount)
|
|
{
|
|
if (!IsServer)
|
|
{
|
|
Debug.LogError("[GoldManager] AwardGold called on a client! " +
|
|
"Only server code should call this directly.");
|
|
return;
|
|
}
|
|
|
|
if (amount <= 0) return;
|
|
currentGold.Value += amount;
|
|
}
|
|
|
|
// --- Server-side RPC ----------------------------------------------
|
|
|
|
// [Rpc(SendTo.Server, ...)] means: a client calls this locally, but
|
|
// NGO routes the call and executes the method on the server.
|
|
//
|
|
// RequireOwnership = false lets any client call it (correct for a
|
|
// shared GoldManager). For per-player NetworkObjects you'd usually
|
|
// leave the default ownership requirement in place.
|
|
//
|
|
// Naming convention: methods with [Rpc] attributes must end with "Rpc".
|
|
// The source generator relies on this suffix.
|
|
[Rpc(SendTo.Server, RequireOwnership = false)]
|
|
private void SpendGoldRpc(int amount, RpcParams rpcParams = default)
|
|
{
|
|
// This method body runs on the server only.
|
|
// Validate everything — do not trust the client.
|
|
|
|
// Validation 1: reject non-positive amounts. A negative amount
|
|
// would let a malicious client GAIN gold if we just subtracted.
|
|
if (amount <= 0)
|
|
{
|
|
Debug.LogWarning($"[GoldManager] Rejected spend of {amount} " +
|
|
$"from client {rpcParams.Receive.SenderClientId}: " +
|
|
$"amount must be positive.");
|
|
return;
|
|
}
|
|
|
|
// Validation 2: can't spend more than current balance.
|
|
if (currentGold.Value < amount)
|
|
{
|
|
Debug.LogWarning($"[GoldManager] Rejected spend of {amount} " +
|
|
$"from client {rpcParams.Receive.SenderClientId}: " +
|
|
$"insufficient funds (have {currentGold.Value}).");
|
|
return;
|
|
}
|
|
|
|
// Server applies the change. NetworkVariable syncs to clients
|
|
// automatically at the next network tick.
|
|
currentGold.Value -= amount;
|
|
}
|
|
}
|
|
} |