I was usint Unity 2022.3. My netcode was working just fine, but I needed that WebGL support on my netcode, because my target audience does more web-based gaming.
I wanted to upgrade to 6 in order to have those sweet funcitonalities, and at last, WebGL works. I hacve A OnNetworkSpawn I’m using to signal a client readiness to receive configuration from server. This was working fine before, but now, only the OnNetworkSpawn on the Server side works, and clients never get thiss OnNetworkSpawn.
The scenes the server is going through are:
LoginScene > LoadingScreenScene (server readiness is signaled here, users are connecting on this scene) > RankedLobbyScene (Scene with the OnNetworkSpawn on clients).
I have no idea why it is only being called on the server and not the clients after the migraiton. Any of you guys have an idea what’s going on?
Not without any technical details ie code or the scene setup. Also check the browser console for logs.
If an object does not run OnNetworkSpawn it could be anything from simply misspelled method name, not subclassing NetworkBehaviour, not having the NetworkObject component on the same object, not network-loading the scene in question, not having the spawned prefab in the network prefabs list, an exception in the script before it runs OnNetworkSpawn, and more possibilities.
Hey there CodeSmile!.
First of all, Thank you for your switf reply. I’ll put some code snippets here to help bring light to the matter:
This class is on the LoadingScreenScene in order to let players join and make sure all users are there before the next scene starts:
public class GameLoadingSceneController : MonoBehaviour
{
#if DEDICATED_SERVER
void Start()
{
StartCoroutine(LoadGameScene());
}
private IEnumerator LoadGameScene()
{
yield return new WaitForSeconds(3);
NetworkManager.Singleton.SceneManager.LoadScene("RankedLobbyScene", LoadSceneMode.Single);
}
#endif
}
There is also the ServerManager, who is a game object that stays alive since the server hits the LoginScene:
public class ServerManager : MonoBehaviour
{
async void Awake()
{
if (!Singleton)
{
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 60;
var serverConfig = MultiplayService.Instance.ServerConfig;
NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData("0.0.0.0", serverConfig.Port, "0.0.0.0");
NetworkManager.Singleton.StartServer();
await MultiplayService.Instance.ReadyServerForPlayersAsync();
try
{
await UnityServices.InitializeAsync();
await AuthenticationService.Instance.SignInAnonymouslyAsync();
string payloadJson = await MultiplayService.Instance.GetPayloadAllocationAsPlainText();
if (payloadJson.Contains("PlayerOneSelectedRules"))
{
Debug.Log("Parsing rules received. This game is NOT considered a ranked game. in payload: " + payloadJson);
RankedGameConfigDto.RulesDTO rulesDto = JsonConvert.DeserializeObject<RankedGameConfigDto.RulesDTO>(payloadJson);
rulesDto.SkipRuleSelection = true;
RankedGameConfigDto gameConfig = ScriptableObject.CreateInstance<RankedGameConfigDto>();
gameConfig.FromConfigDTO(rulesDto);
Debug.Log("Rules injected successfuly");
rulesDto.IsRanked = false;
}
}
finally
{
MultiplayEventCallbacks serverCallbacks = new MultiplayEventCallbacks();
serverCallbacks.Allocate += OnAllocate;
serverQueryHandler = await MultiplayService.Instance.StartServerQueryHandlerAsync(2, "WarringTriad", "Random", "test", "board");
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect;
if (serverConfig.AllocationId != "")
{
Debug.Log("Server Started!");
}
}
gameObject.AddComponent<ServerAsyncActions>();
Singleton = this;
isInitialized = true;
NetworkManager.Singleton.SceneManager.LoadScene("GameLoadingScene", LoadSceneMode.Single);
}
else
{
Destroy(gameObject);
}
}
public async void FillPlayerData(PlayerController playerController)
{
#if DEDICATED_SERVER
while (!isInitialized)
{
}
playerController.playerRank = await ServerAsyncActions.Singleton.GetPlayerRankServerSide(playerController.PlayerData.Id);
Players.Add(playerController);
OnPlayerReady?.Invoke(playerController);
#endif
}
}
Then finally, here is the RankedLobbyScene, where RankedLobbyController does the rule injection for each client that enters the scene:
public class RankedLobbyController : NetworkBehaviour
{
#if DEDICATED_SERVER
//This OnNetworkSpawn Works just fine on the Server. The method it contains calls a ClientRpc, though. And that method can't reach the clients because the GameObject hasn't spawned yet.
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
ReloadClientsRules();
}
#endif
#if !DEDICATED_SERVER
//This OnNetworkSpawn never triggers on clients
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
StartCoroutine(WaitForNetworkReadinessAndSignalClientReadiness());
PrepareRulesOnClientServerRpc(NetworkManager.LocalClientId);
}
private IEnumerator WaitForNetworkReadinessAndSignalClientReadiness()
{
yield return new WaitForSeconds(5);
PrepareRulesOnClientServerRpc(NetworkManager.LocalClientId);
}
#endif
}
//This is the method that is never being called
[ServerRpc(RequireOwnership = false)]
public void PrepareRulesOnClientServerRpc(ulong clientId)
{
#if DEDICATED_SERVER
ReloadClientsRules();
#endif
}
private async void ReloadClientsRules()
{
if (RankedGameConfigDto.Singleton.rulesDTO.IsRanked)
{
RankDTO.RankType lowestRankTypeForPlayers = RankDTO.RankType.PLATINUM;
bool foundPlayerRankLevels = false;
foreach (PlayerController player in ServerManager.Singleton.Players)
{
if (player.playerRank.rankType <= lowestRankTypeForPlayers)
{
lowestRankTypeForPlayers = player.playerRank.rankType;
foundPlayerRankLevels = true;
}
}
if (foundPlayerRankLevels)
{
RankedLevelsPerClassWrapper rankedLevelsPerClassWrapper = await ServerAsyncActions.Singleton.GetAvailableRankedLevels(lowestRankTypeForPlayers);
RankedGameConfigDto.Singleton.rulesDTO.Levels = rankedLevelsPerClassWrapper.rankedLevelsPerClass;
RankedGameConfigDto.Singleton.rulesDTO.GameRankType = lowestRankTypeForPlayers;
}
}
if (ServerManager.Singleton && ServerManager.Singleton.Players != null)
{
foreach (var player in ServerManager.Singleton.Players)
{
PrepareRulesOnClient(player.OwnerClientId);
}
}
}
//This is the method with the magic sauce. It is the one that calls each user to inject the right rules.
public void PrepareRulesOnClient(ulong clientId)
{
try
{
Debug.Log("Rule setup has been requested by client: " + clientId);
RankedGameConfigDto.Singleton.rulesDTO.PlayerIds.Add(clientId);
if (SkipRuleSelection)
{
Debug.Log("Rules selection phase has been flagged as skipped. Ordering client to skip rule selection: " + clientId);
int[] deckIds = new int[RankedGameConfigDto.Singleton.rulesDTO.Decks.Count];
for (int i = 0; i < deckIds.Length; i++)
{
deckIds[i] = (int)RankedGameConfigDto.Singleton.rulesDTO.Decks[i];
}
SetupDecksAndLevelsClientRpc(deckIds, RankedGameConfigDto.Singleton.rulesDTO.Levels.ToArray());
ClickNextClientRpc();
}
else
{
Debug.Log("Populating rules for Client: " + clientId);
SetupDecksAndLevelsClientRpc(null, RankedGameConfigDto.Singleton.rulesDTO.Levels.ToArray());
PopulateRulesClientRpc(RuleNamesSerializable, clientId);
}
}
catch (Exception e)
{
Debug.LogError("Error while preparing rules for client: " + clientId + " Error: " + e.Message);
Debug.LogException(e);
}
}
As I mentioned, this worked before on 2022.3, so typos and other similar errors are off the table.
UPDATE:
I moved this code into the awake fort clients
StartCoroutine(WaitForNetworkReadinessAndSignalClientReadiness());
now I’m getting this error. Perhaps this shines some light!:
[Netcode] [Deferred OnSpawn] Messages were received for a trigger of type ClientRpcMessage associated with id (2), but the NetworkObject was not received within the timeout period 1 second(s).
0x00007ffe912776ed (Unity) StackWalker::ShowCallstack
KeyNotFoundException: The given key ‘RankedLobbyController’ was not present in the dictionary.
From what I understand, it seems that the IDs in the server and client do not match. I made sure to validate the Prefab was in the list of NetworkPrefabsList.
Waiting for 3 seconds doesn’t make sure all clients have connected! It’s just going to wait for 3 seconds and then continue and most of the time you’ll be correct that all players did join but there is in no way a guarantee that this will be the case for every situation! On the other hand you force all players to wait 3 seconds even though they connected within 100 ms.
Do check the number of connected clients plus a timeout in case you lost one.
A few things worth pointing out:
- ReadyServerForPlayersAsync => called outside try/catch but may throw exceptions
- there’s no “catch” in your try/finally so the finally part may run and throw follow-up exceptions. You have to catch and handle exceptions!
- Consider first initializing services as this is independent of the network session
- consider splitting service calls to individual classes/methods handling each because exceptions vary between service calls and your own code
- if the Singleton is initialized (
Singleton != null
) and a scene with that ServerManager gets loaded again, it will actually Destroy the ServerManager object and do nothing else. This is a glaring bug. You should use DontDestroyOnLoad and make sure the ServerManager or any other global object isn’t ever going to be loaded again by putting it in the very first “never to be loaded again” bootstrap scene.
Avoid using #if to conditionally compile code. This is going to be troublesome throughout development. Also you an simply use #else
rather than doing the !DEDICATED_SERVER
check again, except negated.
The conditional compiling may even be causing your OnNetworkSpawn to malfunction depending on how that callback is implemented or whether there is any code that verifies that both scripts on the server and client side are identical. NGO provides you with the IsServer
property for instance.
WaitForNetworkReadinessAndSignalClientReadiness
=> again waiting five seconds does NOT mean “ready”. It just means it waited 5 seconds!
So I really don’t know what to say but it’s just the way you wrote this code that I’m not surprised that it is brittle and will fail either occassionally or sporadically or even persistently from then on. This code needs some thorough cleanup and in the process I’m sure some issues will resolve themselves.
After trying to remove the #if and replace them with IsServer, nothing really changed. Still getting hte same issue. From what I 've gathered, the engine just does not understand that the RAnkedLobby controler on the server and the on eon the client are one and the same NetworkBehaviour. IT may be some issue with the Client initialization because it has id 0, while that on the server is id 2. I’m still investigating ths issue. Here’s a screenshot of the configuration of the game object in clients:
I have multiple NetworkBehaviours but this is the only one having this issue.
Finally managed to find the cause of this issue and fix it.
It had nothing to do with “Brittle code” or any other issues related to separation of responsabilities mechanism, or hiding server code from clients.
It ended up that Unity NGO libray is uncapable of instantiating NetworkBehaviour in scenewhen transitioning from one scene to the other. I know thissounds crazy, but I have proof.
As I mentioned, the server is going through three scenes:
LoginScene > LoadingScreenScene > RankedLobbyScene
I did it this way to avoid race conditions between incoming data from CloudSave about the connected users (things as: the cards they posses, the rank they’re on, and other things that influence the rules the player can choose for this ranked game), and User Interface Network Behavior spawning that need the information from CloudSave to fill the content of it’s ui elements.
the issue was with the third scene “RankedLobbyScene”. A NetworkBehaviour there was not being initialized on clients (although it WAS initialized in the server), when the scene was loaded by the NetworkManager’s SceneManager. When I noticed that OTHER NetworkBEhaviours were actually working, I noticed this component was the isolated cause of the issue. So I moved it up the hierarchy to the first scene GameLoadingScene and guess what! magically instantiated as it should have been!.
To solve the issue I just god rid of the middle scene, and now I have to find another way around the race condition, but at least my game is playable again. If I can I’ll flag this post as a bug.
after HOURS, and I mean like 4 HOURS! xD I found this:
This was indeed the bug I was experiencing. After updating from 2.1.1 to 2.2.0, this solved the issue right there. I’m so glad I kept pushig 