Skip to main content

Dynamic Addressables Network Prefabs Sample

The DynamicAddressablesNetworkPrefabs Sample showcases the available ways you can use dynamic Prefabs to dynamically load network Prefabs at runtime, either pre-connection or post-connection. Doing so allows you to add new spawnable NetworkObject Prefabs to Netcode for GameObjects.

This sample covers a variety of possible ways you can use dynamically loaded network Prefabs:

  • Pre-loading: Add dynamic Prefabs to all peers before starting the connection.
  • Connection approval: Apply server-side validation to late-joining clients after loading dynamic Prefabs.
  • Server-authoritative pre-load all Prefabs asynchronously: Simplify server spawn management by having the server send a load request to all clients to load a set of network Prefabs.
  • Server-authoritative try spawn synchronously: Ensure all connected clients individually load a network Prefab before spawning it on the server.
  • Server-authoritative network visibility spawning: Spawn a network Prefab server-side as soon as it has loaded it locally, and only change the visibility of the spawned NetworkObject when each client loads the Prefab load.

There's also the APIPlayground, which serves as an API playground that implements all post-connection uses together.

note

Note: This sample leverages Addressables to load dynamic Prefabs.

Scene 00_Preloading Dynamic Prefabs

The 00_Preloading Dynamic Prefabs scene is the simplest implementation of a dynamic Prefab. It instructs all game instances to load a network Prefab (it can be just one, it can also be a set of network Prefabs) and inject them to a NetworkManager's NetworkPrefabs list before starting the server. What's important is that it doesn't matter where the Prefab comes from. It can be a simple Prefab or it can be an Addressable - it's all the same.

This is the lesser intrusive option for your development, as you don't have any extra spawning and Addressables management to perform later in your game.

Here, the sample serializes the AssetReferenceGameObject to this class, but ideally you want to authenticate players when your game starts up and have them fetch network Prefabs from services such as UGS (see Remote Config). You should also note that this is a technique that can serve to decrease the install size of your application, since you'd be streaming in networked game assets dynamically.

The entirety of this use-case is performed pre-connection time, and all game instances would execute this bit of code inside the Preloading.cs Start() method:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading/Preloading.cs
async void Start()
{
await PreloadDynamicPlayerPrefab();
//after we've waited for the prefabs to load - we can start the host or the client
}

The logic of this method invoked on Start() is defined below:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading/Preloading.cs
// It's important to note that this isn't limited to PlayerPrefab, despite the method name you can add any
// prefab to the list of prefabs that will be spawned.
async Task PreloadDynamicPlayerPrefab()
{
Debug.Log($"Started to load addressable with GUID: {m_DynamicPrefabReference.AssetGUID}");
var op = Addressables.LoadAssetAsync<GameObject>(m_DynamicPrefabReference);
var prefab = await op.Task;
Addressables.Release(op);

//it's important to actually add the player prefab to the list of network prefabs - it doesn't happen
//automatically
m_NetworkManager.AddNetworkPrefab(prefab);
Debug.Log($"Loaded prefab has been assigned to NetworkManager's PlayerPrefab");

// at this point we can easily change the PlayerPrefab
m_NetworkManager.NetworkConfig.PlayerPrefab = prefab;

// Forcing all game instances to load a set of network prefabs and having each game instance inject network
// prefabs to NetworkManager's NetworkPrefabs list pre-connection time guarantees that all players will have
// matching NetworkConfigs. This is why NetworkManager.ForceSamePrefabs is set to true. We let Netcode for
// GameObjects validate the matching NetworkConfigs between clients and the server. If this is set to false
// on the server, clients may join with a mismatching NetworkPrefabs list from the server.
m_NetworkManager.NetworkConfig.ForceSamePrefabs = true;
}

First, the sample waits for the dynamic Prefab asset to load from its address and into memory. After the Prefab is ready, the game instance adds it to NetworkManger's list of NetworkPrefabs, then it marks the NetworkObject as a NetworkManager's PlayerPrefab.

Lastly, the sample forces the NetworkManager to check for matching NetworkConfigs between a client and the server by setting ForceSamePrefabs to true. If the server detects a mismatch between the server's and client's NetworkManager's NetworkPrefabs list when a client is trying to connect, it denies the connection automatically.

Scene 01_Connection Approval Required For Late Joining

The 01_Connection Approval Required For Late Joining scene uses a class that walks through what a server needs to approve a client when dynamically loading network Prefabs. This is another simple example; it's just the implementation of the connection approval callback, which is an optional feature from Netcode for GameObjects. To enable it, enable the Connection Approval option on the NetworkManager in your scene. This example enables connection approval functionality to support late-joining clients, and it works best in combination with the techniques from the other example scenes. The other example scenes don't allow for reconciliation after the server loads a Prefab dynamically, except for scene 05_API Playground Showcasing All Post-Connection Uses, where all post-connection use-cases are integrated in one scene.

To begin, this section walks you through what the client sends to the server when trying to connect. This is done inside of ClientConnectingState.cs' StartClient() method:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/Shared/ConnectionStates/ClientConnectingState.cs
void StartClient()
{
var transport = m_ConnectionManager.m_NetworkManager.GetComponent<UnityTransport>();
transport.SetConnectionData(m_ConnectionManager.m_ConnectAddress, m_ConnectionManager.m_Port);
m_ConnectionManager.m_NetworkManager.NetworkConfig.ConnectionData =
DynamicPrefabLoadingUtilities.GenerateRequestPayload();
m_ConnectionManager.m_NetworkManager.StartClient();
}

Before invoking NetworkManager.StartClient(), the client defines the ConnectionData to send along with the connection request, gathered from DynamicPrefabLoadingUtilities.cs:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/Shared/DynamicPrefabLoadingUtilities.cs
public static byte[] GenerateRequestPayload()
{
var payload = JsonUtility.ToJson(new ConnectionPayload()
{
hashOfDynamicPrefabGUIDs = HashOfDynamicPrefabGUIDs
});

return System.Text.Encoding.UTF8.GetBytes(payload);
}

For simplicity's sake, this method generates a hash that uniquely describes the dynamic Prefabs that a client has loaded. This hash is what the server uses as validation to decide whether a client has approval for a connection.

Next, take a look at how the server handles incoming ConnectionData. The sample listens for the NetworkManager's ConnectionApprovalCallback and defines the behavior to invoke inside of ConnectionApproval.cs. This is done on Start():

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection%20Approval/ConnectionApproval.cs
void Start()
{
DynamicPrefabLoadingUtilities.Init(m_NetworkManager);

// In the use-cases where connection approval is implemented, the server can begin to validate a user's
// connection payload, and either approve or deny connection to the joining client.
m_NetworkManager.NetworkConfig.ConnectionApproval = true;

// Here, we keep ForceSamePrefabs disabled. This will allow us to dynamically add network prefabs to Netcode
// for GameObject after establishing a connection. In this implementation of the connection approval
// callback, the server validates the client's connection payload based on the hash of their dynamic prefabs
// loaded, and either approves or denies connection to the joining client. If a client is denied connection,
// the server provides a disconnection payload through NetworkManager's DisconnectReason, so that a
// late-joining client can load dynamic prefabs locally and reattempt connection.
m_NetworkManager.NetworkConfig.ForceSamePrefabs = false;
m_NetworkManager.ConnectionApprovalCallback += ConnectionApprovalCallback;

// to force a simple connection approval on all joining clients, the server will load a dynamic prefab as
// soon as the server is started
// for more complex use-cases where the server must wait for all connected clients to load the same network
// prefab, see the other use-cases inside this sample
m_NetworkManager.OnServerStarted += LoadAPrefab;
}

Unlike the earlier use-case, ForceSamePrefab is to false; this allows you to add NetworkObject Prefabs to a NetworkManager's NetworkPrefabs list after establishing a connection on both the server and clients. Before walking through what this class' connection approval callback looks like, it's worth noting here that the sample forces a mismatch of NetworkPrefabs between server and clients, because as soon as the server starts, it loads a dynamic Prefab and registers it to the server's NetworkPrefabs list:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection%20Approval/ConnectionApproval.cs
async void LoadAPrefab()
{
var assetGuid = new AddressableGUID() { Value = m_AssetReferenceGameObject.AssetGUID };

// server is starting to load a prefab, update UI
m_InGameUI.ClientLoadedPrefabStatusChanged(NetworkManager.ServerClientId,
assetGuid.GetHashCode(),
"Undefined",
InGameUI.LoadStatus.Loading);

await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, 0);

// server loaded a prefab, update UI with the loaded asset's name
DynamicPrefabLoadingUtilities.TryGetLoadedGameObjectFromGuid(assetGuid, out var loadedGameObject);
m_InGameUI.ClientLoadedPrefabStatusChanged(NetworkManager.ServerClientId, assetGuid.GetHashCode(), loadedGameObject.Result.name, InGameUI.LoadStatus.Loaded);
}

This section walks you through the connection approval defined in this class in steps. First, its worth noting that the connection approval invokes on the host. As a result, you allow the host to establish a connection:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection%20Approval/ConnectionApproval.cs
if (clientId == m_NetworkManager.LocalClientId)
{
//allow the host to connect
Approve();
return;
}

Next, a the sample introduces a few more validation steps. First, this sample only allows four connected clients. If the server detects any more connections past that limit, it denies the requesting client a connection. Secondly, if the ConnectionData is above a certain size threshold, it denies it outright.

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection%20Approval/ConnectionApproval.cs
// A sample-specific denial on clients after k_MaxConnectedClientCount clients have been connected
if (m_NetworkManager.ConnectedClientsList.Count >= k_MaxConnectedClientCount)
{
ImmediateDeny();
return;
}

if (connectionData.Length > k_MaxConnectPayload)
{
// If connectionData is too big, deny immediately to avoid wasting time on the server. This is intended as
// a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage.
ImmediateDeny();
return;
}

