Skip to content

Commit dbf112c

Browse files
feat: allowing users to subscribe to a networked message channel while offline [MTT-3684] [MTT-3690] (#670)
* allowing users to subscribe to a message channel while offline * Adding tests for NetworkedMessageChannel
1 parent a775625 commit dbf112c

File tree

8 files changed

+311
-36
lines changed

8 files changed

+311
-36
lines changed

Assets/Scripts/ApplicationLifecycle/ApplicationController.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ protected override void Configure(IContainerBuilder builder)
3030
{
3131
base.Configure(builder);
3232
builder.RegisterComponent(m_UpdateRunner);
33+
builder.RegisterComponent(m_ConnectionManager.NetworkManager);
3334
builder.RegisterComponent(m_ConnectionManager);
3435

3536
//the following singletons represent the local representations of the lobby that we're in and the user that we are
@@ -48,10 +49,10 @@ protected override void Configure(IContainerBuilder builder)
4849

4950
//these message channels are essential and persist for the lifetime of the lobby and relay services
5051
//they are networked so that the clients can subscribe to those messages that are published by the server
51-
builder.RegisterInstance(new NetworkedMessageChannel<LifeStateChangedEventMessage>()).AsImplementedInterfaces();
52-
builder.RegisterInstance(new NetworkedMessageChannel<ConnectionEventMessage>()).AsImplementedInterfaces();
52+
builder.RegisterComponent(new NetworkedMessageChannel<LifeStateChangedEventMessage>()).AsImplementedInterfaces();
53+
builder.RegisterComponent(new NetworkedMessageChannel<ConnectionEventMessage>()).AsImplementedInterfaces();
5354
#if UNITY_EDITOR || DEVELOPMENT_BUILD
54-
builder.RegisterInstance(new NetworkedMessageChannel<CheatUsedMessage>()).AsImplementedInterfaces();
55+
builder.RegisterComponent(new NetworkedMessageChannel<CheatUsedMessage>()).AsImplementedInterfaces();
5556
#endif
5657

5758
//this message channel is essential and persists for the lifetime of the lobby and relay services

Assets/Scripts/Infrastructure/PubSub/NetworkedMessageChannel.cs

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,66 @@
22
using Unity.Collections;
33
using Unity.Netcode;
44
using UnityEngine;
5+
using VContainer;
56

67
namespace Unity.Multiplayer.Samples.BossRoom.Shared.Infrastructure
78
{
89
/// <summary>
910
/// This type of message channel allows the server to publish a message that will be sent to clients as well as
10-
/// being published locally. Clients and the server both can subscribe to it. However, that subscription needs to be
11-
/// done after the NetworkManager has initialized. On objects whose lifetime is bigger than a networked session,
12-
/// subscribing will be required each time a new session starts.
11+
/// being published locally. Clients and the server both can subscribe to it.
1312
/// </summary>
1413
/// <typeparam name="T"></typeparam>
1514
public class NetworkedMessageChannel<T> : MessageChannel<T> where T : unmanaged, INetworkSerializeByMemcpy
1615
{
17-
string m_Name;
16+
NetworkManager m_NetworkManager;
1817

19-
bool m_HasRegisteredHandler;
18+
string m_Name;
2019

2120
public NetworkedMessageChannel()
2221
{
2322
m_Name = $"{typeof(T).FullName}NetworkMessageChannel";
2423
}
2524

26-
public override void Dispose()
25+
[Inject]
26+
void InjectDependencies(NetworkManager networkManager)
2727
{
28-
if (!IsDisposed)
28+
m_NetworkManager = networkManager;
29+
m_NetworkManager.OnClientConnectedCallback += OnClientConnected;
30+
if (m_NetworkManager.IsListening)
2931
{
30-
if (NetworkManager.Singleton != null && NetworkManager.Singleton.CustomMessagingManager != null && m_HasRegisteredHandler)
31-
{
32-
NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler(m_Name);
33-
}
34-
35-
m_HasRegisteredHandler = false;
32+
RegisterHandler();
3633
}
37-
base.Dispose();
3834
}
3935

40-
public override IDisposable Subscribe(Action<T> handler)
36+
public override void Dispose()
4137
{
42-
if (NetworkManager.Singleton != null && NetworkManager.Singleton.IsListening)
38+
if (!IsDisposed)
4339
{
44-
// Only register message handler on clients
45-
if (!m_HasRegisteredHandler && !NetworkManager.Singleton.IsServer)
40+
if (m_NetworkManager != null && m_NetworkManager.CustomMessagingManager != null)
4641
{
47-
NetworkManager.Singleton.CustomMessagingManager.RegisterNamedMessageHandler(m_Name, ReceiveMessageThroughNetwork);
48-
m_HasRegisteredHandler = true;
49-
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect;
42+
m_NetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler(m_Name);
5043
}
51-
52-
return base.Subscribe(handler);
5344
}
45+
base.Dispose();
46+
}
5447

55-
Debug.LogError("Cannot subscribe to NetworkedMessageChannel. NetworkManager is not initialized.");
56-
return null;
48+
void OnClientConnected(ulong clientId)
49+
{
50+
RegisterHandler();
5751
}
5852

59-
void OnClientDisconnect(ulong clientId)
53+
void RegisterHandler()
6054
{
61-
m_HasRegisteredHandler = false;
62-
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientDisconnect;
63-
NetworkManager.Singleton.CustomMessagingManager.UnregisterNamedMessageHandler(m_Name);
55+
// Only register message handler on clients
56+
if (!m_NetworkManager.IsServer)
57+
{
58+
m_NetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(m_Name, ReceiveMessageThroughNetwork);
59+
}
6460
}
6561

6662
public override void Publish(T message)
6763
{
68-
if (NetworkManager.Singleton.IsServer)
64+
if (m_NetworkManager.IsServer)
6965
{
7066
// send message to clients, then publish locally
7167
SendMessageThroughNetwork(message);
@@ -81,7 +77,7 @@ void SendMessageThroughNetwork(T message)
8177
{
8278
var writer = new FastBufferWriter(FastBufferWriter.GetWriteSize<T>(), Allocator.Temp);
8379
writer.WriteValueSafe(message);
84-
NetworkManager.Singleton.CustomMessagingManager.SendNamedMessageToAll(m_Name, writer);
80+
m_NetworkManager.CustomMessagingManager.SendNamedMessageToAll(m_Name, writer);
8581
}
8682

8783
void ReceiveMessageThroughNetwork(ulong clientID, FastBufferReader reader)

Assets/Scripts/Infrastructure/Unity.BossRoom.Infrastructure.asmdef

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"rootNamespace": "",
44
"references": [
55
"Unity.Netcode.Runtime",
6-
"Unity.TextMeshPro"
6+
"Unity.TextMeshPro",
7+
"VContainer"
78
],
89
"includePlatforms": [],
910
"excludePlatforms": [],
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
using System;
2+
using System.Collections;
3+
using NUnit.Framework;
4+
using Unity.Multiplayer.Samples.BossRoom.Shared.Infrastructure;
5+
using Unity.Netcode;
6+
using UnityEngine;
7+
using Unity.Netcode.TestHelpers.Runtime;
8+
using UnityEngine.TestTools;
9+
using VContainer;
10+
using Assert = UnityEngine.Assertions.Assert;
11+
12+
namespace Unity.Multiplayer.Samples.BossRoom.Tests.Runtime
13+
{
14+
public class NetworkedMessageChannelTests : NetcodeIntegrationTest
15+
{
16+
struct EmptyMessage : INetworkSerializeByMemcpy { }
17+
18+
struct GenericMessage : INetworkSerializeByMemcpy
19+
{
20+
public bool value;
21+
}
22+
23+
protected override int NumberOfClients => 2;
24+
25+
DisposableGroup m_Subscriptions;
26+
27+
IObjectResolver[] m_ClientScopes;
28+
IObjectResolver m_ServerScope;
29+
30+
int m_NbMessagesReceived;
31+
32+
protected override IEnumerator OnSetup()
33+
{
34+
m_NbMessagesReceived = 0;
35+
return base.OnSetup();
36+
}
37+
38+
protected override void OnServerAndClientsCreated()
39+
{
40+
m_ClientScopes = new IObjectResolver[NumberOfClients];
41+
for (int i = 0; i < NumberOfClients; i++)
42+
{
43+
var clientBuilder = new ContainerBuilder();
44+
clientBuilder.RegisterInstance(m_ClientNetworkManagers[i]);
45+
m_ClientScopes[i] = clientBuilder.Build();
46+
m_ClientNetworkManagers[i].NetworkConfig.EnableSceneManagement = false;
47+
}
48+
49+
var serverBuilder = new ContainerBuilder();
50+
serverBuilder.RegisterInstance(m_ServerNetworkManager);
51+
m_ServerScope = serverBuilder.Build();
52+
m_ServerNetworkManager.NetworkConfig.EnableSceneManagement = false;
53+
54+
base.OnServerAndClientsCreated();
55+
}
56+
57+
protected override IEnumerator OnTearDown()
58+
{
59+
m_Subscriptions.Dispose();
60+
for (int i = 0; i < NumberOfClients; i++)
61+
{
62+
m_ClientScopes[i].Dispose();
63+
}
64+
m_ServerScope.Dispose();
65+
return base.OnTearDown();
66+
}
67+
68+
void InitializeNetworkedMessageChannels<T>(int nbClients, int nbSubscribers, T expectedValue, out NetworkedMessageChannel<T>[] emptyMessageChannelClients, out NetworkedMessageChannel<T> emptyMessageChannelServer) where T : unmanaged, INetworkSerializeByMemcpy
69+
{
70+
emptyMessageChannelClients = new NetworkedMessageChannel<T>[nbClients];
71+
for (int i = 0; i < nbClients; i++)
72+
{
73+
emptyMessageChannelClients[i] = new NetworkedMessageChannel<T>();
74+
m_ClientScopes[i].Inject(emptyMessageChannelClients[i]);
75+
}
76+
77+
emptyMessageChannelServer = new NetworkedMessageChannel<T>();
78+
m_ServerScope.Inject(emptyMessageChannelServer);
79+
80+
m_Subscriptions = new DisposableGroup();
81+
for (int i = 0; i < nbClients; i++)
82+
{
83+
for (int j = 0; j < nbSubscribers; j++)
84+
{
85+
var numClient = i;
86+
var numSub = j;
87+
m_Subscriptions.Add(emptyMessageChannelClients[i].Subscribe(message =>
88+
{
89+
Debug.Log($"Received message on client {numClient} in subscription {numSub}.");
90+
m_NbMessagesReceived++;
91+
Assert.AreEqual(expectedValue, message, "Message received with unexpected value.");
92+
}));
93+
}
94+
}
95+
96+
for (int j = 0; j < nbSubscribers; j++)
97+
{
98+
var numSub = j;
99+
m_Subscriptions.Add(emptyMessageChannelServer.Subscribe(message =>
100+
{
101+
Debug.Log($"Received message on server in subscription {numSub}.");
102+
m_NbMessagesReceived++;
103+
Assert.AreEqual(expectedValue, message, "Message received with unexpected value.");
104+
}));
105+
}
106+
}
107+
108+
[UnityTest]
109+
public IEnumerator EmptyNetworkedMessageIsReceivedByAllSubscribersOnAllClientsAndServer([Values(0, 1, 2)] int nbClients, [Values(0, 1, 2)] int nbSubscribers)
110+
{
111+
InitializeNetworkedMessageChannels(nbClients, nbSubscribers, new EmptyMessage(), out var emptyMessageChannelClients, out var emptyMessageChannelServer);
112+
113+
emptyMessageChannelServer.Publish(new EmptyMessage());
114+
115+
// wait for the custom named message to be sent on the server and received on the clients
116+
yield return null;
117+
yield return null;
118+
119+
Assert.AreEqual((nbClients + 1) * nbSubscribers, m_NbMessagesReceived);
120+
121+
}
122+
123+
[UnityTest]
124+
public IEnumerator NetworkedMessageContentIsProperlyReceivedOnAllClientsAndServer([Values(0, 1, 2)] int nbClients, [Values(0, 1, 2)] int nbSubscribers)
125+
{
126+
InitializeNetworkedMessageChannels(nbClients, nbSubscribers, new GenericMessage() { value = true }, out var genericMessageChannelClients, out var genericMessageChannelServer);
127+
128+
genericMessageChannelServer.Publish(new GenericMessage() { value = true });
129+
130+
// wait for the custom named message to be sent on the server and received on the client
131+
yield return null;
132+
yield return null;
133+
134+
Assert.AreEqual((nbClients + 1) * nbSubscribers, m_NbMessagesReceived);
135+
}
136+
137+
[UnityTest]
138+
public IEnumerator NetworkedMessagesAreStillReceivedAfterNetworkManagerShutsDownAndRestarts([Values(0, 1, 2)] int nbClients, [Values(0, 1, 2)] int nbSubscribers)
139+
{
140+
InitializeNetworkedMessageChannels(nbClients, nbSubscribers, new EmptyMessage(), out var emptyMessageChannelClients, out var emptyMessageChannelServer);
141+
142+
emptyMessageChannelServer.Publish(new EmptyMessage());
143+
144+
// wait for the custom named message to be sent on the server and received on the clients
145+
yield return null;
146+
yield return null;
147+
148+
Assert.AreEqual((nbClients + 1) * nbSubscribers, m_NbMessagesReceived);
149+
150+
m_NbMessagesReceived = 0;
151+
152+
// Shutdown the server and clients
153+
NetcodeIntegrationTestHelpers.StopOneClient(m_ServerNetworkManager, false);
154+
NetcodeIntegrationTestHelpers.StopOneClient(m_ClientNetworkManagers[0], false);
155+
NetcodeIntegrationTestHelpers.StopOneClient(m_ClientNetworkManagers[1], false);
156+
157+
yield return new WaitWhile(() => m_ServerNetworkManager.ShutdownInProgress);
158+
yield return new WaitWhile(() => m_ClientNetworkManagers[0].ShutdownInProgress);
159+
yield return new WaitWhile(() => m_ClientNetworkManagers[1].ShutdownInProgress);
160+
161+
// Restart the server and clients
162+
m_ServerNetworkManager.StartHost();
163+
NetcodeIntegrationTestHelpers.StartOneClient(m_ClientNetworkManagers[0]);
164+
NetcodeIntegrationTestHelpers.StartOneClient(m_ClientNetworkManagers[1]);
165+
166+
yield return WaitForClientsConnectedOrTimeOut();
167+
168+
// Test sending a message a second time
169+
emptyMessageChannelServer.Publish(new EmptyMessage());
170+
171+
// wait for the custom named message to be sent on the server and received on the clients
172+
yield return null;
173+
yield return null;
174+
175+
Assert.AreEqual((nbClients + 1) * nbSubscribers, m_NbMessagesReceived);
176+
}
177+
178+
[UnityTest]
179+
public IEnumerator NetworkedMessagesAreReceivedIfClientsSubscribeBeforeConnecting([Values(0, 1, 2)] int nbClients, [Values(0, 1, 2)] int nbSubscribers)
180+
{
181+
// Shutdown the clients
182+
NetcodeIntegrationTestHelpers.StopOneClient(m_ClientNetworkManagers[0], false);
183+
NetcodeIntegrationTestHelpers.StopOneClient(m_ClientNetworkManagers[1], false);
184+
185+
yield return new WaitWhile(() => m_ClientNetworkManagers[0].ShutdownInProgress);
186+
yield return new WaitWhile(() => m_ClientNetworkManagers[1].ShutdownInProgress);
187+
188+
InitializeNetworkedMessageChannels(nbClients, nbSubscribers, new EmptyMessage(), out var emptyMessageChannelClients, out var emptyMessageChannelServer);
189+
190+
// Restart the clients
191+
NetcodeIntegrationTestHelpers.StartOneClient(m_ClientNetworkManagers[0]);
192+
NetcodeIntegrationTestHelpers.StartOneClient(m_ClientNetworkManagers[1]);
193+
194+
yield return WaitForClientsConnectedOrTimeOut();
195+
196+
emptyMessageChannelServer.Publish(new EmptyMessage());
197+
198+
// wait for the custom named message to be sent on the server and received on the clients
199+
yield return null;
200+
yield return null;
201+
202+
Assert.AreEqual((nbClients + 1) * nbSubscribers, m_NbMessagesReceived);
203+
}
204+
205+
[UnityTest]
206+
public IEnumerator NetworkedMessagesAreNotReceivedWhenClientsAreShutDown([Values(0, 1, 2)] int nbClients, [Values(0, 1, 2)] int nbSubscribers)
207+
{
208+
InitializeNetworkedMessageChannels(nbClients, nbSubscribers, new EmptyMessage(), out var emptyMessageChannelClients, out var emptyMessageChannelServer);
209+
210+
// Shutdown the clients
211+
NetcodeIntegrationTestHelpers.StopOneClient(m_ClientNetworkManagers[0], false);
212+
NetcodeIntegrationTestHelpers.StopOneClient(m_ClientNetworkManagers[1], false);
213+
214+
yield return new WaitWhile(() => m_ClientNetworkManagers[0].ShutdownInProgress);
215+
yield return new WaitWhile(() => m_ClientNetworkManagers[1].ShutdownInProgress);
216+
217+
emptyMessageChannelServer.Publish(new EmptyMessage());
218+
219+
// wait for the custom named message to be sent on the server and received on the clients
220+
yield return null;
221+
yield return null;
222+
223+
Assert.AreEqual(nbSubscribers, m_NbMessagesReceived);
224+
}
225+
226+
[UnityTest]
227+
public IEnumerator NetworkedMessagesAreNotReceivedWhenServerIsShutDown([Values(0, 1, 2)] int nbClients, [Values(0, 1, 2)] int nbSubscribers)
228+
{
229+
InitializeNetworkedMessageChannels(nbClients, nbSubscribers, new EmptyMessage(), out var emptyMessageChannelClients, out var emptyMessageChannelServer);
230+
231+
// Shutdown the server
232+
NetcodeIntegrationTestHelpers.StopOneClient(m_ServerNetworkManager, false);
233+
234+
yield return new WaitWhile(() => m_ServerNetworkManager.ShutdownInProgress);
235+
236+
LogAssert.Expect(LogType.Error, "Only a server can publish in a NetworkedMessageChannel");
237+
emptyMessageChannelServer.Publish(new EmptyMessage());
238+
239+
// wait for the custom named message to be sent on the server and received on the clients
240+
yield return null;
241+
yield return null;
242+
243+
Assert.AreEqual(0, m_NbMessagesReceived);
244+
}
245+
246+
[UnityTest]
247+
public IEnumerator NetworkedMessagesCannotBePublishedFromClient()
248+
{
249+
InitializeNetworkedMessageChannels(2, 1, new EmptyMessage(), out var emptyMessageChannelClients, out var emptyMessageChannelServer);
250+
251+
LogAssert.Expect(LogType.Error, "Only a server can publish in a NetworkedMessageChannel");
252+
emptyMessageChannelClients[0].Publish(new EmptyMessage());
253+
254+
// wait for the custom named message to be sent on the server and received on the clients
255+
yield return null;
256+
yield return null;
257+
258+
Assert.AreEqual(0, m_NbMessagesReceived);
259+
}
260+
}
261+
}

0 commit comments

Comments
 (0)