I am in fact a silly goose, I think I have a decent grasp on the networking now. It's pretty nice and simple seems the intent was met (:
At first, I thought logic just “ran on both sides” and would magically stay in sync, but that’s not how it works. I guess the best way to think of it is that the host (server) is authoritative, and clients just get data from it via [Sync] and RPC
For anyone else struggling like I was here's a little rundown of my process
- Made an empty GameObject and attached my
RoundController component. - Set Network Mode → Network Object, Orphaned Mode → Host, and Owner Transfer → Fixed.
- Let the host run all the logic, and used
[Sync] to replicate round info to clients. [Change] to print out the current round when updated on both client and host for debugging
At first, I made everything static, which doesn’t replicate; each client had its own copy.
The fix was removing static and using [Sync] fields on a networked object.
I also discovered Networking.IsHost and Networking.IsClient, which work like the old “server/client” checks.
and here's my very barebones code The host runs all the round logic and prints countdowns. Clients automatically receive synced [Sync] values and print their current round.
public sealed class RoundController : Component
{
public bool Started { get; set; } = false;
public int RoundState { get; set; } = 0;
public float RoundDuration { get; set; } = 10f;
public float IntermissionDuration { get; set; } = 5f;
private float TimeRemaining { get; set; } = 0f;
[Sync, Change ("NewRound")] public int CurrentRound { get; set; } = 1;
// This is hooked into the variable above and will run when that value is changed.
private void NewRound( int oldValue, int newValue )
{
if (Networking.IsHost)
Log.Info($"[Host] Round {newValue}");
else if (Networking.IsClient)
Log.Info($"[Client] Round {newValue}");
}
public void Initialize()
{
if (Started) { Log.Info("Gamemode already started"); return; }
Started = true;
StartRound();
}
public async void StartRound()
{
if (!Started || RoundState == 1) return;
RoundState = 1;
TimeRemaining = RoundDuration;
Log.Info($"Round Started: {CurrentRound}");
while (RoundState == 1 && TimeRemaining > 0)
{
await GameTask.DelaySeconds(1);
TimeRemaining -= 1;
if (Networking.IsHost)
Log.Info($"Time Remaining: {TimeRemaining}");
}
if (Started)
{
Log.Info("Out of time");
EndRound();
}
}
public async void EndRound()
{
if (!Started || RoundState == 2) return;
RoundState = 0;
CurrentRound++;
RoundState = 2;
TimeRemaining = IntermissionDuration;
Log.Info( "Beginning intermission..." );
while (RoundState == 2 && TimeRemaining > 0)
{
await GameTask.DelaySeconds(1);
TimeRemaining -= 1;
}
StartRound();
}
protected override void OnStart()
{
if (Networking.IsHost) // Make sure the system is only being ran from the host
Initialize();
}
protected override void OnDestroy()
{
if (Networking.IsHost)
{
Started = false;
RoundState = 0;
}
}
}