Skip to content

WorldPositionStays update #778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Oct 27, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 80 additions & 48 deletions docs/advanced-topics/networkobject-parenting.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,31 @@ title: NetworkObject Parenting
description: A `NetworkObject` parenting solution within Netcode for GameObjects (Netcode) to help developers with synchronizing transform parent-child relationships of `NetworkObjects`.

---

:::important Opt-OUT
This feature is behind a bool flag that can be toggled on the `NetworkObject` inspector UI. It will be enabled by default but you can opt-out from it if you want to implement your own solution
### Overview
If you aren't completely familiar with transform parenting in Unity, then it is highly recommended to [review over the existing Unity documentation](https://docs.unity3d.com/Manual/class-Transform.html) before reading further. In order to properly synchronize all connected clients with any change in a `GameObject`'s transform parented status, Netcode for GameObjects (NGO) requires that the parent and child `GameObject`s have `NetworkObject` components attached to them.

### Parenting Rules
- Setting the parent of a child's `Transform` directly (i.e. `transform.parent = childTransform;`) will always use the default `WorldPositionStays` value of `true`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Setting the parent of a child's Transform directly (for example, transform.parent = childTransform;) will always use the default WorldPositionStays value of true.

I'm not sure I understand what this sentence is saying. Are you saying that if you directly set the parent of a child's Transform, the WorldPositionStays value will always be true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

// WorldPositionStays defaults to true, clients are not synchronized but since this is the default it is the same as if it was synchronized
NetworkObject.transform.parent = parent.transform;  
// WorldPoisitonStays defaults to true, clients are not synchronized but since this is the default it is the same as if it was synchronized
NetworkObject.transform.SetParent(parent.transform);  
// WorldPositionStays will be false, but will not synchronize clients
NetworkObject.transform.SetParent(parent.transform, false); 
// WorldPositionStays will always be true (like the transform.SetParent), but NGO will synchronize this with clients
NetworkObject.TrySetParent(parent); 
// WorldPositionStays is false and NGO will synchronize this with clients
NetworkObject.TrySetParent(parent, false); 

- It is recommended to always use the `NetworkObject.TrySetParent` method when parenting if you plan on changing the `WorldPositionStays` default value.
Copy link
Contributor

@armstrongl armstrongl Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is recommended to always use the NetworkObject.TrySetParent method when parenting if you plan on changing the WorldPositionStays default value.

I'm not a huge fan of this phrasing because it starts by saying that the user should always use the NetworkObject.TrySetParent method, then continues to say that they shouldn't always; only if they plan to change the default value of WorldPositionStays

How does this sound?

It's best practice to use the NetworkObject.TrySetParent method when parenting if you anticipate changing the default value of WorldPositionStays.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that better too!

- Likewise, it is also recommended to use the `NetworkObject.TryRemoveParent` method to remove a parent from a child.
- When a server parents a spawned `NetworkObject` under another spawned `NetowrkObject` during a netcode game session this parent child relationship is replicated across the network to all connected and future late joining clients.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a server parents a spawned NetworkObject under another spawned NetowrkObject during a netcode game session, this parent-child relationship is replicated across the network to all connected and future late-joining clients.

Side question: what replicates the parent-child relationship across all clients?

Copy link
Contributor Author

@NoelStephensUnity NoelStephensUnity Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server sends a ParentSyncMessage to all clients when a NetworkObject's parenting status changes.
Really, now that I am reading over all of this...I think we should possibly have a couple of sections on parenting. Possibly an introduction section that describes how a server synchronizes clients with parenting changes (which we could dive into these kinds of details there), another that uses a very simple parenting walk through, and then a section that describes more advanced topics like how a child's scale is impacted by its parent and how WorldPositionStays impacts position, rotation, and scale.... perhaps that too could be broken into two or more sections.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a great idea! if you start a draft, I'm more than happy to help :)

- If, while editing a scene, you place an in-scene placed `NetworkObject` under a `GameObject` that does not have a `NetworkObject` component attached to it, NGO will preserve that parenting relationship.
- During runtime, this parent-child hierarchy will remain true unless user code removes the GameObject parent from the child NetworkObject.
- _Note: Once removed, NGO will not allow you to re-parent the `NetworkObject` back under the same or another `GameObject` that with no `NetworkObject` component attached to it._
- You can perform the same parenting actions with in-scene placed `NetworkObject`s as you can with dynamically spawned `NetworkObject`s.
- Only in-scene placed `NetworkObject`s can have multiple generations of nested `NetworkObject` children.
- You can parent dynamically spawned `NetworkObject`s under in-scene placed `NetworkObject`s and vice versa.
- To adjust a child's transform values when parenting or when removing a parent:
- Override the `NetworkBehaviour.OnNetworkObjectParentChanged` virtual method within a `NetworkBehaviour` attached to the child NetworkObject.
- When `OnNetworkObjectParentChanged` is invoked, on the server side, adjust the child's transform values within the overridden method.
- NGO will then synchronize all clients with the child's parenting and transform changes.

