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
ClientUnity project/scene.Add a GameObject with
ClientNetworkand 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
Destroydirectly 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
ServerOnlyand includes a target client ID as an argument; the server forwards withSingleClient).
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 (
GetComponentonce).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.Instantiateand 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).