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