From 12b0ef4f86d2efad4c25c206746adeeae4df4fe2 Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Thu, 15 Dec 2022 15:41:52 -0500 Subject: [PATCH 01/22] fix: EnemyPortals' VFX get disabled/enabled when crystals are broken [MTT-4893] (#784) * re-adding breakable component reference to enemyportal * changelog addition --- Assets/Prefabs/Game/EnemySpawner.prefab | 4 ++-- .../Gameplay/GameplayObjects/Breakable.cs | 17 ++++++++++------- .../Gameplay/GameplayObjects/EnemyPortal.cs | 19 ++++++------------- CHANGELOG.md | 2 ++ 4 files changed, 20 insertions(+), 22 deletions(-) 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/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/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/CHANGELOG.md b/CHANGELOG.md index d59190ffc..1a722fa23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [unreleased] - yyyy-mm-dd +### Fixed +* EnemyPortals' VFX get disabled and re-enabled once the breakable crystals are broken (#784) ## [2.0.4] - 2022-12-13 ### Changed From 449db34afe9e06b12c05632938f801378101b7d2 Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Thu, 15 Dec 2022 16:34:18 -0500 Subject: [PATCH 02/22] fix: multiple warnings on insufficient AudioSources available [MTT-2834] (#785) * untoggling Tank graphics hitreact audio loop property * removing loop audio toggle from Rogue's dash end * changelog addition --- .../CharGFX/PlayerGraphics_Rogue.prefab | 24 +++++++++---------- .../CharGFX/PlayerGraphics_Tank.prefab | 24 +++++++++---------- CHANGELOG.md | 1 + 3 files changed, 25 insertions(+), 24 deletions(-) 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/CHANGELOG.md b/CHANGELOG.md index 1a722fa23..2ea9d0510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ## [unreleased] - yyyy-mm-dd ### 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) ## [2.0.4] - 2022-12-13 ### Changed From 3b391ef341df0fea51af64c6a8de4d5232f6f2d6 Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Fri, 16 Dec 2022 11:50:52 -0500 Subject: [PATCH 03/22] chore: ClientCharacter TODO cleanup on anticipation [MTT-1744] (#786) * clientcharacter todo cleanup * changelog addition --- .../Character/ClientCharacter.cs | 22 ++++++------------- CHANGELOG.md | 3 +++ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs b/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs index d3cf2f067..e94818761 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; @@ -129,20 +126,19 @@ public override void OnNetworkSpawn() 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 +162,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 +177,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)) @@ -282,9 +274,9 @@ void Update() // 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/CHANGELOG.md b/CHANGELOG.md index 2ea9d0510..2dafc7e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [unreleased] - yyyy-mm-dd +### Cleanup +* Clarified a TODO comment inside ClientCharacter, detailing how anticipation should only be executed on owning client players (#786) + ### 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) From 309072e5db1d02b24e53cd00fefffd205dafb7ee Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Fri, 16 Dec 2022 13:02:36 -0500 Subject: [PATCH 04/22] chore: Removing (now) redundant cached NetworkBehaviour status (#799) * removing (now) redundant cached NetworkBehaviour status * changelog addition --- .../Gameplay/GameplayObjects/Character/ClientCharacter.cs | 6 +----- Assets/Scripts/Utils/NetworkOverlay/NetworkStats.cs | 6 +----- CHANGELOG.md | 1 + 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs b/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs index e94818761..c59a04884 100644 --- a/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs +++ b/Assets/Scripts/Gameplay/GameplayObjects/Character/ClientCharacter.cs @@ -63,8 +63,6 @@ public class ClientCharacter : NetworkBehaviour Quaternion m_LerpedRotation; - bool m_IsHost; - float m_CurrentSpeed; /// @@ -120,8 +118,6 @@ public override void OnNetworkSpawn() enabled = true; - m_IsHost = IsHost; - m_ClientActionViz = new ClientActionPlayer(this); m_ServerCharacter = GetComponentInParent(); @@ -268,7 +264,7 @@ 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 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/CHANGELOG.md b/CHANGELOG.md index 2dafc7e42..8ff5201fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ## [unreleased] - yyyy-mm-dd ### 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) ### Fixed * EnemyPortals' VFX get disabled and re-enabled once the breakable crystals are broken (#784) From 3c409f223e291f06adbd42009d77dcf8b87a734e Mon Sep 17 00:00:00 2001 From: Sam Bellomo <71790295+SamuelBellomo@users.noreply.github.com> Date: Fri, 16 Dec 2022 13:28:09 -0500 Subject: [PATCH 05/22] feat: Adding build menus to automatically build all platforms for our platests (#798) --- Assets/Scripts/Editor/BuildHelpers.cs | 243 +++++++++++++++++++++ Assets/Scripts/Editor/BuildHelpers.cs.meta | 11 + 2 files changed, 254 insertions(+) create mode 100644 Assets/Scripts/Editor/BuildHelpers.cs create mode 100644 Assets/Scripts/Editor/BuildHelpers.cs.meta 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: From c5f0f25ba2221b0046377b4f4dd34784a7ba0c58 Mon Sep 17 00:00:00 2001 From: Sam Bellomo <71790295+SamuelBellomo@users.noreply.github.com> Date: Tue, 3 Jan 2023 11:16:01 -0500 Subject: [PATCH 06/22] wrong comment correction (#800) --- Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); /// From bcac45b04843c158f7f523d115c6802a6fd09e4c Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Fri, 6 Jan 2023 14:27:43 -0500 Subject: [PATCH 07/22] fix: wrong base class for ClientConnectedState [MTT-5293] (#801) * set ClientConnectedState inheriting from OnlineState * moved k_DtlsConnType constant to ConnectionMethod where it is used --- Assets/Scripts/ConnectionManagement/ConnectionMethod.cs | 5 +++-- .../ConnectionState/ClientConnectedState.cs | 8 +------- .../ConnectionManagement/ConnectionState/OnlineState.cs | 2 -- CHANGELOG.md | 1 + 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs index 4ac0cbac0..11f78decb 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs @@ -21,6 +21,7 @@ public abstract class ConnectionMethodBase protected ConnectionManager m_ConnectionManager; readonly ProfileManager m_ProfileManager; protected readonly string m_PlayerName; + protected const string k_DtlsConnType = "dtls"; public abstract Task SetupHostConnectionAsync(); @@ -128,7 +129,7 @@ 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 SetupHostConnectionAsync() @@ -152,7 +153,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/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/CHANGELOG.md b/CHANGELOG.md index 8ff5201fe..9fdbd3e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### 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) ## [2.0.4] - 2022-12-13 ### Changed From f4144efaa8004925543e8d8aa3ed9d46e0690c96 Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Fri, 17 Feb 2023 11:44:14 -0500 Subject: [PATCH 08/22] fix: yamato jobs failing because of unsupported bokken image (#806) * updating test framework package * using android-execution-base image for android builds --- .yamato/mobile-build-and-run.yml | 2 +- Packages/manifest.json | 2 +- Packages/packages-lock.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.yamato/mobile-build-and-run.yml b/.yamato/mobile-build-and-run.yml index 332083314..f6a761c8d 100644 --- a/.yamato/mobile-build-and-run.yml +++ b/.yamato/mobile-build-and-run.yml @@ -37,7 +37,7 @@ 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: diff --git a/Packages/manifest.json b/Packages/manifest.json index f142a1e3c..b3cab124c 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -17,7 +17,7 @@ "com.unity.services.authentication": "2.3.1", "com.unity.services.lobby": "1.0.3", "com.unity.services.relay": "1.0.3", - "com.unity.test-framework": "1.1.31", + "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..95dea68eb 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -319,7 +319,7 @@ "url": "https://packages.unity.com" }, "com.unity.test-framework": { - "version": "1.1.31", + "version": "1.1.33", "depth": 0, "source": "registry", "dependencies": { From 555f64285b68bf59e886bc98d012379886e064fd Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Wed, 22 Feb 2023 11:17:34 -0500 Subject: [PATCH 09/22] chore: update android bokken image for CI (#809) * updating image * removing arg to specify player connection IP --- .yamato/mobile-build-and-run.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.yamato/mobile-build-and-run.yml b/.yamato/mobile-build-and-run.yml index f6a761c8d..3733e0131 100644 --- a/.yamato/mobile-build-and-run.yml +++ b/.yamato/mobile-build-and-run.yml @@ -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: From db908139f96c75c0f471e4c1edcd9a1ff124cd8a Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Mon, 6 Mar 2023 10:45:31 -0500 Subject: [PATCH 10/22] feat: simplifying reconnection (#804) * Simplifying reconnection * Adding possibility of early failure when reconnecting if lobby no longer exists --- .yamato/mobile-build-and-run.yml | 4 +- .../ConnectionManagement/ConnectionManager.cs | 1 - .../ConnectionManagement/ConnectionMethod.cs | 52 ++++++++++++++++++- .../ConnectionState/ClientConnectingState.cs | 8 +-- .../ClientReconnectingState.cs | 45 ++++++---------- CHANGELOG.md | 3 ++ 6 files changed, 73 insertions(+), 40 deletions(-) diff --git a/.yamato/mobile-build-and-run.yml b/.yamato/mobile-build-and-run.yml index 3733e0131..a52461345 100644 --- a/.yamato/mobile-build-and-run.yml +++ b/.yamato/mobile-build-and-run.yml @@ -13,7 +13,7 @@ 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 + - 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 {{ editor }} --fast --wait - curl -s https://artifactory.prd.it.unity3d.com/artifactory/unity-tools-local/utr-standalone/utr --output utr - chmod +x ./utr @@ -42,7 +42,7 @@ Build_Player_With_Tests_Android_{{ project.name }}_{{ editor }}: 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 diff --git a/Assets/Scripts/ConnectionManagement/ConnectionManager.cs b/Assets/Scripts/ConnectionManagement/ConnectionManager.cs index b2b2fa42c..3932807c8 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; diff --git a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs index 11f78decb..c70e155ed 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs @@ -23,10 +23,28 @@ public abstract class ConnectionMethodBase 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; @@ -82,6 +100,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 @@ -91,7 +115,7 @@ public override async Task SetupHostConnectionAsync() } /// - /// UTP's Relay connection setup + /// UTP's Relay connection setup using the Lobby integration /// class ConnectionMethodRelay : ConnectionMethodBase { @@ -132,6 +156,32 @@ public override async Task SetupClientConnectionAsync() 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(m_LocalLobby.LobbyID); + var success = lobby != null; + if (success) + { + Debug.Log("Successfully reconnected to Lobby."); + } + else + { + Debug.Log("Failed to reconnect to Lobby."); + } + return (success, true); // return a success if reconnecting to lobby returns a lobby + } + public override async Task SetupHostConnectionAsync() { Debug.Log("Setting up Unity Relay host"); diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs index d8b7106db..be9a0512c 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs @@ -1,9 +1,7 @@ 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 +11,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) { diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs index e6e47ff42..2977b67a3 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs @@ -19,7 +19,6 @@ class ClientReconnectingState : ClientConnectingState IPublisher m_ReconnectMessagePublisher; Coroutine m_ReconnectCoroutine; - string m_LobbyCode = ""; int m_NbAttempts; const float k_TimeBetweenAttempts = 5; @@ -27,7 +26,6 @@ class ClientReconnectingState : ClientConnectingState public override void Enter() { m_NbAttempts = 0; - m_LobbyCode = m_LobbyServiceFacade.CurrentUnityLobby != null ? m_LobbyServiceFacade.CurrentUnityLobby.LobbyCode : ""; m_ReconnectCoroutine = m_ConnectionManager.StartCoroutine(ReconnectCoroutine()); } @@ -108,36 +106,25 @@ IEnumerator ReconnectCoroutine() 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); + var reconnectingSetupTask = m_ConnectionMethod.SetupClientReconnectionAsync(); + yield return new WaitUntil(() => reconnectingSetupTask.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); - } - } - else // If not using Lobby, simply try to reconnect to the server directly + 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 connectingToRelay = ConnectClientAsync(); + yield return new WaitUntil(() => connectingToRelay.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/CHANGELOG.md b/CHANGELOG.md index 9fdbd3e7f..0a19f1f02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ Additional documentation and release notes are available at [Multiplayer Documen * 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) +### Changed +* 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. + ### 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) From 5b40458848dfd4f8fa2c1ac272bc3ce9ecb4492e Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Wed, 12 Apr 2023 11:35:30 -0400 Subject: [PATCH 11/22] feat: replacing polling for lobby updates with LobbyEvents [MTT-5425] (#805) * replacing polling for lobby updates with lobby events * adding support for deleted lobby events * updating to lobby 1.1.0-pre.3 * fixing issue with UpdateRunner using wrong dt when updating subscribers * simplifying LobbyServiceFacade delete, leaveLobby and reconnect method calls * Adding early return in reconnection when lobby is deleted * removing GetLobby unused method * Adding short delay before first reconnect attempt to give time for lobby to be properly updated Co-authored-by: Sam Bellomo <71790295+SamuelBellomo@users.noreply.github.com> --- .yamato/mobile-build-and-run.yml | 2 +- .../ConnectionManagement/ConnectionMethod.cs | 11 +- .../ClientReconnectingState.cs | 10 + Assets/Scripts/Gameplay/UI/RoomNameBox.cs | 7 +- Assets/Scripts/Infrastructure/UpdateRunner.cs | 6 +- .../Lobbies/LobbyAPIInterface.cs | 10 +- .../Lobbies/LobbyServiceFacade.cs | 180 +++++++++++------- CHANGELOG.md | 5 + Packages/manifest.json | 3 +- Packages/packages-lock.json | 18 +- README.md | 1 + 11 files changed, 156 insertions(+), 97 deletions(-) diff --git a/.yamato/mobile-build-and-run.yml b/.yamato/mobile-build-and-run.yml index a52461345..94865b87c 100644 --- a/.yamato/mobile-build-and-run.yml +++ b/.yamato/mobile-build-and-run.yml @@ -14,7 +14,7 @@ Build_Player_With_Tests_iOS_{{ project.name }}_{{ editor }}: commands: - 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 {{ editor }} --fast --wait + - 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 diff --git a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs index c70e155ed..90b940b3e 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs @@ -169,16 +169,9 @@ public override async Task SetupClientConnectionAsync() // 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(m_LocalLobby.LobbyID); + var lobby = await m_LobbyServiceFacade.ReconnectToLobbyAsync(); var success = lobby != null; - if (success) - { - Debug.Log("Successfully reconnected to Lobby."); - } - else - { - Debug.Log("Failed to reconnect to Lobby."); - } + 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 } diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs index 2977b67a3..7da62da1b 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs @@ -21,6 +21,7 @@ class ClientReconnectingState : ClientConnectingState Coroutine m_ReconnectCoroutine; int m_NbAttempts; + const float k_TimeBeforeFirstAttempt = 1; const float k_TimeBetweenAttempts = 5; public override void Enter() @@ -105,6 +106,15 @@ 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)); + + // 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); + } + m_NbAttempts++; var reconnectingSetupTask = m_ConnectionMethod.SetupClientReconnectionAsync(); yield return new WaitUntil(() => reconnectingSetupTask.IsCompleted); 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/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/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/CHANGELOG.md b/CHANGELOG.md index 0a19f1f02..6bc32d931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [unreleased] - yyyy-mm-dd + +### 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. +* ### 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) @@ -18,6 +22,7 @@ Additional documentation and release notes are available at [Multiplayer Documen * 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) ## [2.0.4] - 2022-12-13 ### Changed diff --git a/Packages/manifest.json b/Packages/manifest.json index b3cab124c..9d0469411 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -15,8 +15,9 @@ "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.services.wire": "1.0.0", "com.unity.test-framework": "1.1.33", "com.unity.textmeshpro": "3.0.6", "com.unity.timeline": "1.6.4", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 95dea68eb..e24f16460 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -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, diff --git a/README.md b/README.md index 01190b5e2..b6ab2d1e5 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,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) From fd5b1dd7da48f92dc266e2e88227448b4adcbd76 Mon Sep 17 00:00:00 2001 From: Sam Bellomo <71790295+SamuelBellomo@users.noreply.github.com> Date: Wed, 12 Apr 2023 12:15:10 -0400 Subject: [PATCH 12/22] chore: Adding quick comment explaining player ID (#816) * Update ConnectionMethod.cs * Update Assets/Scripts/ConnectionManagement/ConnectionMethod.cs --- Assets/Scripts/ConnectionManagement/ConnectionMethod.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs index 90b940b3e..9e56ecf0d 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionMethod.cs @@ -66,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) From 90c517f30e895ab8577199cd7afeeb8c455d4b31 Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Thu, 20 Apr 2023 15:02:39 -0400 Subject: [PATCH 13/22] fix: better input sanitization [MTT-5894] (#821) * using NetworkEndPoint.TryParse to validate IP address and port --- Assets/Prefabs/UI/IPPopup.prefab | 10 +++++---- Assets/Scripts/Gameplay/UI/IPHostingUI.cs | 10 ++++++--- Assets/Scripts/Gameplay/UI/IPJoiningUI.cs | 10 ++++++--- Assets/Scripts/Gameplay/UI/IPUIMediator.cs | 26 +++++++++++++++++++--- CHANGELOG.md | 1 + 5 files changed, 44 insertions(+), 13 deletions(-) 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/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/CHANGELOG.md b/CHANGELOG.md index 6bc32d931..f5469eb6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Additional documentation and release notes are available at [Multiplayer Documen * 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. ## [2.0.4] - 2022-12-13 ### Changed From 0281f5ed12d68493fb26462d223cc2fd924d0203 Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Thu, 20 Apr 2023 16:36:08 -0400 Subject: [PATCH 14/22] fix: Adding INetworkSerializeByMemcpy where recommended for network serialization [MTT-5912] (#822) * adding INetworkSerializeByMemcpy where recommended * changelog cleanup & addition * comments inside lobbyplayerstate, index added, changelog revision --- Assets/Scripts/Gameplay/Action/ActionID.cs | 7 +------ .../Scripts/Gameplay/GameState/NetworkCharSelection.cs | 6 ++++++ Assets/Scripts/Infrastructure/NetworkGuid.cs | 8 +------- CHANGELOG.md | 9 ++++----- README.md | 1 + 5 files changed, 13 insertions(+), 18 deletions(-) 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/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/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/CHANGELOG.md b/CHANGELOG.md index f5469eb6d..ed6908937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,12 @@ Additional documentation and release notes are available at [Multiplayer Documen ### 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. + ### 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) - -### Changed -* 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. +* 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) diff --git a/README.md b/README.md index b6ab2d1e5..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) From 512bb557a736c7a9758cfe978f8b3c32deb40201 Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Fri, 21 Apr 2023 14:13:46 -0400 Subject: [PATCH 15/22] refactor: replacing custom pool implementation with ObjectPool [MTT-6263] (#824) * replacing custom pool implementation with native one * removing unnecessary external public apis --- .../Infrastructure/NetworkObjectPool.cs | 170 +++++++----------- CHANGELOG.md | 1 + 2 files changed, 70 insertions(+), 101 deletions(-) diff --git a/Assets/Scripts/Infrastructure/NetworkObjectPool.cs b/Assets/Scripts/Infrastructure/NetworkObjectPool.cs index 4c18daa1a..ea3dcf59a 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,16 +74,12 @@ 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. @@ -89,51 +94,54 @@ 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); + m_PooledObjects[prefab].Release(networkObject); } /// - /// Adds a prefab to the list of spawnable prefabs. + /// Builds up the cache for a prefab. /// - /// The prefab to add. - /// - public void AddPrefab(GameObject prefab, int prewarmCount = 0) + void RegisterPrefabInternal(GameObject prefab, int prewarmCount) { - var networkObject = prefab.GetComponent(); + NetworkObject CreateFunc() + { + return Instantiate(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."); + void ActionOnGet(NetworkObject networkObject) + { + networkObject.gameObject.SetActive(true); + } - RegisterPrefabInternal(prefab, prewarmCount); - } + void ActionOnRelease(NetworkObject networkObject) + { + networkObject.gameObject.SetActive(false); + } - /// - /// Builds up the cache for a prefab. - /// - private void RegisterPrefabInternal(GameObject prefab, int prewarmCount) - { - prefabs.Add(prefab); + void ActionOnDestroy(NetworkObject networkObject) + { + Destroy(networkObject.gameObject); + } - var prefabQueue = new Queue(); - pooledObjects[prefab] = prefabQueue; - for (int i = 0; i < prewarmCount; i++) + m_Prefabs.Add(prefab); + + // Create the pool + m_PooledObjects[prefab] = new ObjectPool(CreateFunc, ActionOnGet, ActionOnRelease, ActionOnDestroy, defaultCapacity: prewarmCount); + + // Populate the pool + var prewarmNetworkObjects = new List(); + for (var i = 0; i < prewarmCount; i++) + { + prewarmNetworkObjects.Add(m_PooledObjects[prefab].Get()); + } + foreach (var networkObject in prewarmNetworkObjects) { - var go = CreateInstance(prefab); - ReturnNetworkObject(go.GetComponent(), prefab); + m_PooledObjects[prefab].Release(networkObject); } // 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 /// @@ -141,55 +149,16 @@ private GameObject CreateInstance(GameObject prefab) /// /// /// - private NetworkObject GetNetworkObjectInternal(GameObject prefab, Vector3 position, Quaternion rotation) + NetworkObject GetNetworkObjectInternal(GameObject prefab, Vector3 position, Quaternion rotation) { - var queue = pooledObjects[prefab]; - - NetworkObject networkObject; - if (queue.Count > 0) - { - networkObject = queue.Dequeue(); - } - else - { - networkObject = CreateInstance(prefab).GetComponent(); - } - - // Here we must reverse the logic in ReturnNetworkObject. - var go = networkObject.gameObject; - go.SetActive(true); + var networkObject = m_PooledObjects[prefab].Get(); - go.transform.position = position; - go.transform.rotation = rotation; + var noTransform = networkObject.transform; + noTransform.position = position; + noTransform.rotation = rotation; return networkObject; } - - /// - /// Registers all objects in to the cache. - /// - public void InitializePool() - { - if (m_HasInitialized) return; - foreach (var configObject in PooledPrefabsList) - { - RegisterPrefabInternal(configObject.Prefab, configObject.PrewarmCount); - } - m_HasInitialized = true; - } - - /// - /// Unregisters all objects in from the cache. - /// - public void ClearPool() - { - foreach (var prefab in prefabs) - { - // Unregister Netcode Spawn handlers - NetworkManager.Singleton.PrefabHandler.RemoveHandler(prefab); - } - pooledObjects.Clear(); - } } [Serializable] @@ -212,8 +181,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/CHANGELOG.md b/CHANGELOG.md index ed6908937..a71278746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### 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) ### Cleanup * Clarified a TODO comment inside ClientCharacter, detailing how anticipation should only be executed on owning client players (#786) From fc27774d5e2a820252dd6df76d29cd3eb6909610 Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Tue, 25 Apr 2023 13:28:19 -0400 Subject: [PATCH 16/22] feat: upgrade to NGOv1.3.1 [MTT-6243] (#828) * upgrade to NGO 1.3.1, creating NetworkPrefabs ScriptableObject * changelog addition --- Assets/GameData/NetworkPrefabs.asset | 3 + Assets/GameData/NetworkPrefabs.asset.meta | 8 +++ Assets/Prefabs/NetworkingManager.prefab | 62 ++----------------- CHANGELOG.md | 1 + Packages/manifest.json | 2 +- Packages/packages-lock.json | 6 +- .../NetcodeForGameObjects.settings | 15 +++++ 7 files changed, 36 insertions(+), 61 deletions(-) create mode 100644 Assets/GameData/NetworkPrefabs.asset create mode 100644 Assets/GameData/NetworkPrefabs.asset.meta create mode 100644 ProjectSettings/NetcodeForGameObjects.settings 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/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/CHANGELOG.md b/CHANGELOG.md index a71278746..0d9be8e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Additional documentation and release notes are available at [Multiplayer Documen * 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) +* Upgraded Boss Room to NGO 1.3.1 (#828) NetworkPrefabs inside NetworkManager's NetworkPrefabs list have been converted to NetworkPrefabsList ScriptableObject. ### Cleanup * Clarified a TODO comment inside ClientCharacter, detailing how anticipation should only be executed on owning client players (#786) diff --git a/Packages/manifest.json b/Packages/manifest.json index 9d0469411..99cad2c9b 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -10,7 +10,7 @@ "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.3.1", "com.unity.performance.profile-analyzer": "1.1.1", "com.unity.postprocessing": "3.2.2", "com.unity.render-pipelines.universal": "12.1.8", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index e24f16460..8807d1a83 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.3.1", "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" }, @@ -373,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 From d91023555d5623c0a4eb9076a1254da846357805 Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Tue, 25 Apr 2023 13:55:45 -0400 Subject: [PATCH 17/22] refactor: network object pool part 2 (#827) * removing an unneeded method --- .../Infrastructure/NetworkObjectPool.cs | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/Assets/Scripts/Infrastructure/NetworkObjectPool.cs b/Assets/Scripts/Infrastructure/NetworkObjectPool.cs index ea3dcf59a..2dede95bd 100644 --- a/Assets/Scripts/Infrastructure/NetworkObjectPool.cs +++ b/Assets/Scripts/Infrastructure/NetworkObjectPool.cs @@ -86,7 +86,13 @@ public void OnValidate() /// 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; } /// @@ -141,24 +147,6 @@ void ActionOnDestroy(NetworkObject networkObject) // Register Netcode Spawn handlers NetworkManager.Singleton.PrefabHandler.AddHandler(prefab, new PooledPrefabInstanceHandler(prefab, this)); } - - /// - /// This matches the signature of - /// - /// - /// - /// - /// - NetworkObject GetNetworkObjectInternal(GameObject prefab, Vector3 position, Quaternion rotation) - { - var networkObject = m_PooledObjects[prefab].Get(); - - var noTransform = networkObject.transform; - noTransform.position = position; - noTransform.rotation = rotation; - - return networkObject; - } } [Serializable] From 05d4a5ac711bd79fa6f685dbd11e291da170668e Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Wed, 26 Apr 2023 13:13:50 -0400 Subject: [PATCH 18/22] feat: Boss Room updated to NGO v1.4.0 (#829) * Boss Room updated to NGO v1.4.0 * changelog addition --- CHANGELOG.md | 3 ++- Packages/manifest.json | 2 +- Packages/packages-lock.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d9be8e23..13c130a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ Additional documentation and release notes are available at [Multiplayer Documen * 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) -* 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.3.1 (#828) NetworkPrefabs inside NetworkManager's NetworkPrefabs list have been converted to NetworkPrefabsList ScriptableObject. +* Upgraded Boss Room to NGO 1.4.0 (#829) ### Cleanup * Clarified a TODO comment inside ClientCharacter, detailing how anticipation should only be executed on owning client players (#786) diff --git a/Packages/manifest.json b/Packages/manifest.json index 99cad2c9b..ee01fee25 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -10,7 +10,7 @@ "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.3.1", + "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", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 8807d1a83..74e32d2a1 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -142,7 +142,7 @@ "url": "https://packages.unity.com" }, "com.unity.netcode.gameobjects": { - "version": "1.3.1", + "version": "1.4.0", "depth": 0, "source": "registry", "dependencies": { From 0c9804576878639a0ed18e02cdd401f9879ce745 Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Wed, 26 Apr 2023 14:36:25 -0400 Subject: [PATCH 19/22] feat: profile name authentication truncating [MTT-6241] (#831) * truncating generated GUID string, & displaying authentication exception * feedback, and comments on authentication service name requirements * formatting * changelog addition --- .../Gameplay/GameState/ClientMainMenuState.cs | 50 +++++++++++-------- .../Scripts/Gameplay/UI/UIProfileSelector.cs | 6 ++- .../Auth/AuthenticationServiceFacade.cs | 26 +++++++++- Assets/Scripts/Utils/ProfileManager.cs | 4 +- CHANGELOG.md | 1 + 5 files changed, 62 insertions(+), 25 deletions(-) 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/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/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/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/CHANGELOG.md b/CHANGELOG.md index 13c130a4b..78ae0a7de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Additional documentation and release notes are available at [Multiplayer Documen * Replaced our custom pool implementation using queues with ObjectPool (#824) * 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) From 32f4732f99f122d71b833b948c1d526c91b2a385 Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Wed, 26 Apr 2023 15:57:53 -0400 Subject: [PATCH 20/22] fix: decoupling SceneLoaderWrapper and ConnectionStates [MTT-6242] (#830) * removing SceneLoaderWrapper.AddOnSceneEventCallback and replacing it with registrations to NetworkManager callbacks --- .../ConnectionState/ClientConnectingState.cs | 3 -- .../ConnectionState/HostingState.cs | 3 -- .../Runtime/ConnectionManagementTests.cs | 2 - CHANGELOG.md | 5 +- .../CHANGELOG.md | 3 ++ .../SceneManagement/SceneLoaderWrapper.cs | 46 +++++++++++++------ 6 files changed, 39 insertions(+), 23 deletions(-) diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs index be9a0512c..9421de1c7 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Unity.Multiplayer.Samples.Utilities; using UnityEngine; namespace Unity.BossRoom.ConnectionManagement @@ -68,8 +67,6 @@ internal async Task ConnectClientAsync() { throw new Exception("NetworkManager StartClient failed"); } - - SceneLoaderWrapper.Instance.AddOnSceneEventCallback(); } catch (Exception e) { diff --git a/Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs index daf1bcd58..e3890fbae 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); diff --git a/Assets/Tests/Runtime/ConnectionManagementTests.cs b/Assets/Tests/Runtime/ConnectionManagementTests.cs index 5254b95f7..68a39497b 100644 --- a/Assets/Tests/Runtime/ConnectionManagementTests.cs +++ b/Assets/Tests/Runtime/ConnectionManagementTests.cs @@ -55,8 +55,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) { } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ae0a7de..78d34c147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ Additional documentation and release notes are available at [Multiplayer Documen ### 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) -* Upgraded Boss Room to NGO 1.3.1 (#828) NetworkPrefabs inside NetworkManager's NetworkPrefabs list have been converted to NetworkPrefabsList ScriptableObject. +* 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) @@ -27,6 +27,7 @@ Additional documentation and release notes are available at [Multiplayer Documen * 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. +* 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 diff --git a/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md b/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md index e66dc5d41..adbd77a7d 100644 --- a/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md +++ b/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md @@ -2,6 +2,9 @@ ## [unreleased] - yyyy-mm-dd +### 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(); } /// From 8bcf2d6af7ec1b96a3675cbeb69af48046577567 Mon Sep 17 00:00:00 2001 From: LPLafontaineB Date: Wed, 26 Apr 2023 16:30:55 -0400 Subject: [PATCH 21/22] fix: fixing connection management test [MTT-4492][MTT-6242] (#826) * Adding OnServerShutdown event to ConnectionManager and ConnectionStates * Fixing failing test --- .../ConnectionManagement/ConnectionManager.cs | 7 ++++ .../ConnectionState/ClientConnectingState.cs | 6 +-- .../ClientReconnectingState.cs | 4 +- .../ConnectionState/ConnectionState.cs | 2 + .../ConnectionState/HostingState.cs | 12 +++--- .../ConnectionState/StartingHostState.cs | 27 ++++++------- .../Runtime/ConnectionManagementTests.cs | 39 ++++++++++++++----- CHANGELOG.md | 4 ++ 8 files changed, 67 insertions(+), 34 deletions(-) diff --git a/Assets/Scripts/ConnectionManagement/ConnectionManager.cs b/Assets/Scripts/ConnectionManagement/ConnectionManager.cs index 3932807c8..22068b375 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionManager.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionManager.cs @@ -98,6 +98,7 @@ void Start() NetworkManager.OnServerStarted += OnServerStarted; NetworkManager.ConnectionApprovalCallback += ApprovalCheck; NetworkManager.OnTransportFailure += OnTransportFailure; + NetworkManager.OnServerStopped += OnServerStopped; } void OnDestroy() @@ -107,6 +108,7 @@ void OnDestroy() NetworkManager.OnServerStarted -= OnServerStarted; NetworkManager.ConnectionApprovalCallback -= ApprovalCheck; NetworkManager.OnTransportFailure -= OnTransportFailure; + NetworkManager.OnServerStopped -= OnServerStopped; } internal void ChangeState(ConnectionState nextState) @@ -146,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/ConnectionState/ClientConnectingState.cs b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs index 9421de1c7..fc2fd12e4 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientConnectingState.cs @@ -36,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)) @@ -72,7 +72,7 @@ internal async Task ConnectClientAsync() { 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 7da62da1b..3b7d0397e 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs @@ -122,8 +122,8 @@ IEnumerator ReconnectCoroutine() if (!reconnectingSetupTask.IsFaulted && reconnectingSetupTask.Result.success) { // If this fails, the OnClientDisconnect callback will be invoked by Netcode - var connectingToRelay = ConnectClientAsync(); - yield return new WaitUntil(() => connectingToRelay.IsCompleted); + var connectingTask = ConnectClientAsync(); + yield return new WaitUntil(() => connectingTask.IsCompleted); } else { 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 e3890fbae..60c575377 100644 --- a/Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs +++ b/Assets/Scripts/ConnectionManagement/ConnectionState/HostingState.cs @@ -47,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) @@ -80,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/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/Tests/Runtime/ConnectionManagementTests.cs b/Assets/Tests/Runtime/ConnectionManagementTests.cs index 68a39497b..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; @@ -355,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(); @@ -368,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++) @@ -377,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++; + } } })); } @@ -396,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 78d34c147..8d0414439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Additional documentation and release notes are available at [Multiplayer Documen ## [unreleased] - yyyy-mm-dd +### 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. @@ -27,6 +30,7 @@ Additional documentation and release notes are available at [Multiplayer Documen * 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 From a20d6f5de1332132e324b8cc3f1afd32eb48d6ba Mon Sep 17 00:00:00 2001 From: Fernando Cortez Date: Thu, 27 Apr 2023 10:41:37 -0400 Subject: [PATCH 22/22] chore: Boss Room bundle version upgraded, changelogs updated (#833) --- CHANGELOG.md | 2 ++ Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md | 2 ++ ProjectSettings/ProjectSettings.asset | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0414439..7a32e9032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ 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. diff --git a/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md b/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md index adbd77a7d..1935ddf37 100644 --- a/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md +++ b/Packages/com.unity.multiplayer.samples.coop/CHANGELOG.md @@ -2,6 +2,8 @@ ## [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. 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