Search Unity

How many network objects in a scene is too many?

Discussion in 'Netcode for GameObjects' started by slaymuel, Feb 5, 2022.

  1. slaymuel

    slaymuel

    Joined:
    Jan 18, 2021
    Posts:
    2
    Hi all,
    i have a design question regarding network objects.

    My game contains a lot of trees, mineable stone etc which the player can cut down / mine. I want the health (and possibly other properties) of these objects to be shared across the network, i.e networkvariables. Hence (naively?) all my trees have network objects. This means that i have a lot of network objects in my scene (several hundred). Is this the wrong way to do it? Should i instead have some kind of managers which manages several objects and only have one network object themselves? Afaik i cant add networkobject components dynamically?

    As it is right now it seems like having many network objects is killing performance? In my profiler NetworkUpdate takes about 20% of my CPU time. I have very few (2) network transforms in my scene.

    Thanks.

    Regards,
    Slaymuel
     
    Last edited: Feb 5, 2022
  2. cerestorm

    cerestorm

    Joined:
    Apr 16, 2020
    Posts:
    664
    You could reduce the number to only spawn those in view of each player. If it's a large map maybe reduce the size of the map loaded around the player. I'd also be interested in more tips on this.

    Incidentally how are you spawning so many objects, I usually run into trouble when I go into the hundreds.
     
  3. slaymuel

    slaymuel

    Joined:
    Jan 18, 2021
    Posts:
    2
    Thanks for your reply. I guess spawning only the objects that are in the view might work. Might be a bit dangerous when there is latency though, and i guess it wont be very performance friendly.

    The trees are scene objects so they are automatically spawned, in previous versions of netcode it broke down when spawning the trees (because they were too many) but seems to work now. Except for the (large) performance loss.

    I think i will try to create a "manager" which keeps track of trees and the properties. As a player damages a tree, for instance the host, the manager would update and send the new health to the clients. Then only one network object is needed. If this fails i will think more about how to implement your idea.
     
  4. CosmicStud

    CosmicStud

    Joined:
    Jun 13, 2017
    Posts:
    55
  5. mikawendt

    mikawendt

    Joined:
    Sep 5, 2019
    Posts:
    14
    Did you by any chance solve this issue? I have the exact same problem.. tons of trees and minable stones.

    My idea is to use what i call a passive network object. The idea here is that until a tree is cut down or something, it will remain dormant.
    When a player then interact with the tree, it will send a ServerRPC that this tree is now an active network object, and spawn a network object based on the coordinates of the tree.

    Anyone have a better solution?
     
  6. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    259
    To answer the original question:
    If you have a bunch of in-scene placed objects that you want to maintain state via NetworkVariable(s), then there are a couple of possible paths to take.

    What Is a NetworkObject Really?

    In reality, a NetworkObject boils down to the following things:
    • It represents a GameObject in a scene hierarchy
    • It is associated with a GlobalObjectIdHash value that tells everyone "What kind of Object" it is.
      • For dynamically spawned, this tells clients which network prefab to use when instantiating
      • For in-scene placed, this tells clients which object "in a specific scene" it is (the prefab instance resolution is handled when the scene is loaded).
        • Since you can load the same scene additively many times, if you have scene management enabled it uses the scene handle and the GlobalObjectIdHash value to determine "which instance" it is.
    • When spawned, it is assigned a NetworkObjectId.
      • The NetworkObjectId is used to determine "which spawned NetworkObject" a message might be associated with.
        • ** Think of this as the beginning "address" when communicating something associated with the NetworkObject changed.
    How NetworkBehaviours Are Associated With NetworkObjects:
    When a NetworkObject is instantiated and spawned, all NetworkBehaviour components associated with the NetworkObject are collected into a single list. Each NetworkBehaviour is identified by its position in this list (i.e. its index value) and this index value is actually the NetworkBehaviourId.

    How NetworkVariables Are Associated With NetworkBehaviours and NetworkObjects:
    When a NetworkVariable changes, the message sent to synchronize all clients will contain:
    • The NetworkObjectId: To be able to know which NetworkObject the message pertains to
    • The NetworkBehaviourId: To be able to know which NetworkBehaviour associated with the NetworkObject the message pertains to.
    • The NetworkVariable Field Identifier: an internal value determined very similarly to the NetworkBehaviourId that tells what NetworkVariable field the changes should be applied to.
    The main idea is that you can have one NetworkObject with many NetworkBehaviours that each have (n) NetworkVariables.

    One NetworkObject Many Children (With NetworkBehaviours)
    This option does require you to handle any visualizations based on changes to NetworkVariables (i.e. chopping down a tree might iterate through a few different visual phases until there is nothing or a stump) for all clients include late joining. You can nest as many children as you want under a single NetworkObject and everything should "work as expected". Of course, you should also think about the cost to synchronize clients and how you are handling your resources' states.
    If you have a bunch of NetworkVariables using things like individual numeric types (i.e. not a struct or class that implements INetworkSerializable), then each NetworkVariable will have a "cost" associated with it. It consumes space in the NetworkVariable Field table. It is individually processed during synchronization (i.e. each NetworkVariable is serialized individually and added to the over-all serialized data block associated with the NetworkObject). As such, if you have several "state related NetworkVariable properties" to track an in-game resource you might think about:
    • Combining all of the properties into a single struct or class that implements INetworkSerializable
    • As opposed to using several bool values to determine "on-off" states, you might think about using flags with a byte or short (depending upon the number of states you might have).
    • If you have several properties that "might or might not" change, then you might think about using flags to determine which property has changed
      • So, as opposed to serializing everything, you first serialize the "Changes" ushort value that contains the flag values of the properties updated first (primarily for when reading the serialized data you need to know which properties are included in the serialized stream reader).
      • You would want to set a "synchronize all" flag when first Synchronizing (see NetworkBehaviour.OnSynchronize) to send everything to a newly joined client.
        • The idea is that whatever your last set of "changed properties" flag was when you sent an update would still be set, so OnSynchronize is invoked when a full synchronization is required...as such you would set all flags or a "synch all" flag to denote that you want to write all properties and read all properties.
    If you already have a bunch of in-scene placed NetworkObjects and want to "re-design" your approach this way, you can first create your (in the context of trees) "Forest" GameObject and add a NetworkObject component to that. Then you can drag and drop your trees under the Forest. Then you should be able to reselect all of your trees together (now children of the Forest) and in the inspector view remove their NetworkObject components. At this point, you will have a single Forest parent NetworkObject and a bunch of children GameObjects with NetworkBehaviour components that will all be associated with the single NetworkObject.

    Of course, if the resource (i.e. tree) is fully consumed you will have to have a strategy to making that visually appear as "consumed" (whether nothing renders or a stump renders or the like). This logic would be based on the resource's change in state and need to be invoked on all client instances when state changes.
    (Remember, OnValueChanged only invokes after a NetworkObject is spawned so you would need to invoke this same logic during NetworkBehaviour.OnNetworkSpawn to make sure late joining clients properly visually synchronize with the resource's current state).


    Single NetworkObject Forrest Management
    This is a bit more of a complex approach, and I could write a whole bunch on various ways to handle this. This is where you don't have any children but provide some form of "data" (an array, a bitmap where each pixel represents a tree, etc.) that represents the forest. You would want to follow a similar pattern (i.e. INetworkSerializable implementation) that would send everything when first Synchronizing (see NetworkBehaviour.OnSynchronize) and then each time a resource is consumed only a specific block of that data is updated and upon being updated the tree(s) the data is associated with change their visual representation (or the like).


    The idea for both approaches is that you don't really need to have a NetworkObject represent everything in your game that you want to be synchronized. It is something we really need to have better documentation for and can be easy to start a design using a NetworkObject for almost everything in your game...only to realize later that it can become a costly design pattern.

    Do You Really Need a NetworkObject Per Instance?
    Some guides to determine if using a NetworkObject per instance could become too costly:
    • How many instances do you plan on having at any given time?
      • If you are in the 100's upwards of 1000+, then you need to think of alternate approaches.
        • This only applies to things that don't really "move"... if you are wanting 1000's of things moving around then you definitely need to come up with some cleaver custom approach.
    • Is your object strictly for maintaining state and does not move?
      • If this is true, then you might think about nesting it as a child under some "GlobalManager" NetworkObject where several other things could be managed this way.
        • Say you have a castle with 30+ doors that simply open and close...they are in-scene placed and don't move...when opened they animate and rotate/slide open... but their status is really just "open or closed"...so a single NetworkObject with 30+ child GameObject doors that each have a DoorNetworkBehaviour component would be better than 30+ NetworkObjects that all have a DoorNetworkBehaviour component.

    Let me know if any of this information helps?
     
    Last edited: Nov 12, 2023
  7. NoelStephens_Unity

    NoelStephens_Unity

    Unity Technologies

    Joined:
    Feb 12, 2022
    Posts:
    259
    As a follow up to anyone looking over this answer, the included sample project provides a way to "enable and disable" various children where you have only 1 NetworkObject and 1 NetworkBehaviour that control several "objects".
     

    Attached Files: