Changing rooms and spawning players with Multiplayer Example

The Premise

So, I have finished the mechanics for a single player hack n slash game and I want to incorporate mulitplayer functions into it that will allow a second, third, fourth, etc amount of players to team up with you. The game won't be spread out into one level, but into dungeons that contain multiple rooms (i.e levels). This game is intended to run on LAN.

My problem is moving a player from one level to another. Now, I started off using the TPS-Auth room from the multiplayer example. Now, I wanted the player to start off in a login room, seperate from the first level. What I did was duplicated the TPS_Auth room and called it level one, then I went back to the original TPS-Auth and removed the camera and floor object.

The New Problem January 20th edit

The game compiles and I am able to make a standalone. I run two instances of the client. The game starts off in the connection room. I run the server, then have each client connect to it. Then I hit the Level1 button to proceed to the first room and and click "spawnplayer" and it spawns an instance in each clients, but each client can control both characters (not what I want). I'm not using the NetworkLoadLevel script to change rooms (which is attached to the "DirectConnectGUI" object in the TPS-Auth room (the "Actual" first level")

I'm not sure what to do at this point and level changing is vital for this game. If I can't even get past logging in to the game (via a login script I already have) I don't know how to continue. Any suggestions or example of changing rooms/levels from online room using multiple players? I wanted it restrict the party from changing room until they are all at the exit, then spawn them together at the entrance of the next room.

I uploaded the project incase you want to see what stupid mistake I did or if anyone can come up with a solution. (Maybe even figure out why my NAT isn't working smh...)

Download Project File Here

Alright, so I figured out why your Lerpz is acting funny when you load a new level. It turns out that it isn't the GraduallyUpdateState script, but during the process of level changing your Network ID is lost. So its important to stop all information being sent as well as being received, set the Network level, load the level on the client, wait, and then re-enable sending and receiving. Here the code that does this:

    Network.SetSendingEnabled(0, false);    
    Network.isMessageQueueRunning = false;
    Network.SetLevelPrefix(levelPrefix);
    Application.LoadLevel(level);
    yield;
    yield;
    Network.isMessageQueueRunning = true;
    Network.SetSendingEnabled(0, true);

Alright, so once thats done, the ID's will be properly updated and you can see you're player update. However, as of right now, it loads the level on the server also. No matter what I do, so I know it isn't the answer you're looking for. But it would allow you to transition between levels with a party. Here is the important script: Network Level Load. Its already in the project and is named NetworkLevelLoad. Just attach that to the ConnectGUI game object, and specify which level you want to load. You could muck around in the code and do it automatically, but I still can't figure out how to Buffer it to all clients, but just not to the Server. This is EXTREMELY important and I'm kind of annoyed that the M2H Tutorial didn't cover this topic.

The first thing I notice in your question is the code:

function OnConnectedToServer() {
    // Notify our objects that the level and the network is ready
    for (var go in FindObjectsOfType(GameObject)) {
        go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);
        Application.LoadLevel("Level1");
    }       
}

So this means that for each currently loaded GameObject, you do "Application.LoadLevel("Level1")" ... this doesn't really make sense. If Unity checks whether the "current level" is already loaded this wouldn't be a problem - but in any case it's not making a lot of sense to have Application.LoadLevel("Level1"); in a loop over all game objects.


That said: Regarding the NetworkViewIDs you have to watch out because there are some cases in which Unity won't update them properly in the scenes. I just ran into that two days ago myself: I had added a NetworkView to a prefab and the only way to make sure all NetworkViewIDs were set correctly was to open each scene and save it again (might also have had to change something).

What Daniel posted regarding the level prefixes is correct: To prevent disasters from happening you should use levelPrefix when loading levels, and this is documented in Network Level Loading.

However, you are in a somewhat special situation:

