Best Practices for Developing a Server

This guide collects practical patterns and guardrails for building an authoritative game server using this library (Lidgren-based, Unity-hosted). It assumes you’re working with ServerNetwork / ClientNetwork, UCNetwork, and the NetworkSync component.

Goals

  • Keep the server authoritative over game state.

  • Minimize bandwidth via areas, lite sync, and targeted RPCs.

  • Validate all client input; never trust the client.

  • Make behavior predictable and observable (logging/metrics).

  • Design for disconnects, ownership transfer, and hot joins.

Project & Scene Structure

  • One server scene, one root: Create a dedicated “Server” scene with a single GameObject that has ServerNetwork and your server logic MonoBehaviours.

  • Headless build (preferred): Run the Unity server as a headless build on your host. (Editor play works for development, but headless is closer to prod.)

  • No physics authority on clients: Simulate authoritative physics on the server; clients render/interpolate.

Server Startup & Config

ServerNetwork config is created in Awake():

  • App ID: NetPeerConfiguration("UpsilonCircuit") — keep this stable; use a new string if you fork the protocol.

  • Port: Default is UCNetwork.port (603). Expose a CLI arg or config file if you need variants.

  • Max connections: Set maxConnections sensibly; reject overflow cleanly.

  • Connection approval: Already enabled; you must implement the callback.

Example: connection approval hook you own

ServerNetwork sends your script a ConnectionRequestInfo via SendMessage("ConnectionRequest", data). Approve/deny explicitly:

public class AuthGate : MonoBehaviour
{
    public ServerNetwork server;

    // Called by ServerNetwork via SendMessage
    void ConnectionRequest(ServerNetwork.ConnectionRequestInfo info)
    {
        bool ok = Validate(info.username, info.password, info.clientType, info.uniqueId, info.ip);

        if (ok) server.ConnectionApproved((long)info.id);
        else    server.ConnectionDenied((long)info.id);
    }

    bool Validate(string user, string pass, string type, ulong uid, string ip)
    {
        // TODO: check banlists, versions, tokens, rate limits, etc.
        return !string.IsNullOrEmpty(user);
    }
}

Authoritative Mindset

  • All state changes must be validated server-side.

  • Never accept client SyncUpdate for objects they do not own (the server already enforces this and replies with OwnershipLost).

  • Prefer server-driven RPCs to inform clients of results rather than letting clients decide outcomes.

Areas (Interest Management)

  • Use areas to send updates only to relevant clients.

  • Joining an area triggers a state snapshot via SendInitializationData (Instantiate + latest Sync/LiteSync).

  • Keep areas coarse (rooms/floors/zones) rather than tiny cells to avoid bookkeeping overhead.

  • For features like team chat, put players in overlapping areas (location area + team area) and target RPCs to that area.

Ownership & Object Lifecycle

  • Default ownership: - Server-spawned → server owns. - Client-spawned → that client owns (server confirms).

  • On disconnect: the server calls FindNewOwners for objects that do not followsClient; otherwise it destroys or persists them.

  • When transferring ownership, the server emits OwnershipLost / OwnershipGained; clients get OnLoseOwnership / OnGainOwnership.

  • Treat scene objects (with NetworkSync) as server-owned, persistent simulation elements.

ID Allocation & Spawning

  • Server issues ID ranges to each client (default block = 500).

  • Immediate local instantiate is possible on the client while preserving global uniqueness.

  • If your game is server-spawn-heavy, prefer server-created objects to centralize authority and avoid client-side abuse.

RPCs: Targeting & Design

  • Pick the narrowest recipient: - ServerOnly for client → server requests/validation - SingleClient for private replies - AllClientsInArea / OtherClientsInArea for local events - AllClients only for truly global events

  • Keep payloads small. Send identifiers, not blobs.

  • Whitelist function names / validate arguments server-side. Reflection is powerful—don’t allow arbitrary method reachability on sensitive scripts.

  • Don’t spam RPCs in Update(); use Sync or LiteSync for frequent data.

Sync & LiteSync

  • Use SyncUpdate for position/rotation + custom bytes.

  • Use LiteSyncUpdate for custom bytes only (animation state, ammo count, cooldowns).

  • Rate-limit and batch where possible; prioritize objects near players.

  • Clients should interpolate to hide latency; server sends the ground truth.