A trivial approval for an incoming connection request occurs when the server hasn't yet loaded any dynamic Prefabs. Assuming the client hasn't injected any Prefabs outside of the DynamicPrefabLoadingUtilities system, the client is approved for a connection:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection%20Approval/ConnectionApproval.cs
if (DynamicPrefabLoadingUtilities.LoadedPrefabCount == 0)
{
//immediately approve the connection if we haven't loaded any prefabs yet
Approve();
return;
}

Another case for connection approval is when the requesting client's Prefab hash is identical to that of the server:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection%20Approval/ConnectionApproval.cs
        var payload = System.Text.Encoding.UTF8.GetString(connectionData);
var connectionPayload = JsonUtility.FromJson<ConnectionPayload>(payload); // https://docs.unity3d.com/2020.2/Documentation/Manual/JSONSerialization.html

int clientPrefabHash = connectionPayload.hashOfDynamicPrefabGUIDs;
int serverPrefabHash = DynamicPrefabLoadingUtilities.HashOfDynamicPrefabGUIDs;

//if the client has the same prefabs as the server - approve the connection
if (clientPrefabHash == serverPrefabHash)
{
Approve();

DynamicPrefabLoadingUtilities.RecordThatClientHasLoadedAllPrefabs(clientId);

return;
}

If the requesting client has a mismatching Prefab hash, it means that the client hasn't yet loaded the appropriate dynamic Prefabs. When this occurs, the sample leverages the NetworkManager's ConnectionApprovalResponse's Reason string field, and populates it with a payload containing the GUIDs of the dynamic Prefabs the client should load locally before re-attempting connection:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection%20Approval/ConnectionApproval.cs
DynamicPrefabLoadingUtilities.RefreshLoadedPrefabGuids();

response.Reason = DynamicPrefabLoadingUtilities.GenerateDisconnectionPayload();

ImmediateDeny();