:::tip
When a NetworkObject is parented, NGO will synchronize both the parenting information along with the child's transform values. NGO uses the `WorldPositionStays` setting to determine whether to synchronize the local or world space transform values of the child `NetworkObject`. This means that a `NetworkObject` does not require you to include a `NetworkTransform` component if it never moves around, rotates, or changes its scale when it is not parented. This can be useful for world items a player might pickup (i.e. parent the item under the player) and the item in question needs to be adjusted relative to the player when it is parented or the parent is removed (i.e. dropped). This helps to reduce the item's over-all bandwidth and processing resources consumption.
:::

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is, NetworkObject doesn't need to have a NetworkTransform component if it never moves around when it has no parent.

[`MonoBehaviour.OnTransformParentChanged()`](https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnTransformParentChanged.html) under `NetworkObject` is utilized to catch `transform.parent` changes.

Three additional state variables are stored in `NetworkObject`:

```csharp
bool m_IsReparented; // did parent change compared to initial scene hierarchy?
ulong? m_LatestParent; // who (NetworkObjectId) is our latest (current) parent if we changed our parent?
Transform m_CachedParent; // who (Transform) was our previously assigned parent?
```

`NetworkBehaviour` includes a virtual method you can override to be notified when a `NetworkObject`'s parent has changed:
### OnNetworkObjectParentChanged
[`NetworkBehaviour.OnNetworkObjectParentChanged`](https://docs-multiplayer.unity3d.com/netcode/current/api/Unity.Netcode.NetworkBehaviour#onnetworkobjectparentchangednetworkobject) is a virtual method you can override to be notified when a `NetworkObject`'s parent has changed. The [`MonoBehaviour.OnTransformParentChanged()`](https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnTransformParentChanged.html) method is used by `NetworkObject` to catch `transform.parent` changes and notify its associated `NetworkBehaviour`s.

```csharp
/// <summary>
Expand All @@ -28,30 +37,10 @@ Transform m_CachedParent; // who (Transform) was our previously assigned parent?
virtual void OnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { }
```

You need to consider two main code paths when synchronizing `NetworkObject` parenting:

1. At Object Spawn
- Client spawns objects including static scene objects and dynamic spawned objects on join.
- Serialize `NetworkObject`s with their payloads (such as `NetworkBehaviour`s etc.)
- Write `m_IsReparented` and `m_LatestParent` fields to sync on the client-side
2. During Gameplay
- When a server parents a spawned `NetworkObject` under another spawned `NetowrkObject` during a netcode game session this parent child relationship is replicated across the network to all connected clients.
:::info
The server will writes the `m_IsReparented` and `m_LatestParent` fields into a `NetworkBuffer` and sends a `PARENT_SYNC` message on the `MLAPI_INTERNAL` channel to all connected clients.
:::caution Multi-Generation Children and Scale
If you are dealing with more than one generation of nested children where each parent and child have scale values other than `Vector3.one`, then mixing the `WorldPositionStays` value when parenting and removing a parent will impact how the final scale is calculated! If you want to maintain the same values prior to parenting when removing a parent from a child, then you need to use the same `WorldPositionStays` value used when the child was parented.
:::

:::important
Transform parent synchronization relies on the initial formation of transforms in the scene hierarchy being identical on all standalone instances.
:::

## NetworkObject Parenting Rules
A few basic `NetworkObject` Parenting rules are listed below.

:::warning Limiting Non-Networked NetworkObject Transform Parenting
Rules outlined below are applied and enforced even while not networking (not hosting or connected). Specifically, if you were to try parenting a `NetworkObject` under a non-`NetworkObject`, that'd be invalid and reverted even though you are not hosting or connected to a server.
:::


### Only A Server (or A Host) Can Parent NetworkObjects
Similar to [Ownership](../basics/networkobject#ownership), only the server (or host) can control `NetworkObject` parenting.

Expand All @@ -60,27 +49,64 @@ If you run into a situation where your client must trigger parenting a `NetworkO
:::

### Only Parent Under A `NetworkObject` Or Nothing (i.e. The Root or null)
A `NetworkObject` can only be parented under another `NetworkObject`. The only exception is if you don't want the `NetworkObject` to have any parent. Under this case, you would parent to the root of the scene hierarchy (i.e. setting the `transform.parent` to `null`).

:::info
The `NetworkObject` requirement is primarily for identification purposes (i.e. knowing which GameObject's transform we are going to parent under).
:::
You can only parent a NetworkObject under another NetworkObject. The only exception is if you don't want the NetworkObject to have a parent. In this case, you would can remove the NetworkObject's parent by invoking `NetworkObject.TryRemoveParent`. If this operation succeeds, the the parent of the child will be set to `null` (root of the scene hierarchy).

### Only Spawned NetworkObjects Can Be Parented
A `NetworkObject` can only be parented if it is spawned and can only be parented under another spawned `NetworkObject`. This also means that `NetworkObject` parenting can only occur during a network session (netcode enabled game session). Think of `NetworkObject` parenting as a netcode event. In order for it to happen, you must have, at very minimum, a server or host instance started and listening.

### Invalid `NetworkObject` Parenting Will Be Reverted
If an invalid/unsupported `NetworkObject` parenting happens, Netcode will immediately revert it back to its original parenting status.
### Invalid Parenting Actions Are Reverted
If an invalid/unsupported `NetworkObject` parenting action occurs, the attempted parenting action will be reverted back to the `NetworkObject`'s original parenting state.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NGO immediately reverts invalid or unsupported NetworkObject parenting to the original parenting status.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is simpler, less to read, and conveys the same point.
I like it!

**For example:**
If you had a `NetworkObject` who's current parent was root and tried to parent it in an invalid way (i.e. under a GameObject without a `NetworkObject` component) then a warning message would be logged and the `NetworkObject` would revert back to having root as its parent.

### In-scene NetworkObject parenting of players
In-scene placed `NetworkObject` parenting of players requires the client to be synchronized first. Since a server can only perform parenting related actions, the server must have already received the `NetworkSceneManager` generated `SceneEventType.SynchronizeComplete` message before the server can parent the client's player `NetworkObject`.
:::info
For more information, see the "[Real World In-scene NetworkObject Parenting of Players Solution](inscene_parenting_player.md)".
### In-scene Object Parenting and Player Objects
If you plan on parenting in-scene placed `NetworkObject`s with a player `NetworkObject` when it is initially spawned, you need to wait until the client has finished synchronizing with the server first. Since parenting can only be performed on the server side, you should perform the parenting action only when the server has received the `NetworkSceneManager` generated `SceneEventType.SynchronizeComplete` message from the client that owns the player `NetworkObject` to be parented (as a child or parent).
:::info For More Information
- [Real World In-scene NetworkObject Parenting of Players Solution](inscene_parenting_player.md) <br />
- [Scene Event Notifications](../basics/scenemanagement/scene-events#scene-event-notifications) <br />
- [In-Scene NetworkObjects](../basics/scenemanagement/inscene-placed-networkobjects.md)
:::

### WorldPositionStays usage
When using the `NetworkObject.TrySetParent` or `NetworkObject.TryRemoveParent` methods, the `WorldPositionStays` parameter is synchronized with currently connected and late joining clients. When removing a child from its parent, you should use the same `WorldPositionStays` value that was used to parent the child. More specifically when `WorldPositionStays` is set to false this applies, but if you are using the defalt value of `true` then this is not required (because it is the default).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using the NetworkObject.TrySetParent or NetworkObject.TryRemoveParent methods, the WorldPositionStays parameter is synchronized with currently connected and late-joining clients. You should use the same WorldPositionStays value that was used to parent the child when removing a child from its parent. More specifically, when WorldPositionStays is set to false, this applies, but if you are using the default value of true, then this is not required (because it is the default).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... that adjustment in the middle has a better flow to it.


When the `WorldPositionStays` parameter in `NetworkObject.TrySetParent` is the default value of `true`, this will preserve the world space values of the child `NetworkObject` relative to the parent. However, sometimes you might want to only preserve the local space values (i.e. pick up an object that only has some initial changes to the child's transform when parented). Through a combination of `NetworkObject.TrySetParent` and `NetworkBehaviour.OnNetworkObjectParentChanged` you can accomplish this without the need for a `NetworkTransform`. To better understand how this works, it is important to understand the order of operations for both of these two methods:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the WorldPositionStays parameter in NetworkObject.TrySetParent is the default value of true, NGO preserves the world space values of the child NetworkObject relative to the parent. However, sometimes you might want only to preserve the local space values (that is, pick up an object that only has some initial changes to the child's transform when parented). You can accomplish this without needing a NetworkTransform through a combination of NetworkObject.TrySetParent and NetworkBehaviour.OnNetworkObjectParentChanged. To better understand how this works, it is important to understand the order of operations for both of these two methods:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are allowed to use NGO?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question - one I've been wondering, too. In the gaming service documentation (e.g., Relay), we refer to Netcode for GameObjects as NGO (after introducing the acronym). Maybe that's not how we want to refer to it, though. Especially since we called it "netcode" everywhere else, this confused me a bit when I first started working with netcode.

Do you know who might be able to say authoritatively what we should and shouldn't use to refer to netcode?


**Server-Side**
- `NetworkObject.TrySetParent` invokes `NetworkBehaviour.OnNetworkObjectParentChanged`
- You can make adjustments to the child's position, rotation, and scale in the overridden `OnNetworkObjectParentChanged` method.
- The ParentSyncMessage is generated, the transform values are added to the message, and the message is then sent.
- ParentSyncMessage includes the child's position, rotation, and scale
- Currently connected or late joining clients will be synchronized with the parenting and the child's associated transform values

**When to use a NetworkTransform** <br />
If you plan on the child `NetowrkObject` moving around, rotating, or scaling independently (when parented or not) then you will still want to use a NetworkTransform.
If you only plan on making a one time adjustment to the child `NetworkObject`'s transform when parented or having a parent removed, then the child does not need a `NetworkTransform` component.

:::info For More Information
- [Learn More About WorldPositionStays](https://docs.unity3d.com/ScriptReference/Transform.SetParent.html)
:::

### Network Prefabs, Parenting, and NetworkTransforms
Since the `NetworkTransform` component synchronizes the transform of a `GameObject` (with a `NetworkObject` component attached to it), it can become tricky to understand the parent-child transform relationship and how that translates when synchronizing late joining clients. Currently, a network prefab can only have one `NetworkObject` component within on the root `GameObject` of the prefab. However, you can have a complex hierarchy of `GameObject`s nested under the root `GameObjet` and each child `GameObject` can have a `NetworkBehaviour` attached to it. Since a `NetworkTransform` synchronizes the transform of the `GameObject` it is attached to, you might be tempted to setup a network prefab like this:

```
Network Prefab Root (GameObject with NetworkObject and NetworkTransform components attached to it)
├─ Child #1 (GameObject with NetworkTransform component attached to it)
└─ Child #2 (GameObject with NetworkTransform component attached to it)
```
While this will not give you any warnings and, depending upon the transform settings of Child #1 & #2, it might appear to work properly (i.e. synchronizes clients, etc.), it is important to understand how the child `GameObject`, with no `NetworkObject` component attached to it, and parent `GameObject`, that does have a `NetworkObject` component attached to it, will be synchronized when a client connects to an already in-progress network session (i.e. late joins or late joining client). If Child #1 or Child #2 have had changes to their respective `GameObject`'s transform prior to a client joining, then upon a client late joining the two child `GameObject`'s transforms will not get synchronized during the initial synchronization period because they do not have `NetworkObject` components attached to them:

```
Network Prefab Root (Late joining client is synchronized with `GameObject`'s current transform state)
├─ Child #1 (Late joining client *is not synchronized* with `GameObject`'s current transform state)
└─ Child #2 (Late joining client *is not synchronized* with `GameObject`'s current transform state)
```
This *is important* to understand because the `NetworkTransform` component initializes itself, during `NetworkTransform.OnNetworkSpawn`, with the `GameObject`'s current transform state. Just below, in the parenting examples, we provide you with some valid and invalid parenting rules. As such, you should take these rules into consideration when using `NetworkTransform` components if you plan on using a complex parent-child hierarchy and should make sure to organize your project's assets where any children that have `NetworkTransform` components attached to them also have `NetworkObject` components attached to them to avoid late-joining client synchronization issues.

## Parenting Examples

### Simple Example:
Expand Down Expand Up @@ -157,4 +183,10 @@ Vehicle (GameObject->NetworkObject)
├─Seat1 (GameObject->NetworkObject)
│ └─Player (GameObject->NetworkObject)
└─Seat2 (GameObject->NetworkObject)
```
```

:::note Optional Auto Synchronized Parenting
The Auto Object Parent Sync property of NetworkObject, enabled by default, allows you to disable automatic parent change synchronization in the event you want to implement your own parenting solution for one or more NetworkObjects.
:::