Syncing player stats to server on login

This is a bit of a read, because I have a specific question related to implementation and want to provide appropriate context for my work. So buckle up.

I’m building a 3D networked game. I have a Login scene and a Lobby scene. The Login scene handles parsing of the player credentials with a function that looks like this:

public void OnLoginButtonPressed()
{
    if (DBConnectionManager.Instance == null)
    {
        Debug.LogError("DBConnectionManager.Instance is null. Make sure it exists in the scene.");
        return;
    }

    Debug.Log("OnLoginButtonPressed called");

    string username = usernameInputField.text;
    string password = passwordInputField.text;

    if (DBConnectionManager.Instance.ValidateLogin(username, password))
    {
        NetworkPlayerStatsManager.Instance.LoadStatsFromDatabase(username);
        PlayerStats loadedStats = NetworkPlayerStatsManager.Instance.GetPlayerStats(username);

        if (loadedStats != null)
        {
            // Add the loaded stats to the global dictionary
            GlobalPlayerData.Instance.AddPlayerStats(username, loadedStats);

            StartCoroutine(FadeOut(loginCanvasGroup, fadeDuration, null));

            if (loadedStats.hasSelectedClass && loadedStats.hasSelectedFaction)
            {
                StartCoroutine(TransitionToGame());
            }
            else if (!loadedStats.hasSelectedClass)
            {
                StartCoroutine(TransitionToClassSelection());
            }
            else if (!loadedStats.hasSelectedFaction)
            {
                StartCoroutine(TransitionToFactionSelection());
            }
        }
        else
        {
            Debug.LogError("Failed to load player stats.");
        }
    }
    else
    {
        Debug.LogError("Invalid username or password.");
    }
}

Username and Password are validated, and player stats are fetched from the database. The supporting DB connection functions work, I’m not concerned with those.

Once player stats are fetched, they are added to a global dictionary that holds player stats from all active players (to allow connection of multiple players asynchronously without mixing up player stats). This part is a new implementation that I’m still testing for efficiency and effectiveness.

If the player has already selected class and faction, they are transitioned to the game scene using the following function:

private IEnumerator TransitionToGame()
{
    // Fade out the ClassSelectionCanvas if it is visible
    if (classSelectionCanvasGroup.alpha > 0)
    {
        yield return StartCoroutine(FadeOut(classSelectionCanvasGroup, fadeDuration, null));
    }

    // Fade out the FactionSelectionCanvas if it is visible
    if (factionSelectionCanvasGroup.alpha > 0)
    {
        yield return StartCoroutine(FadeOut(factionSelectionCanvasGroup, fadeDuration, null));
    }

    // Fade in the black screen, which will stay visible during the scene load
    yield return StartCoroutine(FadeIn(blackScreenCanvasGroup, fadeDuration));

    // Load the NetworkLobbyScene
    SceneManager.LoadScene("NetworkLobbyScene");

    // Wait for one frame to ensure the scene has fully loaded
    yield return null;

    // Fade out the BlackScreenCanvas after the scene has loaded
    yield return StartCoroutine(FadeOut(blackScreenCanvasGroup, fadeDuration, null));
}

Thus, the Lobby scene is loaded. The Lobby scene, being a networked multiplayer scene, has a CustomNetworkManager script that I have written to handle player connection and disconnection. It appropriately handles checking if a server exists, and whether to connect as host or client. Next comes loading appropriate player data unique to the player that logged in the Login scene, including prefab and material. This is my existing OnServerAddPlayer function, and this is where I’m running into issues:

