Spawning networked prefabs, choosing a server topology, tuning bandwidth, and a step-by-step migration checklist.
Spawning networked objects
Same idea everywhere - instantiate, then tell the network about it - but Alteruna funnels it through a Spawner component rather than a static/manager call, and any peer may call it.
C#
var go = spawner.Spawn("Crate", pos, rot);
spawner.Despawn(go);
C#
var go = PhotonNetwork.Instantiate("Prefabs/Crate", pos, rot);
PhotonNetwork.Destroy(go);
C#
var go = Runner.Spawn(prefab, pos, rot);
Runner.Despawn(go);
C#
var go = Instantiate(prefab, pos, rot);
go.GetComponent<NetworkObject>().Spawn();
networkObject.Despawn();
C#
var go = Instantiate(prefab, pos, rot);
ServerManager.Spawn(go);
ServerManager.Despawn(go);
Server topology
This is the section that changes how you should think about everything above. Most of these frameworks assume a server tier exists before you write a line of gameplay code; Alteruna makes it a deployment choice you bolt on afterward.
Photon PUN 2 - every peer connects to Photon's cloud; one client is elected Master Client and gets first say on room-level decisions.
Photon Fusion - chosen per session via GameMode: Host (one peer is host+client) or Server (dedicated), both with a single state authority - or Shared, where every peer simulates and state authority can move between them, the one mode here that's actually peer-symmetric like Alteruna.
Netcode for GameObjects - one process is the host or server, chosen at StartHost()/StartServer(). All writes route through it by construction.
FishNet - built dedicated-server-first; host mode (server and client in one process) is supported but treated as the special case, not the default.
Alteruna - peer-symmetric by default, every peer runs identical code. Host() starts a local host for others to join directly; Connect() + CreateRoom() goes through Alteruna's relay with the lowest-index user auto-elected coordinator for tie-breaks; StartRemoteServer() spins up a headless dedicated instance when you need real authority.
Practical takeaway: write gameplay logic as if any peer could be the one running it, then add StartRemoteServer() later if you need server-side validation - do not reach for a "who's the server" check as your first instinct the way you would in host/dedicated Fusion, Netcode, or FishNet.
Bandwidth and update-rate control
Alteruna generalizes what Photon does per-PhotonTransformView into a five-tier LOD system that any Synchronizable or AttributesSync component can opt into: a far-away object can sit in the Low bucket at 10 Hz while a nearby one stays in Max at up to 120 Hz - no separate code path, same Commit() call either way. See User Guide Part 02 for how Buckets are configured on the Multiplayer Component. Every Sync, BroadcastRemoteMethod, and InvokeRemoteMethod call also takes a Reliability (Reliable/Unreliable) - the same lever as Photon's SendOptions.
Migration checklist
Work top to bottom - each step assumes the previous one is done.
Swap the connection bootstrap: ConnectUsingSettings/StartHost/StartConnection → multiplayer.Connect() (or Host() for a LAN-style session), then CreateRoom()/JoinRoom().
Replace PhotonView/NetworkObject identity with a CommunicationBridgeUID-derived base - AttributesSync for attribute-driven sync, Synchronizable for hand-written serialization, Avatar for the player object itself.
Convert [PunRPC]/[ServerRpc]/[ClientRpc]/[ObserversRpc] methods to [SynchronizableMethod], and their call sites to BroadcastRemoteMethod/InvokeRemoteMethod.
Convert OnPhotonSerializeView fields, NetworkVariable<T>, and SyncVar<T> to [SynchronizableField] - or a hand-written Synchronizable where you need byte-level control.
Replace ownership checks (IsMine/IsOwner/Owner) with avatar.IsMe/avatar.Possessor, re-anchoring any "am I allowed to act on this" logic around Avatar possession.
Wrap any mutation two peers could race on in TryLockResource/UnlockResource - this is new work, not a swap, since no implicit authority is doing it for you.
Swap PhotonNetwork.Instantiate/NetworkObject.Spawn/ServerManager.Spawn for a Spawner component's Spawn()/Despawn().
Decide topology deliberately: ship peer-symmetric (the default), self-host with Host(), or add StartRemoteServer() for authoritative validation.
Tune Bucket behaviors and Reliability per Synchronizable instead of relying on one global tick rate.
Mental-model shifts to watch for
There is no implicit "server" to trust. Any peer can call BroadcastRemoteMethod, InvokeRemoteMethod, or Spawn(). Nothing blocks two peers from doing so at once - that is precisely what TryLockResource is for.
Field sync is diffed on LateUpdate, not replicated every network tick. Unlike NetworkVariable's automatic dirty-flag replication, batch related [SynchronizableField] writes before the frame ends to avoid sending a torn intermediate state.
Ownership lives on the Avatar, not on every synced component. A Synchronizable/AttributesSync script has no owner field of its own - check the Avatar that logically controls it, or build a possession pattern for standalone objects.
[SynchronizableField]/[SynchronizableMethod] order is positional. Declaration order becomes wire index. Reordering members or inserting one in the middle shifts every index after it - treat it with the same discipline as Protobuf field numbers, and append rather than insert.
BroadcastRemoteMethod and InvokeRemoteMethod are not interchangeable. The former always includes the sender; the latter defaults to everyone but the sender, and accepts a specific user or list. See Part 03 for the full comparison.