Skip to content

Commit 5b40458

Browse files
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 <[email protected]>
1 parent db90813 commit 5b40458

File tree

11 files changed

+156
-97
lines changed

11 files changed

+156
-97
lines changed

.yamato/mobile-build-and-run.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Build_Player_With_Tests_iOS_{{ project.name }}_{{ editor }}:
1414

1515
commands:
1616
- pip install unity-downloader-cli==1.2.0 --index-url https://artifactory.prd.it.unity3d.com/artifactory/api/pypi/pypi/simple --upgrade
17-
- unity-downloader-cli -c Editor -c iOS -u {{ editor }} --fast --wait
17+
- unity-downloader-cli -c Editor -c iOS -u 2021.3.15f1 --fast --wait
1818
- curl -s https://artifactory.prd.it.unity3d.com/artifactory/unity-tools-local/utr-standalone/utr --output utr
1919
- chmod +x ./utr
2020
- ./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

Assets/Scripts/ConnectionManagement/ConnectionMethod.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,9 @@ public override async Task SetupClientConnectionAsync()
169169
// some time to attempt to reconnect (defined by the "Disconnect removal time" parameter on the dashboard),
170170
// after which they will be removed from the lobby completely.
171171
// See https://docs.unity.com/lobby/reconnect-to-lobby.html
172-
var lobby = await m_LobbyServiceFacade.ReconnectToLobbyAsync(m_LocalLobby.LobbyID);
172+
var lobby = await m_LobbyServiceFacade.ReconnectToLobbyAsync();
173173
var success = lobby != null;
174-
if (success)
175-
{
176-
Debug.Log("Successfully reconnected to Lobby.");
177-
}
178-
else
179-
{
180-
Debug.Log("Failed to reconnect to Lobby.");
181-
}
174+
Debug.Log(success ? "Successfully reconnected to Lobby." : "Failed to reconnect to Lobby.");
182175
return (success, true); // return a success if reconnecting to lobby returns a lobby
183176
}
184177

Assets/Scripts/ConnectionManagement/ConnectionState/ClientReconnectingState.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ClientReconnectingState : ClientConnectingState
2121
Coroutine m_ReconnectCoroutine;
2222
int m_NbAttempts;
2323

24+
const float k_TimeBeforeFirstAttempt = 1;
2425
const float k_TimeBetweenAttempts = 5;
2526

2627
public override void Enter()
@@ -105,6 +106,15 @@ IEnumerator ReconnectCoroutine()
105106
yield return new WaitWhile(() => m_ConnectionManager.NetworkManager.ShutdownInProgress); // wait until NetworkManager completes shutting down
106107
Debug.Log($"Reconnecting attempt {m_NbAttempts + 1}/{m_ConnectionManager.NbReconnectAttempts}...");
107108
m_ReconnectMessagePublisher.Publish(new ReconnectMessage(m_NbAttempts, m_ConnectionManager.NbReconnectAttempts));
109+
110+
// If first attempt, wait some time before attempting to reconnect to give time to services to update
111+
// (i.e. if in a Lobby and the host shuts down unexpectedly, this will give enough time for the lobby to be
112+
// properly deleted so that we don't reconnect to an empty lobby
113+
if (m_NbAttempts == 0)
114+
{
115+
yield return new WaitForSeconds(k_TimeBeforeFirstAttempt);
116+
}
117+
108118
m_NbAttempts++;
109119
var reconnectingSetupTask = m_ConnectionMethod.SetupClientReconnectionAsync();
110120
yield return new WaitUntil(() => reconnectingSetupTask.IsCompleted);

Assets/Scripts/Gameplay/UI/RoomNameBox.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ private void InjectDependencies(LocalLobby localLobby)
2222
{
2323
m_LocalLobby = localLobby;
2424
m_LocalLobby.changed += UpdateUI;
25-
UpdateUI(localLobby);
2625
}
2726

2827
void Awake()
2928
{
30-
gameObject.SetActive(false);
29+
UpdateUI(m_LocalLobby);
3130
}
3231

3332
private void OnDestroy()
@@ -44,6 +43,10 @@ private void UpdateUI(LocalLobby localLobby)
4443
gameObject.SetActive(true);
4544
m_CopyToClipboardButton.gameObject.SetActive(true);
4645
}
46+
else
47+
{
48+
gameObject.SetActive(false);
49+
}
4750
}
4851

