Skip to content

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.netcode Unity package as an optional addon (like the CharacterMotor); 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 — a ref struct view over MotorState exposing the Fusion-KCCData-familiar member names (Velocity, IsGrounded, LookYaw, DeltaTime…), reading/writing through to the real replicated block.
  • IKccProcessor — the KCCProcessor analogue (void Process(ref KccData, in MotorInput, float dt, ICollisionProbe)); paste your Fusion Process body in.
  • KccProcessorStep : IMotorStep — runs an IKccProcessor inside 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:

  1. The local load is plain Unity: SceneManager.LoadSceneAsync(...). Its returned AsyncOperation is your NetworkSceneAsyncOp — you await/yield on it exactly the same way.
  2. The authority broadcasts "load scene X" as a reliable custom event.
  3. Every peer (including the authority) loads locally, then reports READY back via another reliable event.
  4. 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 Spawn can never land in a peer that is still on the loading screen. That is precisely the guarantee NetworkSceneManagerDefault gives 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 LoadScene for the active scene (a directed SendEventToPeer), and they run the identical load→ready path before being spawned in.
  • Additive scenes work too — pass LoadSceneMode.Additive and 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:

  1. The owning client samples local devices each tick into a plain POD input struct.
  2. It ships that struct to the authority as a reliable custom event targeting Server, the POD blitted into the payload.
  3. The authority's OnEvent handler 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 FixedUpdateNetwork is 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] field Interpolated = true to 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_relay binary) 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 opaque sessionHandle for { 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 /matchmakePOST /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 ingredientsshared/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-resolve and 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 RequestAuthority on 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 adapter mode auto-converts your KCCProcessors into it.
  • 🧩 ReplicatedTransform / ReplicatedBody (feature 3) — Runtime/ReplicatedBody/. The authority-sim + replicate answer for every NetworkRigidbody/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 as CharacterMotor; trigger-via-event helper bundled.
  • 🧩 NetworkObjectPool (feature 4) — Runtime/NetworkObjectPool/. Pools the Unity GameObject (the expensive part) behind Spawn/Despawn, keyed by NetworkType, with PoolPrefabConfig and an IPoolable recycle hook.
  • 🧩 NetworkDebugOverlay (feature 10) — the F3 stats overlay.

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:

  1. InputRouter + a tiny input helper (from feature 6). Highest payoff. Input routing is Manual for every Fusion port (no GetInput/INetworkInput in the binding), the code is identical each time (blit POD → event → mailbox by netid), and CharacterMotor.SetInput already assumes its existence. Shipping it closes the loop on the motor addon and removes a guaranteed-required manual step. Small surface, high frequency.

  2. NetworkSceneManager helper (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