public override void OnServerAddPlayer(NetworkConnectionToClient conn)
{
    string playerName = null;

    PlayerStats loadedStats = GlobalPlayerData.Instance.GetPlayerStats(playerName);
    if (loadedStats == null)
    {
        Debug.LogError($"No player stats found for player: {playerName}");
        return;
    }

    Vector3 spawnPosition = GetSpawnPositionForPlayer(loadedStats.faction);
    GameObject playerPrefab = Resources.Load<GameObject>($"Prefabs/{loadedStats.className}Class");

    if (playerPrefab == null)
    {
        Debug.LogError($"Player prefab for class {loadedStats.className} not found in Resources/Prefabs!");
        return;
    }

    GameObject playerInstance = Instantiate(playerPrefab, spawnPosition, Quaternion.identity);
    NetworkServer.AddPlayerForConnection(conn, playerInstance);

    NetworkPlayerSetup playerSetup = playerInstance.GetComponent<NetworkPlayerSetup>();
    if (playerSetup != null)
    {
        playerSetup.CmdRequestSpawnPlayer();
    }
    else
    {
        Debug.LogError("NetworkPlayerSetup component not found on the player prefab.");
    }
}

Notice the string playerName = null. I put it there as a placeholder, but I need to figure out a way to pass the username from the Login scene to that specific value to retrieve the stats tied to that specific username from the global stats dictionary.

Can I just create a global variable in the Lobby scene, and after the Lobby scene is loaded in the login scene via:

// Load the NetworkLobbyScene
SceneManager.LoadScene("NetworkLobbyScene");

can I just do something like:

CustomNetworkManager.playerName = username;

Or will that create continuity issues and confuse player data?

Is there a better solution here to managing the player data between the two scenes? In some previous tests, I was able to instantiate multiple players, but the prefabs were not loaded correctly for anyone but the first player to connect, because all subsequent players would not be able to appropriately detect the faction/class name, so they would show up as the prefab for the first class, untextured because the faction name would not be read correctly from the database.

Am I on the right track? I’m going to continue testing and developing, but I would greatly appreciate any assistance anyone could offer as I feel I have lost the forest for the trees here.

Thanks in advance.

Not splitting them up into separate scenes?

At some point in a Unity developer’s lifecycle, there’s this telling revelation that a scene is no more or less a container of content than a prefab is. Or any object in the scene that has a complex hierarchy.

Most tend towards additive scene loading however. But generally speaking you can have two objects in the same scene and to switch functionality and visuals, you just enable one and disable the other. Then you can keep objects around naturally without having to resort to DontDestroyOnLoad (which is also an option btw).

Do you sanitize the user input to prevent code injection, ie SQL injection specifically? If not, you must!

Of course you’re correct.

A single scene would remove the loading issues between scenes, but that wasn’t the approach I was going for. I didn’t want the login screen to be an overlay on the game scene. It was a design choice I specifically made, as in my previous, non-networked version of the game (I built all of this functionality in a non-networked version before attempting networking), the login screen was just an overlay on the lobby scene and I didn’t like it because of the amount of management code in that one scene.

So, with that in mind, do you have any suggestions on implementing what I asked?

Re: data sanitization, I don’t plan on sql injecting my own database on my own test server, so that’s not really a development priority.

I don’t completely follow your code, but the issue you are having is commonplace: how to get data from one script to another. I just don’t understand whether this data needs to be transferred over the network or not. I will assume it’s required to be sent over the network to the server.

So either you call an RPC method after the client has already connected and structure your code to wait for a post-connection initialization RPC. Or you send that name with the initial payload during the connection approval process. Most network frameworks have that “payload” option when you call “StartClient” or something. If you wrote your own networking code, you already have a connection request process where you can punch extra data through, right?

Since you don’t seem to be using an existing networking framework (at least none that I recognize from the code), I can’t really tell you how to do that.

If I understand correctly you just need to pass the username that user entered into a text field in the Login scene, to the Lobby scene. And you are asking what is a good way and if passing it through a static field in CustomNetworkManager is ok.

IMO yes, it’s ok. Passing data between scenes through static variables is a valid way, I do it often. The only thing to be careful about with this is to not pass data that would hold a reference to a Monobehaviour because this would prevent memory connected to that Monobehaviour from being garbage collected, even after the Monohaviour is destroyed, as long as there is this static reference. But if you are passing a string then you are fine.

A slight design consideration to note. Since this static field for username can be in any class, it’s good practice to put it in a class where it belongs logically, which is not neccessarily the class that reads it. In your case I might rather put it in some Login class than in CustomNetworkManager, since, as a username, it belongs more to login than to network management.