4952
public void CopyToClipboard()

Assets/Scripts/Infrastructure/UpdateRunner.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class SubscriberData
1414
{
1515
public float Period;
1616
public float NextCallTime;
17+
public float LastCallTime;
1718
}
1819

1920
readonly Queue<Action> m_PendingHandlers = new Queue<Action>();
@@ -56,7 +57,7 @@ public void Subscribe(Action<float> onUpdate, float updatePeriod)
5657
{
5758
if (m_Subscribers.Add(onUpdate))
5859
{
59-
m_SubscriberData.Add(onUpdate, new SubscriberData() { Period = updatePeriod, NextCallTime = 0 });
60+
m_SubscriberData.Add(onUpdate, new SubscriberData() { Period = updatePeriod, NextCallTime = 0, LastCallTime = Time.time });
6061
}
6162
});
6263
}
@@ -90,7 +91,8 @@ void Update()
9091

9192
if (Time.time >= subscriberData.NextCallTime)
9293
{
93-
subscriber.Invoke(Time.deltaTime);
94+
subscriber.Invoke(Time.time - subscriberData.LastCallTime);
95+
subscriberData.LastCallTime = Time.time;
9496
subscriberData.NextCallTime = Time.time + subscriberData.Period;
9597
}
9698
}

Assets/Scripts/UnityServices/Lobbies/LobbyAPIInterface.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,6 @@ public async Task<QueryResponse> QueryAllLobbies()
107107
return await LobbyService.Instance.QueryLobbiesAsync(queryOptions);
108108
}
109109

110-
public async Task<Lobby> GetLobby(string lobbyId)
111-
{
112-
return await LobbyService.Instance.GetLobbyAsync(lobbyId);
113-
}
114-
115110
public async Task<Lobby> UpdateLobby(string lobbyId, Dictionary<string, DataObject> data, bool shouldLock)
116111
{
117112
UpdateLobbyOptions updateOptions = new UpdateLobbyOptions { Data = data, IsLocked = shouldLock };
@@ -133,5 +128,10 @@ public async void SendHeartbeatPing(string lobbyId)
133128
{
134129
await LobbyService.Instance.SendHeartbeatPingAsync(lobbyId);
135130
}
131+
132+
public async Task<ILobbyEvents> SubscribeToLobby(string lobbyId, LobbyEventCallbacks eventCallbacks)
133+
{
134+
return await LobbyService.Instance.SubscribeToLobbyEventsAsync(lobbyId, eventCallbacks);
135+
}
136136
}
137137
}

Assets/Scripts/UnityServices/Lobbies/LobbyServiceFacade.cs

Lines changed: 106 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@ public class LobbyServiceFacade : IDisposable, IStartable
3737

3838
public Lobby CurrentUnityLobby { get; private set; }
3939

40+
ILobbyEvents m_LobbyEvents;
41+
4042
bool m_IsTracking = false;
4143