Delivery, Channels & Reliability

  • Delivery methods: - ReliableOrdered for important or stateful events (spawns, destroys, RPCs). - UnreliableSequenced for time-series state (movement, aim).

  • Sequence channels: - SyncSequenceChannel = 10 for sync/lite sync streams. - VoiceSequenceChannel = 11 reserved (voice chat is stubbed / optional).

  • Don’t overload a single channel with unrelated high-frequency traffic.

Validation, Security & Abuse Prevention

  • Sanity-check all client input: - Is the sender owner of the object they are syncing? - Is the prefab name allowed? (Use a whitelist.) - Are area transitions legal? (Prevent “teleporting” by spoof.) - Is the RPC allowed for this GameObject?

  • Rate-limit noisy actions per-connection (RPCs, spawns, chat).

  • Disconnect on repeated protocol violations.

  • Add version checks (client build/protocol) in connection approval.

  • Implement admin kick/ban tools using serverNetwork.Kick(clientId).

Logging, Metrics & Observability

  • Enable file logging: server.EnableLogging("log.txt").

  • Use existing logs for RPC counts, message types, and per-client traffic stats.

  • Expose a simple console/endpoint to emit GetStatsText() for operational insight (connections, objects, areas, ownership lists).

  • Avoid excessive Debug.Log in hot paths; prefer aggregated periodic logs.

Performance Patterns

  • Avoid per-frame allocations in hotspots: - Reuse List<NetConnection> and buffers; pool if necessary. - Reuse NetOutgoingMessage when possible (create, write, send, recycle).

  • Batch operations: - Build a single outgoing message for many recipients instead of

    per-client duplicates when feasible.

  • Guard your Update(): - The library’s UCNetwork.Update() processes inbound queues. - Keep your own server Update() lean; schedule heavier tasks on

    timers or coroutines.

  • Tick consistency: - Consider a fixed simulation step for server-side gameplay systems. - Translate to client-friendly “visual ticks” via interpolation.

Clean Shutdown & Fault Handling

  • On shutdown: call connection.Shutdown("Peace out") (already done in OnDestroy()) and persist any needed state in your OnDestroyNetworkObject or related hooks.

  • Be resilient to mid-flight disconnects: - Always wrap ownership reassignment and area cleanup in null/exists checks.

  • Protect persistence calls (e.g., hooks like PersistInactiveSavedObject / UpdateSaveObject) with try/catch and minimal I/O on the main thread.

Example: Minimal Server Orchestration

public class GameServer : MonoBehaviour
{
    public ServerNetwork server;

    // Fired by ServerNetwork
    void OnClientConnected(long clientId)
    {
        Debug.Log($"Client connected: {clientId}");
        // Place in default area
        server.AddToArea(clientId, 1); // e.g., Town
    }

    // Fired by ServerNetwork
    void OnClientDisconnected(long clientId)
    {
        Debug.Log($"Client disconnected: {clientId}");
        // Ownership transfers & cleanup are handled by ServerNetwork
    }

    // RPC from clients: server validates and reroutes
    public void RequestOpenDoor(int doorNetId)
    {
        if (!ValidateDoor(doorNetId)) return;

        // Broadcast to area only
        var areaIds = server.GetNetObjById(doorNetId)?.areaIds;
        if (areaIds != null && areaIds.Count > 0)
            server.CallRPCToArea("OpenDoor", UCNetwork.MessageReceiver.AllClientsInArea, areaIds, doorNetId);
    }

    bool ValidateDoor(int netId)
    {
        var obj = server.GetNetObjById(netId);
        return obj != null && obj.prefabName == "Door";
    }
}

Testing & Tooling

  • Create simulated clients that spam typical actions at realistic rates.

  • Add protocol fuzzers to try bad arguments, wrong ownership, and invalid area transitions; confirm the server rejects them gracefully.

  • Use loopback testing first, then LAN, then WAN to evaluate latency, packet loss, and bandwidth headroom.

Voice Chat (Optional / Stubbed)

  • The codebase includes VoiceData message handling and a VoIPManager stub; treat this as opt-in and complete it only if needed. It should use separate channels and its own rate limiting.

Summary

A reliable server with this library is authoritative, minimal, and selective. Validate everything, send only what’s needed (areas, lite sync, narrow RPC targets), and build strong observability. Your clients will be smoother, your bandwidth lower, and your game harder to cheat.