Confused About Networking

Started by Kairos 2 posts 148 views

Reply on sbox.game
  1. #1
    Kairos 375

    Maybe I’m overthinking this, but I can’t quite wrap my head around how networking works...

    The docs mention “Serverside code only works when running local projects”  what exactly does that mean?

    Context
    Trying to learn c#'s syntax and s&box in general so I originally built a simple round controller as its own class all just backend with some print/logs and called it from GameManager.OnHostInitialize. It worked fine:
    • start round

    • countdown timer

    • end round

    • run intermission

    • start next round

    Now I want to replicate the current round and remaining time to clients. I read that [Sync] is the right way (instead of firing an RPC every second), but it requires making it a Component. That feels odd is this actually the intended use for system-type logic? Should I just make an empty GameObject and attach a component script to handle it?

    Where I’m stuck
    When I test networking by joining from a second instance, the client seems to run its own copy of the script.
    They’re not synced, and it looks like the logic is all client-side.
    So:
    • Does every client always get its own instance of components?

    • How do I make one authoritative “server-side” instance that drives the state?

    • Are [Sync] variables only valid if the object was spawned with NetworkSpawn()?

    • Is the idea that everything exists on both sides but the host owns and replicates the authoritative version?
    TL;DR

    I’m confused about:
    • what “serverside code only works when running local projects” really means,

    • why my component script runs on every client,

    • and the correct way to have a single authoritative instance replicate data to all clients via [Sync].

    Any clarification or examples would be appreciated, or if I'm just a retard let me know!

  2. edited Oct 2025 #2
    Kairos Started this 375
    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;
            }
        }
        
        
    }