44+
LobbyEventConnectionState m_LobbyEventConnectionState = LobbyEventConnectionState.Unknown;
45+
4246
public void Start()
4347
{
4448
m_ServiceScope = m_ParentScope.CreateChild(builder =>
@@ -77,88 +81,29 @@ public void BeginTracking()
7781
if (!m_IsTracking)
7882
{
7983
m_IsTracking = true;
80-
// 2s update cadence is arbitrary and is here to demonstrate the fact that this update can be rather infrequent
81-
// the actual rate limits are tracked via the RateLimitCooldown objects defined above
82-
m_UpdateRunner.Subscribe(UpdateLobby, 2f);
84+
SubscribeToJoinedLobbyAsync();
8385
m_JoinedLobbyContentHeartbeat.BeginTracking();
8486
}
8587
}
8688

87-
public Task EndTracking()
89+
public void EndTracking()
8890
{
89-
var task = Task.CompletedTask;
9091
if (CurrentUnityLobby != null)
9192
{
92-
CurrentUnityLobby = null;
93-
94-
var lobbyId = m_LocalLobby?.LobbyID;
95-
96-
if (!string.IsNullOrEmpty(lobbyId))
93+
if (m_LocalUser.IsHost)
9794
{
98-
if (m_LocalUser.IsHost)
99-
{
100-
task = DeleteLobbyAsync(lobbyId);
101-
}
102-
else
103-
{
104-
task = LeaveLobbyAsync(lobbyId);
105-
}
95+
DeleteLobbyAsync();
96+
}
97+
else
98+
{
99+
LeaveLobbyAsync();
106100
}
107-
108-
m_LocalUser.ResetState();
109-
m_LocalLobby?.Reset(m_LocalUser);
110101
}
111102

112103
if (m_IsTracking)
113104
{
114-
m_UpdateRunner.Unsubscribe(UpdateLobby);
115105
m_IsTracking = false;
116-
m_HeartbeatTime = 0;
117-
m_JoinedLobbyContentHeartbeat.EndTracking();
118-
}
119-
120-
return task;
121-
}
122-
123-
async void UpdateLobby(float unused)
124-
{
125-
if (!m_RateLimitQuery.CanCall)
126-
{
127-
return;
128-
}
129-
130-
try
131-
{
132-
var lobby = await m_LobbyApiInterface.GetLobby(m_LocalLobby.LobbyID);
133-
134-
CurrentUnityLobby = lobby;
135-
m_LocalLobby.ApplyRemoteData(lobby);
136-
137-
// as client, check if host is still in lobby
138-
if (!m_LocalUser.IsHost)
139-
{
140-
foreach (var lobbyUser in m_LocalLobby.LobbyUsers)
141-
{
142-
if (lobbyUser.Value.IsHost)
143-
{
144-
return;
145-
}
146-
}
147-
m_UnityServiceErrorMessagePub.Publish(new UnityServiceErrorMessage("Host left the lobby", "Disconnecting.", UnityServiceErrorMessage.Service.Lobby));
148-
await EndTracking();
149-
// no need to disconnect Netcode, it should already be handled by Netcode's callback to disconnect
150-
}
151-
}
152-
catch (LobbyServiceException e)
153-
{
154-
if (e.Reason == LobbyExceptionReason.RateLimited)
155-
{
156-
m_RateLimitQuery.PutOnCooldown();
157-
}
158-
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.
159-
{
160-
PublishError(e);
161-
}
106+
UnsubscribeToJoinedLobbyAsync();
162107
}
163108
}
164109

@@ -264,6 +209,91 @@ async void UpdateLobby(float unused)
264209
return (false, null);
265210
}
266211

212+
void ResetLobby()
213+
{
214+
CurrentUnityLobby = null;
215+
m_LocalUser.ResetState();
216+
m_LocalLobby?.Reset(m_LocalUser);
217+
218+
// no need to disconnect Netcode, it should already be handled by Netcode's callback to disconnect
219+
}
220+
221+
void OnLobbyChanges(ILobbyChanges changes)
222+
{
223+
if (changes.LobbyDeleted)
224+
{
225+
Debug.Log("Lobby deleted");
226+
ResetLobby();
227+
}
228+
else
229+
{
230+
Debug.Log("Lobby updated");
231+
changes.ApplyToLobby(CurrentUnityLobby);
232+
m_LocalLobby.ApplyRemoteData(CurrentUnityLobby);
233+
234+
// as client, check if host is still in lobby
235+
if (!m_LocalUser.IsHost)
236+
{
237+
foreach (var lobbyUser in m_LocalLobby.LobbyUsers)
238+
{
239+
if (lobbyUser.Value.IsHost)
240+
{
241+
return;
242+
}
243+
}
244+
245+
m_UnityServiceErrorMessagePub.Publish(new UnityServiceErrorMessage("Host left the lobby", "Disconnecting.", UnityServiceErrorMessage.Service.Lobby));
246+
EndTracking();
247+
// no need to disconnect Netcode, it should already be handled by Netcode's callback to disconnect
248+
}
249+
}
250+
}
251+
252+
void OnKickedFromLobby()
253+
{
254+
Debug.Log("Kicked from Lobby");
255+
ResetLobby();
256+
}
257+
258+
void OnLobbyEventConnectionStateChanged(LobbyEventConnectionState lobbyEventConnectionState)
259+
{
260+
m_LobbyEventConnectionState = lobbyEventConnectionState;
261+
Debug.Log($"LobbyEventConnectionState changed to {lobbyEventConnectionState}");
262+
}
263+
264+
async void SubscribeToJoinedLobbyAsync()
265+
{
266+
var lobbyEventCallbacks = new LobbyEventCallbacks();
267+
lobbyEventCallbacks.LobbyChanged += OnLobbyChanges;
268+
lobbyEventCallbacks.KickedFromLobby += OnKickedFromLobby;
269+
lobbyEventCallbacks.LobbyEventConnectionStateChanged += OnLobbyEventConnectionStateChanged;
270+
// The LobbyEventCallbacks object created here will now be managed by the Lobby SDK. The callbacks will be
271+
// unsubscribed from when we call UnsubscribeAsync on the ILobbyEvents object we receive and store here.
272+
m_LobbyEvents = await m_LobbyApiInterface.SubscribeToLobby(m_LocalLobby.LobbyID, lobbyEventCallbacks);
273+
m_JoinedLobbyContentHeartbeat.BeginTracking();
274+
}
275+
276+
async void UnsubscribeToJoinedLobbyAsync()
277+
{
278+
if (m_LobbyEvents != null && m_LobbyEventConnectionState != LobbyEventConnectionState.Unsubscribed)
279+
{
280+
try
281+
{
282+
await m_LobbyEvents.UnsubscribeAsync();
283+
}
284+
catch (ObjectDisposedException e)
285+
{
286+
// This exception occurs in the editor when exiting play mode without first leaving the lobby.
287+
// This is because Wire disposes of subscriptions internally when exiting play mode in the editor.
288+
Debug.Log("Subscription is already disposed of, cannot unsubscribe.");
289+
Debug.Log(e.Message);
290+
}
291+
292+
}
293+
m_HeartbeatTime = 0;
294+
m_JoinedLobbyContentHeartbeat.EndTracking();
295+
}
296+
267297
/// <summary>
268298
/// Used for getting the list of all active lobbies, without needing full info for each.
269299
/// </summary>
@@ -293,11 +323,11 @@ public async Task RetrieveAndPublishLobbyListAsync()
293323
}
294324
}
295325

