Best Practices for Developing a Client (with Networked Objects)

This guide focuses on building smooth, bandwidth-friendly, and robust clients with this networking library (ClientNetwork, NetworkSync, UCNetwork). It assumes your game uses networked objects for continuous state and RPCs for discrete events.

Goals

  • Make the client feel responsive (latency hiding via interpolation/prediction).

  • Keep traffic lean (areas, lite sync, narrow RPC targets).

  • Respect ownership (only the owner sends authoritative updates).

  • Be resilient to disconnects, late joins, and object lifecycle changes.

  • Keep the UI clear about connection state and control/ownership.

Scene & Setup

  • Create a Client Unity project/scene.

  • Add a GameObject with ClientNetwork and your client bootstrap MonoBehaviours.

  • Optional: add a simple status overlay to reflect callbacks (see below).

Minimal bootstrap

public class ClientBoot : MonoBehaviour
{
    public ClientNetwork client;

    void Start()
    {
        client.Connect("127.0.0.1", 603, "Student01", "", "Player", 0);
    }

    // Status callbacks from UCNetwork
    void OnNetStatusConnected()    => Debug.Log("Connected");
    void OnNetStatusDisconnected() => Debug.Log("Disconnected");
}

Ownership on the Client

  • You may only sync objects you own. The server enforces this. Violations can result in ownership loss messages.

  • Handle ownership transitions on every object that matters:

public class PlayerController : MonoBehaviour
{
    bool hasControl;
    void OnGainOwnership() { hasControl = true;  /* enable input, cameras */ }
    void OnLoseOwnership() { hasControl = false; /* disable input, hand off */ }
}

Spawning & Prefabs (Resources)

  • Instantiate networked objects with ``ClientNetwork.Instantiate`` (not Unity’s).

  • The prefab name must exist in a Resources/ folder on every client.

GameObject avatar = client.Instantiate("PlayerAvatar", spawnPos, Quaternion.identity);
  • Immediate local instantiate works because the client uses an ID pool from the server.

  • Tip: Centralize allowed prefab names in a shared enum or list to avoid typos and spoofing.

Destroying

  • If you own the object, destroy it via the networking layer:

client.Destroy(mySync.GetId());
  • If you don’t own the object, request via an RPC to the server; never call Unity’s Destroy directly for networked entities.

Areas (Interest Management)

  • Join the areas relevant to the local player to receive only the updates you need:

client.AddToArea(AreaIds.Town);
client.RemoveFromArea(AreaIds.Dungeon);
  • When you enter a new area, the server sends an initialization snapshot (instantiates current objects + latest sync). Be ready to instantiate and then render with interpolation (see below).

  • For per-object scoping (e.g., a pet that should only exist in a dungeon):

client.AddObjectToArea(petSync.GetId(), AreaIds.Dungeon);

RPCs (Use Narrow Targets)

  • Prefer ``ServerOnly`` for client → server intent (validation on server).

  • For “show to others” actions, prefer ``OtherClientsInArea`` over AllClients:

// Play jump animation on everyone *else* in my area
mySync.CallRPC("PlayJump", UCNetwork.MessageReceiver.OtherClientsInArea);
  • To message a single client, route through the server (client uses ServerOnly and includes a target client ID as an argument; the server forwards with SingleClient).

Sync vs Lite Sync (Minimize Bandwidth)

  • Use SyncUpdate when you need to send transform + data.

  • Use LiteSyncUpdate when you need data only (e.g., ammo, anim state).

Sending extra custom bytes

// Example: send health as 4 bytes in a LiteSync
void SendHealthLite(NetworkSync sync, int health)
{
    byte[] buf = System.BitConverter.GetBytes(health);
    sync.SendLiteSyncData(buf); // wraps UCNetwork.LiteSyncNetworkData
}

Client-Side Interpolation (Hide Latency)

  • Sync messages arrive at discrete intervals. Interpolate between the two most recent snapshots to avoid jitter.

public class NetInterp : MonoBehaviour
{
    Vector3 lastPos, nextPos;
    Quaternion lastRot, nextRot;
    float lastT, nextT; // timestamps sent by server or NetTime.Now stamps