You'll very likely want the server to keep all levels loaded. If you have a lot of levels and the players are sparsely distributed, you might also want to dynamically load and destroy scenes on the server but that will make things way more complex. So for now let's assume that you don't have a terrible amount of scenes and that you don't need to remove scenes from the server. In other words: When the server starts up, you'll simply load all the scenes one after the other. If you really need to, you can still add that complexity later.

Here's some code from Traces of Illumination that illustrates how loading all levels on the server can be done (this code is executed only on the server ... it doesn't even exist in the clients - but in my setup, server and clients are the same project nevertheless; I use conditional compilation to keep the clients "clean from server code").

This runs in a Coroutine among a lot of other server-initialization stuff that has to be done:

public IEnumerator StartupStandaloneServer() {
    // wait until all Awake()s have been executed...
    yield return new WaitForEndOfFrame();

    // load all levels (and keep them in memory all the time)

    // GameGroupData is a custom class that, among other things
    // has a list of all available levels
    GameGroupData gameGroup = myGameGroup;

    // LevelStatus is a custom class that I have attached as script
    // to the root of each level (see discussion below)
    LevelStatus lastLevelStatus = null;

    // LevelData is a custom class that contains meta-data on the
    // levels, like name, a level ID etc., gameGroup.levels obviously
    // is the list of all levels (actually, all "LevelData" instances)
    foreach (LevelData levelData in gameGroup.levels) {
        isLoadingLevel = true;
        log.InfoFormat("Pre-Loading level: {0}", levelData.fullName);
        Network.SetLevelPrefix(levelData.levelID + 10);
        Application.LoadLevel(levelData.fileName);

        // see below to understand when "isLoadingLevel" is set to false

        while (isLoadingLevel) { // wait until level is loaded...
            yield return new WaitForSeconds(0.1F);
        }
        lastLevelStatus = null;
        while (lastLevelStatus == null) {
            try {

                // once the level is loaded, it makes it's LevelStatus
                // available in a static lookup table; this may take a moment, though

                lastLevelStatus = LevelStatus.GetLevelByData(levelData);
            } catch { // KeyNotFoundException to be ignored...
                // this logging statement helps me find evil levels that have broken implementations ;-)
                log.DebugFormat("Waiting for level status to become available: {0}", levelData.fullName);
            }
            if (lastLevelStatus == null) {
                yield return new WaitForSeconds(0.1F); // be patient ;-)
            }
        }

        // this is important, it assures that the levels don't get overwritten
        // when the next level is loaded (making them all be loaded simultaneously)
        DontDestroyOnLoad(lastLevelStatus.gameObject);
    }
}

Ok, so in the same class, you also need this, which makes sure that the loop above will terminate once the level has been loaded. Of course, the class/script this is all happening also must be persistent through LevelLoad (so the game object it is attached to must have been marked with DontDestroyOnLoad(...)).

public void OnLevelWasLoaded() {
    isLoadingLevel = false;
}

Your scenes need to be built according to the following scheme for this to work:

  • RootItem with LevelStatus-script attached (well, your implementation of something similar)
    • Level Object A
    • Level Object B
      • Child of Object B (if you wish ;-) )
    • ... and so on ... as long as everything is a child or grand-child or deeper of RootItem

With that setup, on the clients, you'd load the levels "on demand" but it's very likely that you'd still keep multiple levels loaded at the same time. Of course, you have to make sure that the LevelPrefixes between server and clients match. And of course, you have to make sure that any player objects don't get destroyed when loading new levels (you could probably also use Application.LoadLevelAdditive(string name) instead of using the approach with DontDestroyOnLoad but keeping a root object will still help you (also when you want to unload scenes/levels).

And, of course, you have to make sure that the levels are built in a way so that they are distributed correctly in space. That's another thing where the RootItem will help you a lot, as you can simply move the whole level around as you wish.


What I would recommend is building a very simple test-project with a few scenes from scratch where you take parts from your project and make sure you understand each part. Unless you fully understand what every single line of code is used for in a networked game, working on it will be terribly frustrating (and I'm speaking from experience ;-) ).