296-
public async Task<Lobby> ReconnectToLobbyAsync(string lobbyId)
326+
public async Task<Lobby> ReconnectToLobbyAsync()
297327
{
298328
try
299329
{
300-
return await m_LobbyApiInterface.ReconnectToLobby(lobbyId);
330+
return await m_LobbyApiInterface.ReconnectToLobby(m_LocalLobby.LobbyID);
301331
}
302332
catch (LobbyServiceException e)
303333
{
@@ -314,12 +344,13 @@ public async Task<Lobby> ReconnectToLobbyAsync(string lobbyId)
314344
/// <summary>
315345
/// Attempt to leave a lobby
316346
/// </summary>
317-
public async Task LeaveLobbyAsync(string lobbyId)
347+
public async void LeaveLobbyAsync()
318348
{
319349
string uasId = AuthenticationService.Instance.PlayerId;
320350
try
321351
{
322-
await m_LobbyApiInterface.RemovePlayerFromLobby(uasId, lobbyId);
352+
await m_LobbyApiInterface.RemovePlayerFromLobby(uasId, m_LocalLobby.LobbyID);
353+
ResetLobby();
323354
}
324355
catch (LobbyServiceException e)
325356
{
@@ -351,13 +382,14 @@ public async void RemovePlayerFromLobbyAsync(string uasId, string lobbyId)
351382
}
352383
}
353384

354-
public async Task DeleteLobbyAsync(string lobbyId)
385+
public async void DeleteLobbyAsync()
355386
{
356387
if (m_LocalUser.IsHost)
357388
{
358389
try
359390
{
360-
await m_LobbyApiInterface.DeleteLobby(lobbyId);
391+
await m_LobbyApiInterface.DeleteLobby(m_LocalLobby.LobbyID);
392+
ResetLobby();
361393
}
362394
catch (LobbyServiceException e)
363395
{

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
77
Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com).
88

99
## [unreleased] - yyyy-mm-dd
10+
11+
### Changed
12+
* 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.
13+
*
1014
### Cleanup
1115
* Clarified a TODO comment inside ClientCharacter, detailing how anticipation should only be executed on owning client players (#786)
1216
* 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
1822
* EnemyPortals' VFX get disabled and re-enabled once the breakable crystals are broken (#784)
1923
* 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)
2024
* ClientConnectedState now inherits from OnlineState instead of the base ConnectionState (#801)
25+
* UpdateRunner now sends the right value for deltaTime when updating its subscribers (#805)
2126

2227
## [2.0.4] - 2022-12-13
2328
### Changed

0 commit comments

Comments
 (0)