    public void OnSync(Vector3 pos, Quaternion rot, float t)
    {
        lastPos = nextPos; lastRot = nextRot; lastT = nextT;
        nextPos = pos;     nextRot = rot;     nextT = t;
    }

    void Update()
    {
        float t = Mathf.InverseLerp(lastT, nextT, (float)Lidgren.Network.NetTime.Now);
        transform.position = Vector3.Lerp(lastPos, nextPos, t);
        transform.rotation = Quaternion.Slerp(lastRot, nextRot, t);
    }
}
  • Keep an interpolation buffer (~100–200ms) to smooth irregular arrival times.

  • For fast movers, add extrapolation using last known velocity as a fallback.

Input, Prediction & Reconciliation (Optional Pattern)

  • For responsive movement, consider client-side prediction: - Locally apply input immediately. - Send input to server via RPC or Sync. - When the server’s authoritative state returns, reconcile:

    snap or smoothly correct small errors.

  • Keep correction thresholds small; prefer smooth blending over hard snaps.

Throttling Sends (Don’t Spam)

  • Don’t send Sync/RPC every frame. Throttle to a sensible rate (e.g., 10–20 Hz for transforms, higher only if needed).

float syncInterval = 1f / 15f, acc;
void Update()
{
    acc += Time.deltaTime;
    if (!mySync.IsOwner()) return;

    if (acc >= syncInterval)
    {
        acc = 0f;
        mySync.SendFullSyncData(/* optional extra bytes */);
    }
}

Physics Considerations

  • Let the owner drive physics and send states; replicas interpolate.

  • Use Rigidbody interpolation for local visuals if you own the object.

  • Avoid sending physics every tick; send state on important changes + at a capped rate.

UI & Feedback

  • Reflect connection state:

void OnNetStatusConnected()    { ShowToast("Connected"); }
void OnNetStatusDisconnected() { ShowToast("Disconnected, retry?"); }
  • Reflect ownership on controllable objects (e.g., crosshair color, input lockouts).

  • Show area changes (“Entered: Dungeon 2F”).

Error Handling & Resilience

  • Treat missing objects and late messages as normal in a networked world. Always null-check lookups by network ID.

  • Handle duplicate instantiates defensively (ignore if already spawned).

  • On disconnect: - Clear transient UI and input. - Optionally offer reconnect flow; upon reconnection, rejoin areas and let the

    server resync world state.

Performance Tips (Client)

  • Avoid per-frame allocations in hot paths (recycle temp buffers, lists).

  • Cache component references (GetComponent once).

  • Pool common networked prefabs to avoid GC and load spikes.

  • Keep sequence channels separate for continuous vs discrete streams (Sync on channel 10 as provided; don’t mix with unrelated high-freq RPCs).

Security & Validation (Client Expectations)

  • Assume the server will validate everything; the client should not rely on being trusted.

  • Never try to send Sync for objects you don’t own.

  • Send minimal intent; let the server compute results (e.g., send “fire” input, not “I hit player 7 for 500 dmg”).

Common Patterns (Snippets)

Call an object-scoped RPC to others in area

// Broadcast a local emote without echoing to myself
mySync.CallRPC("PlayEmote", UCNetwork.MessageReceiver.OtherClientsInArea, "wave");

Send a private request to server

// Ask server to open a door I don't own
client.CallRPC("RequestOpenDoor", UCNetwork.MessageReceiver.ServerOnly, -1, doorNetId);

Join a dungeon and receive state snapshot

client.AddToArea(AreaIds.Dungeon2F); // server will instantiate existing floor objects for me

Summary

  • Use ClientNetwork.Instantiate and NetworkSync on all networked objects.

  • Only owners sync; replicas interpolate.

  • Prefer OtherClientsInArea / SingleClient over global RPCs.

  • Use LiteSync for cheap data; throttle transform Sync.

  • Join areas to receive relevant updates and automatic snapshots.

  • Design for latency (interpolation, optional prediction) and resilience (clean handling of disconnects, late joins, and lifecycle events).