A client attempting to connect receives a callback from Netcode that it has been disconnected inside of ClientConnectingState:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/Shared/ConnectionStates/ClientConnectingState.cs
public override void OnClientDisconnect(ulong _)
{
// client ID is for sure ours here
var disconnectReason = m_ConnectionManager.m_NetworkManager.DisconnectReason;
if (string.IsNullOrEmpty(disconnectReason))
{
m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline);
}
else
{
var disconnectionPayload = JsonUtility.FromJson<DisconnectionPayload>(disconnectReason);

switch (disconnectionPayload.reason)
{
case DisconnectReason.Undefined:
Debug.Log("Disconnect reason is undefined");
m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline);
break;
case DisconnectReason.ClientNeedsToPreload:
{
Debug.Log("Client needs to preload");
m_ConnectionManager.m_ClientPreloading.disconnectionPayload = disconnectionPayload;
m_ConnectionManager.ChangeState(m_ConnectionManager.m_ClientPreloading);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}

If the parsed DisconnectReason string is valid, and the parsed reason is of type DisconnectReason.ClientNeedsToPreload, the client will be instructed to load the Prefabs by their GUID. This is done inside of ClientPreloadingState.cs:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/Shared/ConnectionStates/ClientPreloadingState.cs
async void HandleDisconnectReason()
{
var guids = disconnectionPayload.guids.Select(item => new AddressableGUID() { Value = item }).ToArray();

await DynamicPrefabLoadingUtilities.LoadDynamicPrefabs(guids);
Debug.Log("Restarting client");
m_ConnectionManager.ChangeState(m_ConnectionManager.m_ClientConnecting);
}

After the client loads the necessary Prefabs, it once again transitions to the ClientConnectingState, and retries the connection to the server, sending along a new Prefab hash.

note

Note: This sample leveraged a state machine to handle connection management. A state machine isn't by any means necessary for connection approvals to work—it serves to compartmentalize connection logic per state, and to be a debug-friendly tool to step through connection steps.

Scene 02_Server Authoritative Load All Prefabs Asynchronously

The 02_Server Authoritative Load All Prefabs Asynchronously scene is a simple scenario where the server notifies all clients to pre-load a collection of network Prefabs. The server won't invoke a spawn in this scenario; instead, it incrementally loads each dynamic Prefab, one at a time.

This technique might benefit a scenario where, after all clients connect, the host arrives at a point in the game where it expects that will need to load Prefabs soon. In such a case, the server instructs all clients to preemptively load those Prefabs. Later in the same game session, the server needs to perform a spawn, and can do so knowing all clients have loaded said dynamic Prefab (since it already did so preemptively). This allows for simple spawn management.

This sample is different from the 00_Preloading Dynamic Prefabs scene, in that it occurs after clients connect and join the game. It allows for more gameplay flexibility and loading different Prefabs based on where players are at in the game, for example.

The logic that drives the behaviour for this use-case resides inside ServerAuthoritativeLoadAllAsync.cs. Its Start() method is as follows:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server%20Authoritative%20Load%20All%20Async/ServerAuthoritativeLoadAllAsync.cs
void Start()
{
DynamicPrefabLoadingUtilities.Init(m_NetworkManager);

// In the use-cases where connection approval is implemented, the server can begin to validate a user's
// connection payload, and either approve or deny connection to the joining client.
// Note: we will define a very simplistic connection approval below, which will effectively deny all
// late-joining clients unless the server has not loaded any dynamic prefabs. You could choose to not define
// a connection approval callback, but late-joining clients will have mismatching NetworkConfigs (and
// potentially different world versions if the server has spawned a dynamic prefab).
m_NetworkManager.NetworkConfig.ConnectionApproval = true;

// Here, we keep ForceSamePrefabs disabled. This will allow us to dynamically add network prefabs to Netcode
// for GameObject after establishing a connection.
m_NetworkManager.NetworkConfig.ForceSamePrefabs = false;

// This is a simplistic use-case of a connection approval callback. To see how a connection approval should
// be used to validate a user's connection payload, see the connection approval use-case, or the
// APIPlayground, where all post-connection techniques are used in harmony.
m_NetworkManager.ConnectionApprovalCallback += ConnectionApprovalCallback;

// hooking up UI callbacks
m_InGameUI.LoadAllAsyncButtonPressed += OnClickedPreload;
}

Similarly to the last use-case, this use-case has ConnectionApproval set to true and ForceSamePrefabs set to false. The sample defines a trimmed-down ConnectionApproval callback inside of this class resembling that of the last use-case to allow for denying connections to clients that have mismatched NetworkPrefabs lists to that of the server. If ConnectionApproval is set to false, all incoming connection are automatically approved.

The sample also binds a UI (user interface) button's pressed callback to a method inside this class. This method, shown below, iterates through the serialized list of AssetReferenceGameObjects, and generates a set of tasks to asynchronously load the Prefabs on every connected client (and the server).

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server%20Authoritative%20Load%20All%20Async/ServerAuthoritativeLoadAllAsync.cs
async void PreloadPrefabs()
{
var tasks = new List<Task>();
foreach (var p in m_DynamicPrefabReferences)
{
tasks.Add(PreloadDynamicPrefabOnServerAndStartLoadingOnAllClients(p.AssetGUID));
}

await Task.WhenAll(tasks);
}

The task to load an Addressable individually is as follows:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server%20Authoritative%20Load%20All%20Async/ServerAuthoritativeLoadAllAsync.cs
/// <summary>
/// This call preloads the dynamic prefab on the server and sends a client rpc to all the clients to do the same.
/// </summary>
/// <param name="guid"></param>
async Task PreloadDynamicPrefabOnServerAndStartLoadingOnAllClients(string guid)
{
if (m_NetworkManager.IsServer)
{
var assetGuid = new AddressableGUID()
{
Value = guid
};

if (DynamicPrefabLoadingUtilities.IsPrefabLoadedOnAllClients(assetGuid))
{
Debug.Log("Prefab is already loaded by all peers");
return;
}

// update UI for each client that is requested to load a certain prefab
foreach (var client in m_NetworkManager.ConnectedClients.Keys)
{
m_InGameUI.ClientLoadedPrefabStatusChanged(client, assetGuid.GetHashCode(), "Undefined", InGameUI.LoadStatus.Loading);
}

Debug.Log("Loading dynamic prefab on the clients...");
LoadAddressableClientRpc(assetGuid);

await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_InGameUI.ArtificialDelayMilliseconds);

// server loaded a prefab, update UI with the loaded asset's name
DynamicPrefabLoadingUtilities.TryGetLoadedGameObjectFromGuid(assetGuid, out var loadedGameObject);

// every client loaded dynamic prefab, their respective ClientUIs in case they loaded first
foreach (var client in m_NetworkManager.ConnectedClients.Keys)
{
m_InGameUI.ClientLoadedPrefabStatusChanged(client,
assetGuid.GetHashCode(),
loadedGameObject.Result.name,
InGameUI.LoadStatus.Loading);
}
}
}

First, the sample ensures this block of code only executes on the server. Next, a simple check verifies if the dynamic Prefab has already been loaded. If the dynamic Prefab is loaded, you can early return inside this method.

Next, the server sends out a ClientRpc to every client, instructing them to load an Addressable and add it to their NetworkManager's NetworkPrefabs list. After sending out the ClientRpc, the server begins to asynchronously load the same Prefab. The ClientRpc looks like:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server%20Authoritative%20Load%20All%20Async/ServerAuthoritativeLoadAllAsync.cs
[ClientRpc]
void LoadAddressableClientRpc(AddressableGUID guid, ClientRpcParams rpcParams = default)
{
if (!IsHost)
{
Load(guid);
}

async void Load(AddressableGUID assetGuid)
{
// loading prefab as a client, update UI
m_InGameUI.ClientLoadedPrefabStatusChanged(m_NetworkManager.LocalClientId, assetGuid.GetHashCode(), "Undefined", InGameUI.LoadStatus.Loading);

Debug.Log("Loading dynamic prefab on the client...");
await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_InGameUI.ArtificialDelayMilliseconds);
Debug.Log("Client loaded dynamic prefab");

DynamicPrefabLoadingUtilities.TryGetLoadedGameObjectFromGuid(assetGuid, out var loadedGameObject);
m_InGameUI.ClientLoadedPrefabStatusChanged(m_NetworkManager.LocalClientId, assetGuid.GetHashCode(), loadedGameObject.Result.name, InGameUI.LoadStatus.Loaded);

AcknowledgeSuccessfulPrefabLoadServerRpc(assetGuid.GetHashCode());
}
}

