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?