diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Prefabs/UI.prefab b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Prefabs/UI.prefab
new file mode 100644
index 000000000..c2a15eef9
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Prefabs/UI.prefab
@@ -0,0 +1,264 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &4061078000728451377
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 4484396944538767948}
+ - component: {fileID: 1868617790277435607}
+ m_Layer: 0
+ m_Name: ConnectionType
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &4484396944538767948
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4061078000728451377}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 5916229021262027593}
+ m_RootOrder: 3
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1868617790277435607
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 4061078000728451377}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_PanelSettings: {fileID: 11400000, guid: c023b7db6d50c9e45b6a080ccc23874f, type: 2}
+ m_ParentUI: {fileID: 0}
+ sourceAsset: {fileID: 9197481963319205126, guid: 720681dc71c9789408a1fab0f21e0f93, type: 3}
+ m_SortingOrder: 1
+--- !u!1 &5916229021262027592
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5916229021262027593}
+ - component: {fileID: 1216736892}
+ - component: {fileID: 1085470603439750153}
+ m_Layer: 0
+ m_Name: UI
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &5916229021262027593
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229021262027592}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 5916229021887624318}
+ - {fileID: 5916229022476518943}
+ - {fileID: 5916229022061089010}
+ - {fileID: 4484396944538767948}
+ m_Father: {fileID: 0}
+ m_RootOrder: 0
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1216736892
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229021262027592}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 6c1ed03cc3b245b4ca88b95ef9bea14e, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_ConnectionsUIDoc: {fileID: 5916229021887624317}
+ m_InGameUIDoc: {fileID: 5916229022476518942}
+ m_ConnectionTemplatePrefab: {fileID: 9197481963319205126, guid: 559dc6bda8f869d4b828b29a963e1e89, type: 3}
+ m_RowTemplatePrefab: {fileID: 9197481963319205126, guid: e61c0a6ba4a28444da358d17a074172f, type: 3}
+ m_ShowLoadAllAsyncButton: 1
+ m_ShowTrySpawnSynchronouslyButton: 1
+ m_ShowSpawnUsingVisibilityButton: 1
+--- !u!114 &1085470603439750153
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229021262027592}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: ada9189cf150a904bb8090bcb9e80a6c, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_IPMenuUIDocument: {fileID: 5916229022061089011}
+ m_ConnectionTypeUIDocument: {fileID: 1868617790277435607}
+--- !u!1 &5916229021887624316
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5916229021887624318}
+ - component: {fileID: 5916229021887624317}
+ m_Layer: 0
+ m_Name: Connections
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &5916229021887624318
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229021887624316}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 5916229021262027593}
+ m_RootOrder: 0
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &5916229021887624317
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229021887624316}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_PanelSettings: {fileID: 11400000, guid: c023b7db6d50c9e45b6a080ccc23874f, type: 2}
+ m_ParentUI: {fileID: 0}
+ sourceAsset: {fileID: 9197481963319205126, guid: aa2f8502baa527549a2a096d7db99736, type: 3}
+ m_SortingOrder: 1
+--- !u!1 &5916229022061089009
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5916229022061089010}
+ - component: {fileID: 5916229022061089011}
+ m_Layer: 0
+ m_Name: IPMenu
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &5916229022061089010
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229022061089009}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 5916229021262027593}
+ m_RootOrder: 2
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &5916229022061089011
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229022061089009}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_PanelSettings: {fileID: 11400000, guid: c023b7db6d50c9e45b6a080ccc23874f, type: 2}
+ m_ParentUI: {fileID: 0}
+ sourceAsset: {fileID: 9197481963319205126, guid: f34559e6fa10dd7498466b9da1869b6d, type: 3}
+ m_SortingOrder: 0
+--- !u!1 &5916229022476518941
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5916229022476518943}
+ - component: {fileID: 5916229022476518942}
+ m_Layer: 0
+ m_Name: InGameUI
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &5916229022476518943
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229022476518941}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 5916229021262027593}
+ m_RootOrder: 1
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &5916229022476518942
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5916229022476518941}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_PanelSettings: {fileID: 11400000, guid: c023b7db6d50c9e45b6a080ccc23874f, type: 2}
+ m_ParentUI: {fileID: 0}
+ sourceAsset: {fileID: 9197481963319205126, guid: 16051812210b8ab4bb7c93577ca1410b, type: 3}
+ m_SortingOrder: 1
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Prefabs/UI.prefab.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Prefabs/UI.prefab.meta
new file mode 100644
index 000000000..a6a1ff2a3
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Prefabs/UI.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: efffbf0d4e88eee4c8782180ff1ce1a2
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs.meta
new file mode 100644
index 000000000..0dcc46484
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0ff7f41903c874c7cafc2e967fb48930
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs/PreloadingDynamicPrefabs.cs b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs/PreloadingDynamicPrefabs.cs
new file mode 100644
index 000000000..fba1e761e
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs/PreloadingDynamicPrefabs.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Threading.Tasks;
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+
+namespace Game.PreloadingDynamicPrefabs
+{
+ ///
+ /// This is the simplest case of a dynamic prefab - we instruct all game instances to load a network prefab (it can
+ /// be just one, it could also be a set of network prefabs) and inject them to NetworkManager's NetworkPrefabs list
+ /// before starting the server. What's important is that it doesn't really matter where the prefab comes from. It
+ /// could be a simple prefab or it could be an Addressable - it's all the same.
+ ///
+ ///
+ /// Here, we're serializing the AssetReferenceGameObject to this class, but ideally you'd want to authenticate
+ /// players when your game starts up and have them fetch network prefabs from services such as UGS (see Remote
+ /// Config). It should also be noted that this is a technique that could serve to decrease the install size of your
+ /// application, since you'd be streaming in networked game assets dynamically.
+ ///
+ public sealed class PreloadingDynamicPrefabs : MonoBehaviour
+ {
+ [SerializeField] AssetReferenceGameObject m_DynamicPrefabReference;
+
+ [SerializeField] NetworkManager m_NetworkManager;
+
+ async void Start()
+ {
+ await PreloadDynamicPlayerPrefab();
+ //after we've waited for the prefabs to load - we can start the host or the client
+ }
+
+ // 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(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;
+ }
+ }
+}
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs/PreloadingDynamicPrefabs.cs.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs/PreloadingDynamicPrefabs.cs.meta
new file mode 100644
index 000000000..bf9fffe71
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/00_Preloading Dynamic Prefabs/PreloadingDynamicPrefabs.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8c82d08ffbd524a6799d466e7997b6a0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining.meta
new file mode 100644
index 000000000..d44f0f356
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a94bc61944601e849b4d1c95f15a3290
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining/ConnectionApprovalRequiredForLateJoining.cs b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining/ConnectionApprovalRequiredForLateJoining.cs
new file mode 100644
index 000000000..893cb3119
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining/ConnectionApprovalRequiredForLateJoining.cs
@@ -0,0 +1,144 @@
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+
+namespace Game.ConnectionApprovalRequiredForLateJoining
+{
+ ///
+ /// A class that walks through what a server would need to validate from a client when dynamically loading network
+ /// prefabs. This is another simple use-case scenario, as this is just the implementation of the connection approval
+ /// callback, which is an optional feature from Netcode for GameObjects. To enable it, make sure the "Connection
+ /// Approval" toggle is enabled on the NetworkManager in your scene. Other use-cases don't allow for connection
+ /// after the server has loaded a prefab dynamically, whereas this one enables that functionality. To see it all in
+ /// harmony, see , where all post-connection techniques are showcased in one scene.
+ ///
+ public sealed class ConnectionApprovalRequiredForLateJoining : NetworkBehaviour
+ {
+ [SerializeField]
+ NetworkManager m_NetworkManager;
+
+ [SerializeField]
+ AssetReferenceGameObject m_AssetReferenceGameObject;
+
+ const int k_MaxConnectPayload = 1024;
+
+ 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;
+ }
+
+ async void LoadAPrefab()
+ {
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(
+ new AddressableGUID() { Value = m_AssetReferenceGameObject.AssetGUID},
+ 0);
+ }
+
+ public override void OnDestroy()
+ {
+ m_NetworkManager.ConnectionApprovalCallback -= ConnectionApprovalCallback;
+ m_NetworkManager.OnServerStarted -= LoadAPrefab;
+ DynamicPrefabLoadingUtilities.UnloadAndReleaseAllDynamicPrefabs();
+ base.OnDestroy();
+ }
+
+ void ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
+ {
+ Debug.Log($"Client {request.ClientNetworkId} is trying to connect ");
+ var connectionData = request.Payload;
+ var clientId = request.ClientNetworkId;
+
+ if (clientId == m_NetworkManager.LocalClientId)
+ {
+ //allow the host to connect
+ Approve();
+ 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;
+ }
+
+ if (DynamicPrefabLoadingUtilities.LoadedPrefabCount == 0)
+ {
+ //immediately approve the connection if we haven't loaded any prefabs yet
+ Approve();
+ return;
+ }
+
+ var payload = System.Text.Encoding.UTF8.GetString(connectionData);
+ var connectionPayload = JsonUtility.FromJson(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;
+ }
+
+ // In order for clients to not just get disconnected with no feedback, the server needs to tell the client
+ // why it disconnected it. This could happen after an auth check on a service or because of gameplay
+ // reasons (server full, wrong build version, etc).
+ // The server can do so via the DisconnectReason in the ConnectionApprovalResponse. The guids of the prefabs
+ // the client will need to load will be sent, such that the client loads the needed prefabs, and reconnects.
+
+ // A note: DisconnectReason will not be written to if the string is too large in size. This should be used
+ // only to tell the client "why" it failed -- the client should instead use services like UGS to fetch the
+ // relevant data it needs to fetch & download.
+
+ DynamicPrefabLoadingUtilities.RefreshLoadedPrefabGuids();
+
+ response.Reason = DynamicPrefabLoadingUtilities.GenerateDisconnectionPayload();
+
+ ImmediateDeny();
+
+ // A note: sending large strings through Netcode is not ideal -- you'd usually want to use REST services to
+ // accomplish this instead. UGS services like Lobby can be a useful alternative. Another route may be to
+ // set ConnectionApprovalResponse's Pending flag to true, and send a CustomMessage containing the array of
+ // GUIDs to a client, which the client would load and reattempt a reconnection.
+
+ void Approve()
+ {
+ Debug.Log($"Client {clientId} approved");
+ response.Approved = true;
+ response.CreatePlayerObject = false; //we're not going to spawn a player object for this sample
+ }
+
+ void ImmediateDeny()
+ {
+ Debug.Log($"Client {clientId} denied connection");
+ response.Approved = false;
+ response.CreatePlayerObject = false;
+ }
+ }
+ }
+}
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining/ConnectionApprovalRequiredForLateJoining.cs.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining/ConnectionApprovalRequiredForLateJoining.cs.meta
new file mode 100644
index 000000000..d502943b9
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/01_Connection Approval Required For Late Joining/ConnectionApprovalRequiredForLateJoining.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 46115653413e2ed4c9163c4e5350e569
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously.meta
new file mode 100644
index 000000000..c17d4fb55
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 3a34ed4b12e58324d9bb1201751560d9
+timeCreated: 1667392114
\ No newline at end of file
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously/ServerAuthoritativeLoadAllPrefabsAsynchronously.cs b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously/ServerAuthoritativeLoadAllPrefabsAsynchronously.cs
new file mode 100644
index 000000000..f40bd29ed
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously/ServerAuthoritativeLoadAllPrefabsAsynchronously.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+
+namespace Game.ServerAuthoritativeLoadAllPrefabsAsynchronously
+{
+ ///
+ /// A simple use-case where the server notifies all clients to preload a collection of network prefabs. The server
+ /// will not invoke a spawn in this use-case, and will incrementally load each dynamic prefab, one prefab at a time.
+ ///
+ ///
+ /// A gameplay scenario where this technique would be useful: clients and host are connected, the host arrives at a
+ /// point in the game where they know some prefabs will be needed soon, and so the server instructs all clients to
+ /// preemptively load those prefabs. Some time later in the same session, the server needs to perform a spawn, and
+ /// can do so without making sure all clients have loaded said dynamic prefab, since it already did so preemptively.
+ /// This allows for simple spawn management.
+ ///
+ public sealed class ServerAuthoritativeLoadAllPrefabsAsynchronously : NetworkBehaviour
+ {
+ [SerializeField]
+ NetworkManager m_NetworkManager;
+
+ [SerializeField] List m_DynamicPrefabReferences;
+
+ [SerializeField]
+ int m_ArtificialDelayMilliseconds = 1000;
+
+ const int k_MaxConnectPayload = 1024;
+
+ 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;
+ }
+
+ public override void OnDestroy()
+ {
+ DynamicPrefabLoadingUtilities.UnloadAndReleaseAllDynamicPrefabs();
+ base.OnDestroy();
+ }
+
+ void ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
+ {
+ Debug.Log("Client is trying to connect " + request.ClientNetworkId);
+ var connectionData = request.Payload;
+ var clientId = request.ClientNetworkId;
+
+ if (clientId == m_NetworkManager.LocalClientId)
+ {
+ // allow the host to connect
+ Approve();
+ 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;
+ }
+
+ // simple approval if the server has not loaded any dynamic prefabs yet
+ if (DynamicPrefabLoadingUtilities.LoadedPrefabCount == 0)
+ {
+ Approve();
+ }
+ else
+ {
+ ImmediateDeny();
+ }
+
+ void Approve()
+ {
+ Debug.Log($"Client {clientId} approved");
+ response.Approved = true;
+ response.CreatePlayerObject = false; //we're not going to spawn a player object for this sample
+ }
+
+ void ImmediateDeny()
+ {
+ Debug.Log($"Client {clientId} denied connection");
+ response.Approved = false;
+ response.CreatePlayerObject = false;
+ }
+ }
+
+ // invoked by UI
+ public void OnClickedPreload()
+ {
+ if (!m_NetworkManager.IsServer)
+ {
+ return;
+ }
+
+ PreloadPrefabs();
+ }
+
+ async void PreloadPrefabs()
+ {
+ var tasks = new List();
+ foreach (var p in m_DynamicPrefabReferences)
+ {
+ tasks.Add(PreloadDynamicPrefabOnServerAndStartLoadingOnAllClients(p.AssetGUID));
+ }
+
+ await Task.WhenAll(tasks);
+ }
+
+ ///
+ /// This call preloads the dynamic prefab on the server and sends a client rpc to all the clients to do the same.
+ ///
+ ///
+ 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;
+ }
+
+ Debug.Log("Loading dynamic prefab on the clients...");
+ LoadAddressableClientRpc(assetGuid);
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_ArtificialDelayMilliseconds);
+ }
+ }
+
+ [ClientRpc]
+ void LoadAddressableClientRpc(AddressableGUID guid, ClientRpcParams rpcParams = default)
+ {
+ if (!IsHost)
+ {
+ Load(guid);
+ }
+
+ async void Load(AddressableGUID assetGuid)
+ {
+ Debug.Log("Loading dynamic prefab on the client...");
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_ArtificialDelayMilliseconds);
+ Debug.Log("Client loaded dynamic prefab");
+ AcknowledgeSuccessfulPrefabLoadServerRpc(assetGuid.GetHashCode());
+ }
+ }
+
+ [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);
+ }
+ }
+}
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously/ServerAuthoritativeLoadAllPrefabsAsynchronously.cs.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously/ServerAuthoritativeLoadAllPrefabsAsynchronously.cs.meta
new file mode 100644
index 000000000..5c17d768d
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/02_Server Authoritative Load All Prefabs Asynchronously/ServerAuthoritativeLoadAllPrefabsAsynchronously.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: dfcd3408d3cf98c41a414b4f63e473fc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn.meta
new file mode 100644
index 000000000..99dc91fb3
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5cda589957d9d8b4a9a4e47fa2c08ad8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn/ServerAuthoritativeSynchronousDynamicPrefabSpawn.cs b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn/ServerAuthoritativeSynchronousDynamicPrefabSpawn.cs
new file mode 100644
index 000000000..4505cd4f6
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn/ServerAuthoritativeSynchronousDynamicPrefabSpawn.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using Random = UnityEngine.Random;
+
+namespace Game.ServerAuthoritativeSynchronousDynamicPrefabSpawn
+{
+ ///
+ /// A dynamic prefab loading use-case where the server instructs all clients to load a single network prefab, and
+ /// will only invoke a spawn once all clients have successfully completed their respective loads of said prefab. The
+ /// server will initially send a ClientRpc to all clients, begin loading the prefab on the server, will await
+ /// acknowledgement of a load via ServerRpcs from each client, and will only spawn the prefab over the network once
+ /// it has received an acknowledgement from every client, within m_SynchronousSpawnTimeoutTimer seconds.
+ ///
+ ///
+ /// This use-case is recommended for scenarios where you'd want to guarantee the same world version across all
+ /// connected clients. Since the server will wait until all clients have loaded the same dynamic prefab, the spawn
+ /// of said dynamic prefab will be synchronous. Thus, we recommend using this technique for spawning game-changing
+ /// gameplay elements, assuming you'd want all clients to be able to interact with said gameplay elements from the
+ /// same point forward. For example, you wouldn't want to have an enemy only be visible (network side and/or
+ /// visually) to some clients and not others -- you'd want to delay the enemy's spawn until all clients have
+ /// dynamically loaded it and are able to see it before spawning it server side.
+ ///
+ public sealed class ServerAuthoritativeSynchronousDynamicPrefabSpawn : NetworkBehaviour
+ {
+ [SerializeField]
+ NetworkManager m_NetworkManager;
+
+ [SerializeField] List m_DynamicPrefabReferences;
+
+ [SerializeField]
+ int m_ArtificialDelayMilliseconds = 1000;
+
+ [SerializeField] float m_SpawnTimeoutInSeconds;
+
+ const int k_MaxConnectPayload = 1024;
+
+ float m_SynchronousSpawnTimeoutTimer;
+
+ int m_SynchronousSpawnAckCount = 0;
+
+ 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;
+ }
+
+ public override void OnDestroy()
+ {
+ DynamicPrefabLoadingUtilities.UnloadAndReleaseAllDynamicPrefabs();
+ base.OnDestroy();
+ }
+
+ void ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
+ {
+ Debug.Log("Client is trying to connect " + request.ClientNetworkId);
+ var connectionData = request.Payload;
+ var clientId = request.ClientNetworkId;
+
+ if (clientId == m_NetworkManager.LocalClientId)
+ {
+ // allow the host to connect
+ Approve();
+ 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;
+ }
+
+ // simple approval if the server has not loaded any dynamic prefabs yet
+ if (DynamicPrefabLoadingUtilities.LoadedPrefabCount == 0)
+ {
+ Approve();
+ }
+ else
+ {
+ ImmediateDeny();
+ }
+
+ void Approve()
+ {
+ Debug.Log($"Client {clientId} approved");
+ response.Approved = true;
+ response.CreatePlayerObject = false; //we're not going to spawn a player object for this sample
+ }
+
+ void ImmediateDeny()
+ {
+ Debug.Log($"Client {clientId} denied connection");
+ response.Approved = false;
+ response.CreatePlayerObject = false;
+ }
+ }
+
+ // invoked by UI
+ public 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ 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);
+ //load the prefab on the server, so that any late-joiner will need to load that prefab also
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_ArtificialDelayMilliseconds);
+ var requiredAcknowledgementsCount = IsHost ? m_NetworkManager.ConnectedClients.Count - 1 :
+ m_NetworkManager.ConnectedClients.Count;
+
+ while (m_SynchronousSpawnTimeoutTimer < m_SpawnTimeoutInSeconds)
+ {
+ 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();
+ obj.Spawn();
+ Debug.Log("Spawned dynamic prefab");
+ return obj;
+ }
+ }
+
+ [ClientRpc]
+ void LoadAddressableClientRpc(AddressableGUID guid, ClientRpcParams rpcParams = default)
+ {
+ if (!IsHost)
+ {
+ Load(guid);
+ }
+
+ async void Load(AddressableGUID assetGuid)
+ {
+ Debug.Log("Loading dynamic prefab on the client...");
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_ArtificialDelayMilliseconds);
+ Debug.Log("Client loaded dynamic prefab");
+ AcknowledgeSuccessfulPrefabLoadServerRpc(assetGuid.GetHashCode());
+ }
+ }
+
+ [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);
+ }
+ }
+}
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn/ServerAuthoritativeSynchronousDynamicPrefabSpawn.cs.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn/ServerAuthoritativeSynchronousDynamicPrefabSpawn.cs.meta
new file mode 100644
index 000000000..a827ec46d
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/03_Server Authoritative Synchronous Dynamic Prefab Spawn/ServerAuthoritativeSynchronousDynamicPrefabSpawn.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f128f9485fb85914989ea62eb4ca46c6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility.meta
new file mode 100644
index 000000000..d7f0bb793
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 4fd4a4c5142f468f9b009e5c9cf6a2be
+timeCreated: 1667392114
\ No newline at end of file
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility/ServerAuthoritativeSpawnDynamicPrefabUsingNetworkVisibility.cs b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility/ServerAuthoritativeSpawnDynamicPrefabUsingNetworkVisibility.cs
new file mode 100644
index 000000000..be0160043
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility/ServerAuthoritativeSpawnDynamicPrefabUsingNetworkVisibility.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using Random = UnityEngine.Random;
+
+namespace Game.ServerAuthoritativeSpawnDynamicPrefabUsingNetworkVisibility
+{
+ ///
+ /// A dynamic prefab loading use-case where the server instructs all clients to load a single network prefab via a
+ /// ClientRpc, will spawn said prefab as soon as it is loaded on the server, and will mark it 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 ServerRpc, and the server will mark that spawned NetworkObject as network-visible to
+ /// that client.
+ ///
+ ///
+ /// An important implementation detail to note about this technique: the server will not wait until all clients have
+ /// loaded a dynamic prefab before spawning the corresponding NetworkObject. Thus, this means that a NetworkObject
+ /// will become network-visible for a connected client as soon as it has loaded it as well -- a client is not
+ /// blocked by the loading operation of another client (which may be loading the asset slower or may have failed to
+ /// load it at all). A consequence of this asynchronous loading technique is that clients may experience differing
+ /// world versions momentarily. Therefore, we don't recommend using this technique for spawning game-changing
+ /// gameplay elements (like a boss fight for example) assuming you'd want all clients to interact with the spawned
+ /// NetworkObject as soon as it is spawned on the server.
+ ///
+ public sealed class ServerAuthoritativeSpawnDynamicPrefabUsingNetworkVisibility : NetworkBehaviour
+ {
+ [SerializeField]
+ NetworkManager m_NetworkManager;
+
+ [SerializeField] List m_DynamicPrefabReferences;
+
+ [SerializeField]
+ int m_ArtificialDelayMilliseconds = 1000;
+
+ const int k_MaxConnectPayload = 1024;
+
+ //A storage where we keep association between prefab (hash of it's GUID) and the spawned network objects that use it
+ Dictionary> m_PrefabHashToNetworkObjectId = new Dictionary>();
+
+ 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;
+ }
+
+ public override void OnDestroy()
+ {
+ DynamicPrefabLoadingUtilities.UnloadAndReleaseAllDynamicPrefabs();
+ base.OnDestroy();
+ }
+
+ void ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
+ {
+ Debug.Log("Client is trying to connect " + request.ClientNetworkId);
+ var connectionData = request.Payload;
+ var clientId = request.ClientNetworkId;
+
+ if (clientId == m_NetworkManager.LocalClientId)
+ {
+ // allow the host to connect
+ Approve();
+ 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;
+ }
+
+ // simple approval if the server has not loaded any dynamic prefabs yet
+ if (DynamicPrefabLoadingUtilities.LoadedPrefabCount == 0)
+ {
+ Approve();
+ }
+ else
+ {
+ ImmediateDeny();
+ }
+
+ void Approve()
+ {
+ Debug.Log($"Client {clientId} approved");
+ response.Approved = true;
+ response.CreatePlayerObject = false; //we're not going to spawn a player object for this sample
+ }
+
+ void ImmediateDeny()
+ {
+ Debug.Log($"Client {clientId} denied connection");
+ response.Approved = false;
+ response.CreatePlayerObject = false;
+ }
+ }
+
+ // invoked by UI
+ public 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ async Task SpawnImmediatelyAndHideUntilPrefabIsLoadedOnClient(string guid, Vector3 position, Quaternion rotation)
+ {
+ if (IsServer)
+ {
+ var assetGuid = new AddressableGUID()
+ {
+ Value = guid
+ };
+
+ return await Spawn(assetGuid);
+ }
+
+ return null;
+
+ async Task Spawn(AddressableGUID assetGuid)
+ {
+ var prefab = await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid,
+ m_ArtificialDelayMilliseconds);
+ var obj = Instantiate(prefab, position, rotation).GetComponent();
+
+ if (m_PrefabHashToNetworkObjectId.TryGetValue(assetGuid.GetHashCode(), out var networkObjectIds))
+ {
+ networkObjectIds.Add(obj);
+ }
+ else
+ {
+ m_PrefabHashToNetworkObjectId.Add(assetGuid.GetHashCode(), new HashSet() {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 the client has already loaded the prefab - we can make the object network-visible to them
+ if (DynamicPrefabLoadingUtilities.HasClientLoadedPrefab(clientId, assetGuid.GetHashCode()))
+ {
+ return true;
+ }
+ //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;
+ }
+ }
+
+ void ShowHiddenObjectsToClient(int prefabHash, ulong clientId)
+ {
+ if (m_PrefabHashToNetworkObjectId.TryGetValue(prefabHash, out var networkObjects))
+ {
+ foreach (var obj in networkObjects)
+ {
+ if (!obj.IsNetworkVisibleTo(clientId))
+ {
+ obj.NetworkShow(clientId);
+ }
+ }
+ }
+ }
+
+ [ClientRpc]
+ void LoadAddressableClientRpc(AddressableGUID guid, ClientRpcParams rpcParams = default)
+ {
+ if (!IsHost)
+ {
+ Load(guid);
+ }
+
+ async void Load(AddressableGUID assetGuid)
+ {
+ Debug.Log("Loading dynamic prefab on the client...");
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_ArtificialDelayMilliseconds);
+ Debug.Log("Client loaded dynamic prefab");
+ AcknowledgeSuccessfulPrefabLoadServerRpc(assetGuid.GetHashCode());
+ }
+ }
+
+ [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);
+
+ //the server has all the objects network-visible, no need to do anything
+ if (rpcParams.Receive.SenderClientId != m_NetworkManager.LocalClientId)
+ {
+ // Note: there's a potential security risk here if this technique is tied with gameplay that uses
+ // a NetworkObject's Show() and Hide() methods. For example, a malicious player could invoke a similar
+ // ServerRpc with the guids of enemy players, and it would make those enemies visible (network side
+ // and/or visually) to that player, giving them a potential advantage.
+ ShowHiddenObjectsToClient(prefabHash, rpcParams.Receive.SenderClientId);
+ }
+ }
+ }
+}
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility/ServerAuthoritativeSpawnDynamicPrefabUsingNetworkVisibility.cs.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility/ServerAuthoritativeSpawnDynamicPrefabUsingNetworkVisibility.cs.meta
new file mode 100644
index 000000000..2a72f98ef
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/04_Server Authoritative Spawn Dynamic Prefab Using Network Visibility/ServerAuthoritativeSpawnDynamicPrefabUsingNetworkVisibility.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a695c6b1fd4ab594e875d5dea4cc9fae
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases.meta
new file mode 100644
index 000000000..ebc0c6b87
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: af9f34bc59b364f4692a37f9d1325df0
+timeCreated: 1667392114
\ No newline at end of file
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases/APIPlaygroundShowcasingAllPostConnectionUseCases.cs b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases/APIPlaygroundShowcasingAllPostConnectionUseCases.cs
new file mode 100644
index 000000000..787dc1f57
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases/APIPlaygroundShowcasingAllPostConnectionUseCases.cs
@@ -0,0 +1,389 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Unity.Netcode;
+using UnityEngine;
+using UnityEngine.AddressableAssets;
+using Random = UnityEngine.Random;
+
+namespace Game.APIPlaygroundShowcasingAllPostConnectionUseCases
+{
+ ///
+ /// This class serves as the playground of the dynamic prefab loading use-cases. It integrates API 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, and 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).
+ ///
+ ///
+ /// For more details on the API usage, see the in-project readme (which includes links to further resources,
+ /// including the project's technical document).
+ ///
+ public sealed class APIPlaygroundShowcasingAllPostConnectionUseCases : NetworkBehaviour
+ {
+ [SerializeField]
+ NetworkManager m_NetworkManager;
+
+ [SerializeField] List m_DynamicPrefabReferences;
+
+ const int k_MaxConnectPayload = 1024;
+
+ [SerializeField]
+ int m_ArtificialDelayMilliseconds = 1000;
+
+ [SerializeField] float m_SpawnTimeoutInSeconds;
+
+ //A storage where we keep association between prefab (hash of it's GUID) and the spawned network objects that use it
+ Dictionary> m_PrefabHashToNetworkObjectId = new Dictionary>();
+
+ float m_SynchronousSpawnTimeoutTimer;
+
+ int m_SynchronousSpawnAckCount = 0;
+
+ 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.
+ m_NetworkManager.NetworkConfig.ForceSamePrefabs = false;
+ m_NetworkManager.ConnectionApprovalCallback += ConnectionApprovalCallback;
+ }
+
+ public override void OnDestroy()
+ {
+ m_NetworkManager.ConnectionApprovalCallback -= ConnectionApprovalCallback;
+ DynamicPrefabLoadingUtilities.UnloadAndReleaseAllDynamicPrefabs();
+ base.OnDestroy();
+ }
+
+ void ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
+ {
+ Debug.Log("Client is trying to connect " + request.ClientNetworkId);
+ var connectionData = request.Payload;
+ var clientId = request.ClientNetworkId;
+
+ if (clientId == m_NetworkManager.LocalClientId)
+ {
+ //allow the host to connect
+ Approve();
+ 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;
+ }
+
+ if (DynamicPrefabLoadingUtilities.LoadedPrefabCount == 0)
+ {
+ //immediately approve the connection if we haven't loaded any prefabs yet
+ Approve();
+ return;
+ }
+
+ var payload = System.Text.Encoding.UTF8.GetString(connectionData);
+ var connectionPayload = JsonUtility.FromJson(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;
+ }
+
+ // In order for clients to not just get disconnected with no feedback, the server needs to tell the client
+ // why it disconnected it. This could happen after an auth check on a service or because of gameplay
+ // reasons (server full, wrong build version, etc).
+ // The server can do so via the DisconnectReason in the ConnectionApprovalResponse. The guids of the prefabs
+ // the client will need to load will be sent, such that the client loads the needed prefabs, and reconnects.
+
+ // A note: DisconnectReason will not be written to if the string is too large in size. This should be used
+ // only to tell the client "why" it failed -- the client should instead use services like UGS to fetch the
+ // relevant data it needs to fetch & download.
+
+ DynamicPrefabLoadingUtilities.RefreshLoadedPrefabGuids();
+
+ response.Reason = DynamicPrefabLoadingUtilities.GenerateDisconnectionPayload();
+
+ ImmediateDeny();
+
+ // A note: sending large strings through Netcode is not ideal -- you'd usually want to use REST services to
+ // accomplish this instead. UGS services like Lobby can be a useful alternative. Another route may be to
+ // set ConnectionApprovalResponse's Pending flag to true, and send a CustomMessage containing the array of
+ // GUIDs to a client, which the client would load and reattempt a reconnection.
+
+ void Approve()
+ {
+ Debug.Log($"Client {clientId} approved");
+ response.Approved = true;
+ response.CreatePlayerObject = false; //we're not going to spawn a player object for this sample
+ }
+
+ void ImmediateDeny()
+ {
+ Debug.Log($"Client {clientId} denied connection");
+ response.Approved = false;
+ response.CreatePlayerObject = false;
+ }
+ }
+
+ // invoked by UI
+ public void OnClickedPreload()
+ {
+ if (!m_NetworkManager.IsServer)
+ {
+ return;
+ }
+
+ PreloadPrefabs();
+ }
+
+ // invoked by UI
+ public void OnClickedTrySpawnSynchronously()
+ {
+ if (!m_NetworkManager.IsServer)
+ {
+ return;
+ }
+
+ TrySpawnSynchronously();
+ }
+
+ // invoked by UI
+ public void OnClickedTrySpawnInvisible()
+ {
+ if (!m_NetworkManager.IsServer)
+ {
+ return;
+ }
+
+ TrySpawnInvisible();
+ }
+
+ async void PreloadPrefabs()
+ {
+ var tasks = new List();
+ foreach (var p in m_DynamicPrefabReferences)
+ {
+ tasks.Add(PreloadDynamicPrefabOnServerAndStartLoadingOnAllClients(p.AssetGUID));
+ }
+
+ await Task.WhenAll(tasks);
+ }
+
+ ///
+ /// This call preloads the dynamic prefab on the server and sends a client rpc to all the clients to do the same.
+ ///
+ ///
+ 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;
+ }
+
+ Debug.Log("Loading dynamic prefab on the clients...");
+ LoadAddressableClientRpc(assetGuid);
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_ArtificialDelayMilliseconds);
+ }
+ }
+
+ async void TrySpawnSynchronously()
+ {
+ var randomPrefab = m_DynamicPrefabReferences[Random.Range(0, m_DynamicPrefabReferences.Count)];
+ await TrySpawnDynamicPrefabSynchronously(randomPrefab.AssetGUID, Random.insideUnitCircle * 5, Quaternion.identity);
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ 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 = await Spawn(assetGuid);
+ return (true, obj);
+ }
+
+ m_SynchronousSpawnAckCount = 0;
+ m_SynchronousSpawnTimeoutTimer = 0;
+
+ Debug.Log("Loading dynamic prefab on the clients...");
+ LoadAddressableClientRpc(assetGuid);
+ //load the prefab on the server, so that any late-joiner will need to load that prefab also
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_ArtificialDelayMilliseconds);
+ var requiredAcknowledgementsCount = IsHost ? m_NetworkManager.ConnectedClients.Count - 1 :
+ m_NetworkManager.ConnectedClients.Count;
+
+ while (m_SynchronousSpawnTimeoutTimer < m_SpawnTimeoutInSeconds)
+ {
+ if (m_SynchronousSpawnAckCount >= requiredAcknowledgementsCount)
+ {
+ Debug.Log($"All clients have loaded the prefab in {m_SynchronousSpawnTimeoutTimer} seconds, spawning the prefab on the server...");
+ var obj = await Spawn(assetGuid);
+ return (true, obj);
+ }
+
+ m_SynchronousSpawnTimeoutTimer += Time.deltaTime;
+ await Task.Yield();
+ }
+
+ Debug.LogError("Failed to spawn dynamic prefab - timeout");
+ return (false, null);
+ }
+
+ return (false, null);
+
+ async Task Spawn(AddressableGUID assetGuid)
+ {
+ var prefab = await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid,
+ m_ArtificialDelayMilliseconds);
+ var obj = Instantiate(prefab, position, rotation).GetComponent();
+ obj.Spawn();
+ Debug.Log("Spawned dynamic prefab");
+ return obj;
+ }
+ }
+
+ async void TrySpawnInvisible()
+ {
+ var randomPrefab = m_DynamicPrefabReferences[Random.Range(0, m_DynamicPrefabReferences.Count)];
+ await SpawnImmediatelyAndHideUntilPrefabIsLoadedOnClient(randomPrefab.AssetGUID, Random.insideUnitCircle * 5, Quaternion.identity);
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ async Task SpawnImmediatelyAndHideUntilPrefabIsLoadedOnClient(string guid, Vector3 position, Quaternion rotation)
+ {
+ if (IsServer)
+ {
+ var assetGuid = new AddressableGUID()
+ {
+ Value = guid
+ };
+
+ return await Spawn(assetGuid);
+ }
+
+ return null;
+
+ async Task Spawn(AddressableGUID assetGuid)
+ {
+ var prefab = await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid,
+ m_ArtificialDelayMilliseconds);
+ var obj = Instantiate(prefab, position, rotation).GetComponent();
+
+ if (m_PrefabHashToNetworkObjectId.TryGetValue(assetGuid.GetHashCode(), out var networkObjectIds))
+ {
+ networkObjectIds.Add(obj);
+ }
+ else
+ {
+ m_PrefabHashToNetworkObjectId.Add(assetGuid.GetHashCode(), new HashSet() {obj});
+ }
+
+ obj.CheckObjectVisibility = (clientId) =>
+ {
+ //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;
+ }
+ //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;
+ }
+ }
+
+ void ShowHiddenObjectsToClient(int prefabHash, ulong clientId)
+ {
+ if (m_PrefabHashToNetworkObjectId.TryGetValue(prefabHash, out var networkObjects))
+ {
+ foreach (var obj in networkObjects)
+ {
+ if (!obj.IsNetworkVisibleTo(clientId))
+ {
+ obj.NetworkShow(clientId);
+ }
+ }
+ }
+ }
+
+ [ClientRpc]
+ void LoadAddressableClientRpc(AddressableGUID guid, ClientRpcParams rpcParams = default)
+ {
+ if (!IsHost)
+ {
+ Load(guid);
+ }
+
+ async void Load(AddressableGUID assetGuid)
+ {
+ Debug.Log("Loading dynamic prefab on the client...");
+ await DynamicPrefabLoadingUtilities.LoadDynamicPrefab(assetGuid, m_ArtificialDelayMilliseconds);
+ Debug.Log("Client loaded dynamic prefab");
+ AcknowledgeSuccessfulPrefabLoadServerRpc(assetGuid.GetHashCode());
+ }
+ }
+
+ [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);
+
+ //the server has all the objects network-visible, no need to do anything
+ if (rpcParams.Receive.SenderClientId != m_NetworkManager.LocalClientId)
+ {
+ ShowHiddenObjectsToClient(prefabHash, rpcParams.Receive.SenderClientId);
+ }
+ }
+ }
+}
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases/APIPlaygroundShowcasingAllPostConnectionUseCases.cs.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases/APIPlaygroundShowcasingAllPostConnectionUseCases.cs.meta
new file mode 100644
index 000000000..761069d4b
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/05_API Playground Showcasing All Post-Connection Time Use-Cases/APIPlaygroundShowcasingAllPostConnectionUseCases.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 06f0d49321026254cbd9420e0a8a1f38
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/UI.meta b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/UI.meta
new file mode 100644
index 000000000..3589345a2
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/UI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 166e05c1929e92d438c74d3795c68a06
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/UI/IPMenuUI.cs b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/UI/IPMenuUI.cs
new file mode 100644
index 000000000..4ca08e467
--- /dev/null
+++ b/Basic/DynamicAddressablesNetworkPrefabs/Assets/Scripts/UI/IPMenuUI.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Text.RegularExpressions;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace Game.UI
+{
+ public class IPMenuUI : MonoBehaviour
+ {
+ // UI Documents
+ [SerializeField]
+ UIDocument m_IPMenuUIDocument;
+ [SerializeField]
+ UIDocument m_ConnectionTypeUIDocument;
+
+ // UI Roots
+ VisualElement m_IPMenuUIRoot;
+ VisualElement m_ConnectionTypeUIRoot;
+
+ // UI Elements
+ TextField m_IPInputField;
+ TextField m_PortInputField;
+ Button m_ButtonHost;
+ Button m_ButtonServer;
+ Button m_ButtonClient;
+ Button m_ButtonDisconnect;
+ Label m_ConnectionTypeLabel;
+
+ public string IpAddress { get; private set; } = "127.0.0.1";
+ public ushort Port { get; private set; } = 9998;
+
+ void Awake()
+ {
+ SetupIPInputUI();
+
+ // register UI elements to methods using callbacks for when they're clicked
+ m_ButtonHost.clickable.clicked += HostStarted;
+ m_ButtonServer.clickable.clicked += ServerStarted;
+ m_ButtonClient.clickable.clicked += ClientStarted;
+ m_ButtonDisconnect.clickable.clicked += OnShutdownRequested;
+ m_IPInputField.RegisterValueChangedCallback(OnIpAddressChanged);
+ m_PortInputField.RegisterValueChangedCallback(OnPortChanged);
+ }
+
+ void OnDestroy()
+ {
+ // un-register UI elements from methods using callbacks for when they're clicked
+ m_ButtonHost.clickable.clicked -= HostStarted;
+ m_ButtonServer.clickable.clicked -= ServerStarted;
+ m_ButtonClient.clickable.clicked -= ClientStarted;
+ m_ButtonDisconnect.clickable.clicked -= OnShutdownRequested;
+ m_IPInputField.UnregisterValueChangedCallback(OnIpAddressChanged);
+ m_PortInputField.UnregisterValueChangedCallback(OnPortChanged);
+ }
+
+ void Start()
+ {
+ ResetUI();
+ m_IPInputField.value = IpAddress;
+ m_PortInputField.value = Port.ToString();
+ }
+
+ void HostStarted()
+ {
+ SwitchToInGameUI("Host");
+ }
+
+ void ClientStarted()
+ {
+ SwitchToInGameUI("Client");
+ }
+
+ void ServerStarted()
+ {
+ SwitchToInGameUI("Server");
+ }
+
+ void SwitchToInGameUI(string connectionType)
+ {
+ SetUIElementVisibility(m_IPMenuUIRoot, false);
+ SetUIElementVisibility(m_ConnectionTypeUIRoot, true);
+ m_ConnectionTypeLabel.text = connectionType;
+ }
+
+ void OnShutdownRequested()
+ {
+ ResetUI();
+ }
+
+ void OnClientDisconnect(ulong clientId)
+ {
+ ResetUI();
+ }
+
+ void ResetUI()
+ {
+ SetUIElementVisibility(m_IPMenuUIRoot, true);
+ SetUIElementVisibility(m_ConnectionTypeUIRoot, false);
+ }
+
+ void OnIpAddressChanged(ChangeEvent ipAddress)
+ {
+ SanitizeAndSetIpAddress(ipAddress.newValue);
+ }
+
+ void OnPortChanged(ChangeEvent port)
+ {
+ SanitizeAndSetPort(port.newValue);
+ }
+
+ void SanitizeAndSetPort(string portToSanitize)
+ {
+ var sanitizedPort = Sanitize(portToSanitize);
+ m_PortInputField.value = sanitizedPort;
+ Debug.Log($"sanitized port = {sanitizedPort}");
+ ushort.TryParse(sanitizedPort, out var parsedPort);
+ Port = parsedPort;
+ Debug.Log($"parsed port = {parsedPort}");
+ m_PortInputField.value = Port.ToString();
+ Debug.Log(Port.ToString());
+ }
+
+ void SanitizeAndSetIpAddress(string ipAddressToSanitize)
+ {
+ IpAddress = Sanitize(ipAddressToSanitize);
+ Debug.Log(IpAddress);
+ m_IPInputField.value = IpAddress;
+ }
+
+ ///
+ /// Sanitize user port InputField box allowing only alphanumerics and '.'
+ ///
+ /// string to sanitize.
+ /// Sanitized text string.
+ string Sanitize(string stringToBeSanitized)
+ {
+ return Regex.Replace(stringToBeSanitized, "[^A-Za-z0-9.]", "");
+ }
+
+ void SetUIElementVisibility(VisualElement element, bool isVisible)
+ {
+ element.style.display = isVisible ? DisplayStyle.Flex : DisplayStyle.None;
+ }
+
+ void SetupIPInputUI()
+ {
+ m_IPMenuUIRoot = m_IPMenuUIDocument.rootVisualElement;
+ m_ConnectionTypeUIRoot = m_ConnectionTypeUIDocument.rootVisualElement;
+ m_IPInputField = m_IPMenuUIRoot.Q("IPAddressField");
+ m_PortInputField = m_IPMenuUIRoot.Q("PortField");
+ m_ButtonHost = m_IPMenuUIRoot.Q