This operation should only run on clients. After the Prefab loads on the client, the client sends back an acknowledgement ServerRpc containing the hashcode of the loaded Prefab. The ServerRpc looks like:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server%20Authoritative%20Load%20All%20Async/ServerAuthoritativeLoadAllAsync.cs
[ServerRpc(RequireOwnership = false)]
void AcknowledgeSuccessfulPrefabLoadServerRpc(int prefabHash, ServerRpcParams rpcParams = default)
{
Debug.Log($"Client acknowledged successful prefab load with hash: {prefabHash}");
DynamicPrefabLoadingUtilities.RecordThatClientHasLoadedAPrefab(prefabHash, rpcParams.Receive.SenderClientId);

// a quick way to grab a matching prefab reference's name via its prefabHash
var loadedPrefabName = "Undefined";
foreach (var prefabReference in m_DynamicPrefabReferences)
{
var prefabReferenceGuid = new AddressableGUID() { Value = prefabReference.AssetGUID };
if (prefabReferenceGuid.GetHashCode() == prefabHash)
{
// found the matching prefab reference
if (DynamicPrefabLoadingUtilities.LoadedDynamicPrefabResourceHandles.TryGetValue(
prefabReferenceGuid,
out var loadedGameObject))
{
// if it is loaded on the server, update the name on the ClientUI
loadedPrefabName = loadedGameObject.Result.name;
}
break;
}
}

// client has successfully loaded a prefab, update UI
m_InGameUI.ClientLoadedPrefabStatusChanged(rpcParams.Receive.SenderClientId, prefabHash, loadedPrefabName, InGameUI.LoadStatus.Loaded);
}

The server records that the client has successfully loaded the dynamic Prefab. As hinted by the class name, this use-case only instructs clients to load a set of dynamic Prefabs and doesn't invoke a network spawn.

Scene 03_Server Authoritative Synchronous Dynamic Prefab Spawn

The 03_Server Authoritative Synchronous Dynamic Prefab Spawn scene is a dynamic Prefab loading scenario where the server instructs all clients to load a single network Prefab, and only invokes a spawn after all clients have finish loading the Prefab. The server initially sends a ClientRpc to all clients, begins loading the Prefab on the server, awaits a acknowledgement of a load via a ServerRpcs from each client, and only spawns the Prefab over the network after it receives an acknowledgement from every client, within a predetermined amount of time.

This example implementation works best for scenarios where you want to guarantee the same world version across all connected clients. Because the server waits for all clients to finish loading the same dynamic Prefab, the spawn of said dynamic Prefab will be synchronous.

The technique demonstrated in this sample works best for spawning game-changing gameplay elements, assuming you want all clients to be able to interact with said gameplay elements from the same point forward. For example, you don't want to have an enemy that's only visible (network-side or visually) to some clients and not others—you want to delay the spawning the enemy until all clients have dynamically loaded it and are able to see it before spawning it server-side.

The logic for this use-case resides inside of ServerAuthoritativeSynchronousSpawning.cs. The Start() method of this class resembles that of the last use-case, however, the method invoked by the UI (user interface) is:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server%20Authoritative%20Synchronous%20Spawning/ServerAuthoritativeSynchronousSpawning.cs
// invoked by UI
void OnClickedTrySpawnSynchronously()
{
if (!m_NetworkManager.IsServer)
{
return;
}

TrySpawnSynchronously();
}

async void TrySpawnSynchronously()
{
var randomPrefab = m_DynamicPrefabReferences[Random.Range(0, m_DynamicPrefabReferences.Count)];
await TrySpawnDynamicPrefabSynchronously(randomPrefab.AssetGUID, Random.insideUnitCircle * 5, Quaternion.identity);
}

