Fusion 2.1 → Lattice feature parity & porting cookbook¶
This is the feature-by-feature companion to the two migration guides. Where
Migrating from Photon Fusion 2.1 explains the fusion2lattice
tool and Porting a Fusion KCC + physics game goes deep on movement,
physics, and collections, this page is the map: it walks the whole Photon Fusion 2.1 feature
surface a Unity game depends on and gives each feature an honest verdict plus a concrete
suggestion or pattern.
It also contains the full networked scene-loading recipe — the one piece Fusion gives you
(NetworkSceneManagerDefault) that Lattice deliberately does not, and that you build from a local
scene load plus a coordinating event handshake.
How to read the verdicts
Every feature carries one badge:
- ✅ CORE — Lattice already does this in the netcode core; you use it directly, nothing to port.
- 🧩 ADDON — shipped in the
com.lattice.netcodeUnity package as an optional addon (like theCharacterMotor); drop it in. - 📝 PORT — no drop-in equivalent, but there is a clean pattern (given here, with code) you implement once and reuse.
- ⚠️ GAP — not provided. Either a roadmap item, or a separate product (use a third party or build it). Stated honestly, with the interim workaround.
Accuracy note — what the reference core actually runs live
Lattice's public C ABI (lattice.h) exposes prediction/rollback, snapshot interpolation +
extrapolation, server-authoritative lag compensation, interest management (AoI), reliable
channels, congestion control, transport crypto, NAT/relay, and shared authority. In the reference
skeleton several of these advanced subsystems are exercised through the deterministic
lattice_test_* conformance hooks rather than running live inside the in-memory loopback pump
(the header's own "HONESTY OF FIELDS" note spells out exactly which live stats are real vs
zero-until-test-injected). A production core wires them onto the live runner. This page calls a
capability ✅ CORE when it is a first-class, documented part of the ABI — and says so plainly
where the reference's live wiring is thinner than the documented model.
At a glance¶
| # | Fusion feature | Verdict | Where / how |
|---|---|---|---|
| 1 | KCC character controller | 🧩 ADDON + 📝 auto-convert | CharacterMotor + KCC-compat; --kcc adapter |
| 2 | Scene management | 📝 PORT | Local load + event handshake (full recipe below) |
| 3 | Networked physics / transform | 🧩 ADDON | ReplicatedBody / ReplicatedTransform (authority-sim + replicate) |
| 4 | Object pooling | 🧩 ADDON | NetworkObjectPool over Spawn/Despawn |
| 5 | Networked Animator | 🧩 ADDON | NetworkAnimator ([Networked] animator params) |
| 6 | Input (INetworkInput) |
📝 PORT | InputRouter event pattern |
| 7 | Area of Interest | ✅ CORE | core interest management |
| 8 | Lag compensation / hitboxes | ✅ CORE | core history-rewind |
| 9 | Render interpolation + prediction/rollback | ✅ CORE | core prediction + interp/extrap |
| 10 | In-game statistics | 🧩 ADDON | NetworkDebugOverlay (F3) |
| 11 | Runner callbacks | ✅ CORE (mapped) | NetworkRunner C# events |
| 12 | Bootstrap quick-start | 📝 PORT | minimal NetworkRunner bootstrap |
| 13 | Transport / encryption / relay / NAT | ✅ CORE | Lattice's own transport stack |
| 14 | Matchmaking / lobby | ✅ Platform | lattice-director |
| 15 | Host migration | ⚠️ GAP / roadmap | shared authority + relay; interim pattern |
| 16 | Voice | ⚠️ Third party | Vivox / Dissonance / custom |
The full per-feature reasoning follows; a summary table and a "build-as-an-addon-next" shortlist close the page.
1. KCC character controller → 🧩 ADDON (+ 📝 auto-convert)¶
Fusion provides: the KCC addon — a KCC component, KCCData/KCCSettings, and a stack of
KCCProcessors (environment/gravity, movement, jump, crouch, ladder, auto-step…) that Fusion
predicts and resimulates for you.
Lattice verdict — 🧩 ADDON. The Unity package ships a CharacterMotor addon: a
rollback-ready motor (gravity, planar move, jump + coyote-time, crouch, ground snap) over a
replicated MotorState, with an ordered, customizable step pipeline (IMotorStep) that is the
direct analogue of the KCC processor stack. It is explicitly not netcode core — drop it in,
customize the step list, or replace it wholesale.
It also ships a KCC-compat layer so you can port an existing Fusion KCCProcessor with minimal
edits:
KccData— aref structview overMotorStateexposing the Fusion-KCCData-familiar member names (Velocity,IsGrounded,LookYaw,DeltaTime…), reading/writing through to the real replicated block.IKccProcessor— theKCCProcessoranalogue (void Process(ref KccData, in MotorInput, float dt, ICollisionProbe)); paste your FusionProcessbody in.KccProcessorStep : IMotorStep— runs anIKccProcessorinside the motor's deterministic pipeline.
// Port a Fusion KCCProcessor: copy the Process body, rewrite KCCData.X -> data.X.
sealed class GravityKccProcessor : IKccProcessor
{
readonly float _gravity;
public GravityKccProcessor(float gravity) { _gravity = gravity; }
public void Process(ref KccData data, in MotorInput input, float dt, ICollisionProbe probe)
{
if (data.IsGrounded) return;
var v = data.Velocity;
v.y -= _gravity * data.DeltaTime; // Fusion: KCCData.Velocity.y -= g * KCCData.DeltaTime
data.Velocity = v;
}
}
motor.Steps.Add(new KccProcessorStep(new GravityKccProcessor(24f)));
The fusion2lattice --kcc adapter converter is SHIPPED (and the default)
The CharacterMotor README describes the KCC-compat layer as "the literal target the fusion2lattice
KCC converter emits against" — and that converter now ships. fusion2lattice convert runs with
--kcc adapter by default: it detects each KCCProcessor / NetworkKCCProcessor, rewrites the
base type to IKccProcessor, rewrites the Process signature, and maps KCCData.X → data.X member
access onto the shipped KccData. It is a scaffold, not a finished port: the
kcc.Move(...) → ICollisionProbe collision/sweep wiring and any unmapped members are flagged as
Review TODOs (look for TODO[fusion2lattice]), and the movement feel is yours to tune. Pass
--kcc report to detect-only and port it all by hand instead.
See: the full design in Porting a Fusion KCC + physics game §1,
and Runtime/CharacterMotor/README.md in the Unity package for the KCC-compat recipe.
2. Scene management → 📝 FULL RECIPE¶
Fusion provides: NetworkSceneManagerDefault / INetworkSceneManager, with the host calling
Runner.LoadScene(...) and Fusion driving every peer to load the same scene, returning a
NetworkSceneAsyncOp you can await.
Lattice verdict — 📝 PORT. Lattice has no networked scene manager — and it does not need a new concept to give you one. Scene loading is a local engine operation; scene coordination is just an event. The recipe:
- The local load is plain Unity:
SceneManager.LoadSceneAsync(...). Its returnedAsyncOperationis yourNetworkSceneAsyncOp— you await/yield on it exactly the same way. - The authority broadcasts "load scene X" as a reliable custom event.
- Every peer (including the authority) loads locally, then reports READY back via another reliable event.
- The authority gates spawn/start until all expected peers report ready, then broadcasts "proceed". No object spawns into a half-loaded world.
This is a barrier handshake over two event ids. Here is the whole thing.
The shared pieces¶
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Lattice;
using Lattice.Interop;
namespace Game.Scenes
{
// Event ids for the scene handshake. Pick a band that doesn't collide with your gameplay events.
public static class SceneEvents
{
public const ushort LoadScene = 100; // authority -> all: "load this scene"
public const ushort SceneReady = 101; // peer -> authority: "I finished loading scene N"
public const ushort Proceed = 102; // authority -> all: "everyone's ready, go"
}
// Tiny POD payloads, packed with the BitWriter/BitReader helpers (no reflection on the wire).
public static class SceneCodec
{
public static byte[] LoadScene(int sceneBuildIndex, uint handshakeId)
{
var w = new BitWriter();
w.WriteInt(sceneBuildIndex);
w.WriteInt((int)handshakeId); // a generation id so a stale READY can't satisfy a new load
return w.ToArray();
}
public static (int sceneBuildIndex, uint handshakeId) ReadLoad(byte[] payload)
{
var r = new BitReader(payload);
int idx = r.ReadInt();
uint h = (uint)r.ReadInt();
return (idx, h);
}
public static byte[] Ready(uint handshakeId)
{
var w = new BitWriter();
w.WriteInt((int)handshakeId);
return w.ToArray();
}
public static uint ReadReady(byte[] payload) => (uint)new BitReader(payload).ReadInt();
}
}
The authority side — broadcast, collect READYs, gate, proceed¶
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Lattice;
using Lattice.Interop;
namespace Game.Scenes
{
// Drives a networked scene transition from the authority. Mirrors NetworkSceneManagerDefault:
// tell everyone to load, wait for every expected peer (and ourselves) to report ready, then go.
public sealed class AuthorityScenes : MonoBehaviour
{
[SerializeField] NetworkRunner runner;
uint _handshake; // current load generation
int _pendingScene;
bool _transitioning;
readonly HashSet<ulong> _ready = new(); // peers that reported ready for _handshake
readonly HashSet<ulong> _expected = new(); // who must report (incl. local authority)
void OnEnable() => runner.OnEvent += OnEvent;
void OnDisable() => runner.OnEvent -= OnEvent;
// Call this on the authority to start a networked scene change. Returns the AsyncOperation for
// the LOCAL load so a caller can await it too (this is your NetworkSceneAsyncOp).
public AsyncOperation LoadSceneNetworked(int sceneBuildIndex, IEnumerable<ulong> peers)
{
if (!runner.IsAuthority) { Debug.LogError("LoadSceneNetworked must run on the authority."); return null; }
_handshake++;
_pendingScene = sceneBuildIndex;
_transitioning = true;
_ready.Clear();
_expected.Clear();
_expected.Add(runner.LocalPlayer); // the host loads too
foreach (var p in peers) _expected.Add(p);
// 1. Tell every peer to load. Reliable: a dropped "load" would strand a client.
runner.SendEvent(SceneEvents.LoadScene,
LatticeEventTarget.All,
SceneCodec.LoadScene(sceneBuildIndex, _handshake),
reliable: true);
// 2. The authority loads locally too, and reports its own ready when done.
return BeginLocalLoad(sceneBuildIndex, _handshake);
}
AsyncOperation BeginLocalLoad(int sceneBuildIndex, uint handshake)
{
var op = SceneManager.LoadSceneAsync(sceneBuildIndex, LoadSceneMode.Single);
StartCoroutine(ReportWhenLoaded(op, handshake));
return op;
}
IEnumerator ReportWhenLoaded(AsyncOperation op, uint handshake)
{
yield return op; // <-- await NetworkSceneAsyncOp equivalent
MarkReady(runner.LocalPlayer, handshake); // count the authority's own load
}
void OnEvent(ushort eventId, ulong netid, ulong sender, byte[] payload)
{
if (eventId != SceneEvents.SceneReady || !_transitioning) return;
uint h = SceneCodec.ReadReady(payload);
MarkReady(sender, h); // 'sender' = the peer that finished loading
}
void MarkReady(ulong peer, uint handshake)
{
if (handshake != _handshake) return; // ignore READYs for a superseded load
if (!_expected.Contains(peer)) return;
_ready.Add(peer);
if (_ready.Count >= _expected.Count) // 3. barrier: everyone is in the new scene
{
_transitioning = false;
// 4. Release everyone, THEN spawn into the loaded world.
runner.SendEvent(SceneEvents.Proceed, LatticeEventTarget.All,
SceneCodec.LoadScene(_pendingScene, _handshake), reliable: true);
OnAllReady(_pendingScene);
}
}
// Spawn the gameplay objects here -- now that every peer is guaranteed to be in the scene.
void OnAllReady(int sceneBuildIndex)
{
// e.g. foreach (var p in _expected) SpawnPlayer(p);
Debug.Log($"[scenes] all {_expected.Count} peers ready in scene {sceneBuildIndex}; spawning.");
}
}
}
The client side — load on command, report READY, act on PROCEED¶
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using Lattice;
using Lattice.Interop;
namespace Game.Scenes
{
// The peer half. Listens for the authority's "load scene", loads locally, reports ready,
// and waits for "proceed" before enabling local gameplay.
public sealed class ClientScenes : MonoBehaviour
{
[SerializeField] NetworkRunner runner;
void OnEnable() => runner.OnEvent += OnEvent;
void OnDisable() => runner.OnEvent -= OnEvent;
void OnEvent(ushort eventId, ulong netid, ulong sender, byte[] payload)
{
switch (eventId)
{
case SceneEvents.LoadScene:
{
var (idx, handshake) = SceneCodec.ReadLoad(payload);
StartCoroutine(LoadThenReport(idx, handshake));
break;
}
case SceneEvents.Proceed:
// Authority says everyone's in. Safe to enable input / show the world / unpause.
OnProceed();
break;
}
}
IEnumerator LoadThenReport(int sceneBuildIndex, uint handshake)
{
var op = SceneManager.LoadSceneAsync(sceneBuildIndex, LoadSceneMode.Single);
yield return op; // local AsyncOperation == NetworkSceneAsyncOp
// Report READY to the authority (client events implicitly target the authority anyway,
// but Server is explicit + correct). Reliable so the barrier can never miss us.
runner.SendEvent(SceneEvents.SceneReady, LatticeEventTarget.Server,
SceneCodec.Ready(handshake), reliable: true);
}
void OnProceed()
{
Debug.Log("[scenes] proceed received; enabling local gameplay.");
}
}
}
Why this shape¶
- The barrier is the point. Object spawn waits until every peer is confirmed in the scene, so
a
Spawncan never land in a peer that is still on the loading screen. That is precisely the guaranteeNetworkSceneManagerDefaultgives you, made explicit. - The handshake id defends against overlap. If a second load starts before the first finishes, stale READYs (tagged with the old generation) are ignored — no false barrier release.
- Late joiners get the current scene the same way: on connect, the authority sends them a
LoadScenefor the active scene (a directedSendEventToPeer), and they run the identical load→ready path before being spawned in. - Additive scenes work too — pass
LoadSceneMode.Additiveand key the handshake on a scene set rather than a single build index.
This is the only Fusion feature on this page with no Lattice primitive at all
Everything else maps to an existing core capability, an addon, or a thin pattern. Scene coordination is genuinely yours to own — but it is two events and a counter, not a subsystem.
3. Physics — NetworkRigidbody2D/3D, RunnerSimulatePhysics, NetworkTransform → 🧩 SHIPPED ADDON¶
Fusion provides: networked rigidbodies and transforms, optionally a fully-networked lockstep
physics world (RunnerSimulatePhysics).
Lattice verdict — 🧩 ADDON (ReplicatedBody / ReplicatedTransform). The com.lattice.netcode
package ships ReplicatedTransform and ReplicatedBody
(Runtime/ReplicatedBody/) so you don't hand-roll this. Lattice does not attempt cross-platform
deterministic rigidbody dynamics; instead the addon implements authority-simulates + replicate: one
peer runs ordinary Unity physics in the fixed tick and replicates the result (position/rotation, and
on ReplicatedBody the linear/angular velocity) as [Networked] fields; everyone else is a kinematic
proxy that renders the converged transform (with configurable proxy smoothing). The sketch below shows
the shape of that addon.
[RequireComponent(typeof(Rigidbody))]
public sealed class ReplicatedBody : Lattice.NetworkBehaviour
{
[Networked(Interpolated = true)] public Vector3 Position { get; set; }
[Networked(Interpolated = true)] public Quaternion Rotation { get; set; }
Rigidbody _rb;
public override void Spawned()
{
_rb = GetComponent<Rigidbody>();
_rb.isKinematic = !HasStateAuthority; // only the authority's body is dynamic
}
public override void FixedUpdateNetwork()
{
if (!HasStateAuthority) return;
Position = _rb.position; Rotation = _rb.rotation; // publish Unity's integration result
}
public override void StateUpdated()
{
if (HasStateAuthority) return;
_rb.MovePosition(Position); _rb.MoveRotation(Rotation);
}
}
This same component covers vehicles (input is throttle/steer/brake) and draggables (add an
authority transfer on grab so whoever holds it simulates it). A custom
RunnerSimulatePhysics loop is rebuilt by calling PhysicsScene.Simulate(1f/60f) from
FixedUpdateNetwork on the authority.
See: the full treatment — vehicles, draggables, custom physics runner, and the honest "if you truly need lockstep" note — in Porting a Fusion KCC + physics game §2.
4. Object pooling (NetworkObjectProvider / pool) → 🧩 SHIPPED ADDON¶
Fusion provides: a NetworkObjectProvider / INetworkPrefabSource pool so spawns/despawns reuse
instances instead of instantiating.
Lattice verdict — 🧩 ADDON (NetworkObjectPool). The com.lattice.netcode package ships a
NetworkObjectPool component (Runtime/NetworkObjectPool/, with PoolPrefabConfig and an
IPoolable recycle hook). Lattice spawns a registered NetworkType + initial POD state, not a prefab,
so pooling is a thin layer over Spawn/Despawn: because the netid identifies the live object and
the engine view is created in the OnSpawned callback, the pool reuses the Unity GameObject (the
expensive part) keyed by type, while the netid churns normally. The sketch below is the shape of that
addon.
using System.Collections.Generic;
using UnityEngine;
using Lattice;
namespace Game.Pooling
{
// A GameObject pool keyed by NetworkType. Lattice's Spawn allocates a netid + POD block (cheap);
// the cost we're pooling is the Unity prefab Instantiate/Destroy. Bind a pooled view in OnSpawned,
// return it to the pool in OnDespawned.
public sealed class NetworkObjectPool : MonoBehaviour
{
[SerializeField] NetworkRunner runner;
// One prefab + free-list per registered type.
readonly Dictionary<uint, GameObject> _prefabByType = new();
readonly Dictionary<uint, Stack<GameObject>> _free = new();
readonly Dictionary<ulong, GameObject> _live = new(); // netid -> active view
public void Register(NetworkType type, GameObject prefab)
{
_prefabByType[type.Id] = prefab;
_free[type.Id] = new Stack<GameObject>();
}
void OnEnable() { runner.OnSpawned += Bind; runner.OnDespawned += Release; }
void OnDisable() { runner.OnSpawned -= Bind; runner.OnDespawned -= Release; }
// Authority-only convenience: spawn the object AND get a pooled view.
public NetworkObject Spawn<T>(NetworkType type, in T initial, ulong owner) where T : struct
=> runner.Spawn(type, in initial, owner); // Bind() fires from OnSpawned and rents a view
void Bind(NetworkObject obj)
{
if (!_prefabByType.TryGetValue(obj.TypeId, out var prefab)) return; // not a pooled type
var stack = _free[obj.TypeId];
GameObject go = stack.Count > 0 ? stack.Pop() : Instantiate(prefab);
go.SetActive(true);
_live[obj.NetId] = go;
// ... bind go's NetworkBehaviour(s) to obj here (Runner/Object), call Spawned(), etc.
}
void Release(ulong netid)
{
if (!_live.TryGetValue(netid, out var go)) return;
_live.Remove(netid);
go.SetActive(false); // recycle instead of Destroy
// find the type for this view and push it back:
// _free[typeId].Push(go);
}
}
}
The key insight: don't pool the netid or the POD block (the core owns those and they're cheap);
pool the GameObject behind it. Prewarm by Instantiate-ing N inactive copies into each free-list at
load.
5. Networked Animator (NetworkMecanimAnimator) → 🧩 SHIPPED ADDON¶
Fusion provides: NetworkMecanimAnimator — replicates Mecanim parameters and trigger state so
remote characters animate in sync.
Lattice verdict — 🧩 ADDON (NetworkAnimator). The com.lattice.netcode package ships a
NetworkAnimator component (Runtime/NetworkAnimator/): point it at an Animator, list the
floatParams / intParams / boolParams / triggerParams to sync, and it replicates them as
[Networked] fields — the authority samples the locally-driven Animator each tick; proxies apply the
replicated values in StateUpdated(). Triggers are sent as a reliable event so a one-shot that toggles
within a single snapshot interval is never lost. The sketch below shows the pattern it implements (the
shipped component generalizes it to named parameter lists rather than the hand-picked fields here).
// Illustrative shape (the shipped Lattice.NetworkAnimator generalizes this to named param lists):
public sealed class NetworkedAnimator : Lattice.NetworkBehaviour
{
[SerializeField] Animator _anim;
// Replicate only the params that must sync. Quantize floats with [Networked]'s QuantMin/Max/Precision.
[Networked] public float Speed { get; set; }
[Networked] public bool Grounded{ get; set; }
[Networked] public int StateHash { get; set; } // for full-state sync; or replicate triggers as a bitfield
static readonly int kSpeed = Animator.StringToHash("Speed");
static readonly int kGrounded = Animator.StringToHash("Grounded");
public override void FixedUpdateNetwork()
{
if (!HasStateAuthority) return; // authority samples the locally-driven animator
Speed = _anim.GetFloat(kSpeed);
Grounded = _anim.GetBool(kGrounded);
}
public override void StateUpdated()
{
if (HasStateAuthority) return; // proxies drive the animator from replicated state
_anim.SetFloat(kSpeed, Speed);
_anim.SetBool(kGrounded, Grounded);
}
}
For triggers (one-shot events like "fire", "hit"), don't replicate a bool — fire a reliable
custom event and call _anim.SetTrigger(...) in the handler, so a trigger that
toggles on and off within one snapshot interval is never lost.
This shipped as an addon
It is mechanical, every avatar game needs it, and it has exactly the same authority/proxy shape as
CharacterMotor — so it now ships as the NetworkAnimator addon (Runtime/NetworkAnimator/),
trigger-via-event helper included. Drop it on your character, fill in the parameter lists, done.
6. Input (INetworkInput / GetInput / INetworkInputProvider) → 📝 PORT¶
Fusion provides: an INetworkInput struct, INetworkInputProvider.OnInput to supply it, and
GetInput(out T) to read it back inside the prediction loop.
Lattice verdict — 📝 PORT (InputRouter). The shipped binding has no INetworkInput marker and
no typed GetInput<T> on NetworkBehaviour — input collection is a host concern. The migration
tool strips : INetworkInput and flags GetInput/INetworkInputProvider as Manual. The pattern:
- The owning client samples local devices each tick into a plain POD input struct.
- It ships that struct to the authority as a reliable custom event targeting
Server, the POD blitted into the payload. - The authority's
OnEventhandler stashes it keyed by netid; the simulation reads it for the current tick.
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Game.Movement
{
// The INetworkInputProvider replacement: a per-netid input mailbox the host fills (from OnEvent)
// and the simulation drains. On the owning client it's filled locally each tick before Tick().
public static class InputRouter
{
static readonly Dictionary<ulong, MotorInput> _latest = new();
public static void Set(ulong netid, in MotorInput i) => _latest[netid] = i;
public static bool TryGet(ulong netid, out MotorInput i) => _latest.TryGetValue(netid, out i);
public static byte[] Serialize(in MotorInput i)
{
int n = Marshal.SizeOf<MotorInput>(); var buf = new byte[n];
var h = GCHandle.Alloc(buf, GCHandleType.Pinned);
try { Marshal.StructureToPtr(i, h.AddrOfPinnedObject(), false); } finally { h.Free(); }
return buf;
}
public static MotorInput Deserialize(byte[] buf)
{
var h = GCHandle.Alloc(buf, GCHandleType.Pinned);
try { return Marshal.PtrToStructure<MotorInput>(h.AddrOfPinnedObject()); } finally { h.Free(); }
}
}
}
const ushort EV_INPUT = 1;
// Owner, each tick before runner.Tick(dt):
var mine = SampleLocalDevices();
InputRouter.Set(motor.Object.NetId, mine); // local prediction reads this
runner.SendObjectEvent(EV_INPUT, LatticeEventTarget.Server,
motor.Object.NetId, InputRouter.Serialize(mine));
// Authority, once at startup:
runner.OnEvent += (eventId, netid, sender, payload) =>
{
if (eventId == EV_INPUT) InputRouter.Set(netid, InputRouter.Deserialize(payload));
};
CharacterMotor.SetInput(in MotorInput) is the consumer side of exactly this router.
See: Porting a Fusion KCC + physics game §1 "Input flow".
7. Area of Interest / interest management → ✅ CORE¶
Fusion provides: AreaOfInterest, SetPlayerAlwaysInterested, per-player relevance config on
NetworkBehaviour.
Lattice verdict — ✅ CORE. Interest management lives in the netcode core: a spatial-grid AoI
system tracks each object's world position and, per connection, a focus point + radius, computing the
relevant set (the only objects that connection receives), with enter/exit transitions and scope
reduction. The live stat obj_aoi_relevant reports the relevant count per runner.
Action when porting: remove Fusion's per-object AoI configuration. Relevance is a core/director
concern, not gameplay code. In the shipped binding there is no per-object AoI tuning API — you rely
on core defaults; custom relevance policy is configured at the core/director layer, not on the
NetworkBehaviour. The migration report flags AreaOfInterest as Manual precisely so you delete the
calls rather than port them.
8. Lag compensation / hitboxes → ✅ CORE¶
Fusion provides: lag compensation via Runner.LagCompensation.Raycast/OverlapSphere against
rewound hitboxes, so a client's shot is validated against where targets were on the shooter's screen.
Lattice verdict — ✅ CORE. Server-authoritative lag compensation is a first-class part of the ABI:
the authority keeps per-tick position history and, on a client shot tagged with its view tick,
rewinds targets to that tick to validate the hit — then bounds the rewind to a history window
(rejecting view ticks too old or in the future). The whole path is integer-deterministic, so a verdict
is bit-identical on Linux and Windows. The anti-cheat layer's max_rewind_ticks / future_view_slack
bound rewind abuse.
Action when porting: map Runner.LagCompensation.Raycast(...) to the core's lag-compensated hit
validation; carry the shooter's view tick in your fire RPC/event so the authority rewinds to the
right instant.
9. Render interpolation + client prediction / rollback → ✅ CORE¶
Fusion provides: client-side prediction with resimulation/reconciliation, and render interpolation for remote objects.
Lattice verdict — ✅ CORE. Both are core:
- Prediction/rollback — the owning client predicts its object forward each tick, buffers input,
and on each authoritative snapshot reconciles (rolls back to the authoritative state and re-simulates
unconfirmed inputs). Your only obligation is that
FixedUpdateNetworkis a pure function of (state, input, fixedDt) — the core re-runs it for you. - Interpolation + extrapolation — remote (proxy) objects render in the past by an interpolation
delay, interpolating between buffered samples; when a snapshot is late, the core extrapolates
(dead-reckons) forward bounded by a horizon, then blends the error out when the real snapshot
arrives. Mark a
[Networked]fieldInterpolated = trueto opt a proxy field into it.
Action when porting: delete any hand-rolled reconciliation/interpolation loops. Keep your tick
deterministic (no Time.deltaTime, no Random, integer-tick gating) and let the core do it.
10. In-game statistics → 🧩 ADDON¶
Fusion provides: the FusionStats / NetworkRunnerStats in-game overlay (RTT, bandwidth, object
counts…).
Lattice verdict — 🧩 ADDON (NetworkDebugOverlay). The Unity package ships a toggleable IMGUI
overlay (default key F3) that reads the core's lattice_get_stats() snapshot once per frame and
draws grouped panels — Connection, Latency (with an RTT sparkline), Bandwidth (up/down sparklines),
Objects (total / owned / AoI-relevant), Replication (snapshot/delta bytes, reliable queue, fragments),
Prediction (rollbacks last/total, reconcile error), Security (anti-cheat score), and Build (module
version).
// Drop NetworkDebugOverlay on any GameObject (Lattice ▸ Network Debug Overlay); press F3 in play mode.
var overlay = gameObject.AddComponent<NetworkDebugOverlay>();
overlay.runner = myRunner; // or it auto-finds the first NetworkRunner
overlay.toggleKey = KeyCode.F3;
The same snapshot (NetworkRunner.GetStats() → LatticeStats) is available to build your own HUD.
Reference-skeleton honesty
On the reference runner, the live-sourced stats (connection, peers, object counts, snapshot/delta
bytes, bandwidth, anti-cheat score) are real; the link/reliability/prediction stats (RTT, jitter,
loss, reconcile error, queue depth, rollbacks) report 0 unless test-injected, because those
subsystems run through the lattice_test_* hooks rather than the loopback pump. A production core
populates them live. The overlay renders whatever the snapshot reports.
11. NetworkRunner callbacks (INetworkRunnerCallbacks) → ✅ CORE (mapped)¶
Fusion provides: the INetworkRunnerCallbacks interface you implement
(OnPlayerJoined/OnPlayerLeft/OnInput/OnConnectedToServer/OnObjectEnterAOI…).
Lattice verdict — ✅ CORE, mapped to C# events. Lattice uses events on NetworkRunner, not a
callbacks interface — subscribe to the ones you need. All fire synchronously inside Tick on the
calling thread.
Fusion INetworkRunnerCallbacks |
Lattice NetworkRunner event |
Notes |
|---|---|---|
OnConnectedToServer |
OnConnected |
client connected |
OnDisconnectedFromServer / OnShutdown |
OnDisconnected(int reason) |
reason code |
OnPlayerJoined / OnPlayerLeft |
(via OnSpawned/OnDespawned of the player object, or a join event) |
no separate player-join callback; model players as objects or a custom event |
OnObjectExitAOI / OnObjectEnterAOI |
OnSpawned / OnDespawned (relevance churn) + OnStateUpdated |
AoI is core; enter/exit surfaces as spawn/despawn of relevance |
| (object spawned) | OnSpawned(NetworkObject) |
|
| (object despawned) | OnDespawned(ulong netid) |
|
| (state replicated) | OnStateUpdated(NetworkObject) |
apply to Transform here |
OnInput |
(your InputRouter event — feature 6) |
input is a host concern |
| RPC received | OnRpc(netid, rpcId, payload) |
payload is a managed copy |
| (custom message) | OnEvent(eventId, netid, sender, payload) |
sender is your RpcInfo.Source |
| (shared authority moved) | OnAuthorityChanged(netid, newOwner, tick) |
fires on every peer |
| (logging) | OnLog(level, msg) |
runner.OnConnected += () => Debug.Log("connected");
runner.OnSpawned += obj => Debug.Log($"spawned {obj.NetId}");
runner.OnEvent += (id, netid, sender, payload) => HandleEvent(id, netid, sender, payload);
12. FusionBootstrap quick-start → 📝 PORT¶
Fusion provides: the FusionBootstrap / NetworkDebugStart component for one-click
host/client/server startup in the editor.
Lattice verdict — 📝 PORT (a few lines). Construct a NetworkRunner, register your types, start in
a mode. In the editor NetworkRunner is a MonoBehaviour that drives Tick from FixedUpdate;
headless, it's a plain disposable you tick yourself.
using UnityEngine;
using Lattice;
using Lattice.Interop;
public sealed class Bootstrap : MonoBehaviour
{
NetworkRunner _runner;
void Start()
{
_runner = gameObject.AddComponent<NetworkRunner>(); // editor: ticks from FixedUpdate
RegisterTypes(_runner);
if (IsHostCommandLine())
_runner.StartGame(LatticeGameMode.Host, port: 7777); // host = server + local player
else
{
_runner.StartGame(LatticeGameMode.Client);
_runner.Connect("127.0.0.1", 7777);
}
}
void RegisterTypes(NetworkRunner r) { /* r.RegisterType("Player", size, fields); ... */ }
static bool IsHostCommandLine() => System.Environment.CommandLine.Contains("-host");
}
For a headless dedicated server (no Unity), new NetworkRunner(tickRateHz: 60), then drive
runner.Tick(1.0/60) in your own loop.
See: Getting Started for the full path (register a game → integrate the SDK → pick a hosting model).
13. Transport / encryption / relay / NAT → ✅ CORE¶
Fusion provides: Photon's cloud transport, encryption, and relay.
Lattice verdict — ✅ CORE — and it's Lattice's own, not a third-party cloud. The core ships:
- a custom UDP transport with reliable channels (unreliable, unreliable-sequenced, reliable-unordered, reliable-ordered), congestion control + bandwidth budgeting, and fragmentation/reassembly;
- transport crypto — vendored X25519 + ChaCha20-Poly1305 (an authenticated, replay-protected encrypted session);
- NAT traversal + relay — STUN-style rendezvous/hole-punching with a TURN-style relay fallback
(the
lattice_relaybinary) for peers that can't punch through.
Action when porting: none — this is below your gameplay code. You select transport/relay at the runner/deployment layer; you don't replace a "Photon AppId".
14. Matchmaking / lobby (Photon cloud) → ✅ Platform (lattice-director)¶
Fusion provides: Photon cloud matchmaking, lobbies, room lists, session join.
Lattice verdict — ✅ Platform, via lattice-director. The control-plane director service
(:8444) provides matchmaking + session directory + fleet orchestration:
- Matchmaking (
POST /matchmake) — solo or party roster + criteria (region, mode, max size); assigns a roster to a suitable session (reusing an open one or creating one on the best instance), keeping parties together and respecting capacity. - Session directory (
POST /resolve,GET /sessions/{id}) — exchange an opaquesessionHandlefor{ endpoint, sessionToken }; the director mints a short-lived Ed25519 session token bound to the session, handle, endpoint, and player. - Fleet (
/fleet/register/heartbeat/deregister) — game-server instances advertise capacity/region/modes/load.
Player-facing endpoints require a lattice-auth Bearer token, verified offline.
Action when porting: replace Photon room APIs with director calls — POST /matchmake →
POST /resolve → connect your NetworkRunner to the returned endpoint with the session token.
See: Director (matchmaking/fleet).
15. Host migration → ⚠️ GAP / roadmap¶
Fusion provides: host migration — when the host drops, Fusion promotes a client and resumes the session with state intact.
Lattice verdict — ⚠️ GAP. Be honest: there is no drop-in host migration today. Lattice has the two
ingredients — shared/distributed authority (SHARED_HOST: authority over an object can be held
and transferred per-object) and relay (sessions can run through lattice_relay) — but it does
not ship the orchestration that detects host loss, elects a successor, and re-homes the whole
session's authoritative state onto it.
Interim pattern (what to do until/if it lands):
- Dedicated/managed authority instead of a player-host. Run the authority on your fleet or on
managed WASM hosting so "the host" isn't a player who can rage-quit. The director re-homes a session
to a healthy instance on failure; clients re-
resolveand reconnect. This sidesteps migration entirely and is the recommended production posture. - Shared-authority "soft" continuity for P2P. For peer-hosted sessions, model long-lived objects
as shared objects and, on host-loss detection (heartbeat timeout), have the surviving peers
elect a successor (lowest peer id) that calls
RequestAuthorityon the orphaned objects. State that was last-replicated survives; in-flight host-only state is lost. This is not transparent migration — accept some snap/loss at the seam.
Document this gap to your team up front; do not promise Fusion-equivalent transparent host migration.
16. Voice (Photon Voice) → ⚠️ Third party¶
Fusion provides (separately): Photon Voice — networked VoIP, a distinct product layered on Photon.
Lattice verdict — ⚠️ Not provided; it's a separate concern. Lattice is a state-replication netcode suite; it does not include a voice codec/mixer pipeline. Options:
- Third-party voice SDK — Vivox or Dissonance integrate at the Unity layer independently of your netcode and are the pragmatic choice.
- Roll your own over Lattice events — capture → encode (e.g. Opus) → ship as unreliable custom events → decode/play. Workable for push-to-talk/proximity chat, but you own jitter buffering, mixing, and codec licensing. Only do this if a dependency-free voice path is a hard requirement.
Treat voice as out of scope for the netcode port and pick it as a separate decision.
Summary table¶
| Feature | Fusion provides | Lattice verdict | Where / how |
|---|---|---|---|
| KCC character controller | KCC addon + KCCProcessor stack |
🧩 ADDON + 📝 auto-convert | CharacterMotor + KCC-compat (IKccProcessor/KccData/KccProcessorStep); --kcc adapter codemod (default) auto-converts |
| Scene management | NetworkSceneManagerDefault / NetworkSceneAsyncOp |
📝 PORT | Local LoadSceneAsync + load/ready/proceed event handshake (recipe) |
| Networked physics / transform | NetworkRigidbody2D/3D, NetworkTransform, RunnerSimulatePhysics |
🧩 ADDON | ReplicatedBody / ReplicatedTransform (authority-sim + replicate) |
| Object pooling | NetworkObjectProvider / pool |
🧩 ADDON | NetworkObjectPool (pool the GameObject, not the netid) |
| Networked Animator | NetworkMecanimAnimator |
🧩 ADDON | NetworkAnimator ([Networked] animator params + trigger events) |
| Input | INetworkInput / GetInput / INetworkInputProvider |
📝 PORT | InputRouter (event to authority, mailbox by netid) |
| Area of Interest | AreaOfInterest / SetPlayerAlwaysInterested |
✅ CORE | core interest management; remove Fusion AoI config |
| Lag compensation / hitboxes | Runner.LagCompensation |
✅ CORE | core history-rewind + view-tick window |
| Render interpolation + prediction/rollback | client predict/resim + interp | ✅ CORE | core prediction/rollback + interpolation/extrapolation |
| In-game statistics | FusionStats overlay |
🧩 ADDON | NetworkDebugOverlay (F3) over GetStats() |
| Runner callbacks | INetworkRunnerCallbacks |
✅ CORE (mapped) | NetworkRunner C# events (table above) |
| Bootstrap quick-start | FusionBootstrap |
📝 PORT | minimal NetworkRunner bootstrap |
| Transport / encryption / relay / NAT | Photon cloud | ✅ CORE | Lattice UDP transport + crypto + NAT/relay |
| Matchmaking / lobby | Photon cloud rooms | ✅ Platform | lattice-director (/matchmake, /resolve) |
| Host migration | host migration | ⚠️ GAP / roadmap | shared authority + relay; dedicated-authority interim |
| Voice | Photon Voice (separate product) | ⚠️ Third party | Vivox / Dissonance / custom over events |
Shipped addons¶
These started as 📝 PORT patterns and have since shipped as addons in the com.lattice.netcode
package (Runtime/), so you drop them in rather than re-write them:
- 🧩
CharacterMotor(feature 1) —Runtime/CharacterMotor/. Rollback-ready motor + KCC-compat layer (IKccProcessor/KccData/KccProcessorStep); the codemod's--kcc adaptermode auto-converts yourKCCProcessors into it. - 🧩
ReplicatedTransform/ReplicatedBody(feature 3) —Runtime/ReplicatedBody/. The authority-sim + replicate answer for everyNetworkRigidbody/NetworkTransform, plus vehicles and draggables (with authority transfer). Makes the "kinematic-on-proxy" rule impossible to get wrong. - 🧩
NetworkAnimator(feature 5) —Runtime/NetworkAnimator/. Replicated Mecanim params with the same authority/proxy shape asCharacterMotor; trigger-via-event helper bundled. - 🧩
NetworkObjectPool(feature 4) —Runtime/NetworkObjectPool/. Pools the Unity GameObject (the expensive part) behindSpawn/Despawn, keyed byNetworkType, withPoolPrefabConfigand anIPoolablerecycle hook. - 🧩
NetworkDebugOverlay(feature 10) — the F3 stats overlay.
Recommended to build as an addon next¶
The remaining 📝 PORT items are the candidates for promotion to shipped addons, because they're mechanical, near-universal, and have a stable shape. In priority order, with the reasoning:
-
InputRouter+ a tiny input helper (from feature 6). Highest payoff. Input routing is Manual for every Fusion port (noGetInput/INetworkInputin the binding), the code is identical each time (blit POD → event → mailbox by netid), andCharacterMotor.SetInputalready assumes its existence. Shipping it closes the loop on the motor addon and removes a guaranteed-required manual step. Small surface, high frequency. -
NetworkSceneManagerhelper (from feature 2). Second. The scene handshake in this page is the same every time (load → ready barrier → proceed, with a generation id and late-joiner handling). It's slightly more opinionated than the others (additive vs single, late-join policy), so ship it as a configurable helper rather than a one-liner — but it removes the one feature with no Lattice primitive at all, which is where porters get most stuck.
Not addon candidates: the ✅ CORE features (already in the core — promoting them would be
duplication), and the ⚠️ GAP features (host migration is an orchestration problem for the
director/fleet layer, not a Unity addon; voice is a separate product). The shipped --kcc adapter
KCC conversion is a tool capability (fusion2lattice), not a runtime addon — it emits against the
CharacterMotor addon above.
See also¶
- Migrating from Photon Fusion 2.1 — the
fusion2latticetool, the full concept-mapping table, and the four big rocks. - Porting a Fusion KCC + physics game — deep dives on movement, physics,
and collections (the §1 motor, §2
ReplicatedBody, §3 collection patterns this page references). - The
lattice-gamedevClaude Code skill — teaches an assistant the real spawn / RPC / input / authority idioms used throughout this page. - API reference: Runner · RPC · Custom events · Authority · Callbacks & enums · BitWriter / BitReader
- Director (matchmaking/fleet) · Getting Started