diff --git a/.yamato/mobile-build-and-run.yml b/.yamato/mobile-build-and-run.yml index 332083314..94865b87c 100644 --- a/.yamato/mobile-build-and-run.yml +++ b/.yamato/mobile-build-and-run.yml @@ -13,8 +13,8 @@ Build_Player_With_Tests_iOS_{{ project.name }}_{{ editor }}: flavor: b1.large commands: - - pip install unity-downloader-cli --index-url https://artifactory.prd.it.unity3d.com/artifactory/api/pypi/pypi/simple --upgrade - - unity-downloader-cli -c Editor -c iOS -u {{ editor }} --fast --wait + - pip install unity-downloader-cli==1.2.0 --index-url https://artifactory.prd.it.unity3d.com/artifactory/api/pypi/pypi/simple --upgrade + - unity-downloader-cli -c Editor -c iOS -u 2021.3.15f1 --fast --wait - curl -s https://artifactory.prd.it.unity3d.com/artifactory/unity-tools-local/utr-standalone/utr --output utr - chmod +x ./utr - ./utr --suite=playmode --platform=iOS --editor-location=.Editor --testproject={{ project.path }} --player-save-path=build/players --artifacts_path=build/logs --build-only --testfilter=Unity.BossRoom.Tests.Runtime @@ -37,12 +37,12 @@ Build_Player_With_Tests_Android_{{ project.name }}_{{ editor }}: type: Unity::VM # Any generic image can be used, no need to have Android tools in the image for building # All Android tools will be downloaded by unity-downloader-cli - image: desktop/android-execution-r19:v0.1.1-860408 + image: mobile/android-execution-base:stable flavor: b1.xlarge commands: # Download unity-downloader-cli - - pip install unity-downloader-cli --index-url https://artifactory.prd.it.unity3d.com/artifactory/api/pypi/pypi/simple --upgrade + - pip install unity-downloader-cli==1.2.0 --index-url https://artifactory.prd.it.unity3d.com/artifactory/api/pypi/pypi/simple --upgrade - curl -s https://artifactory.prd.it.unity3d.com/artifactory/unity-tools/utr-standalone/utr.bat --output utr.bat - python .yamato/disable-burst-if-requested.py --project-path {{ project.path }} --platform Android - unity-downloader-cli -c Editor -c Android -u {{ editor }} --fast --wait @@ -103,7 +103,7 @@ mobile_test_android_{{ project.name }}_{{ editor }}: name: {{ project.name }} mobile project tests - {{ editor }} on Android agent: type: Unity::mobile::shield - image: mobile/android-execution-r19:stable + image: mobile/android-execution-base:stable flavor: b1.medium # Skip repository cloning @@ -119,7 +119,7 @@ mobile_test_android_{{ project.name }}_{{ editor }}: start %ANDROID_SDK_ROOT%\platform-tools\adb.exe connect %BOKKEN_DEVICE_IP% start %ANDROID_SDK_ROOT%\platform-tools\adb.exe devices set UTR_VERSION=0.12.0 - ./utr --artifacts_path=build/test-results --testproject={{ project.path }} --editor-location=.Editor --reruncount=2 --suite=playmode --platform=android --player-connection-ip=%BOKKEN_HOST_IP% --player-load-path=build/players --testfilter=Unity.BossRoom.Tests.Runtime + ./utr --artifacts_path=build/test-results --testproject={{ project.path }} --editor-location=.Editor --reruncount=2 --suite=playmode --platform=android --player-load-path=build/players --testfilter=Unity.BossRoom.Tests.Runtime # Set uploadable artifact paths artifacts: logs: diff --git a/Assets/GameData/NetworkPrefabs.asset b/Assets/GameData/NetworkPrefabs.asset new file mode 100644 index 000000000..cfe63dfcf --- /dev/null +++ b/Assets/GameData/NetworkPrefabs.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1602e4cff482bcacf4eaac104fe12fe5d46e71c871a94415b96ada086f44e71 +size 2790 diff --git a/Assets/GameData/NetworkPrefabs.asset.meta b/Assets/GameData/NetworkPrefabs.asset.meta new file mode 100644 index 000000000..acb4a57b5 --- /dev/null +++ b/Assets/GameData/NetworkPrefabs.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 67e4325119a857f48967fab772faf1d7 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/CharGFX/PlayerGraphics_Rogue.prefab b/Assets/Prefabs/CharGFX/PlayerGraphics_Rogue.prefab index 2e9bdc6be..f7a314b86 100644 --- a/Assets/Prefabs/CharGFX/PlayerGraphics_Rogue.prefab +++ b/Assets/Prefabs/CharGFX/PlayerGraphics_Rogue.prefab @@ -130,16 +130,6 @@ PrefabInstance: objectReference: {fileID: 11400000, guid: 23c3465e22da67c4e812c4faf3adb1cb, type: 2} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} ---- !u!1 &8385572559299522089 stripped -GameObject: - m_CorrespondingSourceObject: {fileID: 3736552308919084700, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} - m_PrefabInstance: {fileID: 5153647769164318901} - m_PrefabAsset: {fileID: 0} ---- !u!4 &9157042335656178835 stripped -Transform: - m_CorrespondingSourceObject: {fileID: 4076098699203836966, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} - m_PrefabInstance: {fileID: 5153647769164318901} - m_PrefabAsset: {fileID: 0} --- !u!82 &3541643525863551320 stripped AudioSource: m_CorrespondingSourceObject: {fileID: -674663276945163795, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} @@ -150,6 +140,11 @@ AudioSource: m_CorrespondingSourceObject: {fileID: 8195155732997438405, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} m_PrefabInstance: {fileID: 5153647769164318901} m_PrefabAsset: {fileID: 0} +--- !u!1 &8385572559299522089 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 3736552308919084700, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} + m_PrefabInstance: {fileID: 5153647769164318901} + m_PrefabAsset: {fileID: 0} --- !u!114 &1406736000558620622 MonoBehaviour: m_ObjectHideFlags: 0 @@ -259,7 +254,7 @@ MonoBehaviour: m_SoundEffect: {fileID: 8300000, guid: ef1b245877a39b94d86a631867e20a61, type: 3} m_SoundStartDelaySeconds: 0 m_VolumeMultiplier: 1 - m_LoopSound: 1 + m_LoopSound: 0 - m_AnimatorNodeName: Buff1 m_AnimatorNodeNameHash: -1764501741 m_Prefab: {fileID: 0} @@ -276,4 +271,9 @@ MonoBehaviour: - {fileID: 3541643525863551320} - {fileID: 3908673605568263024} m_Animator: {fileID: 0} - m_ClientCharacterVisualization: {fileID: 0} + m_ClientCharacter: {fileID: 0} +--- !u!4 &9157042335656178835 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 4076098699203836966, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} + m_PrefabInstance: {fileID: 5153647769164318901} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/Prefabs/CharGFX/PlayerGraphics_Tank.prefab b/Assets/Prefabs/CharGFX/PlayerGraphics_Tank.prefab index fdfb27f83..7e801ebe9 100644 --- a/Assets/Prefabs/CharGFX/PlayerGraphics_Tank.prefab +++ b/Assets/Prefabs/CharGFX/PlayerGraphics_Tank.prefab @@ -73,16 +73,6 @@ PrefabInstance: objectReference: {fileID: 11400000, guid: 23c3465e22da67c4e812c4faf3adb1cb, type: 2} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} ---- !u!1 &8385572559299522089 stripped -GameObject: - m_CorrespondingSourceObject: {fileID: 3736552308919084700, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} - m_PrefabInstance: {fileID: 5153647769164318901} - m_PrefabAsset: {fileID: 0} ---- !u!4 &9157042335656178835 stripped -Transform: - m_CorrespondingSourceObject: {fileID: 4076098699203836966, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} - m_PrefabInstance: {fileID: 5153647769164318901} - m_PrefabAsset: {fileID: 0} --- !u!82 &3541643525863551320 stripped AudioSource: m_CorrespondingSourceObject: {fileID: -674663276945163795, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} @@ -93,6 +83,11 @@ AudioSource: m_CorrespondingSourceObject: {fileID: 8195155732997438405, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} m_PrefabInstance: {fileID: 5153647769164318901} m_PrefabAsset: {fileID: 0} +--- !u!1 &8385572559299522089 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 3736552308919084700, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} + m_PrefabInstance: {fileID: 5153647769164318901} + m_PrefabAsset: {fileID: 0} --- !u!114 &1781430389835594798 MonoBehaviour: m_ObjectHideFlags: 0 @@ -202,12 +197,17 @@ MonoBehaviour: m_SoundEffect: {fileID: 8300000, guid: 5ef809d665d13b245b559cd6170f5794, type: 3} m_SoundStartDelaySeconds: 0 m_VolumeMultiplier: 1 - m_LoopSound: 1 + m_LoopSound: 0 m_AudioSources: - {fileID: 3541643525863551320} - {fileID: 3908673605568263024} m_Animator: {fileID: 0} - m_ClientCharacterVisualization: {fileID: 0} + m_ClientCharacter: {fileID: 0} +--- !u!4 &9157042335656178835 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 4076098699203836966, guid: d396ab139e993ee43b2eb29978bba8ff, type: 3} + m_PrefabInstance: {fileID: 5153647769164318901} + m_PrefabAsset: {fileID: 0} --- !u!1001 &5238530588286883888 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/Game/EnemySpawner.prefab b/Assets/Prefabs/Game/EnemySpawner.prefab index 9d776c394..0e18d957f 100644 --- a/Assets/Prefabs/Game/EnemySpawner.prefab +++ b/Assets/Prefabs/Game/EnemySpawner.prefab @@ -466,8 +466,8 @@ MonoBehaviour: m_EditorClassIdentifier: m_BreakableElements: [] m_DormantCooldown: 180 - IsBroken: - m_InternalValue: 0 + m_Breakable: {fileID: 5343699662503375493} + m_WaveSpawner: {fileID: 4844841199312666291} --- !u!114 &5343699662503375493 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/NetworkingManager.prefab b/Assets/Prefabs/NetworkingManager.prefab index 93f7165ce..e05f071b1 100644 --- a/Assets/Prefabs/NetworkingManager.prefab +++ b/Assets/Prefabs/NetworkingManager.prefab @@ -51,62 +51,9 @@ MonoBehaviour: ProtocolVersion: 0 NetworkTransport: {fileID: 8549047561508999566} PlayerPrefab: {fileID: 4927145850774787080, guid: 1d3f5528d25661949890bcd7f47fe81a, type: 3} - NetworkPrefabs: - - Override: 0 - Prefab: {fileID: 6009713983291384756, guid: 8237adf32a9b6de4892e6febe6b4bdef, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 3713729372785093424, guid: 6cdd52f1fa2ed34469a487ae6477eded, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 3688950541947916333, guid: 365e94337fd10fe4ebde1906df413ac7, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 2842198241268549130, guid: 30c420f004b8f6445ad2bdb2addb234a, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 2842198241268549130, guid: 7e3b8103f5622f64fa677352730f295c, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 2842198241268549130, guid: 411974b75a8b43d4e9b3c9069a5067fb, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 2842198241268549130, guid: 0251e08eeed89e844a8527b3a7874cc2, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 176558388678216176, guid: 98fafd094d0c0fa41abe5c3322251839, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 3106828016798330210, guid: 5c107a985e30aa2469a62ecf015d43a8, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 5473352307376472481, guid: 3e5c32e5766633a4eaf9e7c393418b34, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} - - Override: 0 - Prefab: {fileID: 1583543423304314434, guid: 4c1a321755b60c54099d0402be05fa2e, type: 3} - SourcePrefabToOverride: {fileID: 0} - SourceHashToOverride: 0 - OverridingTargetPrefab: {fileID: 0} + Prefabs: + NetworkPrefabsLists: + - {fileID: 11400000, guid: 67e4325119a857f48967fab772faf1d7, type: 2} TickRate: 30 ClientConnectionBufferTimeout: 5 ConnectionApproval: 1 @@ -122,6 +69,7 @@ MonoBehaviour: LoadSceneTimeOut: 20 SpawnTimeout: 1 EnableNetworkLogs: 1 + OldPrefabList: [] --- !u!114 &8549047561508999566 MonoBehaviour: m_ObjectHideFlags: 0 @@ -144,7 +92,7 @@ MonoBehaviour: ConnectionData: Address: 127.0.0.1 Port: 7777 - ServerListenAddress: + ServerListenAddress: 127.0.0.1 DebugSimulator: PacketDelayMS: 0 PacketJitterMS: 0 diff --git a/Assets/Prefabs/UI/IPPopup.prefab b/Assets/Prefabs/UI/IPPopup.prefab index 396108747..6de763413 100644 --- a/Assets/Prefabs/UI/IPPopup.prefab +++ b/Assets/Prefabs/UI/IPPopup.prefab @@ -173,7 +173,7 @@ MonoBehaviour: m_faceColor: serializedVersion: 2 rgba: 4294967295 - m_fontSize: 20.3 + m_fontSize: 33 m_fontSizeBase: 36 m_fontWeight: 400 m_enableAutoSizing: 1 @@ -785,7 +785,7 @@ MonoBehaviour: m_faceColor: serializedVersion: 2 rgba: 4294967295 - m_fontSize: 32.2 + m_fontSize: 50 m_fontSizeBase: 36 m_fontWeight: 400 m_enableAutoSizing: 1 @@ -935,6 +935,7 @@ MonoBehaviour: m_IPInputField: {fileID: 783666621484907260} m_PortInputField: {fileID: 3692047279709044436} m_CanvasGroup: {fileID: 3432270648822068983} + m_HostButton: {fileID: 8503688101831781139} --- !u!1 &2513356161705610835 GameObject: m_ObjectHideFlags: 0 @@ -2211,7 +2212,7 @@ MonoBehaviour: m_faceColor: serializedVersion: 2 rgba: 4294967295 - m_fontSize: 32.2 + m_fontSize: 50 m_fontSizeBase: 36 m_fontWeight: 400 m_enableAutoSizing: 1 @@ -3287,6 +3288,7 @@ MonoBehaviour: m_CanvasGroup: {fileID: 6846323567751854231} m_IPInputField: {fileID: 2677382141616317261} m_PortInputField: {fileID: 7282211495594724544} + m_JoinButton: {fileID: 8754602378570439514} --- !u!1 &5924530127146065184 GameObject: m_ObjectHideFlags: 0 @@ -3577,7 +3579,7 @@ MonoBehaviour: m_faceColor: serializedVersion: 2 rgba: 4294967295 - m_fontSize: 20.3 + m_fontSize: 33 m_fontSizeBase: 36 m_fontWeight: 400 m_enableAutoSizing: 1 diff --git a/Assets/Scripts/ConnectionManagement/ConnectionManager.cs b/Assets/Scripts/ConnectionManagement/ConnectionManager.cs index b2b2fa42c..22068b375 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionManager.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Unity.BossRoom.Utils; -using Unity.Collections; using Unity.Netcode; using UnityEngine; using UUnity.BossRoom.ConnectionManagement; @@ -99,6 +98,7 @@ void Start() NetworkManager.OnServerStarted += OnServerStarted; NetworkManager.ConnectionApprovalCallback += ApprovalCheck; NetworkManager.OnTransportFailure += OnTransportFailure; + NetworkManager.OnServerStopped += OnServerStopped; } void OnDestroy() @@ -108,6 +108,7 @@ void OnDestroy() NetworkManager.OnServerStarted -= OnServerStarted; NetworkManager.ConnectionApprovalCallback -= ApprovalCheck; NetworkManager.OnTransportFailure -= OnTransportFailure; + NetworkManager.OnServerStopped -= OnServerStopped; } internal void ChangeState(ConnectionState nextState) @@ -147,6 +148,11 @@ void OnTransportFailure() m_CurrentState.OnTransportFailure(); } + void OnServerStopped(bool _) // we don't need this parameter as the ConnectionState already carries the relevant information + { + m_CurrentState.OnServerStopped(); + } + public void StartClientLobby(string playerName) { m_CurrentState.StartClientLobby(playerName); diff --git a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs index 4ac0cbac0..9e56ecf0d 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs @@ -21,11 +21,30 @@ public abstract class ConnectionMethodBase protected ConnectionManager m_ConnectionManager; readonly ProfileManager m_ProfileManager; protected readonly string m_PlayerName; + protected const string k_DtlsConnType = "dtls"; + /// + /// Setup the host connection prior to starting the NetworkManager + /// + /// public abstract Task SetupHostConnectionAsync(); + + /// + /// Setup the client connection prior to starting the NetworkManager + /// + /// public abstract Task SetupClientConnectionAsync(); + /// + /// Setup the client for reconnection prior to reconnecting + /// + /// + /// success = true if succeeded in setting up reconnection, false if failed. + /// shouldTryAgain = true if we should try again after failing, false if not. + /// + public abstract Task<(bool success, bool shouldTryAgain)> SetupClientReconnectionAsync(); + public ConnectionMethodBase(ConnectionManager connectionManager, ProfileManager profileManager, string playerName) { m_ConnectionManager = connectionManager; @@ -47,6 +66,13 @@ protected void SetConnectionPayload(string playerId, string playerName) m_ConnectionManager.NetworkManager.NetworkConfig.ConnectionData = payloadBytes; } + /// Using authentication, this makes sure your session is associated with your account and not your device. This means you could reconnect + /// from a different device for example. A playerId is also a bit more permanent than player prefs. In a browser for example, + /// player prefs can be cleared as easily as cookies. + /// The forked flow here is for debug purposes and to make UGS optional in Boss Room. This way you can study the sample without + /// setting up a UGS account. It's recommended to investigate your own initialization and IsSigned flows to see if you need + /// those checks on your own and react accordingly. We offer here the option for offline access for debug purposes, but in your own game you + /// might want to show an error popup and ask your player to connect to the internet. protected string GetPlayerId() { if (Services.Core.UnityServices.State != ServicesInitializationState.Initialized) @@ -81,6 +107,12 @@ public override async Task SetupClientConnectionAsync() utp.SetConnectionData(m_Ipaddress, m_Port); } + public override async Task<(bool success, bool shouldTryAgain)> SetupClientReconnectionAsync() + { + // Nothing to do here + return (true, true); + } + public override async Task SetupHostConnectionAsync() { SetConnectionPayload(GetPlayerId(), m_PlayerName); // Need to set connection payload for host as well, as host is a client too @@ -90,7 +122,7 @@ public override async Task SetupHostConnectionAsync() } /// - /// UTP's Relay connection setup + /// UTP's Relay connection setup using the Lobby integration /// class ConnectionMethodRelay : ConnectionMethodBase { @@ -128,7 +160,26 @@ public override async Task SetupClientConnectionAsync() // Configure UTP with allocation var utp = (UnityTransport)m_ConnectionManager.NetworkManager.NetworkConfig.NetworkTransport; - utp.SetRelayServerData(new RelayServerData(joinedAllocation, OnlineState.k_DtlsConnType)); + utp.SetRelayServerData(new RelayServerData(joinedAllocation, k_DtlsConnType)); + } + + public override async Task<(bool success, bool shouldTryAgain)> SetupClientReconnectionAsync() + { + if (m_LobbyServiceFacade.CurrentUnityLobby == null) + { + Debug.Log("Lobby does not exist anymore, stopping reconnection attempts."); + return (false, false); + } + + // When using Lobby with Relay, if a user is disconnected from the Relay server, the server will notify the + // Lobby service and mark the user as disconnected, but will not remove them from the lobby. They then have + // some time to attempt to reconnect (defined by the "Disconnect removal time" parameter on the dashboard), + // after which they will be removed from the lobby completely. + // See https://docs.unity.com/lobby/reconnect-to-lobby.html + var lobby = await m_LobbyServiceFacade.ReconnectToLobbyAsync(); + var success = lobby != null; + Debug.Log(success ? "Successfully reconnected to Lobby." : "Failed to reconnect to Lobby."); + return (success, true); // return a success if reconnecting to lobby returns a lobby } public override async Task SetupHostConnectionAsync() @@ -152,7 +203,7 @@ public override async Task SetupHostConnectionAsync() // Setup UTP with relay connection info var utp = (UnityTransport)m_ConnectionManager.NetworkManager.NetworkConfig.NetworkTransport; - utp.SetRelayServerData(new RelayServerData(hostAllocation, OnlineState.k_DtlsConnType)); // This is with DTLS enabled for a secure connection + utp.SetRelayServerData(new RelayServerData(hostAllocation, k_DtlsConnType)); // This is with DTLS enabled for a secure connection } } } diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectedState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectedState.cs index a87bce2ca..32b1f503f 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectedState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectedState.cs @@ -9,7 +9,7 @@ namespace Unity.BossRoom.ConnectionManagement /// Connection state corresponding to a connected client. When being disconnected, transitions to the /// ClientReconnecting state if no reason is given, or to the Offline state. /// - class ClientConnectedState : ConnectionState + class ClientConnectedState : OnlineState { [Inject] protected LobbyServiceFacade m_LobbyServiceFacade; @@ -39,11 +39,5 @@ public override void OnClientDisconnect(ulong _) m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); } } - - public override void OnUserRequestedShutdown() - { - m_ConnectStatusPublisher.Publish(ConnectStatus.UserRequestedDisconnect); - m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); - } } } diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs index d8b7106db..fc2fd12e4 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs @@ -1,9 +1,6 @@ using System; using System.Threading.Tasks; -using Unity.BossRoom.UnityServices.Lobbies; -using Unity.Multiplayer.Samples.Utilities; using UnityEngine; -using VContainer; namespace Unity.BossRoom.ConnectionManagement { @@ -13,11 +10,7 @@ namespace Unity.BossRoom.ConnectionManagement /// class ClientConnectingState : OnlineState { - [Inject] - protected LobbyServiceFacade m_LobbyServiceFacade; - [Inject] - protected LocalLobby m_LocalLobby; - ConnectionMethodBase m_ConnectionMethod; + protected ConnectionMethodBase m_ConnectionMethod; public ClientConnectingState Configure(ConnectionMethodBase baseConnectionMethod) { @@ -43,10 +36,10 @@ public override void OnClientConnected(ulong _) public override void OnClientDisconnect(ulong _) { // client ID is for sure ours here - StartingClientFailedAsync(); + StartingClientFailed(); } - protected void StartingClientFailedAsync() + void StartingClientFailed() { var disconnectReason = m_ConnectionManager.NetworkManager.DisconnectReason; if (string.IsNullOrEmpty(disconnectReason)) @@ -74,14 +67,12 @@ internal async Task ConnectClientAsync() { throw new Exception("NetworkManager StartClient failed"); } - - SceneLoaderWrapper.Instance.AddOnSceneEventCallback(); } catch (Exception e) { Debug.LogError("Error connecting client, see following exception"); Debug.LogException(e); - StartingClientFailedAsync(); + StartingClientFailed(); throw; } } diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs index e6e47ff42..3b7d0397e 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs @@ -19,15 +19,14 @@ class ClientReconnectingState : ClientConnectingState IPublisher m_ReconnectMessagePublisher; Coroutine m_ReconnectCoroutine; - string m_LobbyCode = ""; int m_NbAttempts; + const float k_TimeBeforeFirstAttempt = 1; const float k_TimeBetweenAttempts = 5; public override void Enter() { m_NbAttempts = 0; - m_LobbyCode = m_LobbyServiceFacade.CurrentUnityLobby != null ? m_LobbyServiceFacade.CurrentUnityLobby.LobbyCode : ""; m_ReconnectCoroutine = m_ConnectionManager.StartCoroutine(ReconnectCoroutine()); } @@ -107,37 +106,35 @@ IEnumerator ReconnectCoroutine() yield return new WaitWhile(() => m_ConnectionManager.NetworkManager.ShutdownInProgress); // wait until NetworkManager completes shutting down Debug.Log($"Reconnecting attempt {m_NbAttempts + 1}/{m_ConnectionManager.NbReconnectAttempts}..."); m_ReconnectMessagePublisher.Publish(new ReconnectMessage(m_NbAttempts, m_ConnectionManager.NbReconnectAttempts)); - m_NbAttempts++; - if (!string.IsNullOrEmpty(m_LobbyCode)) // Attempting to reconnect to lobby. - { - // When using Lobby with Relay, if a user is disconnected from the Relay server, the server will notify - // the Lobby service and mark the user as disconnected, but will not remove them from the lobby. They - // then have some time to attempt to reconnect (defined by the "Disconnect removal time" parameter on - // the dashboard), after which they will be removed from the lobby completely. - // See https://docs.unity.com/lobby/reconnect-to-lobby.html - var reconnectingToLobby = m_LobbyServiceFacade.ReconnectToLobbyAsync(m_LocalLobby?.LobbyID); - yield return new WaitUntil(() => reconnectingToLobby.IsCompleted); - // If succeeded, attempt to connect to Relay - if (!reconnectingToLobby.IsFaulted && reconnectingToLobby.Result != null) - { - // If this fails, the OnClientDisconnect callback will be invoked by Netcode - var connectingToRelay = ConnectClientAsync(); - yield return new WaitUntil(() => connectingToRelay.IsCompleted); - } - else - { - Debug.Log("Failed reconnecting to lobby."); - // Calling OnClientDisconnect to mark this attempt as failed and either start a new one or give up - // and return to the Offline state - OnClientDisconnect(0); - } + // If first attempt, wait some time before attempting to reconnect to give time to services to update + // (i.e. if in a Lobby and the host shuts down unexpectedly, this will give enough time for the lobby to be + // properly deleted so that we don't reconnect to an empty lobby + if (m_NbAttempts == 0) + { + yield return new WaitForSeconds(k_TimeBeforeFirstAttempt); } - else // If not using Lobby, simply try to reconnect to the server directly + + m_NbAttempts++; + var reconnectingSetupTask = m_ConnectionMethod.SetupClientReconnectionAsync(); + yield return new WaitUntil(() => reconnectingSetupTask.IsCompleted); + + if (!reconnectingSetupTask.IsFaulted && reconnectingSetupTask.Result.success) { // If this fails, the OnClientDisconnect callback will be invoked by Netcode - var connectingClient = ConnectClientAsync(); - yield return new WaitUntil(() => connectingClient.IsCompleted); + var connectingTask = ConnectClientAsync(); + yield return new WaitUntil(() => connectingTask.IsCompleted); + } + else + { + if (!reconnectingSetupTask.Result.shouldTryAgain) + { + // setting number of attempts to max so no new attempts are made + m_NbAttempts = m_ConnectionManager.NbReconnectAttempts; + } + // Calling OnClientDisconnect to mark this attempt as failed and either start a new one or give up + // and return to the Offline state + OnClientDisconnect(0); } } } diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/ConnectionState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ConnectionState.cs index 44251e45a..eed9a87da 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ConnectionState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ConnectionState.cs @@ -39,5 +39,7 @@ public virtual void OnUserRequestedShutdown() { } public virtual void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response) { } public virtual void OnTransportFailure() { } + + public virtual void OnServerStopped() { } } } diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs index daf1bcd58..60c575377 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using Unity.BossRoom.Infrastructure; using Unity.BossRoom.UnityServices.Lobbies; using Unity.Multiplayer.Samples.BossRoom; @@ -26,8 +25,6 @@ class HostingState : OnlineState public override void Enter() { - SceneLoaderWrapper.Instance.AddOnSceneEventCallback(); - //The "BossRoom" server always advances to CharSelect immediately on start. Different games //may do this differently. SceneLoaderWrapper.Instance.LoadScene("CharSelect", useNetworkSceneManager: true); @@ -50,11 +47,7 @@ public override void OnClientConnected(ulong clientId) public override void OnClientDisconnect(ulong clientId) { - if (clientId == m_ConnectionManager.NetworkManager.LocalClientId) - { - m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); - } - else + if (clientId != m_ConnectionManager.NetworkManager.LocalClientId) { var playerId = SessionManager.Instance.GetPlayerId(clientId); if (playerId != null) @@ -83,6 +76,12 @@ public override void OnUserRequestedShutdown() m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); } + public override void OnServerStopped() + { + m_ConnectStatusPublisher.Publish(ConnectStatus.GenericDisconnect); + m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); + } + /// /// This logic plugs into the "ConnectionApprovalResponse" exposed by Netcode.NetworkManager. It is run every time a client connects to us. /// The complementary logic that runs when the client starts its connection can be found in ClientConnectingState. diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/OnlineState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/OnlineState.cs index 89cf92c71..1930d0422 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/OnlineState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/OnlineState.cs @@ -5,8 +5,6 @@ namespace Unity.BossRoom.ConnectionManagement /// abstract class OnlineState : ConnectionState { - public const string k_DtlsConnType = "dtls"; - public override void OnUserRequestedShutdown() { // This behaviour will be the same for every online state diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/StartingHostState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/StartingHostState.cs index eca47b3b6..a752cda77 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/StartingHostState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/StartingHostState.cs @@ -33,20 +33,6 @@ public override void Enter() public override void Exit() { } - public override void OnClientDisconnect(ulong clientId) - { - if (clientId == m_ConnectionManager.NetworkManager.LocalClientId) - { - StartHostFailed(); - } - } - - void StartHostFailed() - { - m_ConnectStatusPublisher.Publish(ConnectStatus.StartHostFailed); - m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); - } - public override void OnServerStarted() { m_ConnectStatusPublisher.Publish(ConnectStatus.Success); @@ -72,6 +58,11 @@ public override void ApprovalCheck(NetworkManager.ConnectionApprovalRequest requ } } + public override void OnServerStopped() + { + StartHostFailed(); + } + async void StartHost() { try @@ -82,7 +73,7 @@ async void StartHost() // NGO's StartHost launches everything if (!m_ConnectionManager.NetworkManager.StartHost()) { - OnClientDisconnect(m_ConnectionManager.NetworkManager.LocalClientId); + StartHostFailed(); } } catch (Exception) @@ -91,5 +82,11 @@ async void StartHost() throw; } } + + void StartHostFailed() + { + m_ConnectStatusPublisher.Publish(ConnectStatus.StartHostFailed); + m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); + } } } diff --git a/Assets/Scripts/Editor/BuildHelpers.cs b/Assets/Scripts/Editor/BuildHelpers.cs new file mode 100644 index 000000000..e52a4a89b --- /dev/null +++ b/Assets/Scripts/Editor/BuildHelpers.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEditor; +using UnityEditor.Build.Reporting; + +/// +/// Utility menus to easily create our builds for our playtests. If you're just exploring this project, you shouldn't need those. They are mostly to make +/// multiplatform build creation easier and is meant for internal usage. +/// +internal static class BuildHelpers +{ + const string k_MenuRoot = "Boss Room/Playtest Builds/"; + const string k_Build = k_MenuRoot + "Build"; + const string k_DeleteBuilds = k_MenuRoot + "Delete All Builds (keeps cache)"; + const string k_AllToggleName = k_MenuRoot + "Toggle All"; + const string k_MobileToggleName = k_MenuRoot + "Toggle Mobile"; + const string k_IOSToggleName = k_MenuRoot + "Toggle iOS"; + const string k_AndroidToggleName = k_MenuRoot + "Toggle Android"; + const string k_DesktopToggleName = k_MenuRoot + "Toggle Desktop"; + const string k_MacOSToggleName = k_MenuRoot + "Toggle MacOS"; + const string k_WindowsToggleName = k_MenuRoot + "Toggle Windows"; + const string k_DisableProjectIDToggleName = k_MenuRoot + "Skip Project ID Check"; // double negative in the name since menu is unchecked by default + const string k_SkipAutoDeleteToggleName = k_MenuRoot + "Skip Auto Delete Builds"; + + const int k_MenuGroupingBuild = 0; // to add separator in menus + const int k_MenuGroupingPlatforms = 11; + const int k_MenuGroupingOtherToggles = 22; + + static BuildTarget s_CurrentEditorBuildTarget; + static BuildTargetGroup s_CurrentEditorBuildTargetGroup; + static int s_NbBuildsDone; + + static string BuildPathRootDirectory => Path.Combine(Path.GetDirectoryName(Application.dataPath), "Builds", "Playtest"); + static string BuildPathDirectory(string platformName) => Path.Combine(BuildPathRootDirectory, platformName); + public static string BuildPath(string platformName) => Path.Combine(BuildPathDirectory(platformName), "BossRoomPlaytest"); + + [MenuItem(k_Build, false, k_MenuGroupingBuild)] + static async void Build() + { + s_NbBuildsDone = 0; + bool buildiOS = Menu.GetChecked(k_IOSToggleName); + bool buildAndroid = Menu.GetChecked(k_AndroidToggleName); + bool buildMacOS = Menu.GetChecked(k_MacOSToggleName); + bool buildWindows = Menu.GetChecked(k_WindowsToggleName); + + bool skipAutoDelete = Menu.GetChecked(k_SkipAutoDeleteToggleName); + + Debug.Log($"Starting build: buildiOS?:{buildiOS} buildAndroid?:{buildAndroid} buildMacOS?:{buildMacOS} buildWindows?:{buildWindows}"); + if (string.IsNullOrEmpty(CloudProjectSettings.projectId) && !Menu.GetChecked(k_DisableProjectIDToggleName)) + { + string errorMessage = $"Project ID was supposed to be setup and wasn't, make sure to set it up or disable project ID check with the [{k_DisableProjectIDToggleName}] menu"; + EditorUtility.DisplayDialog("Error Custom Build", errorMessage, "ok"); + throw new Exception(errorMessage); + } + + SaveCurrentBuildTarget(); + + try + { + // deleting so we don't end up testing on outdated builds if there's a build failure + if (!skipAutoDelete) DeleteBuilds(); + + if (buildiOS) await BuildPlayerUtilityAsync(BuildTarget.iOS, "", true); + if (buildAndroid) await BuildPlayerUtilityAsync(BuildTarget.Android, ".apk", true); // there's the possibility of an error where it + + // complains about NDK missing. Building manually on android then trying again seems to work? Can't find anything on this. + if (buildMacOS) await BuildPlayerUtilityAsync(BuildTarget.StandaloneOSX, ".app", true); + if (buildWindows) await BuildPlayerUtilityAsync(BuildTarget.StandaloneWindows64, ".exe", true); + } + catch + { + EditorUtility.DisplayDialog("Exception while building", "See console for details", "ok"); + throw; + } + finally + { + Debug.Log($"Count builds done: {s_NbBuildsDone}"); + RestoreBuildTarget(); + } + } + + [MenuItem(k_Build, true)] + static bool CanBuild() + { + return Menu.GetChecked(k_IOSToggleName) || + Menu.GetChecked(k_AndroidToggleName) || + Menu.GetChecked(k_MacOSToggleName) || + Menu.GetChecked(k_WindowsToggleName); + } + + static void RestoreBuildTarget() + { + Debug.Log($"restoring editor to initial build target {s_CurrentEditorBuildTarget}"); + EditorUserBuildSettings.SwitchActiveBuildTarget(s_CurrentEditorBuildTargetGroup, s_CurrentEditorBuildTarget); + } + + static void SaveCurrentBuildTarget() + { + s_CurrentEditorBuildTarget = EditorUserBuildSettings.activeBuildTarget; + s_CurrentEditorBuildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup; + } + + [MenuItem(k_AllToggleName, false, k_MenuGroupingPlatforms)] + static void ToggleAll() + { + var newValue = ToggleMenu(k_AllToggleName); + ToggleMenu(k_DesktopToggleName, newValue); + ToggleMenu(k_MacOSToggleName, newValue); + ToggleMenu(k_WindowsToggleName, newValue); + ToggleMenu(k_MobileToggleName, newValue); + ToggleMenu(k_IOSToggleName, newValue); + ToggleMenu(k_AndroidToggleName, newValue); + } + + [MenuItem(k_MobileToggleName, false, k_MenuGroupingPlatforms)] + static void ToggleMobile() + { + var newValue = ToggleMenu(k_MobileToggleName); + ToggleMenu(k_IOSToggleName, newValue); + ToggleMenu(k_AndroidToggleName, newValue); + } + + [MenuItem(k_IOSToggleName, false, k_MenuGroupingPlatforms)] + static void ToggleiOS() + { + ToggleMenu(k_IOSToggleName); + } + + [MenuItem(k_AndroidToggleName, false, k_MenuGroupingPlatforms)] + static void ToggleAndroid() + { + ToggleMenu(k_AndroidToggleName); + } + + [MenuItem(k_DesktopToggleName, false, k_MenuGroupingPlatforms)] + static void ToggleDesktop() + { + var newValue = ToggleMenu(k_DesktopToggleName); + ToggleMenu(k_MacOSToggleName, newValue); + ToggleMenu(k_WindowsToggleName, newValue); + } + + [MenuItem(k_MacOSToggleName, false, k_MenuGroupingPlatforms)] + static void ToggleMacOS() + { + ToggleMenu(k_MacOSToggleName); + } + + [MenuItem(k_WindowsToggleName, false, k_MenuGroupingPlatforms)] + static void ToggleWindows() + { + ToggleMenu(k_WindowsToggleName); + } + + [MenuItem(k_DisableProjectIDToggleName, false, k_MenuGroupingOtherToggles)] + static void ToggleProjectID() + { + ToggleMenu(k_DisableProjectIDToggleName); + } + + [MenuItem(k_SkipAutoDeleteToggleName, false, k_MenuGroupingOtherToggles)] + static void ToggleAutoDelete() + { + ToggleMenu(k_SkipAutoDeleteToggleName); + } + + static bool ToggleMenu(string menuName, bool? valueToSet = null) + { + bool toSet = !Menu.GetChecked(menuName); + if (valueToSet != null) + { + toSet = valueToSet.Value; + } + + Menu.SetChecked(menuName, toSet); + return toSet; + } + + static async Task BuildPlayerUtilityAsync(BuildTarget buildTarget = BuildTarget.NoTarget, string buildPathExtension = null, bool buildDebug = false) + { + s_NbBuildsDone++; + Debug.Log($"Starting build for {buildTarget.ToString()}"); + + await Task.Delay(100); // skipping some time to make sure debug logs are flushed before we build + + var buildPathToUse = BuildPath(buildTarget.ToString()); + buildPathToUse += buildPathExtension; + + var buildPlayerOptions = new BuildPlayerOptions(); + + List scenesToInclude = new List(); + foreach (var scene in EditorBuildSettings.scenes) + { + if (scene.enabled) + { + scenesToInclude.Add(scene.path); + } + } + + buildPlayerOptions.scenes = scenesToInclude.ToArray(); + buildPlayerOptions.locationPathName = buildPathToUse; + buildPlayerOptions.target = buildTarget; + var buildOptions = BuildOptions.None; + if (buildDebug) + { + buildOptions |= BuildOptions.Development; + } + + buildOptions |= BuildOptions.StrictMode; + buildPlayerOptions.options = buildOptions; + + BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions); + BuildSummary summary = report.summary; + + if (summary.result == BuildResult.Succeeded) + { + Debug.Log($"Build succeeded: {summary.totalSize} bytes at {summary.outputPath}"); + } + else + { + string debugString = buildDebug ? "debug" : "release"; + throw new Exception($"Build failed for {debugString}:{buildTarget}! {report.summary.totalErrors} errors"); + } + } + + [MenuItem(k_DeleteBuilds, false, k_MenuGroupingBuild)] + public static void DeleteBuilds() + { + if (Directory.Exists(BuildPathRootDirectory)) + { + Directory.Delete(BuildPathRootDirectory, recursive: true); + Debug.Log($"deleted {BuildPathRootDirectory}"); + } + else + { + Debug.Log($"Build directory does not exist ({BuildPathRootDirectory}). No cleanup to do"); + } + } +} diff --git a/Assets/Scripts/Editor/BuildHelpers.cs.meta b/Assets/Scripts/Editor/BuildHelpers.cs.meta new file mode 100644 index 000000000..406984516 --- /dev/null +++ b/Assets/Scripts/Editor/BuildHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3034a09a9ffb54a08be046bd5398a476 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Gameplay/Action/ActionID.cs b/Assets/Scripts/Gameplay/Action/ActionID.cs index 0c8b01f7d..ff28adad4 100644 --- a/Assets/Scripts/Gameplay/Action/ActionID.cs +++ b/Assets/Scripts/Gameplay/Action/ActionID.cs @@ -7,15 +7,10 @@ namespace Unity.BossRoom.Gameplay.Actions /// This struct is used by Action system (and GameDataSource) to refer to a specific action in runtime. /// It wraps a simple integer. /// - public struct ActionID : INetworkSerializable, IEquatable + public struct ActionID : INetworkSerializeByMemcpy, IEquatable { public int ID; - public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter - { - serializer.SerializeValue(ref ID); - } - public bool Equals(ActionID other) { return ID == other.ID; diff --git a/Assets/Scripts/Gameplay/GameState/ClientMainMenuState.cs b/Assets/Scripts/Gameplay/GameState/ClientMainMenuState.cs index c1b2a691f..53da328ae 100644 --- a/Assets/Scripts/Gameplay/GameState/ClientMainMenuState.cs +++ b/Assets/Scripts/Gameplay/GameState/ClientMainMenuState.cs @@ -22,20 +22,31 @@ namespace Unity.BossRoom.Gameplay.GameState /// public class ClientMainMenuState : GameStateBehaviour { - public override GameState ActiveState { get { return GameState.MainMenu; } } - - [SerializeField] NameGenerationData m_NameGenerationData; - [SerializeField] LobbyUIMediator m_LobbyUIMediator; - [SerializeField] IPUIMediator m_IPUIMediator; - [SerializeField] Button m_LobbyButton; - [SerializeField] GameObject m_SignInSpinner; - [SerializeField] UIProfileSelector m_UIProfileSelector; - [SerializeField] UITooltipDetector m_UGSSetupTooltipDetector; - - [Inject] AuthenticationServiceFacade m_AuthServiceFacade; - [Inject] LocalLobbyUser m_LocalUser; - [Inject] LocalLobby m_LocalLobby; - [Inject] ProfileManager m_ProfileManager; + public override GameState ActiveState => GameState.MainMenu; + + [SerializeField] + NameGenerationData m_NameGenerationData; + [SerializeField] + LobbyUIMediator m_LobbyUIMediator; + [SerializeField] + IPUIMediator m_IPUIMediator; + [SerializeField] + Button m_LobbyButton; + [SerializeField] + GameObject m_SignInSpinner; + [SerializeField] + UIProfileSelector m_UIProfileSelector; + [SerializeField] + UITooltipDetector m_UGSSetupTooltipDetector; + + [Inject] + AuthenticationServiceFacade m_AuthServiceFacade; + [Inject] + LocalLobbyUser m_LocalUser; + [Inject] + LocalLobby m_LocalLobby; + [Inject] + ProfileManager m_ProfileManager; protected override void Awake() { @@ -61,17 +72,12 @@ protected override void Configure(IContainerBuilder builder) builder.RegisterComponent(m_IPUIMediator); } - private async void TrySignIn() { try { - var unityAuthenticationInitOptions = new InitializationOptions(); - var profile = m_ProfileManager.Profile; - if (profile.Length > 0) - { - unityAuthenticationInitOptions.SetProfile(profile); - } + var unityAuthenticationInitOptions = + m_AuthServiceFacade.GenerateAuthenticationOptions(m_ProfileManager.Profile); await m_AuthServiceFacade.InitializeAndSignInAsync(unityAuthenticationInitOptions); OnAuthSignIn(); @@ -92,6 +98,7 @@ private void OnAuthSignIn() Debug.Log($"Signed in. Unity Player ID {AuthenticationService.Instance.PlayerId}"); m_LocalUser.ID = AuthenticationService.Instance.PlayerId; + // The local LobbyUser object will be hooked into UI before the LocalLobby is populated during lobby join, so the LocalLobby must know about it already when that happens. m_LocalLobby.AddUser(m_LocalUser); } @@ -103,6 +110,7 @@ private void OnSignInFailed() m_LobbyButton.interactable = false; m_UGSSetupTooltipDetector.enabled = true; } + if (m_SignInSpinner) { m_SignInSpinner.SetActive(false); diff --git a/Assets/Scripts/Gameplay/GameState/NetworkCharSelection.cs b/Assets/Scripts/Gameplay/GameState/NetworkCharSelection.cs index 5d0d17ad3..a56e4bc72 100644 --- a/Assets/Scripts/Gameplay/GameState/NetworkCharSelection.cs +++ b/Assets/Scripts/Gameplay/GameState/NetworkCharSelection.cs @@ -20,6 +20,12 @@ public enum SeatState : byte /// /// Describes one of the players in the lobby, and their current character-select status. /// + /// + /// Putting FixedString inside an INetworkSerializeByMemcpy struct is not recommended because it will lose the + /// bandwidth optimization provided by INetworkSerializable -- an empty FixedString128Bytes serialized normally + /// or through INetworkSerializable will use 4 bytes of bandwidth, but inside an INetworkSerializeByMemcpy, that + /// same empty value would consume 132 bytes of bandwidth. + /// public struct LobbyPlayerState : INetworkSerializable, IEquatable { public ulong ClientId; diff --git a/Assets/Scripts/Gameplay/GameplayObjects/Breakable.cs b/Assets/Scripts/Gameplay/GameplayObjects/Breakable.cs index 00aa20303..246811311 100644 --- a/Assets/Scripts/Gameplay/GameplayObjects/Breakable.cs +++ b/Assets/Scripts/Gameplay/GameplayObjects/Breakable.cs @@ -45,7 +45,6 @@ public class Breakable : NetworkBehaviour, IDamageable, ITargetable [SerializeField] private GameObject[] m_UnbrokenGameObjects; - /// /// Is the item broken or not? /// @@ -158,10 +157,12 @@ private void OnBreakableStateChanged(bool wasBroken, bool isBroken) private void PerformBreakVisualization(bool onStart) { - foreach (var gameObject in m_UnbrokenGameObjects) + foreach (var unbrokenGameObject in m_UnbrokenGameObjects) { - if (gameObject) - gameObject.SetActive(false); + if (unbrokenGameObject) + { + unbrokenGameObject.SetActive(false); + } } if (m_CurrentBrokenVisualization) @@ -180,10 +181,12 @@ private void PerformUnbreakVisualization() { Destroy(m_CurrentBrokenVisualization); } - foreach (var gameObject in m_UnbrokenGameObjects) + foreach (var unbrokenGameObject in m_UnbrokenGameObjects) { - if (gameObject) - gameObject.SetActive(true); + if (unbrokenGameObject) + { + unbrokenGameObject.SetActive(true); + } } } diff --git a/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs b/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs index d3cf2f067..c59a04884 100644 --- a/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs +++ b/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs @@ -40,7 +40,6 @@ public class ClientCharacter : NetworkBehaviour /// public Material ReticuleFriendlyMat => m_VisualizationConfiguration.ReticuleFriendlyMat; - CharacterSwap m_CharacterSwapper; public CharacterSwap CharacterSwap => m_CharacterSwapper; @@ -57,8 +56,6 @@ public class ClientCharacter : NetworkBehaviour RotationLerper m_RotationLerper; - PhysicsWrapper m_PhysicsWrapper; - // this value suffices for both positional and rotational interpolations; one may have a constant value for each const float k_LerpTime = 0.08f; @@ -66,8 +63,6 @@ public class ClientCharacter : NetworkBehaviour Quaternion m_LerpedRotation; - bool m_IsHost; - float m_CurrentSpeed; /// @@ -123,26 +118,23 @@ public override void OnNetworkSpawn() enabled = true; - m_IsHost = IsHost; - m_ClientActionViz = new ClientActionPlayer(this); m_ServerCharacter = GetComponentInParent(); - m_PhysicsWrapper = m_ServerCharacter.GetComponent(); - m_ServerCharacter.IsStealthy.OnValueChanged += OnStealthyChanged; m_ServerCharacter.MovementStatus.OnValueChanged += OnMovementStatusChanged; OnMovementStatusChanged(MovementStatus.Normal, m_ServerCharacter.MovementStatus.Value); // sync our visualization position & rotation to the most up to date version received from server - transform.SetPositionAndRotation(m_PhysicsWrapper.Transform.position, m_PhysicsWrapper.Transform.rotation); + transform.SetPositionAndRotation(serverCharacter.physicsWrapper.Transform.position, + serverCharacter.physicsWrapper.Transform.rotation); m_LerpedPosition = transform.position; m_LerpedRotation = transform.rotation; // similarly, initialize start position and rotation for smooth lerping purposes - m_PositionLerper = new PositionLerper(m_PhysicsWrapper.Transform.position, k_LerpTime); - m_RotationLerper = new RotationLerper(m_PhysicsWrapper.Transform.rotation, k_LerpTime); + m_PositionLerper = new PositionLerper(serverCharacter.physicsWrapper.Transform.position, k_LerpTime); + m_RotationLerper = new RotationLerper(serverCharacter.physicsWrapper.Transform.rotation, k_LerpTime); if (!m_ServerCharacter.IsNpc) { @@ -166,7 +158,7 @@ public override void OnNetworkSpawn() if (m_ServerCharacter.TryGetComponent(out ClientInputSender inputSender)) { - // TODO: revisit; anticipated actions would play twice on the host + // anticipated actions will only be played on non-host, owning clients if (!IsServer) { inputSender.ActionInputEvent += OnActionInput; @@ -181,10 +173,6 @@ public override void OnNetworkDespawn() { if (m_ServerCharacter) { - //m_NetState.DoActionEventClient -= PerformActionFX; - //m_NetState.CancelAllActionsEventClient -= CancelAllActionFXs; - //m_NetState.CancelActionsByPrototypeIDEventClient -= CancelActionFXByPrototypeID; - //m_NetState.OnStopChargingUpClient -= OnStoppedChargingUpClient; m_ServerCharacter.IsStealthy.OnValueChanged -= OnStealthyChanged; if (m_ServerCharacter.TryGetComponent(out ClientInputSender sender)) @@ -276,15 +264,15 @@ void Update() // the game camera tracks a GameObject moving in the Update loop and therefore eliminate any camera jitter, // this graphics GameObject's position is smoothed over time on the host. Clients do not need to perform any // positional smoothing since NetworkTransform will interpolate position updates on the root GameObject. - if (m_IsHost) + if (IsHost) { // Note: a cached position (m_LerpedPosition) and rotation (m_LerpedRotation) are created and used as // the starting point for each interpolation since the root's position and rotation are modified in // FixedUpdate, thus altering this transform (being a child) in the process. m_LerpedPosition = m_PositionLerper.LerpPosition(m_LerpedPosition, - m_PhysicsWrapper.Transform.position); + serverCharacter.physicsWrapper.Transform.position); m_LerpedRotation = m_RotationLerper.LerpRotation(m_LerpedRotation, - m_PhysicsWrapper.Transform.rotation); + serverCharacter.physicsWrapper.Transform.rotation); transform.SetPositionAndRotation(m_LerpedPosition, m_LerpedRotation); } diff --git a/Assets/Scripts/Gameplay/GameplayObjects/EnemyPortal.cs b/Assets/Scripts/Gameplay/GameplayObjects/EnemyPortal.cs index 399c5d1a8..ffed31f2f 100644 --- a/Assets/Scripts/Gameplay/GameplayObjects/EnemyPortal.cs +++ b/Assets/Scripts/Gameplay/GameplayObjects/EnemyPortal.cs @@ -30,27 +30,20 @@ public class EnemyPortal : NetworkBehaviour, ITargetable [Tooltip("When all breakable elements are broken, wait this long before respawning them (and reactivating)")] float m_DormantCooldown; - - /// - /// Is the item broken or not? - /// - public NetworkVariable IsBroken; + [SerializeField] + Breakable m_Breakable; public bool IsNpc { get { return true; } } - public bool IsValidTarget { get { return !IsBroken.Value; } } + public bool IsValidTarget { get { return !m_Breakable.IsBroken.Value; } } // cached reference to our components + [SerializeField] ServerWaveSpawner m_WaveSpawner; // currently active "wait X seconds and then restart" coroutine Coroutine m_CoroDormant; - private void Awake() - { - m_WaveSpawner = GetComponent(); - } - public override void OnNetworkSpawn() { if (!IsServer) @@ -97,7 +90,7 @@ private void MaintainState() } } - IsBroken.Value = !hasUnbrokenBreakables; + m_Breakable.IsBroken.Value = !hasUnbrokenBreakables; m_WaveSpawner.SetSpawnerEnabled(hasUnbrokenBreakables); if (!hasUnbrokenBreakables && m_CoroDormant == null) { @@ -124,7 +117,7 @@ void Restart() } } - IsBroken.Value = false; + m_Breakable.IsBroken.Value = false; m_WaveSpawner.SetSpawnerEnabled(true); m_CoroDormant = null; } diff --git a/Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs b/Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs index bb2d8bb8e..23185d631 100644 --- a/Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs +++ b/Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs @@ -14,7 +14,7 @@ public interface IDamageable /// Receives HP damage or healing. /// /// The Character responsible for the damage. May be null. - /// The damage done. Positive value is damage, negative is healing. + /// The damage done. Negative value is damage, positive is healing. void ReceiveHP(ServerCharacter inflicter, int HP); /// diff --git a/Assets/Scripts/Gameplay/UI/IPHostingUI.cs b/Assets/Scripts/Gameplay/UI/IPHostingUI.cs index 1d970682b..3ec93b8d2 100644 --- a/Assets/Scripts/Gameplay/UI/IPHostingUI.cs +++ b/Assets/Scripts/Gameplay/UI/IPHostingUI.cs @@ -13,6 +13,9 @@ public class IPHostingUI : MonoBehaviour [SerializeField] CanvasGroup m_CanvasGroup; + [SerializeField] + Button m_HostButton; + [Inject] IPUIMediator m_IPUIMediator; void Awake() @@ -43,7 +46,8 @@ public void OnCreateClick() /// public void SanitizeIPInputText() { - m_IPInputField.text = IPUIMediator.Sanitize(m_IPInputField.text); + m_IPInputField.text = IPUIMediator.SanitizeIP(m_IPInputField.text); + m_HostButton.interactable = IPUIMediator.AreIpAddressAndPortValid(m_IPInputField.text, m_PortInputField.text); } /// @@ -51,8 +55,8 @@ public void SanitizeIPInputText() /// public void SanitizePortText() { - var inputFieldText = IPUIMediator.Sanitize(m_PortInputField.text); - m_PortInputField.text = inputFieldText; + m_PortInputField.text = IPUIMediator.SanitizePort(m_PortInputField.text); + m_HostButton.interactable = IPUIMediator.AreIpAddressAndPortValid(m_IPInputField.text, m_PortInputField.text); } } } diff --git a/Assets/Scripts/Gameplay/UI/IPJoiningUI.cs b/Assets/Scripts/Gameplay/UI/IPJoiningUI.cs index 8b79e7dd9..bc214c978 100644 --- a/Assets/Scripts/Gameplay/UI/IPJoiningUI.cs +++ b/Assets/Scripts/Gameplay/UI/IPJoiningUI.cs @@ -14,6 +14,9 @@ public class IPJoiningUI : MonoBehaviour [SerializeField] InputField m_PortInputField; + [SerializeField] + Button m_JoinButton; + [Inject] IPUIMediator m_IPUIMediator; void Awake() @@ -44,7 +47,8 @@ public void OnJoinButtonPressed() /// public void SanitizeIPInputText() { - m_IPInputField.text = IPUIMediator.Sanitize(m_IPInputField.text); + m_IPInputField.text = IPUIMediator.SanitizeIP(m_IPInputField.text); + m_JoinButton.interactable = IPUIMediator.AreIpAddressAndPortValid(m_IPInputField.text, m_PortInputField.text); } /// @@ -52,8 +56,8 @@ public void SanitizeIPInputText() /// public void SanitizePortText() { - var inputFieldText = IPUIMediator.Sanitize(m_PortInputField.text); - m_PortInputField.text = inputFieldText; + m_PortInputField.text = IPUIMediator.SanitizePort(m_PortInputField.text); + m_JoinButton.interactable = IPUIMediator.AreIpAddressAndPortValid(m_IPInputField.text, m_PortInputField.text); } } } diff --git a/Assets/Scripts/Gameplay/UI/IPUIMediator.cs b/Assets/Scripts/Gameplay/UI/IPUIMediator.cs index 568e8e834..2f0b97eee 100644 --- a/Assets/Scripts/Gameplay/UI/IPUIMediator.cs +++ b/Assets/Scripts/Gameplay/UI/IPUIMediator.cs @@ -4,6 +4,7 @@ using TMPro; using Unity.BossRoom.ConnectionManagement; using Unity.BossRoom.Infrastructure; +using Unity.Networking.Transport; using UnityEngine; using VContainer; @@ -171,13 +172,32 @@ public void CancelConnectingWindow() } /// - /// Sanitize user port InputField box allowing only alphanumerics and '.' + /// Sanitize user IP address InputField box allowing only numbers and '.'. This also prevents undesirable + /// invisible characters from being copy-pasted accidentally. /// /// string to sanitize. /// Sanitized text string. - public static string Sanitize(string dirtyString) + public static string SanitizeIP(string dirtyString) { - return Regex.Replace(dirtyString, "[^A-Za-z0-9.]", ""); + return Regex.Replace(dirtyString, "[^0-9.]", ""); + } + + /// + /// Sanitize user port InputField box allowing only numbers. This also prevents undesirable invisible characters + /// from being copy-pasted accidentally. + /// + /// string to sanitize. + /// Sanitized text string. + public static string SanitizePort(string dirtyString) + { + + return Regex.Replace(dirtyString, "[^0-9]", ""); + } + + public static bool AreIpAddressAndPortValid(string ipAddress, string port) + { + var portValid = ushort.TryParse(port, out var portNum); + return portValid && NetworkEndPoint.TryParse(ipAddress, portNum, out var networkEndPoint); } } } diff --git a/Assets/Scripts/Gameplay/UI/RoomNameBox.cs b/Assets/Scripts/Gameplay/UI/RoomNameBox.cs index 945e2133f..06525f6b3 100644 --- a/Assets/Scripts/Gameplay/UI/RoomNameBox.cs +++ b/Assets/Scripts/Gameplay/UI/RoomNameBox.cs @@ -22,12 +22,11 @@ private void InjectDependencies(LocalLobby localLobby) { m_LocalLobby = localLobby; m_LocalLobby.changed += UpdateUI; - UpdateUI(localLobby); } void Awake() { - gameObject.SetActive(false); + UpdateUI(m_LocalLobby); } private void OnDestroy() @@ -44,6 +43,10 @@ private void UpdateUI(LocalLobby localLobby) gameObject.SetActive(true); m_CopyToClipboardButton.gameObject.SetActive(true); } + else + { + gameObject.SetActive(false); + } } public void CopyToClipboard() diff --git a/Assets/Scripts/Gameplay/UI/UIProfileSelector.cs b/Assets/Scripts/Gameplay/UI/UIProfileSelector.cs index c6300cb38..7699605df 100644 --- a/Assets/Scripts/Gameplay/UI/UIProfileSelector.cs +++ b/Assets/Scripts/Gameplay/UI/UIProfileSelector.cs @@ -26,6 +26,9 @@ public class UIProfileSelector : MonoBehaviour [Inject] IObjectResolver m_Resolver; [Inject] ProfileManager m_ProfileManager; + // Authentication service only accepts profile names of 30 characters or under + const int k_AuthenticationMaxProfileLength = 30; + void Awake() { m_ProfileListItemPrototype.gameObject.SetActive(false); @@ -44,7 +47,8 @@ public void SanitizeProfileNameInputText() string SanitizeProfileName(string dirtyString) { - return Regex.Replace(dirtyString, "[^a-zA-Z0-9]", ""); + var output = Regex.Replace(dirtyString, "[^a-zA-Z0-9]", ""); + return output[..Math.Min(output.Length, k_AuthenticationMaxProfileLength)]; } public void OnNewProfileButtonPressed() diff --git a/Assets/Scripts/Infrastructure/NetworkGuid.cs b/Assets/Scripts/Infrastructure/NetworkGuid.cs index 6c28d2b3f..c111f5f42 100644 --- a/Assets/Scripts/Infrastructure/NetworkGuid.cs +++ b/Assets/Scripts/Infrastructure/NetworkGuid.cs @@ -3,16 +3,10 @@ namespace Unity.BossRoom.Infrastructure { - public class NetworkGuid : INetworkSerializable + public struct NetworkGuid : INetworkSerializeByMemcpy { public ulong FirstHalf; public ulong SecondHalf; - - public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter - { - serializer.SerializeValue(ref FirstHalf); - serializer.SerializeValue(ref SecondHalf); - } } public static class NetworkGuidExtensions diff --git a/Assets/Scripts/Infrastructure/NetworkObjectPool.cs b/Assets/Scripts/Infrastructure/NetworkObjectPool.cs index 4c18daa1a..2dede95bd 100644 --- a/Assets/Scripts/Infrastructure/NetworkObjectPool.cs +++ b/Assets/Scripts/Infrastructure/NetworkObjectPool.cs @@ -1,53 +1,62 @@ using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using Unity.Netcode; using UnityEngine; using UnityEngine.Assertions; +using UnityEngine.Pool; namespace Unity.BossRoom.Infrastructure { /// - /// Object Pool for networked objects, used for controlling how objects are spawned by Netcode. Netcode by default will allocate new memory when spawning new - /// objects. With this Networked Pool, we're using custom spawning to reuse objects. + /// Object Pool for networked objects, used for controlling how objects are spawned by Netcode. Netcode by default + /// will allocate new memory when spawning new objects. With this Networked Pool, we're using the ObjectPool to + /// reuse objects. /// Boss Room uses this for projectiles. In theory it should use this for imps too, but we wanted to show vanilla spawning vs pooled spawning. - /// Hooks to NetworkManager's prefab handler to intercept object spawning and do custom actions + /// Hooks to NetworkManager's prefab handler to intercept object spawning and do custom actions. /// public class NetworkObjectPool : NetworkBehaviour { - private static NetworkObjectPool _instance; - - public static NetworkObjectPool Singleton { get { return _instance; } } + public static NetworkObjectPool Singleton { get; private set; } [SerializeField] List PooledPrefabsList; - HashSet prefabs = new HashSet(); - - Dictionary> pooledObjects = new Dictionary>(); + HashSet m_Prefabs = new HashSet(); - private bool m_HasInitialized = false; + Dictionary> m_PooledObjects = new Dictionary>(); public void Awake() { - if (_instance != null && _instance != this) + if (Singleton != null && Singleton != this) { - Destroy(this.gameObject); + Destroy(gameObject); } else { - _instance = this; + Singleton = this; } } public override void OnNetworkSpawn() { - InitializePool(); + // Registers all objects in PooledPrefabsList to the cache. + foreach (var configObject in PooledPrefabsList) + { + RegisterPrefabInternal(configObject.Prefab, configObject.PrewarmCount); + } } public override void OnNetworkDespawn() { - ClearPool(); + // Unregisters all objects in PooledPrefabsList from the cache. + foreach (var prefab in m_Prefabs) + { + // Unregister Netcode Spawn handlers + NetworkManager.Singleton.PrefabHandler.RemoveHandler(prefab); + m_PooledObjects[prefab].Clear(); + } + m_PooledObjects.Clear(); + m_Prefabs.Clear(); } public void OnValidate() @@ -65,23 +74,25 @@ public void OnValidate() /// /// Gets an instance of the given prefab from the pool. The prefab must be registered to the pool. /// - /// - /// - public NetworkObject GetNetworkObject(GameObject prefab) - { - return GetNetworkObjectInternal(prefab, Vector3.zero, Quaternion.identity); - } - - /// - /// Gets an instance of the given prefab from the pool. The prefab must be registered to the pool. - /// + /// + /// To spawn a NetworkObject from one of the pools, this must be called on the server, then the instance + /// returned from it must be spawned on the server. This method will then also be called on the client by the + /// PooledPrefabInstanceHandler when the client receives a spawn message for a prefab that has been registered + /// here. + /// /// /// The position to spawn the object at. /// The rotation to spawn the object with. /// public NetworkObject GetNetworkObject(GameObject prefab, Vector3 position, Quaternion rotation) { - return GetNetworkObjectInternal(prefab, position, rotation); + var networkObject = m_PooledObjects[prefab].Get(); + + var noTransform = networkObject.transform; + noTransform.position = position; + noTransform.rotation = rotation; + + return networkObject; } /// @@ -89,106 +100,52 @@ public NetworkObject GetNetworkObject(GameObject prefab, Vector3 position, Quate /// public void ReturnNetworkObject(NetworkObject networkObject, GameObject prefab) { - var go = networkObject.gameObject; - go.SetActive(false); - pooledObjects[prefab].Enqueue(networkObject); - } - - /// - /// Adds a prefab to the list of spawnable prefabs. - /// - /// The prefab to add. - /// - public void AddPrefab(GameObject prefab, int prewarmCount = 0) - { - var networkObject = prefab.GetComponent(); - - Assert.IsNotNull(networkObject, $"{nameof(prefab)} must have {nameof(networkObject)} component."); - Assert.IsFalse(prefabs.Contains(prefab), $"Prefab {prefab.name} is already registered in the pool."); - - RegisterPrefabInternal(prefab, prewarmCount); + m_PooledObjects[prefab].Release(networkObject); } /// /// Builds up the cache for a prefab. /// - private void RegisterPrefabInternal(GameObject prefab, int prewarmCount) + void RegisterPrefabInternal(GameObject prefab, int prewarmCount) { - prefabs.Add(prefab); - - var prefabQueue = new Queue(); - pooledObjects[prefab] = prefabQueue; - for (int i = 0; i < prewarmCount; i++) + NetworkObject CreateFunc() { - var go = CreateInstance(prefab); - ReturnNetworkObject(go.GetComponent(), prefab); + return Instantiate(prefab).GetComponent(); } - // Register Netcode Spawn handlers - NetworkManager.Singleton.PrefabHandler.AddHandler(prefab, new PooledPrefabInstanceHandler(prefab, this)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private GameObject CreateInstance(GameObject prefab) - { - return Instantiate(prefab); - } - - /// - /// This matches the signature of - /// - /// - /// - /// - /// - private NetworkObject GetNetworkObjectInternal(GameObject prefab, Vector3 position, Quaternion rotation) - { - var queue = pooledObjects[prefab]; - - NetworkObject networkObject; - if (queue.Count > 0) + void ActionOnGet(NetworkObject networkObject) { - networkObject = queue.Dequeue(); + networkObject.gameObject.SetActive(true); } - else + + void ActionOnRelease(NetworkObject networkObject) { - networkObject = CreateInstance(prefab).GetComponent(); + networkObject.gameObject.SetActive(false); } - // Here we must reverse the logic in ReturnNetworkObject. - var go = networkObject.gameObject; - go.SetActive(true); + void ActionOnDestroy(NetworkObject networkObject) + { + Destroy(networkObject.gameObject); + } - go.transform.position = position; - go.transform.rotation = rotation; + m_Prefabs.Add(prefab); - return networkObject; - } + // Create the pool + m_PooledObjects[prefab] = new ObjectPool(CreateFunc, ActionOnGet, ActionOnRelease, ActionOnDestroy, defaultCapacity: prewarmCount); - /// - /// Registers all objects in to the cache. - /// - public void InitializePool() - { - if (m_HasInitialized) return; - foreach (var configObject in PooledPrefabsList) + // Populate the pool + var prewarmNetworkObjects = new List(); + for (var i = 0; i < prewarmCount; i++) { - RegisterPrefabInternal(configObject.Prefab, configObject.PrewarmCount); + prewarmNetworkObjects.Add(m_PooledObjects[prefab].Get()); } - m_HasInitialized = true; - } - - /// - /// Unregisters all objects in from the cache. - /// - public void ClearPool() - { - foreach (var prefab in prefabs) + foreach (var networkObject in prewarmNetworkObjects) { - // Unregister Netcode Spawn handlers - NetworkManager.Singleton.PrefabHandler.RemoveHandler(prefab); + m_PooledObjects[prefab].Release(networkObject); } - pooledObjects.Clear(); + + // Register Netcode Spawn handlers + NetworkManager.Singleton.PrefabHandler.AddHandler(prefab, new PooledPrefabInstanceHandler(prefab, this)); } } @@ -212,8 +169,7 @@ public PooledPrefabInstanceHandler(GameObject prefab, NetworkObjectPool pool) NetworkObject INetworkPrefabInstanceHandler.Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) { - var netObject = m_Pool.GetNetworkObject(m_Prefab, position, rotation); - return netObject; + return m_Pool.GetNetworkObject(m_Prefab, position, rotation); } void INetworkPrefabInstanceHandler.Destroy(NetworkObject networkObject) diff --git a/Assets/Scripts/Infrastructure/UpdateRunner.cs b/Assets/Scripts/Infrastructure/UpdateRunner.cs index 2a9a954b7..180f8e791 100644 --- a/Assets/Scripts/Infrastructure/UpdateRunner.cs +++ b/Assets/Scripts/Infrastructure/UpdateRunner.cs @@ -14,6 +14,7 @@ class SubscriberData { public float Period; public float NextCallTime; + public float LastCallTime; } readonly Queue m_PendingHandlers = new Queue(); @@ -56,7 +57,7 @@ public void Subscribe(Action onUpdate, float updatePeriod) { if (m_Subscribers.Add(onUpdate)) { - m_SubscriberData.Add(onUpdate, new SubscriberData() { Period = updatePeriod, NextCallTime = 0 }); + m_SubscriberData.Add(onUpdate, new SubscriberData() { Period = updatePeriod, NextCallTime = 0, LastCallTime = Time.time }); } }); } @@ -90,7 +91,8 @@ void Update() if (Time.time >= subscriberData.NextCallTime) { - subscriber.Invoke(Time.deltaTime); + subscriber.Invoke(Time.time - subscriberData.LastCallTime); + subscriberData.LastCallTime = Time.time; subscriberData.NextCallTime = Time.time + subscriberData.Period; } } diff --git a/Assets/Scripts/UnityServices/Auth/AuthenticationServiceFacade.cs b/Assets/Scripts/UnityServices/Auth/AuthenticationServiceFacade.cs index c8f8396aa..ef26eceeb 100644 --- a/Assets/Scripts/UnityServices/Auth/AuthenticationServiceFacade.cs +++ b/Assets/Scripts/UnityServices/Auth/AuthenticationServiceFacade.cs @@ -10,7 +10,28 @@ namespace Unity.BossRoom.UnityServices.Auth { public class AuthenticationServiceFacade { - [Inject] IPublisher m_UnityServiceErrorMessagePublisher; + [Inject] + IPublisher m_UnityServiceErrorMessagePublisher; + + public InitializationOptions GenerateAuthenticationOptions(string profile) + { + try + { + var unityAuthenticationInitOptions = new InitializationOptions(); + if (profile.Length > 0) + { + unityAuthenticationInitOptions.SetProfile(profile); + } + + return unityAuthenticationInitOptions; + } + catch (Exception e) + { + var reason = $"{e.Message} ({e.InnerException?.Message})"; + m_UnityServiceErrorMessagePublisher.Publish(new UnityServiceErrorMessage("Authentication Error", reason, UnityServiceErrorMessage.Service.Authentication, e)); + throw; + } + } public async Task InitializeAndSignInAsync(InitializationOptions initializationOptions) { @@ -37,6 +58,7 @@ public async Task SwitchProfileAndReSignInAsync(string profile) { AuthenticationService.Instance.SignOut(); } + AuthenticationService.Instance.SwitchProfile(profile); try @@ -67,6 +89,7 @@ public async Task EnsurePlayerIsAuthorized() { var reason = $"{e.Message} ({e.InnerException?.Message})"; m_UnityServiceErrorMessagePublisher.Publish(new UnityServiceErrorMessage("Authentication Error", reason, UnityServiceErrorMessage.Service.Authentication, e)); + //not rethrowing for authentication exceptions - any failure to authenticate is considered "handled failure" return false; } @@ -78,6 +101,5 @@ public async Task EnsurePlayerIsAuthorized() throw; } } - } } diff --git a/Assets/Scripts/UnityServices/Lobbies/LobbyAPIInterface.cs b/Assets/Scripts/UnityServices/Lobbies/LobbyAPIInterface.cs index 0b163be1b..426e9ec47 100644 --- a/Assets/Scripts/UnityServices/Lobbies/LobbyAPIInterface.cs +++ b/Assets/Scripts/UnityServices/Lobbies/LobbyAPIInterface.cs @@ -107,11 +107,6 @@ public async Task QueryAllLobbies() return await LobbyService.Instance.QueryLobbiesAsync(queryOptions); } - public async Task GetLobby(string lobbyId) - { - return await LobbyService.Instance.GetLobbyAsync(lobbyId); - } - public async Task UpdateLobby(string lobbyId, Dictionary data, bool shouldLock) { UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = data, IsLocked = shouldLock }; @@ -133,5 +128,10 @@ public async void SendHeartbeatPing(string lobbyId) { await LobbyService.Instance.SendHeartbeatPingAsync(lobbyId); } + + public async Task SubscribeToLobby(string lobbyId, LobbyEventCallbacks eventCallbacks) + { + return await LobbyService.Instance.SubscribeToLobbyEventsAsync(lobbyId, eventCallbacks); + } } } diff --git a/Assets/Scripts/UnityServices/Lobbies/LobbyServiceFacade.cs b/Assets/Scripts/UnityServices/Lobbies/LobbyServiceFacade.cs index 9b3eea329..1692c4ade 100644 --- a/Assets/Scripts/UnityServices/Lobbies/LobbyServiceFacade.cs +++ b/Assets/Scripts/UnityServices/Lobbies/LobbyServiceFacade.cs @@ -37,8 +37,12 @@ public class LobbyServiceFacade : IDisposable, IStartable public Lobby CurrentUnityLobby { get; private set; } + ILobbyEvents m_LobbyEvents; + bool m_IsTracking = false; + LobbyEventConnectionState m_LobbyEventConnectionState = LobbyEventConnectionState.Unknown; + public void Start() { m_ServiceScope = m_ParentScope.CreateChild(builder => @@ -77,88 +81,29 @@ public void BeginTracking() if (!m_IsTracking) { m_IsTracking = true; - // 2s update cadence is arbitrary and is here to demonstrate the fact that this update can be rather infrequent - // the actual rate limits are tracked via the RateLimitCooldown objects defined above - m_UpdateRunner.Subscribe(UpdateLobby, 2f); + SubscribeToJoinedLobbyAsync(); m_JoinedLobbyContentHeartbeat.BeginTracking(); } } - public Task EndTracking() + public void EndTracking() { - var task = Task.CompletedTask; if (CurrentUnityLobby != null) { - CurrentUnityLobby = null; - - var lobbyId = m_LocalLobby?.LobbyID; - - if (!string.IsNullOrEmpty(lobbyId)) + if (m_LocalUser.IsHost) { - if (m_LocalUser.IsHost) - { - task = DeleteLobbyAsync(lobbyId); - } - else - { - task = LeaveLobbyAsync(lobbyId); - } + DeleteLobbyAsync(); + } + else + { + LeaveLobbyAsync(); } - - m_LocalUser.ResetState(); - m_LocalLobby?.Reset(m_LocalUser); } if (m_IsTracking) { - m_UpdateRunner.Unsubscribe(UpdateLobby); m_IsTracking = false; - m_HeartbeatTime = 0; - m_JoinedLobbyContentHeartbeat.EndTracking(); - } - - return task; - } - - async void UpdateLobby(float unused) - { - if (!m_RateLimitQuery.CanCall) - { - return; - } - - try - { - var lobby = await m_LobbyApiInterface.GetLobby(m_LocalLobby.LobbyID); - - CurrentUnityLobby = lobby; - m_LocalLobby.ApplyRemoteData(lobby); - - // as client, check if host is still in lobby - if (!m_LocalUser.IsHost) - { - foreach (var lobbyUser in m_LocalLobby.LobbyUsers) - { - if (lobbyUser.Value.IsHost) - { - return; - } - } - m_UnityServiceErrorMessagePub.Publish(new UnityServiceErrorMessage("Host left the lobby", "Disconnecting.", UnityServiceErrorMessage.Service.Lobby)); - await EndTracking(); - // no need to disconnect Netcode, it should already be handled by Netcode's callback to disconnect - } - } - catch (LobbyServiceException e) - { - if (e.Reason == LobbyExceptionReason.RateLimited) - { - m_RateLimitQuery.PutOnCooldown(); - } - else if (e.Reason != LobbyExceptionReason.LobbyNotFound && !m_LocalUser.IsHost) // If Lobby is not found and if we are not the host, it has already been deleted. No need to publish the error here. - { - PublishError(e); - } + UnsubscribeToJoinedLobbyAsync(); } } @@ -264,6 +209,91 @@ async void UpdateLobby(float unused) return (false, null); } + void ResetLobby() + { + CurrentUnityLobby = null; + m_LocalUser.ResetState(); + m_LocalLobby?.Reset(m_LocalUser); + + // no need to disconnect Netcode, it should already be handled by Netcode's callback to disconnect + } + + void OnLobbyChanges(ILobbyChanges changes) + { + if (changes.LobbyDeleted) + { + Debug.Log("Lobby deleted"); + ResetLobby(); + } + else + { + Debug.Log("Lobby updated"); + changes.ApplyToLobby(CurrentUnityLobby); + m_LocalLobby.ApplyRemoteData(CurrentUnityLobby); + + // as client, check if host is still in lobby + if (!m_LocalUser.IsHost) + { + foreach (var lobbyUser in m_LocalLobby.LobbyUsers) + { + if (lobbyUser.Value.IsHost) + { + return; + } + } + + m_UnityServiceErrorMessagePub.Publish(new UnityServiceErrorMessage("Host left the lobby", "Disconnecting.", UnityServiceErrorMessage.Service.Lobby)); + EndTracking(); + // no need to disconnect Netcode, it should already be handled by Netcode's callback to disconnect + } + } + } + + void OnKickedFromLobby() + { + Debug.Log("Kicked from Lobby"); + ResetLobby(); + } + + void OnLobbyEventConnectionStateChanged(LobbyEventConnectionState lobbyEventConnectionState) + { + m_LobbyEventConnectionState = lobbyEventConnectionState; + Debug.Log($"LobbyEventConnectionState changed to {lobbyEventConnectionState}"); + } + + async void SubscribeToJoinedLobbyAsync() + { + var lobbyEventCallbacks = new LobbyEventCallbacks(); + lobbyEventCallbacks.LobbyChanged += OnLobbyChanges; + lobbyEventCallbacks.KickedFromLobby += OnKickedFromLobby; + lobbyEventCallbacks.LobbyEventConnectionStateChanged += OnLobbyEventConnectionStateChanged; + // The LobbyEventCallbacks object created here will now be managed by the Lobby SDK. The callbacks will be + // unsubscribed from when we call UnsubscribeAsync on the ILobbyEvents object we receive and store here. + m_LobbyEvents = await m_LobbyApiInterface.SubscribeToLobby(m_LocalLobby.LobbyID, lobbyEventCallbacks); + m_JoinedLobbyContentHeartbeat.BeginTracking(); + } + + async void UnsubscribeToJoinedLobbyAsync() + { + if (m_LobbyEvents != null && m_LobbyEventConnectionState != LobbyEventConnectionState.Unsubscribed) + { + try + { + await m_LobbyEvents.UnsubscribeAsync(); + } + catch (ObjectDisposedException e) + { + // This exception occurs in the editor when exiting play mode without first leaving the lobby. + // This is because Wire disposes of subscriptions internally when exiting play mode in the editor. + Debug.Log("Subscription is already disposed of, cannot unsubscribe."); + Debug.Log(e.Message); + } + + } + m_HeartbeatTime = 0; + m_JoinedLobbyContentHeartbeat.EndTracking(); + } + /// /// Used for getting the list of all active lobbies, without needing full info for each. /// @@ -293,11 +323,11 @@ public async Task RetrieveAndPublishLobbyListAsync() } } - public async Task ReconnectToLobbyAsync(string lobbyId) + public async Task ReconnectToLobbyAsync() { try { - return await m_LobbyApiInterface.ReconnectToLobby(lobbyId); + return await m_LobbyApiInterface.ReconnectToLobby(m_LocalLobby.LobbyID); } catch (LobbyServiceException e) { @@ -314,12 +344,13 @@ public async Task ReconnectToLobbyAsync(string lobbyId) /// /// Attempt to leave a lobby /// - public async Task LeaveLobbyAsync(string lobbyId) + public async void LeaveLobbyAsync() { string uasId = AuthenticationService.Instance.PlayerId; try { - await m_LobbyApiInterface.RemovePlayerFromLobby(uasId, lobbyId); + await m_LobbyApiInterface.RemovePlayerFromLobby(uasId, m_LocalLobby.LobbyID); + ResetLobby(); } catch (LobbyServiceException e) { @@ -351,13 +382,14 @@ public async void RemovePlayerFromLobbyAsync(string uasId, string lobbyId) } } - public async Task DeleteLobbyAsync(string lobbyId) + public async void DeleteLobbyAsync() { if (m_LocalUser.IsHost) { try { - await m_LobbyApiInterface.DeleteLobby(lobbyId); + await m_LobbyApiInterface.DeleteLobby(m_LocalLobby.LobbyID); + ResetLobby(); } catch (LobbyServiceException e) { diff --git a/Assets/Scripts/Utils/NetworkOverlay/NetworkStats.cs b/Assets/Scripts/Utils/NetworkOverlay/NetworkStats.cs index 67b13b412..e8cf39293 100644 --- a/Assets/Scripts/Utils/NetworkOverlay/NetworkStats.cs +++ b/Assets/Scripts/Utils/NetworkOverlay/NetworkStats.cs @@ -61,14 +61,10 @@ public ExponentialMovingAverageCalculator(float average) ClientRpcParams m_PongClientParams; - bool m_IsServer; - string m_TextToDisplay; public override void OnNetworkSpawn() { - m_IsServer = IsServer; - bool isClientOnly = IsClient && !IsServer; if (!IsOwner && isClientOnly) // we don't want to track player ghost stats, only our own { @@ -98,7 +94,7 @@ void CreateNetworkStatsText() void FixedUpdate() { - if (!m_IsServer) + if (!IsServer) { if (Time.realtimeSinceStartup - m_LastPingTime > k_PingIntervalSeconds) { diff --git a/Assets/Scripts/Utils/ProfileManager.cs b/Assets/Scripts/Utils/ProfileManager.cs index aa7c75713..466e60a8c 100644 --- a/Assets/Scripts/Utils/ProfileManager.cs +++ b/Assets/Scripts/Utils/ProfileManager.cs @@ -80,7 +80,9 @@ static string GetProfile() var hashedBytes = new MD5CryptoServiceProvider() .ComputeHash(Encoding.UTF8.GetBytes(Application.dataPath)); Array.Resize(ref hashedBytes, 16); - return new Guid(hashedBytes).ToString("N"); + // Authentication service only allows profile names of maximum 30 characters. We're generating a GUID based + // on the project's path. Truncating the first 30 characters of said GUID string suffices for uniqueness. + return new Guid(hashedBytes).ToString("N")[..30]; #else return ""; #endif diff --git a/Assets/Tests/Runtime/ConnectionManagementTests.cs b/Assets/Tests/Runtime/ConnectionManagementTests.cs index 5254b95f7..a60217428 100644 --- a/Assets/Tests/Runtime/ConnectionManagementTests.cs +++ b/Assets/Tests/Runtime/ConnectionManagementTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using NUnit.Framework; using Unity.BossRoom.ConnectionManagement; using Unity.BossRoom.Infrastructure; @@ -55,8 +56,6 @@ public override void Awake() public override void Start() { } - public override void AddOnSceneEventCallback() { } - public override void LoadScene(string sceneName, bool useNetworkSceneManager, LoadSceneMode loadSceneMode = LoadSceneMode.Single) { } } @@ -357,8 +356,7 @@ public IEnumerator UnexpectedClientDisconnect_ClientReconnectingSuccessfully() subscriptions.Dispose(); } - - [UnityTest, Ignore("Test fails because shutdowns do not invoke OnClientDisconnect on the host, so the ConnectionManager doesn't properly transition to the Offline state")] + [UnityTest] public IEnumerator UnexpectedServerShutdown_ClientsFailToReconnect() { StartHost(); @@ -370,6 +368,7 @@ public IEnumerator UnexpectedServerShutdown_ClientsFailToReconnect() AssertAllClientsAreConnected(); var nbReconnectingMsgsReceived = 0; + var nbGenericDisconnectMsgReceived = 0; var subscriptions = new DisposableGroup(); for (int i = 0; i < NumberOfClients; i++) @@ -379,8 +378,18 @@ public IEnumerator UnexpectedServerShutdown_ClientsFailToReconnect() // ignoring the first success message that is in the buffer if (message != ConnectStatus.Success) { - Assert.AreEqual(ConnectStatus.Reconnecting, message, "Received unexpected ConnectStatus message."); - nbReconnectingMsgsReceived++; + var possibleMessages = new List(); + possibleMessages.Add(ConnectStatus.Reconnecting); + possibleMessages.Add(ConnectStatus.GenericDisconnect); + Assert.Contains(message, possibleMessages, "Received unexpected ConnectStatus message."); + if (message == ConnectStatus.Reconnecting) + { + nbReconnectingMsgsReceived++; + } + else if (message == ConnectStatus.GenericDisconnect) + { + nbGenericDisconnectMsgReceived++; + } } })); } @@ -398,22 +407,32 @@ public IEnumerator UnexpectedServerShutdown_ClientsFailToReconnect() Assert.IsFalse(m_ClientNetworkManagers[clientId].IsConnectedClient, $"Client{clientId} has not shut down properly after losing connection."); } - // Waiting for clients to fail to automatically reconnect + var maxNbReconnectionAttempts = 0; + for (var i = 0; i < NumberOfClients; i++) { - for (var j = 0; j < m_ClientConnectionManagers[i].NbReconnectAttempts; j++) + var nbReconnectionAttempts = m_ClientConnectionManagers[i].NbReconnectAttempts; + maxNbReconnectionAttempts = Math.Max(maxNbReconnectionAttempts, nbReconnectionAttempts); + for (var j = 0; j < nbReconnectionAttempts; j++) { // Expecting this error for each reconnection attempt for each client LogAssert.Expect(LogType.Error, k_FailedToConnectToServerErrorMessage); } } - yield return WaitForClientsConnectedOrTimeOut(); - for (var i = 0; i < NumberOfClients; i++) + + // Waiting for clients to fail to automatically reconnect. We wait once for each reconnection attempt. + for (var i = 0; i < maxNbReconnectionAttempts; i++) { - Assert.IsFalse(m_ClientNetworkManagers[i].IsConnectedClient, $"Client{i} is connected while no server is running."); + yield return WaitForClientsConnectedOrTimeOut(); + for (var j = 0; j < NumberOfClients; j++) + { + Assert.IsFalse(m_ClientNetworkManagers[j].IsConnectedClient, $"Client{j} is connected while no server is running."); + } + } Assert.AreEqual(NumberOfClients, nbReconnectingMsgsReceived, "Not all clients received a Reconnecting message."); + Assert.AreEqual(NumberOfClients, nbGenericDisconnectMsgReceived, "Not all clients received a GenericDisconnect message."); subscriptions.Dispose(); } diff --git a/CHANGELOG.md b/CHANGELOG.md index d59190ffc..7a32e9032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,33 @@ Additional documentation and release notes are available at [Multiplayer Documen ## [unreleased] - yyyy-mm-dd +## [2.1.0] - 2023-04-27 + +### Added +* Added OnServerStopped event to ConnectionManager and ConnectionState (#826). This allows for the detection of an unexpected shutdown on the server side. + +### Changed +* Replaced our polling for lobby updates with a subscription to the new Websocket based LobbyEvents (#805). This saves up a significant amount of bandwidth usage to and from the service, since updates are infrequent in this game. Now clients and hosts only use up bandwidth on the Lobby service when it is needed. With polling, we used to send a GET request per client once every 2s. The responses were between ~550 bytes and 900 bytes, so if we suppose an average of 725 bytes and 100 000 concurrent users (CCU), this amounted to around 725B * 30 calls per minute * 100 000 CCU = 2.175 GB per minute. Scaling this to a month would get us 93.96 TB per month. In our case, since the only changes to the lobbies happen when a user connects or disconnects, most of that data was not necessary and can be saved to reduce bandwidth usage. Since the cost of using the Lobby service depends on bandwidth usage, this would also save money on an actual game. +* Simplified reconnection flow by offloading responsibility to ConnectionMethod (#804). Now the ClientReconnectingState uses the ConnectionMethod it is configured with to handle setting up reconnection (i.e. reconnecting to the Lobby before trying to reconnect to the Relay server if it is using Relay and Lobby). It can now also fail early and stop retrying if the lobby doesn't exist anymore. +* Replaced our custom pool implementation using queues with ObjectPool (#824)(#827) +* Upgraded Boss Room to NGO 1.3.1 (#828) NetworkPrefabs inside NetworkManager's NetworkPrefabs list have been converted to NetworkPrefabsList ScriptableObject. +* Upgraded Boss Room to NGO 1.4.0 (#829) +* Profile names generated are now only 30 characters or under to fit Authentication Service requirements (#831) + +### Cleanup +* Clarified a TODO comment inside ClientCharacter, detailing how anticipation should only be executed on owning client players (#786) +* Removed now unnecessary cached NetworkBehaviour status on some components, since they now do not allocate memory (#799) +* Certain structs converted to implement interface INetworkSerializeByMemcpy instead of INetworkSerializable (#822) INetworkSerializeByMemcpy optimizes for performance at the cost of bandwidth usage and flexibility, however it will only work with structs containing value types. For more details see the official [doc](https://docs-multiplayer.unity3d.com/netcode/current/advanced-topics/serialization/inetworkserializebymemcpy/index.html). + +### Fixed +* EnemyPortals' VFX get disabled and re-enabled once the breakable crystals are broken (#784) +* Elements inside the Tank's and Rogue's AnimatorTriggeredSpecialFX list have been revised to not loop AudioSource clips, ending the logging of multiple warnings to the console (#785) +* ClientConnectedState now inherits from OnlineState instead of the base ConnectionState (#801) +* UpdateRunner now sends the right value for deltaTime when updating its subscribers (#805) +* Inputs are better sanitized when entering IP address and port (#821). Now all invalid characters are prevented, and UnityTransport's NetworkEndpoint.TryParse is used to verify the validity of the IP address and port that are entered before making the join/host button interactable. +* Fixed failing connection management test (#826). This test had to be ignored previously because there was no mechanism to detect unexpected server shutdowns. With the OnServerStopped callback introduced in NGO 1.4.0, this is no longer an issue. +* Decoupled SceneLoaderWrapper and ConnectionStates (#830). The OnServerStarted and OnClientStarted callbacks available in NGO 1.4.0 allows us to remove the need for an external method to initialize the SceneLoaderWrapper after starting a NetworkingSession. + ## [2.0.4] - 2022-12-13 ### Changed * Updated Boss Room to NGO 1.2.0 (#791). diff --git a/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md b/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md index e66dc5d41..1935ddf37 100644 --- a/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md +++ b/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md @@ -2,6 +2,11 @@ ## [unreleased] - yyyy-mm-dd +## [1.6.0] - 2023-04-27 + +### Changed +* Removed need for SceneLoaderWrapper.AddOnSceneEventCallback (#830). The OnServerStarted and OnClientStarted callbacks available in NGO 1.4.0 allows us to remove the need for an external method to initialize the SceneLoaderWrapper after starting a NetworkingSession. + ## [1.5.1] - 2022-12-13 ### Changed * Bumped RNSM to 1.1.0: Switched x axis units to seconds instead of frames now that it's available. This means adjusting the sample count to a lower value as well to 30 seconds, since the x axis was moving too slowly. (#788) diff --git a/Packages/com.unity.multiplayer.samples.coop/Utilities/SceneManagement/SceneLoaderWrapper.cs b/Packages/com.unity.multiplayer.samples.coop/Utilities/SceneManagement/SceneLoaderWrapper.cs index 0ad640cd3..20488984b 100644 --- a/Packages/com.unity.multiplayer.samples.coop/Utilities/SceneManagement/SceneLoaderWrapper.cs +++ b/Packages/com.unity.multiplayer.samples.coop/Utilities/SceneManagement/SceneLoaderWrapper.cs @@ -21,6 +21,8 @@ public class SceneLoaderWrapper : NetworkBehaviour bool IsNetworkSceneManagementEnabled => NetworkManager != null && NetworkManager.SceneManager != null && NetworkManager.NetworkConfig.EnableSceneManagement; + bool m_IsInitialized; + public static SceneLoaderWrapper Instance { get; protected set; } public virtual void Awake() @@ -39,32 +41,50 @@ public virtual void Awake() public virtual void Start() { SceneManager.sceneLoaded += OnSceneLoaded; + NetworkManager.OnServerStarted += OnNetworkingSessionStarted; + NetworkManager.OnClientStarted += OnNetworkingSessionStarted; + NetworkManager.OnServerStopped += OnNetworkingSessionEnded; + NetworkManager.OnClientStopped += OnNetworkingSessionEnded; } - public override void OnDestroy() + void OnNetworkingSessionStarted() { - SceneManager.sceneLoaded -= OnSceneLoaded; - base.OnDestroy(); + // This prevents this to be called twice on a host, which receives both OnServerStarted and OnClientStarted callbacks + if (!m_IsInitialized) + { + if (IsNetworkSceneManagementEnabled) + { + NetworkManager.SceneManager.OnSceneEvent += OnSceneEvent; + } + + m_IsInitialized = true; + } } - public override void OnNetworkDespawn() + void OnNetworkingSessionEnded(bool unused) { - if (NetworkManager != null && NetworkManager.SceneManager != null) + if (m_IsInitialized) { - NetworkManager.SceneManager.OnSceneEvent -= OnSceneEvent; + if (IsNetworkSceneManagementEnabled) + { + NetworkManager.SceneManager.OnSceneEvent -= OnSceneEvent; + } + + m_IsInitialized = false; } } - /// - /// Initializes the callback on scene events. This needs to be called right after initializing NetworkManager - /// (after StartHost, StartClient or StartServer) - /// - public virtual void AddOnSceneEventCallback() + public override void OnDestroy() { - if (IsNetworkSceneManagementEnabled) + SceneManager.sceneLoaded -= OnSceneLoaded; + if (NetworkManager != null) { - NetworkManager.SceneManager.OnSceneEvent += OnSceneEvent; + NetworkManager.OnServerStarted -= OnNetworkingSessionStarted; + NetworkManager.OnClientStarted -= OnNetworkingSessionStarted; + NetworkManager.OnServerStopped -= OnNetworkingSessionEnded; + NetworkManager.OnClientStopped -= OnNetworkingSessionEnded; } + base.OnDestroy(); } /// diff --git a/Packages/manifest.json b/Packages/manifest.json index f142a1e3c..ee01fee25 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -10,14 +10,15 @@ "com.unity.learn.iet-framework": "2.2.2", "com.unity.memoryprofiler": "0.5.0-preview.1", "com.unity.multiplayer.tools": "1.1.0", - "com.unity.netcode.gameobjects": "1.2.0", + "com.unity.netcode.gameobjects": "1.4.0", "com.unity.performance.profile-analyzer": "1.1.1", "com.unity.postprocessing": "3.2.2", "com.unity.render-pipelines.universal": "12.1.8", "com.unity.services.authentication": "2.3.1", - "com.unity.services.lobby": "1.0.3", + "com.unity.services.lobby": "1.1.0-pre.3", "com.unity.services.relay": "1.0.3", - "com.unity.test-framework": "1.1.31", + "com.unity.services.wire": "1.0.0", + "com.unity.test-framework": "1.1.33", "com.unity.textmeshpro": "3.0.6", "com.unity.timeline": "1.6.4", "com.unity.toolchain.macos-x86_64-linux-x86_64": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index c3c3565ff..74e32d2a1 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -142,12 +142,12 @@ "url": "https://packages.unity.com" }, "com.unity.netcode.gameobjects": { - "version": "1.2.0", + "version": "1.4.0", "depth": 0, "source": "registry", "dependencies": { "com.unity.nuget.mono-cecil": "1.10.1", - "com.unity.transport": "1.3.0" + "com.unity.transport": "1.3.3" }, "url": "https://packages.unity.com" }, @@ -240,18 +240,19 @@ "url": "https://packages.unity.com" }, "com.unity.services.lobby": { - "version": "1.0.3", + "version": "1.1.0-pre.3", "depth": 0, "source": "registry", "dependencies": { - "com.unity.services.core": "1.4.0", + "com.unity.services.core": "1.4.2", "com.unity.modules.unitywebrequest": "1.0.0", "com.unity.modules.unitywebrequestassetbundle": "1.0.0", "com.unity.modules.unitywebrequestaudio": "1.0.0", "com.unity.modules.unitywebrequesttexture": "1.0.0", "com.unity.modules.unitywebrequestwww": "1.0.0", "com.unity.nuget.newtonsoft-json": "3.0.2", - "com.unity.services.authentication": "2.0.0" + "com.unity.services.authentication": "2.1.1", + "com.unity.services.wire": "1.1.0" }, "url": "https://packages.unity.com" }, @@ -286,6 +287,17 @@ }, "url": "https://packages.unity.com" }, + "com.unity.services.wire": { + "version": "1.1.1", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.services.core": "1.4.2", + "com.unity.nuget.newtonsoft-json": "3.0.2", + "com.unity.services.authentication": "2.1.1" + }, + "url": "https://packages.unity.com" + }, "com.unity.settings-manager": { "version": "1.0.3", "depth": 1, @@ -319,7 +331,7 @@ "url": "https://packages.unity.com" }, "com.unity.test-framework": { - "version": "1.1.31", + "version": "1.1.33", "depth": 0, "source": "registry", "dependencies": { @@ -361,7 +373,7 @@ "url": "https://packages.unity.com" }, "com.unity.transport": { - "version": "1.3.0", + "version": "1.3.3", "depth": 1, "source": "registry", "dependencies": { diff --git a/ProjectSettings/NetcodeForGameObjects.settings b/ProjectSettings/NetcodeForGameObjects.settings new file mode 100644 index 000000000..42843e1c0 --- /dev/null +++ b/ProjectSettings/NetcodeForGameObjects.settings @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 0} + m_Name: + m_EditorClassIdentifier: Unity.Netcode.Editor:Unity.Netcode.Editor.Configuration:NetcodeForGameObjectsProjectSettings + GenerateDefaultNetworkPrefabs: 0 diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 3b91da48a..3223fd191 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25524b1df8032e5b5bdd4a5b90b3fd6a0589ca83dd9c6df1af1538337c2b4d9e +oid sha256:d4f9e39c4eab1fd9657810bad1f940055450bcbca579468daa5cf187144a1dad size 25665 diff --git a/README.md b/README.md index 01190b5e2..a07f1e1ff 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ Running the game over internet currently requires setting up a relay. * Client driven movements - Boss Room is server driven with anticipation animation. See [Client Driven bitesize](https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize/tree/main/Basic/ClientDriven) for client driven gameplay * Player spawn - SpawnPlayer() in [Assets/Scripts/Gameplay/GameState/ServerBossRoomState.cs](Assets/Scripts/Gameplay/GameState/ServerBossRoomState.cs) * Player camera setup (with cinemachine) - OnNetworkSpawn() in [Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs](Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs) +* INetworkSerializable (bandwidth optimization) vs INetworkSerializeByMemcpy (performance optimization) usage. See LobbyPlayerState vs ActionID structs [Assets/Scripts/Gameplay/GameState/NetworkCharSelection.cs](Assets/Scripts/Gameplay/GameState/NetworkCharSelection.cs) vs [Assets/Scripts/Gameplay/Action/ActionID.cs](Assets/Scripts/Gameplay/Action/ActionID.cs) ### Game Flow * Application Controller - [Assets/Scripts/ApplicationLifecycle/ApplicationController.cs ](Assets/Scripts/ApplicationLifecycle/ApplicationController.cs) @@ -192,6 +193,7 @@ Running the game over internet currently requires setting up a relay. * Lobby and relay - client join - JoinLobbyRequest() in [Assets/Scripts/Gameplay/UI/Lobby/LobbyUIMediator.cs ](Assets/Scripts/Gameplay/UI/Lobby/LobbyUIMediator.cs) * Relay Join - StartClientLobby() in [Assets/Scripts/ConnectionManagement/ConnectionState/OfflineState.cs ](Assets/Scripts/ConnectionManagement/ConnectionState/OfflineState.cs) * Relay Create - StartHostLobby() in [Assets/Scripts/ConnectionManagement/ConnectionState/OfflineState.cs ](Assets/Scripts/ConnectionManagement/ConnectionState/OfflineState.cs) +* Subscribing to LobbyEvents - SubscribeToJoinedLobby() in [Assets/Scripts/UnityServices/Lobbies/LobbyServiceFacade.cs ](Assets/Scripts/UnityServices/Lobbies/LobbyServiceFacade.cs) * Authentication - EnsurePlayerIsAuthorized() in [Assets/Scripts/UnityServices/Auth/AuthenticationServiceFacade.cs ](Assets/Scripts/UnityServices/Auth/AuthenticationServiceFacade.cs) * Authentication - Profile management for ParrelSync/local instances - GetProfile() in [Assets/Scripts/Utils/ProfileManager.cs](Assets/Scripts/Utils/ProfileManager.cs) * Profile manager for ParrelSync and local play [Assets/Scripts/Utils/ProfileManager.cs](Assets/Scripts/Utils/ProfileManager.cs)