The sample first validates that only the server executes this bit of code. Next, it grabs a AssetReferenceGameObject from the serialized list at random, and invokes an asynchronous task that tries to spawn this dynamic Prefab, positioned inside a random point of a circle:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server%20Authoritative%20Synchronous%20Spawning/ServerAuthoritativeSynchronousSpawning.cs
/// <summary>
/// This call attempts to spawn a prefab by it's addressable guid - it ensures that all the clients have loaded the prefab before spawning it,
/// and if the clients fail to acknowledge that they've loaded a prefab - the spawn will fail.
/// </summary>
/// <param name="guid"></param>
/// <returns></returns>
async Task<(bool Success, NetworkObject Obj)> TrySpawnDynamicPrefabSynchronously(string guid, Vector3 position, Quaternion rotation)
{
if (IsServer)
{
var assetGuid = new AddressableGUID()
{
Value = guid
};

if (DynamicPrefabLoadingUtilities.IsPrefabLoadedOnAllClients(assetGuid))
{
Debug.Log("Prefab is already loaded by all peers, we can spawn it immediately");
var obj = Spawn(assetGuid);
return (true, obj);
}

m_SynchronousSpawnAckCount = 0;
m_SynchronousSpawnTimeoutTimer = 0;

Debug.Log("Loading dynamic prefab on the clients...");
LoadAddressableClientRpc(assetGuid);

// server is starting to load a prefab, update UI
m_InGameUI.ClientLoadedPrefabStatusChanged(NetworkManager.ServerClientId, assetGuid.GetHashCode(), "Undefined", InGameUI.LoadStatus.Loading);

//load the prefab on the server, so that any late-joiner will need to load that prefab also
await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_InGameUI.ArtificialDelayMilliseconds);

// server loaded a prefab, update UI with the loaded asset's name
DynamicPrefabLoadingUtilities.TryGetLoadedGameObjectFromGuid(assetGuid, out var loadedGameObject);
m_InGameUI.ClientLoadedPrefabStatusChanged(NetworkManager.ServerClientId, assetGuid.GetHashCode(), loadedGameObject.Result.name, InGameUI.LoadStatus.Loaded);

var requiredAcknowledgementsCount = IsHost ? m_NetworkManager.ConnectedClients.Count - 1 :
m_NetworkManager.ConnectedClients.Count;

while (m_SynchronousSpawnTimeoutTimer < m_InGameUI.NetworkSpawnTimeoutSeconds)
{
if (m_SynchronousSpawnAckCount >= requiredAcknowledgementsCount)
{
Debug.Log($"All clients have loaded the prefab in {m_SynchronousSpawnTimeoutTimer} seconds, spawning the prefab on the server...");
var obj = Spawn(assetGuid);
return (true, obj);
}

m_SynchronousSpawnTimeoutTimer += Time.deltaTime;
await Task.Yield();
}

// left to the reader: you'll need to be reactive to clients failing to load -- you should either have
// the offending client try again or disconnect it after a predetermined amount of failed attempts
Debug.LogError("Failed to spawn dynamic prefab - timeout");
return (false, null);
}

return (false, null);

NetworkObject Spawn(AddressableGUID assetGuid)
{
if (!DynamicPrefabLoadingUtilities.TryGetLoadedGameObjectFromGuid(assetGuid, out var prefab))
{
Debug.LogWarning($"GUID {assetGuid} is not a GUID of a previously loaded prefab. Failed to spawn a prefab.");
return null;
}
var obj = Instantiate(prefab.Result, position, rotation).GetComponent<NetworkObject>();
obj.Spawn();
Debug.Log("Spawned dynamic prefab");

// every client loaded dynamic prefab, their respective ClientUIs in case they loaded first
foreach (var client in m_NetworkManager.ConnectedClients.Keys)
{
m_InGameUI.ClientLoadedPrefabStatusChanged(client,
assetGuid.GetHashCode(),
prefab.Result.name,
InGameUI.LoadStatus.Loading);
}

return obj;
}
}

The sample checks if the dynamic Prefab is already loaded and if so, it can just spawn it directly:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server%20Authoritative%20Synchronous%20Spawning/ServerAuthoritativeSynchronousSpawning.cs
if (DynamicPrefabLoadingUtilities.IsPrefabLoadedOnAllClients(assetGuid))
{
Debug.Log("Prefab is already loaded by all peers, we can spawn it immediately");
var obj = Spawn(assetGuid);
return (true, obj);
}

Next, the sample resets the variable to track the number of clients that have loaded a Prefab during this asynchronous operation, and the variable to track how long a spawn operation has taken. The server then instructs all clients to load the dynamic Prefab via a ClientRpc:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server%20Authoritative%20Synchronous%20Spawning/ServerAuthoritativeSynchronousSpawning.cs
Debug.Log("Loading dynamic prefab on the clients...");
LoadAddressableClientRpc(assetGuid);

The server now loads the dynamic Prefab. After successfully loading the dynamic Prefab, the server records the necessary number of acknowledgement ServerRpcs it needs to receive to guarantee that all clients have loaded the dynamic Prefab. The server then halts until that condition is met:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server%20Authoritative%20Synchronous%20Spawning/ServerAuthoritativeSynchronousSpawning.cs
var requiredAcknowledgementsCount = IsHost ? m_NetworkManager.ConnectedClients.Count - 1 : 
m_NetworkManager.ConnectedClients.Count;

while (m_SynchronousSpawnTimeoutTimer < m_InGameUI.NetworkSpawnTimeoutSeconds)
{
if (m_SynchronousSpawnAckCount >= requiredAcknowledgementsCount)
{
Debug.Log($"All clients have loaded the prefab in {m_SynchronousSpawnTimeoutTimer} seconds, spawning the prefab on the server...");
var obj = Spawn(assetGuid);
return (true, obj);
}

m_SynchronousSpawnTimeoutTimer += Time.deltaTime;
await Task.Yield();
}

