Search Unity

Network object, one visible enemy, but not shared child objects

Discussion in 'Multiplayer' started by e-sollinger, Mar 6, 2017.

  1. e-sollinger

    e-sollinger

    Joined:
    Dec 30, 2016
    Posts:
    12
    The project is intended to have one shared object "user interface" that dynamically creates buttons, but the client should be able to click on a button or the server should be able to click on the button, and both should see the user interface respond.

    In the Unity Editor, the UserInterface object is disabled.

    I created a NetworkDiscovery and NetworkManager that only spawn the prefab UserInterface to one connection, and auto-connects clients successfully. I only spawn one UserInterface, as if it were an "enemy", since otherwise there ends up being two UserInterfaces visible once the client connects. So this works, such that there is only one UserInterface visible to both screens.

    But, the prefab itself is a generator of the UserInterface dynamic elements, and so anything that the UserInterface creates or changes on each screen appears to be its own instance. They are not synchronized.
    Ideas I've tried, unsuccessfully:
    1. There is one method, where every user interaction occurs, so I've tried using a [Command] Attribute to trigger all clients to sync their "buttons" by name.
    2. Treating the UserInterface as a Player (this causes two UserInterface on screen).
    3. Checking LocalAuthority on all NetworkIdentity objects.
    4. Prefab and registering all NetworkIdentity objects.
    5. NetworkServer.Spawn, whenever I create a dynamic instantiation.
    6. ...
    anyway, the idea is that I've tried a lot of things, and am running out of ideas. I don't know which direction was the correct direction, and would like to discuss the best path forward. Did I head down a correct path and gave up too soon?
     
  2. donnysobonny

    donnysobonny

    Joined:
    Jan 24, 2013
    Posts:
    220
    Hmm... it sounds like your approach might be slightly off. Ultimately, as you've discovered already, if you rely on the NetworkManager to spawn objects on behalf of a player, then that object is set to belong to that player; you are going to want these objects to belong to the "server". The "enemy" approach won't quite work for you. It's more designed to have a local/remote variant of an object that belongs to a player on the network.

    So ultimately, this is going to get pretty tricky, but what you want to do is use a slightly more custom approach, and avoid relying on the NetworkManager's "player prefab". Have a look here at how you can spawn objects over the network https://docs.unity3d.com/Manual/UNetSpawning.html. The idea here is that you want to both ensure that any associated prefabs that need to be spawned over the network are registered to both the client and server sides, via NetworkClient.RegisterPrefab and NetworkServer.RegisterPrefab. In addition to this, custom scripts that you attach to these prefabs should inherit from NetworkBehaviour rather than MonoBehaviour, and the prefab will also need a NetworkIdentity. Most of this is discussed in the link above, however your end goal is to ensure that the objects are spawned by the server, and not by the clients, as this will cause the networked objects to spawn when the client connects, and allow the use of Commans/RPCs to handle your interactions etc.

    Hopefully this helps. Let me know if you're still stuck.
     
    e-sollinger likes this.
  3. e-sollinger

    e-sollinger

    Joined:
    Dec 30, 2016
    Posts:
    12
    I'm getting somewhere with it, but I have come to a point where I'm kinda stuck. I've got thirteen prefabs pre-registered with the NetworkManager, and when I watch the server-side fire up, it is printing asset id/ guids much higher. I've prefab'd and registered everything that I spawn, but when the client-only side starts up, it doesn't know how to make #2 and #13. The items in the list at the NetworkManager seem to coincide with a sequential numbering, but better than that, I found that Unity prints the name of the object it is spawning, as well as the guid. But, I'm still lost ...

    The things that it is puking on are only a few of the dynamically instantiated and modified prefab buttons, it seems.
    OnStartServer CubeButton.12 (UnityEngine.GameObject) GUID:13
    SetLocalObjectOnServer 13 CubeButton.12 (UnityEngine.GameObject)
    SetLocalObject 13 CubeButton.12 (UnityEngine.GameObject)
    SpawnObject instance ID 13 asset ID 62e7fafe83d66ad44b42d201485bc7e7

    OnStartServer CubeButton.33 (UnityEngine.GameObject) GUID:2
    SetLocalObjectOnServer 2 CubeButton.33 (UnityEngine.GameObject)
    SetLocalObject 2 CubeButton.33 (UnityEngine.GameObject)
    SpawnObject instance ID 2 asset ID 62e7fafe83d66ad44b42d201485bc7e7

    But the worst part, is I'm not even to that part of the scene, when it complains on the client about being able to create these:
    Spawn scene object not found for 2
    Not a very helpful message!!

    The buttons that aren't showing up in the scene are other dynamically generated buttons. So I think it's strange that only the ones that wouldn't be in the scene yet are ever complained about.


    Do you know where I might get more info to troubleshoot/debug this problem?

    Here is typical spawning of prefabs for the menu column (a button list):

    Code (CSharp):
    1.     [ServerCallback]
    2.     public void SetMenuColumns(string s)
    3.     {
    4.         RemoveAllColumns();
    5.         if (s == "MainMenu")
    6.         {
    7.             obj_BtnReturn.SetActive(false);
    8.         }
    9.        
    10.         int count = col2names[s].Count;
    11.         int colcnt = 0;
    12.         // make a square
    13.         for (int i = 1; i < count; i++)
    14.             if (i * i <= count)
    15.             {
    16.                 colcnt = i;
    17.             }
    18.             else
    19.                 break;
    20.  
    21.         if (s == "MainMenu" && (colcnt * colcnt < count))
    22.         {
    23.             int target = (colcnt + 1) * (colcnt + 1);
    24.             while (col2names[s].Count < target)
    25.                 col2names[s].Add("Future");
    26.             colcnt++;
    27.             count = col2names[s].Count;
    28.         }
    29.         // make it wider than taller by one
    30.         if (colcnt * colcnt < count)
    31.             colcnt++;
    32.         int jcnt = colcnt;
    33.         if (count < 10)
    34.         {
    35.             colcnt = 1;
    36.             jcnt = count;
    37.         }
    38.         float LeftSide = -(colcnt - 1) * ColumnSpread / 2f;
    39.         // set the columns text
    40.         for (int i = 0; i < colcnt; i++)
    41.         {
    42.             //NetworkHash128 mcAssetId = MenuColumnTemplate.GetComponent<NetworkIdentity>().assetId;
    43.             GameObject go = Instantiate(MenuColumnTemplate);
    44.             go.transform.parent = transform;
    45.             go.transform.localPosition = Vector3.zero;
    46.             go.name = MenuColumnTemplate.name + " " + i;
    47.             MenuColumn mc = go.GetComponent<MenuColumn>();
    48.             mc.ButtonTemplate.SetActive(false);
    49.             cols.Add(mc);
    50.             cols[i].ButtonNames = new SyncListString();
    51.             for (int j = 0; j < jcnt; j++)
    52.             {
    53.                 int index = j * colcnt + i;
    54.                 if (col2names[s].Count > index)
    55.                 {
    56.                     if (col2names[s][index] == "Go Back")
    57.                     {
    58.                         obj_BtnReturn.SetActive(true);
    59.                     }
    60.                     else
    61.                     {
    62.                         cols[i].ButtonNames.Add(col2names[s][index]);
    63.                     }
    64.                 }
    65.             }
    66.             cols[i].ButtonsUpdated = true;
    67.             cols[i].ColumnIndex = i;
    68.             cols[i].ColumnLocation = LeftSide + (ColumnSpread * i);
    69.             cols[i].ButtonHeight = RowSpread / jcnt;
    70.             cols[i].gameObject.SetActive(true);
    71.             NetworkServer.Spawn(cols[i].gameObject);
    72.         }
    73.         CurrentMenu = s;
    74.     }
    75.  
     
  4. donnysobonny

    donnysobonny

    Joined:
    Jan 24, 2013
    Posts:
    220
    Hmm... reading your post above, I'm not sure that I completely understand your issue, however there are a few potential issues that you may be facing.

    Firstly, if you are using a client-server model (where your "players" connect to a central server), then you should only use the NetworkServer class in the code that is run by the dedicated server, calling NetworkServer.Spawn only on the server-side. If you are using a peer-hosted model (where one of the players is both a server and a player), then you need to be checking whether that player is the "server" before calling NetworkServer.Spawn. You don't, for example, want to make use of the NetworkServer class at all from the client-side, this will just cause you problems. If you're already sure you're using it correctly, that's fine. I just wanted to double check.

    Secondly, line 49 in your code looks a bit off. You are making a cols.Add call, which will add the MenuColumn component to the end of the array, but you are working on the array itteratively in a for loop. I may be misunderstanding what it is you are doing but shouldn't line 49 be
    Code (CSharp):
    1. cols[i] = mc
    ? Otherwise your Spawn call on line 71 doesn't make sense.

    Hopefully this helps. Let me know if you are still stuck.
     
  5. e-sollinger

    e-sollinger

    Joined:
    Dec 30, 2016
    Posts:
    12
    Thanks. The code was just one of the places that I do network spawning, which does run only on the server side. I made sure all the spawning only takes place on the server. I am using a peer-hosted model. All of the network comm stuff has been working well. The button code works well. cols = mc wouldn't work since cols doesn't exist yet, until I cols.add it. Anyway, the code was probably a distraction, since the problem/question is whether you know a way to "debug" and resolve what causes the client side to say "Spawn scene object not found for 2" ... how to know what exactly 2 is (the GUID that I listed?) and why the client doesn't know how to spawn it. The peer-host obviously had no problem spawning "2", so how do I get client-only to know what 2 is?

    My suspicion is that there is some element of the "dynamically spawning objects" that I am missing. I 1. Register, 2. Instantiate 3. NetworkServer.Spawn, but what his missing when the object is a modified prefab that is getting generated?
     
  6. donnysobonny

    donnysobonny

    Joined:
    Jan 24, 2013
    Posts:
    220
    Okay well in that case it sounds like you're on the right track for the most-part.

    So just to double check, you are doing a ClientScene.RegisterPrefab call on all of the prefabs that you will be spawning over the network, on both the client and server side?

    Ideally, the peer-host would:
    - register all prefabs when the scene starts up
    - locally instantiate pre-registered prefabs
    - NetworkServer.Spawn the instantiated object once you have finished setting it up locally

    The client side only needs to:
    - register all prefabs when the scene starts up

    Since you're using the HLAPI, the NetworkManager should handle the rest for you, at least at a basic level. I assume you haven't gotten round to thinking about what to do when the peer-host disconnects yet? XD
     
  7. e-sollinger

    e-sollinger

    Joined:
    Dec 30, 2016
    Posts:
    12
    I think I am registering all prefabs in the ClientScene through the public override void OnStartClient, within each class that generates from "template instances", but it seems strange to me that this only gets called on the prefabs, and not the dynamically generated ones that the server generates.

    I'll be double-checking this, now, of course ....
    Ok, so I put OnStartServer and OnStartClient on every NetworkBehavior, and am calling ClientScene.RegisterPrefab(gameObject) in both!! Overkill, but trying to get rid of those client-only complaints that "Spawn scene object not found for 2".

    I'm getting a SyncList not initialized, even though I've set the declaration to new one, AND, as you saw, re-initialize the newly instantiated objects SyncList with a new SyncList. So that is another bother. I only bring it up in case somehow any error is faulting out of the class methods and not completing through to the "RegisterPrefab" ... hmmm, might put them at the top of the methods.

    Those are the only two errors I'm finding. I'll attach the PeerHost and ClientOnly logs, in case they would help you help me.

    Would this be the problem? "The prefab 'VergenceSideMenuColumnTemplate 0' has multiple NetworkIdentity components. There can only be one NetworkIdentity on a prefab, and it must be on the root object."
    What is that caused by? I suspect that it is because all of the prefabs are made out of other prefabs.

    Also, client is auto-disconnected when the peer-host dies. So I already got that working.

    REALLY appreciate all your help and thoughts!
     

    Attached Files:

  8. donnysobonny

    donnysobonny

    Joined:
    Jan 24, 2013
    Posts:
    220
    Okay so first off, I am beginning to understand your terminology of "dynamically" generated prefabs. You are referring to prefabs that are spawned as a result of spawning registered prefabs? If so, then yeah, your dynamic objects are not going to be automatically handled by the network. In cases like this, you would need to set up custom spawning of your "dynamic" prefabs (for example, send a message to spawn the dynamic prefab XXX, and then implement your own synchronization of it). Otherwise, you need to ensure that all objects being spawned over the network are registered prefabs.

    Ultimately, if you want the spawning of an object to be handled automatically by unet, then you must follow the fundamental steps: register, instantiate, NetworkServer.Spawn

    So, I recommend changing your thinking a little bit;
    • any game objects that you want to spawn over the network, make them into prefabs and register all of them using ClientServer.RegisterPrefab within the OnStartClient override
    • if you are trying to avoid making your "dynamic" objects into prefabs, then figure out where their "common" aspect is. What part of it could be reproduced before the more "dynamic" parts of it take place? For example, maybe you want to avoid making your buttons prefabs because they have different colors/text. In this case, attach a script to your button that holds a reference to the color and text property of the button, and make the button into a prefab. Then, when the button is spawned over the network, set up syncvars or another way to synchronize the color/text of the button (or any other "dynamic" properties).
    • remember to add network identities etc to all prefabs that will be spawned over the network
    • so then, in cases where your currently registered prefabs are spawning "dynamic" objects, have them spawn your newly registered prefabs instead using the usual steps
    As for whether to use OnStartServer and OnStartClient, your peer-host will trigger both of these, while your clients will only trigger OnStartClient. You only need to register prefabs once, so just do it in OnStartClient.

    I haven't looked at your log files as I assume that the above is where you're going wrong slightly. If you're still stuck though, let me know.
     
  9. e-sollinger

    e-sollinger

    Joined:
    Dec 30, 2016
    Posts:
    12
    Ok, I have already done what you said (SyncVar on elements of modification to the objects, NetworkIdentities on each).

    But your instructions make me think that when the server creates a menu column (of buttons), it loops and spawns buttons using the template button (prefab), and generates new asset id's for each button during spawn ... BUT, when the client starts a menu column, it does not know what those asset ids of the new buttons are, and seemingly should do a similar loop! So how do you get the client to generate the same buttons during the OnStartClient? Add a specially generated asset id? Should I set up a SyncListStruct of generated asset id's associated with the other corresponding "modifications"?

    Would this help?
    NetworkServer.Spawn(button,button.GetComponent<NetworkIdentity>().assetId);

    Is it possible to make a SyncList<ButtonType> from the MenuColumn that would actually send the whole object over?
     
  10. donnysobonny

    donnysobonny

    Joined:
    Jan 24, 2013
    Posts:
    220
    There are some conflicting points here...

    What do you mean by when a client "starts" a menu column? Are you meaning that the client makes a request to the server (peer-host) to spawn it? If not, then you need to be. Remember that the server (peer-host) should be the only peer that is actually spawning objects (by instantiating them and calling NetworkServer.Spawn). The objects will automatically be spawned on the client-side by the NetworkManager.

    You don't. So you should not be calling Instantiate/Spawn from your client-side. The NetworkManager will spawn the objects for you after the server-side (peer-host) has called NetworkServer.Spawn, based on the fact that the prefab that was spawned is a prefab that you have previously registered (on both the client and server side via ClientScene.RegisterPrefab).

    So to recap and expand on my previous response:
    • ensure that every object that you want to "spawn" over the network exists as a prefab (everything!)
    • every prefab above, register it on both the client and server side within the OnStartClient override before you spawn anything
    • instantiate the above prefabs on the server side only
    • call NetworkServer.Spawn on the above instantiated prefab on the server side only
    • do not instantiate anything on the client-side. Let the NetworkManager handle the spawning of these objects for you automatically
    Does that explain it? Let me know if you're still stuck.
     
  11. e-sollinger

    e-sollinger

    Joined:
    Dec 30, 2016
    Posts:
    12
    Ya, sorry, I was speaking in the context that we had already clarified ... ONLY the server is spawning. HOWEVER, my understanding is that the client is being told to create something that he doesn't know how to create, which causes me to consider that there is object generation on both the server-side "Spawn", and on the onStartClient "RegisterPrefab". The client is "making something" or something is causing him to say "Spawn scene object not found for ... " which causes my speaking-confusion a bit.

    So, here when you say "do not create them on the client" and I say "the client makes a menu-column" ... maybe I need to know if there is something more I can do to prevent "active objects in the scene" from being created, like they usually are when a scene is loaded? That is, I am not intentionally spawning or instantiating on the client at all. (BTW, I am always referring to the "client" as the "client-only" remote network attached instance.)

    All of the dynamically created objects are instantiated from a prefab. Their modifications are all in SyncVars. Still must be missing SOMEthing...

    I think I mentioned that my hierarchy contains prefab parents of prefab children ... the whole tree is blue, although some of the prefabs have non-consequential items that are not prefab and do not get spawned. Cannot there be anything in the tree that is not a prefab? ... that could be the real problem here, and will try it till I hear back from you.
     
    Last edited: Mar 17, 2017
  12. e-sollinger

    e-sollinger

    Joined:
    Dec 30, 2016
    Posts:
    12
    Ok, neither of those things worked.

    I see that the manual says " For more advanced use cases such as object pools or dynamically created assets, there is ClientScene.RegisterSpawnHandler ..."

    But, later, in the same page, I read "When this code runs, the tree objects created on clients will have the correct value for numLeaves from the server."

    So, I'm hesitant to try it, since you said I only need to create things on the server. It is not clear if these handlers are meant to create unSHARED objects; just client copies.

    If I do use client copies of buttons, then I lose the advantage of Let the NetworkManager handle the spawning of these objects for you. Things are in-sync, to where the server is in the scene when the client starts up, EXCEPT at the very first case of the first button. So, if I can get one button to generate on the client, the rest is cake.

    See, in the MainButtons, there is the prefab MenuColumnTemplate, that is used by the server to generate a column named "MenuColumnTemplate 0", then generates a "Button Vergence" from the "MainMenuButtonTemplate" prefab. This "Button Vergence" is the first element that does not generate on the client-only. I'm thinking that the client-only either gets the original "MenuColumnTemplate" or "MenuColumnTemplate 0", and does not know how to generate the buttons or the column.
    upload_2017-3-16_22-1-38.png
     
    Last edited: Mar 17, 2017
  13. e-sollinger

    e-sollinger

    Joined:
    Dec 30, 2016
    Posts:
    12
    So ... once I did what you said, everything works. Really do have to make every stupid thing network ready. So lame.

    Anyway, thanks for your persistence patience and clarity. Very helpful.
     
  14. donnysobonny

    donnysobonny

    Joined:
    Jan 24, 2013
    Posts:
    220
    Sorry for not getting back to your last posts, I was really struggling to think how else to word it! Yeah, the difficult thing with and high-level framework is you've pretty much got to use it how it's designed to be used. If you want more control/customization, then you may want to consider using the LLAPI or a more low-level framework (I don't recommend you do that, but just to give you an explanation as to why you have to do things the way that has been suggested).

    No problem at all. I'm glad that you were able to get things sorted! Good luck with the game!