05 — Networked objects¶
A networked object is a piece of replicated state the authority owns and every peer sees. You:
- register a type (its state layout + which fields replicate),
- spawn an instance on the authority, and
- read replicated changes on every peer via
OnSpawned/OnStateUpdated.
The tic-tac-toe sample has exactly one networked object — the board — so it's a clean, complete example.
Verified against: samples/unity-tictactoe/Assets/TicTacToe/Scripts/TicTacToeProtocol.cs,
TicTacToeSession.cs, and bindings/lattice-unity/com.lattice.netcode/Runtime/NetworkObject.cs,
NetworkTypeSchema.cs.
The replicated state block¶
Replicated state is a blittable struct with [StructLayout(LayoutKind.Sequential)]. It maps
1:1 onto the core's state block, which the binding reads and writes in place. The board is nine
cells + whose turn + the result:
[StructLayout(LayoutKind.Sequential)]
public struct BoardState
{
public int cell0, cell1, cell2;
public int cell3, cell4, cell5;
public int cell6, cell7, cell8;
public int turn; // Mark whose move it is (X=1)
public int result; // GameResult
}
Verified against: TicTacToeProtocol.cs:19-27.
Field width gotcha: RANGED_INT fields must be 4-byte int, not byte
Each board field replicates as a RANGED_INT. The core reads and writes a RANGED_INT as a
4-byte int at the field's offset, so the struct fields must be declared int (4-byte,
aligned at offsets 0, 4, 8, …). A byte-backed layout silently corrupts replication because the
4-byte writes overlap adjacent fields. On the wire each value still costs only ~2 bits — the
int storage is purely to match the ABI's field-access width. This was caught and fixed during
the sample's headless bring-up.
Verified against: TicTacToeProtocol.cs:11-18 and samples/unity-tictactoe/README.md
("Note on the [Networked] field width").
Registering the type¶
You describe each replicated leaf field with a FieldSpec, then call RegisterType. Offsets
come from Marshal.OffsetOf, so the schema matches the struct layout exactly.
public NetworkType RegisterTypes()
{
var fields = new List<FieldSpec>(11);
// Nine cells, each a ranged int 0..2 (Empty/X/O). Field order defines the MarkDirty index.
for (int i = 0; i < 9; i++)
fields.Add(FieldSpec.Of<BoardState>($"cell{i}", LatticeFieldKind.RangedInt, $"cell{i}")
.WithRange(0, 2));
fields.Add(FieldSpec.Of<BoardState>("turn", LatticeFieldKind.RangedInt, nameof(BoardState.turn)).WithRange(0, 2));
fields.Add(FieldSpec.Of<BoardState>("result", LatticeFieldKind.RangedInt, nameof(BoardState.result)).WithRange(0, 3));
_boardType = _runner.RegisterType(
"TicTacToeBoard",
(uint)Marshal.SizeOf<BoardState>(),
fields);
return _boardType;
}
Verified against: TicTacToeSession.cs:66-84 (adapted for brevity; the sample lists the cell names
explicitly).
FieldSpec¶
FieldSpec.Of<TBlock>(name, kind, fieldName) captures the field's offset from the struct; fluent
With… methods add per-kind detail:
| Method | For |
|---|---|
.WithRange(min, max) |
RangedInt |
.WithQuant(min, max, precision) |
CompressedFloat / Vector* |
.WithQuatBits(bits) |
Quaternion (smallest-three) |
.WithCapacity(bytes) |
String / Bytes |
.WithStructType(id) |
nested Struct |
Verified against: NetworkTypeSchema.cs:19-51.
The field kinds¶
LatticeFieldKind: Bool, Int32, Int64, Float, Double, RangedInt, CompressedFloat,
Vector2, Vector3, Vector4, Quaternion, String, Bytes, Struct.
Verified against: LatticeNative.cs:47-63.
What RegisterType returns¶
A NetworkType carries its Id, its ContentHash, and its Key. Both must match across
peers, so the two ends agree on the wire format.
public sealed class NetworkType : IDisposable
{
public uint Id { get; }
public uint ContentHash { get; }
public string Key { get; }
}
Verified against: NetworkTypeSchema.cs:59-63.
Register the same types, in the same order, on every peer — before connecting
Type ids and content hashes are derived from the registration. If host and client disagree,
replication breaks. The sample calls RegisterTypes() on both peers before StartGame/Connect;
the headless harness asserts HostBoardType.Id == ClientBoardType.Id and the content hashes
match.
Verified against: samples/unity-tictactoe/headless/MatchHarness.cs:66-68;
headless/Program.cs:61-63.
Spawning (authority only)¶
Only an authority (Server / Host / SharedHost) may spawn. Spawn<T> takes the type, an
initial state struct, and an owner id, and returns the managed NetworkObject:
public NetworkObject HostStartMatch()
{
if (!IsHost) throw new InvalidOperationException("Only the authority spawns the board.");
var init = new BoardState { turn = (byte)Mark.X, result = (byte)GameResult.InProgress };
_board = _runner.Spawn(_boardType, init, owner: _runner.LocalPlayer);
// …
return _board;
}
Verified against: TicTacToeSession.cs:90-99.
public NetworkObject Spawn<T>(NetworkType type, in T initialState, ulong owner) where T : struct;
Returns null-free: a non-authority spawn throws LatticeException(ErrNotAuthority).
Verified against: NetworkRunner.cs:277-299.
Reading and writing state¶
A NetworkObject exposes the whole state block as a struct:
BoardState s = obj.ReadState<BoardState>(); // read the current block
// … mutate s on the authority …
obj.WriteState(s); // write it back
obj.MarkDirty(BoardField.Turn); // flag a changed field for replication
Verified against: NetworkObject.cs:100-124.
MarkDirty(fieldIndex) tells the authority to replicate that field on the next tick. The field
index is the field's position in the registration order. The sample keeps them as named constants:
public static class BoardField
{
public const uint Cell0 = 0, /* … */ Cell8 = 8, Turn = 9, Result = 10;
public static uint ForCell(int cell) => (uint)cell;
}
Verified against: TicTacToeProtocol.cs:57-64; used at TicTacToeSession.cs:159-161.
Only the authority writes
WriteState + MarkDirty are authority-side operations. Clients read replicated state in
their callbacks and never write the block directly — they ask the authority to change things via
RPCs (chapter 06) or events (chapter 07).
Reacting to replication¶
Every peer learns about objects through the runner's callbacks. The sample subscribes in its session constructor:
_runner.OnSpawned += HandleSpawned;
_runner.OnStateUpdated += HandleStateUpdated;
private void HandleSpawned(NetworkObject obj)
{
if (_boardType == null || obj.TypeId != _boardType.Id) return; // filter by type
_board = obj;
OnBoardChanged?.Invoke(obj.ReadState<BoardState>());
}
private void HandleStateUpdated(NetworkObject obj)
{
if (obj == null || _board == null || obj.NetId != _board.NetId) return;
OnBoardChanged?.Invoke(obj.ReadState<BoardState>());
}
Verified against: TicTacToeSession.cs:56-60, TicTacToeSession.cs:180-191.
OnSpawnedfires on a peer when an object first replicates in. Filter byTypeId(and, if you have many, track byNetId).OnStateUpdatedfires when a replicated field changed. Re-read the block and refresh your view.
NetworkObject identity/authority members you'll use:
| Member | Meaning |
|---|---|
NetId / Id |
The replicated id |
TypeId |
Which registered type |
Owner / InputAuthority |
The participant that owns it |
HasInputAuthority |
Owner == runner.LocalPlayer |
HasStateAuthority |
The local runner is server/host |
IsValid() |
It carries a netid (spawned) |
Verified against: NetworkObject.cs:26-78.
Finding objects¶
The runner keeps an object table. Objects is the live dictionary; Find(netid) (and the
Fusion-compat FindObject / TryFindObject) look one up.
Verified against: NetworkRunner.cs:101, NetworkRunner.cs:306,
NetworkRunner.Simulation.cs:125-132.
Despawning¶
Authority-only. Despawn(netid) or Despawn(obj) removes it; every peer sees OnDespawned(netid).
Verified against: NetworkRunner.cs:301-304.
A note on the [Networked] attribute¶
The binding ships [Networked], [Rpc], and [OnChanged] attributes. In a full Unity install a
Roslyn source generator consumes [Networked] auto-properties and emits the schema for you, so
you'd write:
public sealed class Board : NetworkBehaviour
{
[Networked(RangedMin = 0, RangedMax = 2)] public int Turn { get; set; }
}
Verified against:
bindings/lattice-unity/com.lattice.netcode/Runtime/Attributes.cs:18-41 (NetworkedAttribute).
This tutorial teaches the runtime RegisterType path, which is what the sample uses
The [Networked] source generator is an editor/compile-time artefact; the headless build and
the tic-tac-toe sample exercise the runtime schema path (FieldSpec + RegisterType) the
generator ultimately targets. Because we can't run the Unity editor here, everything in this
tutorial is grounded in the runtime path — which is fully verified. If you use the attribute form
in a real project, the underlying registration is identical.
Verified against: Attributes.cs:1-8 (generator is "an editor-time build artefact");
bindings/lattice-unity/README.md ("Needs the Unity editor — [Networked] source generator").
Next: 06 — RPCs.