If all clients have loaded the dynamic Prefab, and that condition is met within a predetermined amount of seconds, the server is free to instantiate and spawn a NetworkObject over the network. If this loading condition isn't met, the server doesn't instantiate nor spawn the loaded NetworkObject, and returns a failure for this task:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server%20Authoritative%20Synchronous%20Spawning/ServerAuthoritativeSynchronousSpawning.cs
// left to the reader: you'll need to be reactive to clients failing to load -- you should either have
// the offending client try again or disconnect it after a predetermined amount of failed attempts
Debug.LogError("Failed to spawn dynamic prefab - timeout");
return (false, null);

The ClientRpc in this class is identical to that of the last use-case, but the ServerRpc is different since here is where the acknowledgement variable is incremented:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server%20Authoritative%20Synchronous%20Spawning/ServerAuthoritativeSynchronousSpawning.cs
[ServerRpc(RequireOwnership = false)]
void AcknowledgeSuccessfulPrefabLoadServerRpc(int prefabHash, ServerRpcParams rpcParams = default)
{
m_SynchronousSpawnAckCount++;
Debug.Log($"Client acknowledged successful prefab load with hash: {prefabHash}");
DynamicPrefabLoadingUtilities.RecordThatClientHasLoadedAPrefab(prefabHash,
rpcParams.Receive.SenderClientId);

// a quick way to grab a matching prefab reference's name via its prefabHash
var loadedPrefabName = "Undefined";
foreach (var prefabReference in m_DynamicPrefabReferences)
{
var prefabReferenceGuid = new AddressableGUID() { Value = prefabReference.AssetGUID };
if (prefabReferenceGuid.GetHashCode() == prefabHash)
{
// found the matching prefab reference
if (DynamicPrefabLoadingUtilities.LoadedDynamicPrefabResourceHandles.TryGetValue(
prefabReferenceGuid,
out var loadedGameObject))
{
// if it is loaded on the server, update the name on the ClientUI
loadedPrefabName = loadedGameObject.Result.name;
}
break;
}
}

// client has successfully loaded a prefab, update UI
m_InGameUI.ClientLoadedPrefabStatusChanged(rpcParams.Receive.SenderClientId, prefabHash, loadedPrefabName, InGameUI.LoadStatus.Loaded);
}

Scene 04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility

The 04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility scene is a dynamic Prefab loading scenario where the server instructs all clients to load a single network Prefab via a ClientRpc, spawns the Prefab as soon as it's loaded on the server, and marks the Prefab as network-visible only to clients that have already loaded that same Prefab. As soon as a client loads the Prefab locally, it sends an acknowledgement ServerRpcs, and the server marks that spawned NetworkObject as network-visible for that client.

An important implementation detail to note about this technique is that the server won't wait until all clients load a dynamic Prefab before spawning the corresponding NetworkObject. As a result, a NetworkObject becomes network-visible for a connected client as soon as the client loads it—a client isn't blocked by the loading operation of another client (which might take longer to load the asset or fail to load it at all). A consequence of this asynchronous loading technique is that clients might experience differing world versions momentarily. As a result, it's not recommend to use this technique for spawning game-changing gameplay elements (like a boss fight, for example) if you want all clients to interact with the spawned NetworkObject as soon as the server spawns it.

Take a look at the implementation, observed inside ServerAuthoritativeNetworkVisibilitySpawning. As was the case with the last use-case, the Start() method defines the callback to subscribe to from the UI (user interface). The method invoked on the server looks like:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
// invoked by UI
void OnClickedTrySpawnInvisible()
{
if (!m_NetworkManager.IsServer)
{
return;
}

TrySpawnInvisible();
}

async void TrySpawnInvisible()
{
var randomPrefab = m_DynamicPrefabReferences[Random.Range(0, m_DynamicPrefabReferences.Count)];
await SpawnImmediatelyAndHideUntilPrefabIsLoadedOnClient(randomPrefab.AssetGUID, Random.insideUnitCircle * 5, Quaternion.identity);
}

A dynamic Prefab reference is selected at random, and will be spawned by the following method:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
/// <summary>
/// This call spawns an addressable prefab by it's guid. It does not ensure that all the clients have loaded the
/// prefab before spawning it. All spawned objects are network-invisible to clients that don't have the prefab
/// loaded. The server tells the clients that lack the preloaded prefab to load it and acknowledge that they've
/// loaded it, and then the server makes the object network-visible to that client.
/// </summary>
/// <param name="guid"></param>
/// <returns></returns>
async Task<NetworkObject> SpawnImmediatelyAndHideUntilPrefabIsLoadedOnClient(string guid, Vector3 position, Quaternion rotation)
{
if (IsServer)
{
var assetGuid = new AddressableGUID()
{
Value = guid
};

return await Spawn(assetGuid);
}

return null;

async Task<NetworkObject> Spawn(AddressableGUID assetGuid)
{
// server is starting to load a prefab, update UI
m_InGameUI.ClientLoadedPrefabStatusChanged(NetworkManager.ServerClientId, assetGuid.GetHashCode(), "Undefined", InGameUI.LoadStatus.Loading);

var prefab = await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid,
m_InGameUI.ArtificialDelayMilliseconds);

// server loaded a prefab, update UI with the loaded asset's name
DynamicPrefabLoadingUtilities.TryGetLoadedGameObjectFromGuid(assetGuid, out var loadedGameObject);
m_InGameUI.ClientLoadedPrefabStatusChanged(NetworkManager.ServerClientId, assetGuid.GetHashCode(), loadedGameObject.Result.name, InGameUI.LoadStatus.Loaded);

var obj = Instantiate(prefab, position, rotation).GetComponent<NetworkObject>();

if (m_PrefabHashToNetworkObjectId.TryGetValue(assetGuid.GetHashCode(), out var networkObjectIds))
{
networkObjectIds.Add(obj);
}
else
{
m_PrefabHashToNetworkObjectId.Add(assetGuid.GetHashCode(), new HashSet<NetworkObject>() {obj});
}

// This gets called on spawn and makes sure clients currently syncing and receiving spawns have the
// appropriate network visibility settings automatically. This can happen on late join, on spawn, on
// scene switch, etc.
obj.CheckObjectVisibility = (clientId) =>
{
if (clientId == NetworkManager.ServerClientId)
{
// object is loaded on the server, no need to validate for visibility
return true;
}

//if the client has already loaded the prefab - we can make the object network-visible to them
if (DynamicPrefabLoadingUtilities.HasClientLoadedPrefab(clientId, assetGuid.GetHashCode()))
{
return true;
}

// client is loading a prefab, update UI
m_InGameUI.ClientLoadedPrefabStatusChanged(clientId, assetGuid.GetHashCode(), "Undefined", InGameUI.LoadStatus.Loading);

//otherwise the clients need to load the prefab, and after they ack - the ShowHiddenObjectsToClient
LoadAddressableClientRpc(assetGuid, new ClientRpcParams(){Send = new ClientRpcSendParams(){TargetClientIds = new ulong[]{clientId}}});
return false;
};

obj.Spawn();

return obj;
}
}

Similar to the last use-case, this section of code should only run on the server:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
if (IsServer)
{
var assetGuid = new AddressableGUID()
{
Value = guid
};

return await Spawn(assetGuid);
}

This method here will first load the dynamic Prefab on the server, and immediately spawn it on the server:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
var prefab = await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, 
m_InGameUI.ArtificialDelayMilliseconds);
Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
var obj = Instantiate(prefab, position, rotation).GetComponent<NetworkObject>();

After instantiating the NetworkObject, the sample keeps track of the instantiated NetworkObject in a dictionary, keyed by its asset GUID:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
if (m_PrefabHashToNetworkObjectId.TryGetValue(assetGuid.GetHashCode(), out var networkObjectIds))
{
networkObjectIds.Add(obj);
}
else
{
m_PrefabHashToNetworkObjectId.Add(assetGuid.GetHashCode(), new HashSet<NetworkObject>() {obj});
}

NetworkObject.CheckObjectVisibility is the callback that Netcode uses to decide whether to mark a NetworkObject as network-visible to a client. Here the sample explicitly defines a new callback. In this case, network-visibility is determined by whether the server has received an acknowledgement ServerRpc from a client for having loaded the dynamic Prefab.

It's worth noting that this callback also runs on the host, so a quick server check returns a success:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
if (clientId == NetworkManager.ServerClientId)
{
// object is loaded on the server, no need to validate for visibility
return true;
}

If a client has loaded the dynamic Prefab in question, mark the NetworkObject network-visible to them:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
//if the client has already loaded the prefab - we can make the object network-visible to them
if (DynamicPrefabLoadingUtilities.HasClientLoadedPrefab(clientId, assetGuid.GetHashCode()))
{
return true;
}

If a client hasn't loaded the dynamic Prefab, send the client a ClientRpc instructing them to load the dynamic Prefab:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
//otherwise the clients need to load the prefab, and after they ack - the ShowHiddenObjectsToClient 
LoadAddressableClientRpc(assetGuid, new ClientRpcParams(){Send = new ClientRpcSendParams(){TargetClientIds = new ulong[]{clientId}}});
return false;

After the server defines this callback, it spawns the NetworkObject:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
obj.Spawn();

The ClientRpc is identical to that of the last use-case, but this use-case takes a look at the ServerRpc in this class, and makes a distinction unique to this use-case:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
loading...

This marks the NetworkObject network-visible to the non-hosting clients. The specific API used here is:

Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server%20Authoritative%20Network-Visibility%20Spawning/ServerAuthoritativeNetworkVisibilitySpawning.cs
loading...

Scene 05_API Playground Showcasing All Post-Connection Uses

The 05_API Playground Showcasing All Post-Connection Uses scene uses a class that serves as the playground of the dynamic Prefab loading uses. It integrates APIs from this sample to use at post-connection time, such as:

  • Connection approval for syncing late-joining clients.
  • Dynamically loading a collection of network Prefabs on the host and all connected clients.
  • Synchronously spawning a dynamically loaded network Prefab across connected clients.
  • Spawning a dynamically loaded network Prefab as network-invisible for all clients until they load the Prefab locally (in which case it becomes network-visible to the client).