diff --git a/Tools/scripts/Utils/general_utils.py b/Tools/scripts/Utils/general_utils.py index 4f1a025357..6d6255129b 100644 --- a/Tools/scripts/Utils/general_utils.py +++ b/Tools/scripts/Utils/general_utils.py @@ -1,4 +1,4 @@ -"""Helper class for common operations.""" +"""Helper class for common operations.""" #!/usr/bin/env python3 import json import os diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 4f2058e5d4..5cd2ef536a 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added +- Added NetworkRigidbody documentation section. (#3664) - Clicking on the Help icon in the inspector will now redirect to the relevant documentation. (#3663) - Added a `Set` function onto `NetworkList` that takes an optional parameter that forces an update to be processed even if the current value is equal to the previous value. (#3690) @@ -17,6 +18,11 @@ Additional documentation and release notes are available at [Multiplayer Documen - Improved performance of the NetworkVariable. (#3683) - Improved performance around the NetworkBehaviour component. (#3687) +- Changed NetworkTransform now synchronizes `NetworkTransform.SwitchTransformSpaceWhenParented` when it is updated by the motion model authority. (#3664) +- Changed when NetworkObjects pending to be shown to clients can now occur on partial network ticks. If any pending NetworkObjects pending to be shown to clients happens to be ready on a new network tick they still are shown after network variable deltas have been processed. (#3664) +- Changed the default `NetworkDelivery` used by all messages is now reliable fragmented sequenced with the exception of named, unnamed, and any messages sent with a user specified network delivery type. This assures certain order of operations to be preserved when same call-stack changes are applied to a newly spawned, authority side, NetworkObject. (#3664) +- Changed NetworkTransform documentation to better reflect the Teleport methods intended usage along with updates to NetworkObject and physics areas of the documentation. (#3664) + ### Deprecated @@ -27,6 +33,9 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed - Made a variety of small performance improvements. (#3683) +- Fixed NetworkTransform state synchronization issue when `NetworkTransform.SwitchTransformSpaceWhenParented` is enabled and the associated NetworkObject is parented multiple times in a single frame or within a couple of frames. (#3664) +- Fixed issue when spawning, parenting, and immediately re-parenting. (#3664) + ### Security diff --git a/com.unity.netcode.gameobjects/Documentation~/TableOfContents.md b/com.unity.netcode.gameobjects/Documentation~/TableOfContents.md index a3c0dda213..1abcc96e62 100644 --- a/com.unity.netcode.gameobjects/Documentation~/TableOfContents.md +++ b/com.unity.netcode.gameobjects/Documentation~/TableOfContents.md @@ -31,8 +31,9 @@ * [AttachableNode](components/helper/attachablenode.md) * [ComponentController](components/helper/componentcontroller.md) * [NetworkAnimator](components/helper/networkanimator.md) + * [NetworkRigidbody](components/helper/networkrigidbody.md) * [NetworkTransform](components/helper/networktransform.md) - * [Physics](advanced-topics/physics.md) + * [Physics](advanced-topics/physics.md) * [Ownership and authority](ownership-authority.md) * [Understanding ownership and authority](basics/ownership.md) * [Ownership race conditions](basics/race-conditions.md) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/physics.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/physics.md index b2b47e6926..8a309cca24 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/physics.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/physics.md @@ -1,36 +1,12 @@ # Physics -There are many different ways to manage physics simulation in multiplayer games. Netcode for GameObjects (Netcode) has a built in approach which allows for server-authoritative physics where the physics simulation only runs on the server. To enable network physics, add a NetworkRigidBody component to your object. - -## NetworkRigidbody - -NetworkRigidbody is a component that sets the Rigidbody of the GameObject into kinematic mode on all non-authoritative instances (except the instance that has authority). Authority is determined by the NetworkTransform component (required) attached to the same GameObject as the NetworkRigidbody. Whether the NetworkTransform is server authoritative (default) or owner authoritative, the NetworkRigidBody authority model will mirror it. That way, the physics simulation runs on the authoritative instance, and the resulting positions synchronize on the non-authoritative instances, each with their RigidBody being Kinematic, without any interference. - -To use NetworkRigidbody, add a Rigidbody, NetworkTransform, and NetworkRigidbody component to your NetworkObject. - -Some collision events aren't fired when using NetworkRigidBody. -- On the `server`, all collision and trigger events (such as `OnCollisionEnter`) fire as expected and you can access (and change) values of the `Rigidbody` (such as velocity). -- On the `clients`, the `Rigidbody` is kinematic. Trigger events still fire but collision events won't fire when colliding with other networked `Rigidbody` instances. - -> [!NOTE] -> If there's a need for a gameplay event to happen on a collision, you can listen to `OnCollisionEnter` function on the server and synchronize the event via `Rpc(SendTo.ClientsAndHost)` to all clients. - -## NetworkRigidbody2D - -`NetworkRigidbody2D` works in the same way as NetworkRigidbody but for 2D physics (`Rigidbody2D`) instead. - -## NetworkRigidbody and ClientNetworkTransform - -You can use NetworkRigidbody with the [`ClientNetworkTransform`](../components/helper/networktransform.md) package sample to allow the owner client of a NetworkObject to move it authoritatively. In this mode, collisions only result in realistic dynamic collisions if the object is colliding with other NetworkObjects (owned by the same client). - -> [!NOTE] -> Add the ClientNetworkTransform component to your GameObject first. Otherwise the NetworkRigidbody automatically adds a regular NetworkTransform. +There are many different ways to manage physics simulation in multiplayer games. Netcode for GameObjects has a built in approach that allows for server-authoritative physics where the physics simulation only runs on the server. To enable network physics, add a [NetworkRigidBody component](../components/helper/networkrigidbody.md) to your object. ## Physics and latency -A common issue with physics in multiplayer games is lag and how objects update on basically different timelines. For example, a player would be on a timeline that's offset by the network latency relative to your server's objects. One way to prepare for this is to test your game with artificial lag. You might catch some weird delayed collisions that would otherwise make it into production. +A common issue with physics in multiplayer games is lag and how objects update on different timelines. For example, players are often on a timeline that's offset by the network latency relative to your server's objects. You can compensate for this by testing your game with artificial lag to catch any unexpected behavior. -The best way to address the issue of physics latency is to create a custom NetworkTransform with a custom physics-based interpolator. You can also use the [Network Simulator tool](https://docs.unity3d.com/Packages/com.unity.multiplayer.tools@latest?subfolder=/manual/network-simulator) to spot issues with latency. +The best way to address the issue of physics latency is to create a custom [NetworkTransform](../components/helper/networktransform.md) with a custom physics-based interpolator. You can also use the [Network Simulator tool](https://docs.unity3d.com/Packages/com.unity.multiplayer.tools@latest?subfolder=/manual/network-simulator) to spot issues with latency. ## Message processing vs. applying changes to state (timing considerations) @@ -47,12 +23,432 @@ When handling the synchronization of changes to certain physics properties, it's From this list of update stages, the `EarlyUpdate` and `FixedUpdate` stages have the most impact on NetworkVariableDeltaMessage and RpcMessages processing. Inbound messages are processed during the `EarlyUpdate` stage, which means that Rpc methods and NetworkVariable.OnValueChanged callbacks are invoked at that point in time during any given frame. Taking this into consideration, there are certain scenarios where making changes to a Rigidbody could yield undesirable results. -### Rigidbody interpolation example +## Parenting and Rigidbody components + +Since PhysX has no concept of local space, it can be difficult to synchronize two Rigidbodies. Netcode for GameObjects provides two methods for handling this: + +- Use a [Joint](https://docs.unity3d.com/Documentation/ScriptReference/Joint.html), although this is the more complicated option. + - The [Social Hub demo](https://github.com/Unity-Technologies/com.unity.multiplayer.samples.bitesize/tree/main/Basic/DistributedAuthoritySocialHub) project provides an example of using a `FixedJoint`. +- Use an [AttachableBehaviour component](../components/helper/attachablebehaviour.md) to handle this for you. + - Using an AttachableBehaviour component requires some initial prefab hierarchy organization, but generally produces faster and more consistent results. However, it doesn't cover all physics-based parenting scenarios (but does cover most of them). + +## Using AttachableBehaviour or Joint + +The implementation of physics in a networked project differs from a single player project. This is especially true when you're using NetworkTransform and NetworkRigidbody components with [`NetworkRigidbody.UseRigidBodyForMotion`](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.Components.NetworkRigidbodyBase.html#Unity_Netcode_Components_NetworkRigidbodyBase_UseRigidBodyForMotion) enabled. Deciding whether to use a Joint or an AttachableBehaviour component depends on your project's requirements. + +For example, if you want to create world items that players can pick up, you may have the following requirements: + +- Each world item should be impacted by physics whether picked up or not. +- When not picked up, the world item acts like a normal physics object. +- When picked up, it should: + - Add to the player's overall mass. + - Extend the collision boundary of the player. + - This requires the picked up item to ignore the item's colliders but cause the player's Rigidbody to react (collide) based on any interactions the item might have with other physics world objects. +- The implementation should be modular and easy to customize by both level designers and scripting programmers. + +In most cases, it's recommended to [use an AttachableBehaviour component](#using-attachablebehaviour-for-parenting-physics-objects), unless you have specific requirements for your child physics object that necessitate the use of a Joint, such as: + +- Does your child physics object need to interact with other child physics objects? + - Do the interactions involve collisions between the children under the parent? + - Does each child, under the same parent, require having physics-driven velocities independent of, but relative to, the parent? + - Were you planning on using a physics joint (like a [SpringJoint](https://docs.unity3d.com/Documentation/Manual/class-SpringJoint.html)) anyway? + +> [!NOTE] +> Netcode for GameObjects uses the kinematic feature of Unity's Rigidbody and Rigidbody2d to dictate who has physics authority. The authority is non-kinematic which allows physics to impact the object's velocities, collide with other bodies, and have various forces applied (frictional or otherwise). Non-authority instances are kinematic and synchronize the motion of the authority's non-kinematic body via [NetworkTransform](../components/helper/networktransform.md) or writing your own custom transform synchronizing NetworkBehaviour based script. + +### Using AttachableBehaviour for parenting physics objects + +> [!NOTE] +> If you haven't already done so, it's recommended that you refer to the [AttachableBehaviour](../components/helper/attachablebehaviour.md) documentation to better understand the attaching process before proceeding. + +You should use an AttachableBehaviour component if your physics object doesn't require any child object interactions or independent physics-driven velocities relative to the player's motion. This will provide you with the functionality required to handle parenting only a portion of an object underneath another physics object, including: + +- Allowing the world item to have physics applied when picked up or placed in the world somewhere. +- Extending the player's collision boundary and, if the collider has a physics material applied, it will be used when it collides with other non-kinematic bodies. + - A Rigidbody updates its known colliders when an object is parented underneath it. + +Starting with the [AttachableBehaviour](../components/helper/attachablebehaviour.md) world item diagram: + +![image](../images/attachable/AttachablePhysics_BaselineDiagram.png) + +Making some finalizations on the components you would use might initially look something like this: + +![image](../images/attachable/AttachablePhysics_FirstPassDiagram.png) + +The world item has been further defined by including the following components: + +- On the root prefab GameObject: + - Added a Rigidbody + - Added a NetworkTransform + - Added a NetworkRigidbody +- On the AttachedView GameObject: + - Added a Collider + +The logical flow is: + +- When the world item is not picked up, the MeshRenderer and Collider are disabled on the AttachedView. +- When the world item is picked up (attached), the previously mentioned AttachedView's disabled components are enabled while the MeshRenderer and Collider are disabled on the WorldView. + +The next step is to determine what kind of adjustments you might want to make on your player prefab. Relative to the [AttachableBehaviour player prefab diagram](../components/helper/attachablebehaviour.md#player), your end result might look something close to this: + +![image](../images/attachable/AttachablePhysics_PlayerFirstPass.png) + +Where your project doesn't require a left or right hand position but just a single location to attach your items (AttachPoint) which has an [AttachableNode](../components/helper/attachablenode.md) component. In cases where there's no requirement to independently move the item, it makes more sense to let the animation and player's motion drive the position of the item at any given moment since both are already synchronized between instances. + +> [!NOTE] +> By adding a NetworkTransform that synchronizes in local space to the attach point you can introduce a smooth transition to picking something up by teleporting the AttachPoint, in local space, to the location of the item being picked up. You can get the local space player-relative position by performing an inverse transform point by using the player's transform to transform the world space position of the item being picked up. A script would be required to handle the motion of the item to the player. + +### Rigidbody and nested child colliders + +Below is a screenshot of a prototype world item that, when the player (capsule) runs over the item a collider trigger invokes the `OnTriggerEnter` callback that attaches the AttachedView to an AttachableNode. Prior to triggering the attach event, they are viewed as two unique non-kinematic physics objects: + +![image](../images/attachable/CombinedCollidersBefore.png) + +However, once the item is picked up and the AttachedView is parented under the AttachPoint, the player's Rigidbody starts including the AttachedView's collider and (if set) the physics material assigned to the collider in its physics updates: + +![image](../images/attachable/CombinedCollidersAfter.png) + +When moving the player around, if there is another physics object or static collider (world geometry) that impacts the collider on the now parented AttachedView, if using the physics debugger you can see that the player's Rigidbody is detecting the collision: + +![image](../images/attachable/CombinedCollidersCollision.png) + +It's this core mechanic that's leveraged when using the AttachableBehaviour approach to parenting physics objects under physics objects and removes the more complex (to synchronize) physics Joint approach. + +### Combining mass + +The final step to complete the world item feature is to address the last three requirements: + +- Write a script to make sure the parent WorldItem stays in place when AttachView is attached to a player. + - This requires knowing when the item is being attached and detached. +- Write a script to combine the world item's mass with the player's mass. + - This too requires knowing when the item is being attached and detached. +- Write a script that handles detecting player entering the trigger collider to parent the object. + +The high level logical flow would look something like this: + +![image](../images/attachable/WorldItemTogether-1.png) + +Where: + +- AttachTrigger: Derives from NetworkBehaviour, this class handles detecting a player within a pre-defined pick up collider configured as a trigger. +- AttachableLogic: Derives from AttachableBehaviour to leverage from the virtual method `AttachableBehaviour.OnAttachStateChanged` that's invoked when the attachable is attaching, attached, detaching, and detached. +- AttachableNodeLogic: Derives from AttachableNode to leverage from the virtual method `AttachableNode.OnOnDetached` that's invoked when the attachable is detached from the player. + +From the above diagram, you can see that as the player's collider moves into the WorldItem's collider configured as a trigger it notifies the AttachableLogic which, in turn, attaches the AttachedView to the AttachableNodeLogic. + +With the above additional modifications, the WorldItem looks like this: + +![image](../images/attachable/AttachablePhysics_FinalPassDiagram.png) + +- The AttachTrigger is added to handle the trigger event. + - This requires another collider placed on the WorldView so while the item is not picked up it will trigger when the player's collider enters the Collider - Trigger. When attached, the MeshRenderer, Collider, and Collider - Trigger are all disabled. +- The AttachableLogic takes the place of the first pass AttachableBehaviour. + +The Player requires a minor adjustment: + +![image](../images/attachable/AttachablePhysics_PlayerFinalPass.png) + +- The AttachableNode is updated to the new derived class AttachableLogic. + +### Scripts + +For the functionality described above, you would need to implement something like the following scripts: + +#### AttachTrigger + +A relatively simple script that includes a trigger delay to ensure if the object is dropped it doesn't immediately re-attach itself to the player. + +```c# +using Unity.Netcode; +using UnityEngine; + +/// +/// Placed on the world item, this will attempt to attach the AttachedView to the +/// player's AttachableNode. +/// +public class AttachTrigger : NetworkBehaviour +{ + [Tooltip("The amount of time to wait before allowing the same owner to re-trigger this instance")] + public float SameOwnerDelay = 0.5f; + private float m_LastTriggerTime = 0.0f; + private AttachableLogic m_AttachableLogic; + private GameObject m_LastPlayerToAttach; + + private void Awake() + { + // Find the AttachableBehaviour + m_AttachableLogic = transform.parent.GetComponentInChildren(); + } + + /// + /// Used to help prevent from an item re-attaching when dropped by the player + /// + public void SetLastUpdateTime() + { + var previousLast = m_LastTriggerTime; + m_LastTriggerTime = Time.realtimeSinceStartup + SameOwnerDelay; + } + + + private void OnTriggerEnter(Collider other) + { + if(!enabled || !m_AttachableLogic) + { + return; + } + + // Don't retrigger immediately to avoid picking up the object as we drop it. + if (other.gameObject == m_LastPlayerToAttach && m_LastTriggerTime > Time.realtimeSinceStartup) + { + return; + } + + // Attach the item to the player + if (m_AttachableLogic.Triggered(other)) + { + m_LastPlayerToAttach = other.gameObject; + SetLastUpdateTime(); + } + } +} +``` + +#### AttachableLogic + +This script handles making adjustments to the WorldItem's Rigidbody. When it's attaching, the WorldItem's gravity is disabled and when detaching gravity is enabled (to keep the item from endlessly falling). Also note that it zeros out the velocities of the WorldItem to ensure in place while AttachedView is attached and that it does not have any additional velocity when detached (the `Throw` method handles applying a specific force to the object when it is dropped). + +```c# +using Unity.Netcode; +using Unity.Netcode.Components; +using UnityEngine; + +public class AttachableLogic : AttachableBehaviour +{ + public Rigidbody Rigidbody => m_InternalRigidbody; + private Rigidbody m_InternalRigidbody; + private NetworkTransform m_InternalNetworkTransform; + + private TagHandle m_PlayerTag; + + protected override void Awake() + { + base.Awake(); + // Get the world item's Rigidbody + m_InternalRigidbody = transform.root.GetComponent(); + // Get the world item's NetworkTransform + m_InternalNetworkTransform = transform.root.GetComponent(); + // Use tags to filter what triggers the parenting + m_PlayerTag = TagHandle.GetExistingTag("PlayerTag"); + } + + /// + /// Invoked by + /// + /// the collider that caused the trigger event. + /// + public bool Triggered(Collider other) + { + // Don't trigger if the world item is not spawned, is attached or is being attached, + // or something other than the player caused the trigger event. + if (!IsSpawned || m_AttachState != AttachState.Detached || !other.CompareTag(m_PlayerTag)) + { + return false; + } + + // We can only attach to an AttachableNode. Make sure we can find at least one AttachableNode. + var attachableNode = other.gameObject.GetComponentInChildren(); + + // Do not attempt to attach if there is no available AttachableNode, this is not the local player, + // or the player is already carrying something (this could be configured for a specific world item type). + if (!attachableNode || !attachableNode.IsLocalPlayer || attachableNode.HasAttachments) + { + return false; + } + + // If using a distributed authority topology, go ahead and make the local player's client the authority + // (owner) of the world item/ + if (NetworkManager.DistributedAuthorityMode && OwnerClientId != attachableNode.OwnerClientId) + { + NetworkObject.ChangeOwnership(attachableNode.OwnerClientId); + } + + // Attach the object + Attach(attachableNode); + return true; + } + + /// + /// Invoked when the attachable is attaching, attached, detatching, and detatched. + /// + protected override void OnAttachStateChanged(AttachState attachState, AttachableNode attachableNode) + { + if (!HasAuthority || !attachableNode) + { + return; + } + switch (attachState) + { + case AttachState.Detached: + { + // Always get the NetworkObject's transform as it could be parented under another NetworkObject + // Position the item slightly forward, to the right, and up of the player + var newPosition = attachableNode.NetworkObject.transform.position + attachableNode.NetworkObject.transform.forward * 2.0f + attachableNode.NetworkObject.transform.right * 2.0f + attachableNode.transform.root.up * 2.0f; + + // Rotate relative to the player + var newRotation = attachableNode.NetworkObject.transform.rotation; + + if (m_InternalRigidbody) + { + // Assure there is no existing velocities + m_InternalRigidbody.linearVelocity = Vector3.zero; + m_InternalRigidbody.angularVelocity = Vector3.zero; + // Prepare Rigidbody for being in "world view mode". + if (m_InternalRigidbody.IsSleeping()) + { + m_InternalRigidbody.WakeUp(); + } + // Re-enabled gravity + m_InternalRigidbody.useGravity = true; + } + + // Re-position the world item to the current location of the AttachedView + m_InternalNetworkTransform.SetState(newPosition, newRotation, teleportDisabled: false); + break; + } + case AttachState.Attaching: + { + if (m_InternalRigidbody) + { + // Disabled gravity (i.e. don't fall through the world) + m_InternalRigidbody.useGravity = false; + // Assure all velocities are zeroed out + m_InternalRigidbody.linearVelocity = Vector3.zero; + m_InternalRigidbody.angularVelocity = Vector3.zero; + // Sleep the rigid body. + m_InternalRigidbody.Sleep(); + } + break; + } + } + base.OnAttachStateChanged(attachState, attachableNode); + } + + /// + /// Invoked when the item is detatched to provide some motion to the item. + /// + /// amount of impulse force to apply + public void Throw(Vector3 throwForce) + { + m_InternalRigidbody.AddForce(throwForce, ForceMode.Impulse); + } +} +``` + +#### AttachableNodeLogic + +This script adds the WorldItem's mass to the initial (default) player's mass when picked up and removes it when dropped. It also handles throwing the object (you might be able to throw an object with more or less force depending upon how long you hold the throw key/button down) and handles detaching any attachable for example/testing purposes. It also implements the `INetworkUpdateSystem` interface and registers with the `NetworkUpdateLoop` when `EnableTestMode` is enabled. + +```c# + +using Unity.Netcode; +using Unity.Netcode.Components; +using UnityEngine; + +public class AttachableNodeLogic : AttachableNode, INetworkUpdateSystem +{ + [Tooltip("Relative to the player's forward vector.")] + public Vector3 ThrowForce = new Vector3(0, 15.0f, 20.0f); + public bool EnableTestMode; + + private Rigidbody m_PlayerRigidbody; + private float m_DefaultMass; + + private void Awake() + { + m_PlayerRigidbody = transform.root.GetComponent(); + m_DefaultMass = m_PlayerRigidbody.mass; + } + + /// + /// Detatches anything that is attached + /// + public void DetachAll() + { + if (!HasAttachments) + { + return; + } + + for (int i = m_AttachedBehaviours.Count - 1; i >= 0; i--) + { + var attachableNetworkObject = m_AttachedBehaviours[i].NetworkObject; + var attachTrigger = attachableNetworkObject.transform.GetComponentInChildren(); + if (attachTrigger) + { + attachTrigger.SetLastUpdateTime(); + } + m_AttachedBehaviours[i].Detach(); + } + } + + public override void OnNetworkSpawn() + { + if (IsOwner && EnableTestMode) + { + NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); + } + base.OnNetworkSpawn(); + } + + /// + /// Used to register with when is enabled. + /// + public override void OnNetworkDespawn() + { + if (EnableTestMode) + { + NetworkUpdateLoop.UnregisterNetworkUpdate(this, NetworkUpdateStage.Update); + } + base.OnNetworkDespawn(); + } + + public void NetworkUpdate(NetworkUpdateStage updateStage) + { + if (!IsSpawned) + { + return; + } + + // Drop anything picked up + if (Input.GetKeyDown(KeyCode.T) && HasAttachments) + { + DetachAll(); + } + } + + protected override void OnDetached(AttachableBehaviour attachableBehaviour) + { + if (!HasAuthority) + { + return; + } -While NetworkTransform offers interpolation as a way to smooth between delta state updates, it doesn't get applied to the authoritative instance. You can use `Rigidbody.interpolation` for your authoritative instance while maintaining a strict server-authoritative motion model. + // Set the mass back to the default + m_PlayerRigidbody.mass = m_DefaultMass; + var attachableLogic = attachableBehaviour as AttachableLogic; + // Throw the object in a specific direction + attachableLogic.Throw(NetworkObject.transform.right * ThrowForce.x + Vector3.up * ThrowForce.y + NetworkObject.transform.forward * ThrowForce.z); + base.OnDetached(attachableBehaviour); + } -To have a client control their owned objects, you can use either [RPCs](message-system/rpc.md) or [NetworkVariables](../basics/networkvariable.md) on the client-side. However, this often results in the host-client's updates working as expected, but with slight jitter when a client sends updates. You might be scanning for key or device input during the `Update` to `LateUpdate` stages. Any input from the host player is applied after the `FixedUpdate` stage (i.e. physics simulation for the frame has already run), but input from client players is sent via a message and processed, with a half RTT delay, on the host side (or processed 1 network tick + half RTT if using NetworkVariables). Because of when messages are processed, client input updates run the risk of being processed during the `EarlyUpdate` stage which occurs just before the current frame's `FixedUpdate` stage. + protected override void OnAttached(AttachableBehaviour attachableBehaviour) + { + var attachableLogic = attachableBehaviour as AttachableLogic; -To avoid this kind of scenario, it's recommended that you apply any changes received via messages to a Rigidbody _after_ the FixedUpdate has run for the current frame. If you [look at how NetworkTransform handles its changes to transform state](https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/blob/a2c6f7da5be5af077427eef9c1598fa741585b82/com.unity.netcode.gameobjects/Components/NetworkTransform.cs#L3028), you can see that state updates are applied during the `Update` stage, but are received during the `EarlyUpdate` stage. Following this kind of pattern when synchronizing changes to a Rigidbody via messages will help you to avoid unexpected results in your Netcode for GameObjects project. + // Set the mass based off of the default mass plus the attachable's mass + m_PlayerRigidbody.mass = m_DefaultMass + attachableLogic.Rigidbody.mass; -The best way to address the issue of physics latency is to create a custom NetworkTransform with a custom physics-based interpolator. You can also use the [Network Simulator tool](https://docs.unity3d.com/Packages/com.unity.multiplayer.tools@latest?subfolder=/manual/network-simulator) to spot issues with latency. + base.OnAttached(attachableBehaviour); + } +} +``` \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Documentation~/components/core/networkobject.md b/com.unity.netcode.gameobjects/Documentation~/components/core/networkobject.md index a1f96917ef..cf750c6d32 100644 --- a/com.unity.netcode.gameobjects/Documentation~/components/core/networkobject.md +++ b/com.unity.netcode.gameobjects/Documentation~/components/core/networkobject.md @@ -46,6 +46,9 @@ To spawn NetworkObjects with ownership use the following: ```csharp GetComponent().SpawnWithOwnership(clientId); ``` +> [!NOTE] +> When using the `SpawnWithOwnership` method, be aware that any component that has owner-specific checks to perform specific actions won't be invoked on the spawn authority side during the spawn sequence. The spawn authority is the server when using a client-server network topology, and can be any client when using a distributed authority network topology. Using `SpawnWithOwnership` can impact things like [NetworkTransform](../helper/networktransform.md) when using an owner authority motion model, and potentially provide undesired parenting artifacts and/or impact your own scripts if you are planning to have the spawn authority make any further post-spawn adjustments within the same frame. +> To avoid potential issues, it's recommended to use `Spawn`, where the spawn authority starts as the owner throughout the spawn sequence, makes adjustments post-spawn, and then immediately follow with a call to `ChangeOwnership`. To change ownership, use the `ChangeOwnership` method: @@ -58,6 +61,9 @@ To give ownership back to the server use the `RemoveOwnership` method: ```csharp GetComponent().RemoveOwnership(); ``` +> [!NOTE] +> Using `RemoveOwnership` in a distributed authority network topology isn't recommended. + To see if the local client is the owner of a NetworkObject, you can check the [`NetworkBehaviour.IsOwner`](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.NetworkBehaviour.IsOwner.html) property. diff --git a/com.unity.netcode.gameobjects/Documentation~/components/helper/helpercomponents.md b/com.unity.netcode.gameobjects/Documentation~/components/helper/helpercomponents.md index 8f9d13ec9f..64df3f347b 100644 --- a/com.unity.netcode.gameobjects/Documentation~/components/helper/helpercomponents.md +++ b/com.unity.netcode.gameobjects/Documentation~/components/helper/helpercomponents.md @@ -8,5 +8,6 @@ Understand the helper components available to use in your Netcode for GameObject | **[AttachableNode](attachablenode.md)**| Use an AttachableNode component to provide an attachment point for an [AttachableBehaviour](attachablebehaviour.md) component. | | **[ComponentController](componentcontroller.md)**| Use a [ComponentController](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.ComponentController.html) component to enable or disable one or more components depending on the authority state of the ComponentController and have those changes synchronized with non-authority instances. | | **[NetworkAnimator](networkanimator.md)**| The NetworkAnimator component provides you with a fundamental example of how to synchronize animations during a network session. Animation states are synchronized with players joining an existing network session and any client already connected before the animation state changing. | +| **[NetworkRigidbody](networkrigidbody.md)**| NetworkRigidbody is a component that sets the Rigidbody of the GameObject into kinematic mode on all non-authoritative instances. | | **[NetworkTransform](networktransform.md)**| [NetworkTransform](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.Components.NetworkTransform.html) is a concrete class that inherits from [NetworkBehaviour](../core/networkbehaviour.md) and synchronizes [Transform](https://docs.unity3d.com/Manual/class-Transform.html) properties across the network, ensuring that the position, rotation, and scale of a [GameObject](https://docs.unity3d.com/Manual/working-with-gameobjects.html) are replicated to other clients. | | **[Physics](../../advanced-topics/physics.md)**| Netcode for GameObjects has a built in approach which allows for server-authoritative physics where the physics simulation only runs on the server. | diff --git a/com.unity.netcode.gameobjects/Documentation~/components/helper/networkrigidbody.md b/com.unity.netcode.gameobjects/Documentation~/components/helper/networkrigidbody.md new file mode 100644 index 0000000000..8f412f7898 --- /dev/null +++ b/com.unity.netcode.gameobjects/Documentation~/components/helper/networkrigidbody.md @@ -0,0 +1,42 @@ +# NetworkRigidbody + +NetworkRigidbody is a component that sets the Rigidbody of the GameObject into kinematic mode on all non-authoritative instances. Authority is determined by the [NetworkTransform component](networktransform.md) (required) attached to the same GameObject as the NetworkRigidbody. Whether the NetworkTransform is server authoritative (default) or owner authoritative, the NetworkRigidBody authority model will mirror it. That way, the physics simulation runs on the authoritative instance, and the resulting positions synchronize on the non-authoritative instances, each with their RigidBody being kinematic, without any interference. + +## Configure NetworkRigidbody + +![image](../../images/networktransform/NetworkRigidbody-fields.png) + +When looking at a NetworkRigidbody in the Inspector view, there are three exposed values: + +- __Use Rigid Body for Motion__ + - When enabled and using a [NetworkTransform](networktransform.md), the NetworkTransform uses the PhysX position and rotation to synchronize changes during the `FixedUpdate` loop update stage. +- __Auto Update Kinematic State__ + - When enabled, NetworkRigidbody automatically determines whether the current instance should be kinematic or non-kinematic. + - For custom solutions, you can opt to disable this field or derive from [NetworkRigidbodyBase](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@2.5/api/Unity.Netcode.Components.NetworkRigidbodyBase.html) and design your own custom networked Rigidbody handler. +- __Auto Set Kinematic On Despawn__ + - When enabled, this option makes the rigid body kinematic when despawned (which can be useful for [object pools](../../advanced-topics/object-pooling.md)). + +Some collision events aren't fired when using NetworkRigidBody: + +- On the server, all collision and trigger events (such as `OnCollisionEnter`) fire as expected and you can access (and change) values of the `Rigidbody` (such as velocity). +- On clients, Rigidbody is kinematic. Trigger events still fire but collision events won't fire when colliding with other networked Rigidbody instances if your project's physics settings is set to the default contact pairs. +![image](../../images/networktransform/ProjectPhysicsSettings.png) + +- You can adjust the __Contact Pairs Mode__ to use kinematic and non-kinematic by setting it to __Enable All Contact Pairs__.![image](../../images/networktransform/ProjectPhysicsSettings2.png) + +> [!NOTE] +> If there's a need for a gameplay event to happen on a collision, you can listen to the `OnCollisionEnter` function on the server and synchronize the event via `Rpc(SendTo.Everyone)` to all clients. If you plan on handling many collisions, then it's recommended to use the [RigidbodyContactEventManager component](https://docs.unity3d.com/Packages/com.unity.netcode.gameobjects@latest?subfolder=/api/Unity.Netcode.Components.RigidbodyContactEventManager.html) to handle collision checking during a job (`OnCollisionenter` can become expensive from a processing perspective if you have enough instances colliding). + +### NetworkRigidbody2D + +NetworkRigidbody2D works in the same way as NetworkRigidbody but for 2D physics (Rigidbody2D) instead. + +## Rigidbody interpolation example + +While NetworkTransform offers [interpolation](../../learn/clientside-interpolation.md) as a way to smooth between delta state updates, it doesn't get applied to the authoritative instance. You can use `Rigidbody.interpolation` for your authoritative instance while maintaining a strict server-authoritative motion model. + +To have a client control their owned objects, you can use either [RPCs](../../advanced-topics/message-system/rpc.md) or [NetworkVariables](../../basics/networkvariable.md) on the client-side. However, this often results in the host-client's updates working as expected, but with slight jitter when a client sends updates. You might be scanning for key or device input during the `Update` to `LateUpdate` stages. Any input from the host player is applied after the `FixedUpdate` stage (such as physics simulation for the frame has already run), but input from client players is sent via a message and processed, with a half RTT delay, on the host side (or processed 1 network tick + half RTT if using NetworkVariables). Because of when messages are processed, client input updates run the risk of being processed during the `EarlyUpdate` stage which occurs just before the current frame's `FixedUpdate` stage. + +To avoid this kind of scenario, it's recommended that you apply any changes received via messages to a Rigidbody after the `FixedUpdate` has run for the current frame. If you [refer to how NetworkTransform handles its changes to transform state](https://github.com/Unity-Technologies/com.unity.netcode.gameobjects/blob/a2c6f7da5be5af077427eef9c1598fa741585b82/com.unity.netcode.gameobjects/Components/NetworkTransform.cs#L3028), you can see that state updates are applied during the `Update` stage, but are received during the `EarlyUpdate` stage. Following this kind of pattern when synchronizing changes to a Rigidbody via messages will help you to avoid unexpected results in your Netcode for GameObjects project. + +The best way to address the issue of physics latency is to create a custom NetworkTransform with a custom physics-based interpolator. You can also use the [Network Simulator tool](https://docs.unity3d.com/Packages/com.unity.multiplayer.tools@latest?subfolder=/manual/network-simulator) to spot issues with latency. \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Documentation~/components/helper/networktransform.md b/com.unity.netcode.gameobjects/Documentation~/components/helper/networktransform.md index 0ad8366692..b0bca748fe 100644 --- a/com.unity.netcode.gameobjects/Documentation~/components/helper/networktransform.md +++ b/com.unity.netcode.gameobjects/Documentation~/components/helper/networktransform.md @@ -93,9 +93,8 @@ Fortunately, the authority of the NetworkTransform instance can make changes to When disabling an axis to be synchronized for performance purposes, you should always consider that NetworkTransform won't send updates as long as the axis in question doesn't have a change that exceeds its [threshold](#thresholds) value. So, taking the scale example into consideration, it can be simpler to leave those axes enabled if you only ever plan on changing them once or twice because the CPU cost for checking that change isn't as expensive as the serialization and state update sending process. The associated axis threshold values can make the biggest impact on frequency of sending state updates that, in turn, will reduce the number of state updates sent per second at the cost of losing some motion resolution. -:::info -The __Axis to Synchronize__ properties that determine which axes are synchronized don't get synchronized with other instances. If you change ownership and there have been any adjustments to these values that are different from the network prefab's original settings, you'll need to keep those values synchronized and apply them upon the notification that ownership has changed. -::: +> [!NOTE] +> The __Axis to Synchronize__ properties that determine which axes are synchronized don't get synchronized with other instances. If you change ownership and there have been any adjustments to these values that are different from the network prefab's original settings, you'll need to keep those values synchronized and apply them upon the notification that ownership has changed. ### Authority @@ -114,9 +113,8 @@ When using a client-server network topology, you have the option of making the s - The most common use for an owner authority motion model is for player prefabs, when you want the player to have a more immediate response to their inputs. - The most common use for a server authority motion model is for things like AI, NPCs, items that can be picked up, and moving world objects (such as elevators, platforms, and doors). -:::info -When mixing authority motion models and using physics, latency will impact how (and when) things collide and requires additional consideration and planning. -::: +> [!NOTE] +> When mixing authority motion models and using physics, latency will impact how (and when) things collide and requires additional consideration and planning. ### Thresholds @@ -144,7 +142,7 @@ When __Tick Sync Children__ is enabled, the top-most parent NetworkTransform aut #### Network conditions to consider -Sometimes network conditions are poor, with packets experiencing latency and potentially packet loss. When NetworkTransform [interpolation](#interpolation) is enabled, packet loss can mean undesirable visual artifacts such as stutter. To try and mitigate these issues, NetworkTransform defaults to sending delta state updates (such as position, rotation, or scale changes) as unreliable sequenced network-delivered messages. This ensures that if one state is lost then the `BufferedLinearInterpolator` can recover easily, because it doesn't have to wait precisely for the next state update and can just lose a small portion of the overall interpolated path. For example, with a `TickRate` setting of 30, you could lose 5 to 10% of the overall state updates over one second and still have a relatively similar interpolated path to that of a perfectly delivered 30 delta state updates generated path. The [__UseUnreliableDeltas__](#use-unreliable-deltas) NetworkTransform property, which defaults to disabled, controls whether you send your delta state updates unreliably or reliably. +Sometimes network conditions are poor, with packets experiencing latency and potentially packet loss. When NetworkTransform [interpolation](#interpolation) is enabled, packet loss can mean undesirable visual artifacts such as stutter. To try and mitigate these issues, NetworkTransform defaults to sending delta state updates (such as position, rotation, or scale changes) as unreliable sequenced network-delivered messages. This ensures that if one state is lost then the `BufferedLinearInterpolator` can recover easily, because it doesn't have to wait precisely for the next state update and can just lose a small portion of the overall interpolated path. For example, with a `TickRate` setting of 30, you could lose 5 to 10% of the overall state updates over one second and still have a relatively similar interpolated path to that of a perfectly delivered 30 delta state updates generated path. The [__UseUnreliableDeltas__](#unreliable-state-updates) NetworkTransform property, which defaults to disabled, controls whether you send your delta state updates unreliably or reliably. Of course, you might wonder what would happen if 5% of the end of a jumping motion were dropped and how NetworkTransform might recover since each state update sent is only based on axial deltas defined by each axis threshold setting. The answer is that there is a small bandwidth penalty for sending standard delta state updates unreliably, full axial frame synchronization, which assures that in the event there is loss each NetworkTransform will be "auto-corrected" once per second. @@ -168,9 +166,8 @@ By default, NetworkTransform synchronizes the Transform of an object in world sp Enabling the __In Local Space__ property on a parented NetworkTransform can improve the synchronization of the transform when the object gets re-parented because the re-parenting won't change the local space Transform of the object (but does change the world space position) and you only need to update motion of the parented NetworkTransform relative to its parent (if the parent is moving and the child has no motion then there are no delta states to detect for the child but the child moves with the parent). -:::info -The authority instance does synchronize changes to the __In Local Space__ property. As such, you can make adjustments to this property on the authoritative side during runtime and the non-authoritative instances will automatically be updated. -::: +> [!NOTE] +> The authority instance does synchronize changes to the __In Local Space__ property. As such, you can make adjustments to this property on the authoritative side during runtime and the non-authoritative instances will automatically be updated. #### Switch Transform Space When Parented @@ -178,7 +175,188 @@ When changing from world space to local space and vice versa, NetworkTransform c This means that non-authority instances could still have state updates pending to be processed when a NetworkObject is parented (or de-parented) and those buffered state values are still expressed as world (or local) space values. Since parenting is not network tick synchronized, the non-authority instances could still have the previous (world or local space) state updates remaining to be processed. This can create a visual "popping" result on the non-authority instance because it has been placed in a different Transform space while processing the previous Transform space state updates. -To resolve this issue, you can enable the __Switch Transform Space When Parented__ configuration property and the NetworkTransform will automatically detect when its NetworkObject has changed parented status and convert the pending states within each respective axis's `BufferedLinearInterpolator` to the appropriate Transform space values. The end result yields a seamless transition between world and local (and vice versa) when parenting. +To resolve this issue, you can enable the __Switch Transform Space When Parented__ configuration property and let NetworkTransform automatically detect when its associated NetworkObject has changed its parented status, automatically switch to local or world space (parented or not parented), and convert the pending interpolator(s) states within each respective axis's `BufferedLinearInterpolator` to the appropriate Transform space values. The end result yields a seamless transition between world and local (and vice versa) space when parenting. This is the recommended way to handle smooth transitions between world and local space when parenting. + +Things to consider when using __Switch Transform Space When Parented__: + +- This property is synchronized by the authority instance. If you disable it on the authority instance then it will synchronize this adjustment on all non-authority instances. +- When using __Switch Transform Space When Parented__, it's best to not make adjustments to the __NetworkTransform.InLocalSpace__ field and let the NetworkTransform handle this for you. + - While you can still change __In Local Space__ directly via script while __Switch Transform Space When Parented__ is enabled, this could impact the end results. It is recommended to not adjust __In Local Space__ when __Switch Transform Space When Parented__ is enabled. + +### Parenting + +NetworkObject parenting can become complex when: + +- You are parenting a NetworkObject while it's [in motion](#spawning-or-in-motion). +- You are parenting a NetworkObject [while spawning](#when-spawning) (depending upon network topology and the desired authority motion model). + +#### Spawning or in motion + +The __Switch Transform Space When Parented__ field is intended to smooth the transition between world and local spaces. This setting is specifically designed to handle converting all of the non-authority instance's currently queued `NetworkTransformState`s to the appropriate transform space. When parenting, the transform space is automatically switched to local (transform) space on the authority instance that will then send this change in the transform space on the next network tick. This feature can also handle multiple parenting actions that occur on the same frame or over a few frames that are all within the normal network tick update period. + +When __Switch Transform Space When Parented__ is enabled, each parenting action will generate a unique transform message that is immediately added to the outbound message queued to preserve the order of operations. This means for each parenting event that occurs, a transform message will be generated. As such, it is recommended to not parent the same object dozens of times within the same frame to keep message traffic minimalized. + + +> [!NOTE] +> __Switch Transform Space When Parented__ is designed to work with the Unity transform's world and local space capabilities. However, when using a Rigidbody component and setting the NetworkRigidbody to use the Rigidbody for motion, the position and rotation values being synchronized are based on the Rigidbody's position and rotation values and not the Unity transform values. The Rigidbody position and rotation values are separate PhysX values that are adjusted during the `FixedUpdate` update loop stage. Because PhysX has no concept of local space, it's not recommended to enable this field when the authority is synchronizing the Rigidbody's position and rotation values. [You can read more about parenting rigid bodies here](../../advanced-topics//physics.md). + +#### When spawning + +If you want to handle parenting during the spawn sequence of a NetworkObject, then it's recommended that you create a custom NetworkTransform that handles parenting before the `base.OnNetworkSpawn` method is invoked. This ensures that any necessary modifications (to parenting and transform adjustments) are applied before the NetworkTransform has initialized. + +```c# +using Unity.Netcode; +using Unity.Netcode.Components; +using UnityEngine; + +public class ParentAndAdjustOffset : NetworkTransform +{ + public GameObject ParentObject; + public Vector3 Offset; + + /// + /// Prior to being initialized in OnNetworkSpawn, + /// is not yet initialized. We can determine who is going to be the motion authority this way. + /// + private bool IsMotionAuthority() + { + return OnIsServerAuthoritative() && !NetworkManager.DistributedAuthorityMode ? IsServer : IsOwner; + } + + public override void OnNetworkSpawn() + { + // Handle parenting and applying offset prior to + // invoking the base OnNetworkSpawn method + if (IsMotionAuthority()) + { + NetworkObject.TrySetParent(ParentObject, false); + if (!SwitchTransformSpaceWhenParented) + { + InLocalSpace = true; + } + transform.localPosition = Offset; + } + // NetworkTransform initializes + base.OnNetworkSpawn(); + } +} +``` + +The method above works under most conditions, but can have unexpected results when using: + +- A client-server network topology. +- An owner authority motion model (when __Authority Mode__ is set to owner). +- You are spawning with ownership. + +Because a client-server network topology requires the server (or host) to spawn the object, while the network prefab is spawning on the server side the authority will already have been applied to the NetworkTransform. To avoid issues with this particular scenario, it's recommended that you spawn the network prefab initially with the server as the motion authority and change ownership to the intended client when finishing the spawn sequence on the server. + +```c# +using Unity.Netcode; +using UnityEngine; + +public class ObjectSpawner : NetworkBehaviour +{ + public NetworkObject ObjectToSpawn; + // Consider the ParentObject as having already been spawned. + public NetworkObject ParentObject; + + public Vector3 Offset; + + public void SpawnObject(ulong ownerId) + { + var instance = Instantiate(ObjectToSpawn.gameObject).GetComponent(); + var parentAndAdjustOffset = GetComponent(); + parentAndAdjustOffset.ParentObject = ParentObject; + parentAndAdjustOffset.Offset = Offset; + // Both of these calls are collapsed into a single CreateObjectMessage + instance.Spawn(); + instance.ChangeOwnership(ownerId); + } +} +``` + +This results in the spawned object running through the spawn sequence with the server as the authority to ensure that any changes are properly applied, and then immediately after invoking the `Spawn` method it changes ownership. The net result of these two scripts generates a single `CreateObjectMessage`. + +When working with an owner authority motion model, you might want to handle all of the parenting on the intended owning client side and it may seem convenient to use `NetworkObject.SpawnWithOwnership`. But this can produce visual anomalies on the host side (if you are using a host vs a server) where the instance on the host side will have an initial spawn position and then after a period of time, driven by the latency between the owning client and the host, the parenting and offset values will be applied to the host-side instance. To further complicate matters, the host then forwards these messages to any other connected client that increases the latency from the moment the object was spawned to the moment it has been parented and an offset applied. + +The messages generated (under this specific scenario) would be: + +- (Server) + - Spawns the object + - One `CreateObjectMessage` is sent to all connected clients. +- (Authority Client) + - Parents the object. + - One `ParentSyncMessage` is sent to the host. + - The host then forwards this to the other clients. + - Applies an offset. + - At the end of the current tick when the parenting and offset was applied, a `NetworkTransformMessage` is generated to update the object's transform values with the offset applied. The delay between the parenting message and this message could be close to the tick frequency (default would be ~33ms). + - Upon receiving this message, the host forwards it to the rest of the clients. + +This can produce visual anomalies because of the time delta between when the object is spawned and when the object is both parented and an offset applied, which is all dependent on the latency between the owning client and the host, as well as the latency between the host and non-owning clients. + +To avoid this kind of timing issue, it's recommended (in a client-server network topology when using an owner authoritative motion model) to spawn with the server as the initial owner and then change ownership afterwards where all of the actions (spawning, parenting, and applying a local space offset) are included in the single `CreateObjectMessage`. It reduces the bandwidth cost per spawn and avoids having to deal with the above latency-driven timing issues. + +#### Other options + +Netcode for GameObjects also has the [AttachableBehaviour](../helper/attachablebehaviour.md) component that provides an alternative way to handle parenting without having to use the traditional NetworkObject parenting approach. You can also use a distributed authority network topology, since that allows clients to spawn objects locally where they can apply modifications (as if they were the host) prior to the `CreateObjectMessage` being sent to all of the other connected clients. + +### Teleport and SetState + +When you want to move an object to a new position without having non-authoritative instances interpolate between the current and new position, you can use either the `NetworkTransform.Teleport` or `NetworkTransform.SetState` methods. Both of these methods effectively do the same thing, but they have different intended use cases. + +#### Intended use cases + +- Invoke on already spawned objects. + - You can invoke these methods during the spawn process, but it's recommended to directly apply the transform settings when spawning. +- Invoking either of these methods consumes state updates for the current tick. + - This means that if you invoke either of these two methods multiple times in a single tick you will only get the current values of the transform. Calls to these two methods do not stack. + +If you need to make multiple teleports in a short period of time, you can create a custom derived NetworkTransform class that notifies the authority when a state update (like a teleport) has been pushed by deriving from `NetworkTransform.OnAuthorityPushTransformState`. This allows you to queue up multiple teleports and apply them once per tick. + +Here is an example of how you could teleport to multiple locations over a short period of time (with one network tick between each teleport): + +```c# +using System.Collections.Generic; +using Unity.Netcode; +using Unity.Netcode.Components; +using UnityEngine; + +public class TeleportMultiplePoints : NetworkTransform +{ + private List m_TeleportPositions = new List(); + + public void SetTeleportPoint(Vector3 point) + { + var teleportInProgress = m_TeleportPositions.Count > 0; + m_TeleportPositions.Add(point); + if (!teleportInProgress) + { + TeleportToNextPoint(); + } + } + + private void TeleportToNextPoint() + { + var position = m_TeleportPositions.First(); + m_TeleportPositions.RemoveAt(0); + SetState(posIn: position, teleportDisabled: false); + } + + /// + /// Invoked only on the authority side when a NetworkTransformState + /// has been pushed (sent). + /// + protected override void OnAuthorityPushTransformState(ref NetworkTransformState networkTransformState) + { + if (networkTransformState.WasTeleported && m_TeleportPositions.Count > 0) + { + TeleportToNextPoint(); + } + base.OnAuthorityPushTransformState(ref networkTransformState); + } +} +``` +The overall idea is that you should only perform one teleport (set state) per tick on an already spawned NetworkObject. ### Interpolation @@ -279,9 +457,8 @@ Quaternion synchronization comes with a price, however. It increases the bandwid ![image](../../images/networktransform/NetworkTransformQuaternionSynch.png) -:::note -The rotation synchronization axis checkboxes are no longer available when __Use Quaternion Synchronization__ is enabled (since synchronizing the quaternion of a transform always updates all rotation axes) and __Use Quaternion Compression__ becomes a visible option. -::: +> [!NOTE] +> The rotation synchronization axis checkboxes are no longer available when __Use Quaternion Synchronization__ is enabled (since synchronizing the quaternion of a transform always updates all rotation axes) and __Use Quaternion Compression__ becomes a visible option. ### Use Quaternion Compression diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_BaselineDiagram.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_BaselineDiagram.png new file mode 100644 index 0000000000..f15d2a60f2 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_BaselineDiagram.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_FinalPassDiagram.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_FinalPassDiagram.png new file mode 100644 index 0000000000..f6a678e514 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_FinalPassDiagram.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_FirstPassDiagram.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_FirstPassDiagram.png new file mode 100644 index 0000000000..69e576ea40 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_FirstPassDiagram.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_PlayerFinalPass.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_PlayerFinalPass.png new file mode 100644 index 0000000000..5b49fac65c Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_PlayerFinalPass.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_PlayerFirstPass.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_PlayerFirstPass.png new file mode 100644 index 0000000000..c21cf29ce5 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/AttachablePhysics_PlayerFirstPass.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersAfter.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersAfter.png new file mode 100644 index 0000000000..aca927e718 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersAfter.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersBefore.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersBefore.png new file mode 100644 index 0000000000..3c0a89c2d5 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersBefore.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersCollision.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersCollision.png new file mode 100644 index 0000000000..fea99df93a Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/CombinedCollidersCollision.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/attachable/WorldItemTogether-1.png b/com.unity.netcode.gameobjects/Documentation~/images/attachable/WorldItemTogether-1.png new file mode 100644 index 0000000000..32ce94ff21 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/attachable/WorldItemTogether-1.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/networktransform/NetworkRigidbody-fields.png b/com.unity.netcode.gameobjects/Documentation~/images/networktransform/NetworkRigidbody-fields.png new file mode 100644 index 0000000000..cc5457f66f Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/networktransform/NetworkRigidbody-fields.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/networktransform/ProjectPhysicsSettings.png b/com.unity.netcode.gameobjects/Documentation~/images/networktransform/ProjectPhysicsSettings.png new file mode 100644 index 0000000000..2657449282 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/networktransform/ProjectPhysicsSettings.png differ diff --git a/com.unity.netcode.gameobjects/Documentation~/images/networktransform/ProjectPhysicsSettings2.png b/com.unity.netcode.gameobjects/Documentation~/images/networktransform/ProjectPhysicsSettings2.png new file mode 100644 index 0000000000..a008245f47 Binary files /dev/null and b/com.unity.netcode.gameobjects/Documentation~/images/networktransform/ProjectPhysicsSettings2.png differ diff --git a/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs index 53d09b782f..5c79b48e81 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs @@ -192,7 +192,11 @@ private void DisplayNetworkTransformProperties() { m_TickSyncChildren.boolValue = true; } - EditorGUILayout.PropertyField(m_InLocalSpaceProperty); + else + { + // Should only be visible when SwitchTransformSpaceWhenParented is disabled. + EditorGUILayout.PropertyField(m_InLocalSpaceProperty); + } if (!networkTransform.HideInterpolateValue) { if (networkTransform.Interpolate) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index f5f2413ed2..419f0d94b0 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -88,7 +88,11 @@ private float GetPrecision() protected internal struct BufferedItem { /// - /// THe item identifier + /// The transform parent for this specific value measurement. + /// + internal Transform MeasurementParent; + /// + /// The item identifier /// public int ItemId; /// @@ -111,6 +115,7 @@ public BufferedItem(T item, double timeSent, int itemId) Item = item; TimeSent = timeSent; ItemId = itemId; + MeasurementParent = default; } /// @@ -124,6 +129,7 @@ public BufferedItem(T item, double timeSent) TimeSent = timeSent; // Generate a unique item id based on the time to the 2nd decimal place ItemId = (int)(timeSent * 100); + MeasurementParent = default; } } @@ -135,6 +141,7 @@ public BufferedItem(T item, double timeSent) /// internal struct CurrentState { + public Transform TargetParent; public BufferedItem? Target; public double StartTime; public double EndTime; @@ -225,7 +232,9 @@ public void Reset(T currentValue) /// internal float MaxInterpolationBound = 3.0f; internal bool EndOfBuffer => m_BufferQueue.Count == 0; + internal bool AutoConvertTransformSpace; internal bool InLocalSpace; + internal Transform Parent; private double m_LastMeasurementAddedTime = 0.0f; private int m_BufferCount; @@ -257,23 +266,61 @@ public void Clear() /// The target value to reset the interpolator to /// The current server time public void ResetTo(T targetValue, double serverTime) + { + ResetTo(null, targetValue, serverTime); + } + + internal void ResetTo(Transform parent, T targetValue, double serverTime) { // Clear the interpolator Clear(); - InternalReset(targetValue, serverTime); + InternalReset(parent, targetValue, serverTime); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ConvertInterpolateStateValues(Transform parent, bool inLocalSpace) + { + InterpolateState.CurrentValue = OnConvertTransformSpace(parent, InterpolateState.CurrentValue, inLocalSpace); + InterpolateState.NextValue = OnConvertTransformSpace(parent, InterpolateState.NextValue, inLocalSpace); + InterpolateState.PreviousValue = OnConvertTransformSpace(parent, InterpolateState.PreviousValue, inLocalSpace); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ConvertTransformSpace(BufferedItem newTarget) + { + if (!AutoConvertTransformSpace) + { + return; + } + if (InterpolateState.TargetParent != newTarget.MeasurementParent) + { + if (InterpolateState.TargetParent != null) + { + // Convert to world space or local space depending upon what our current parent is. + ConvertInterpolateStateValues(InterpolateState.TargetParent, false); + } + + if (newTarget.MeasurementParent != null) + { + // Convert to local space. + ConvertInterpolateStateValues(newTarget.MeasurementParent, true); + } + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InternalReset(T targetValue, double serverTime, bool addMeasurement = true) + private void InternalReset(Transform parent, T targetValue, double serverTime, bool addMeasurement = true) { m_RateOfChange = default; - // Set our initial value - InterpolateState.Reset(targetValue); + var currentValue = targetValue; + // Set our initial value (what we will interpolate from relative to the next state update received) + InterpolateState.Reset(currentValue); + InterpolateState.TargetParent = parent; if (addMeasurement) { // Add the first measurement for our baseline - AddMeasurement(targetValue, serverTime); + AddMeasurement(parent, targetValue, serverTime); } } @@ -319,6 +366,8 @@ private void TryConsumeFromBuffer(double renderTime, double minDeltaTime, double { if (m_BufferQueue.TryDequeue(out BufferedItem target)) { + ConvertTransformSpace(target); + if (!InterpolateState.Target.HasValue) { InterpolateState.Target = target; @@ -344,6 +393,7 @@ private void TryConsumeFromBuffer(double renderTime, double minDeltaTime, double InterpolateState.SetTimeToTarget(Math.Max(target.TimeSent - startTime, minDeltaTime)); InterpolateState.Target = target; } + InterpolateState.TargetParent = target.MeasurementParent; } } else @@ -466,6 +516,7 @@ private void TryConsumeFromBuffer(double renderTime, double serverTime) { if (m_BufferQueue.TryDequeue(out BufferedItem target)) { + ConvertTransformSpace(target); if (!InterpolateState.Target.HasValue) { InterpolateState.Target = target; @@ -488,6 +539,7 @@ private void TryConsumeFromBuffer(double renderTime, double serverTime) InterpolateState.TimeToTargetValue = InterpolateState.EndTime - InterpolateState.StartTime; InterpolateState.Target = target; } + InterpolateState.TargetParent = target.MeasurementParent; } } @@ -514,7 +566,7 @@ public T Update(float deltaTime, double renderTime, double serverTime) { TryConsumeFromBuffer(renderTime, serverTime); // Only interpolate when there is a start and end point and we have not already reached the end value - if (InterpolateState.Target.HasValue && !InterpolateState.TargetReached) + if (!InterpolateState.TargetReached && InterpolateState.Target.HasValue) { // The original BufferedLinearInterpolator lerping script to assure the Smooth Dampening updates do not impact // this specific behavior. @@ -540,7 +592,7 @@ public T Update(float deltaTime, double renderTime, double serverTime) InterpolateState.TargetReached = IsApproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, GetPrecision()); } else // If the target is reached and we have no more state updates, we want to check to see if we need to reset. - if (m_BufferQueue.Count == 0 && InterpolateState.TargetReached) + if (InterpolateState.TargetReached && m_BufferQueue.Count == 0) { // When the delta between the time sent and the current tick latency time-window is greater than the max delta time // plus the minimum delta time (a rough estimate of time to wait before we consider rate of change equal to zero), @@ -587,8 +639,12 @@ internal T UpdateInternal(float deltaTime, NetworkTime serverTime, int ticksAgo /// The time to record for measurement public void AddMeasurement(T newMeasurement, double sentTime) { - m_NbItemsReceivedThisFrame++; + AddMeasurement(null, newMeasurement, sentTime); + } + internal void AddMeasurement(Transform parent, T newMeasurement, double sentTime) + { + m_NbItemsReceivedThisFrame++; // This situation can happen after a game is paused. When starting to receive again, the server will have sent a bunch of messages in the meantime // instead of going through thousands of value updates just to get a big teleport, we're giving up on interpolation and teleporting to the latest value if (m_NbItemsReceivedThisFrame > k_BufferCountLimit) @@ -598,9 +654,12 @@ public void AddMeasurement(T newMeasurement, double sentTime) // Clear the interpolator Clear(); // Reset to the new value but don't automatically add the measurement (prevents recursion) - InternalReset(newMeasurement, sentTime, false); + InternalReset(parent, newMeasurement, sentTime, false); m_LastMeasurementAddedTime = sentTime; - m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime, m_BufferCount); + m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime, m_BufferCount) + { + MeasurementParent = parent, + }; // Next line keeps renderTime above m_StartTimeConsumed. Fixes pause/unpause issues m_BufferQueue.Enqueue(m_LastBufferedItemReceived); } @@ -611,12 +670,37 @@ public void AddMeasurement(T newMeasurement, double sentTime) if (sentTime > m_LastMeasurementAddedTime || m_BufferCount == 0) { m_BufferCount++; - m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime, m_BufferCount); + m_LastBufferedItemReceived = new BufferedItem(newMeasurement, sentTime, m_BufferCount) + { + MeasurementParent = parent, + }; m_BufferQueue.Enqueue(m_LastBufferedItemReceived); m_LastMeasurementAddedTime = sentTime; } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private T GetParentRelativeValue(T currentValue) + { + if (!AutoConvertTransformSpace || InterpolateState.TargetParent == Parent) + { + return currentValue; + } + + // Just convert on the fly until the next state is reached where it will do a full + // conversion when popped from the queue. + if (InterpolateState.TargetParent) + { + currentValue = OnConvertTransformSpace(InterpolateState.TargetParent, currentValue, false); + } + + if (Parent != null) + { + currentValue = OnConvertTransformSpace(Parent, currentValue, true); + } + return currentValue; + } + /// /// Gets latest value from the interpolator. This is updated every update as time goes by. /// @@ -624,7 +708,23 @@ public void AddMeasurement(T newMeasurement, double sentTime) [MethodImpl(MethodImplOptions.AggressiveInlining)] public T GetInterpolatedValue() { - return InterpolateState.CurrentValue; + var currentValue = InterpolateState.CurrentValue; + if (AutoConvertTransformSpace && InterpolateState.TargetParent != Parent) + { + currentValue = GetParentRelativeValue(currentValue); + + // When there are no more states and we have reached our target, + // hijack the last state as if it was submitted by the current + // parent. + if (m_BufferQueue.Count == 0 && (InterpolateState.TargetReached || !InterpolateState.Target.HasValue)) + { + InterpolateState.CurrentValue = currentValue; + InterpolateState.NextValue = currentValue; + InterpolateState.PreviousValue = currentValue; + InterpolateState.TargetParent = Parent; + } + } + return currentValue; } /// diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 47ea011abe..0dd1c0c35b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -61,6 +61,7 @@ public struct NetworkTransformState : INetworkSerializable private const int k_ReliableSequenced = 0x00080000; private const int k_UseUnreliableDeltas = 0x00100000; private const int k_UnreliableFrameSync = 0x00200000; + private const int k_SwitchTransformSpaceWhenParented = 0x0400000; // (Internal Debugging) When set each state update will contain a state identifier private const int k_TrackStateId = 0x10000000; @@ -125,6 +126,19 @@ internal uint BitSet private FastBufferReader m_Reader; private FastBufferWriter m_Writer; + /// + /// When set, non-authority instances will smoothly transition between + /// world and local space. + /// + internal bool SwitchTransformSpaceWhenParented + { + get => GetFlag(k_SwitchTransformSpaceWhenParented); + set + { + SetFlag(value, k_SwitchTransformSpaceWhenParented); + } + } + /// /// When set, the is operates in local space /// @@ -323,6 +337,14 @@ internal set } } + /// + /// When overriding , if the state that was pushed was a teleport then this will be set to true. + /// + /// + /// Note that will be reset in the event you need to do multiple teleports spread out accoss multiple ticks. + /// + public bool WasTeleported { get; internal set; } + /// /// When set the is uses interpolation. /// @@ -510,7 +532,7 @@ private void SetFlag(bool set, int flag) internal void ClearBitSetForNextTick() { // Clear everything but flags that should persist between state updates until changed by authority - m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp | k_UseUnreliableDeltas; + m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp | k_UseUnreliableDeltas | k_SwitchTransformSpaceWhenParented; IsDirty = false; } @@ -635,6 +657,13 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade positionStart = m_Reader.Position; } +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + var bitSetAndTickSize = 0; + var positionSize = 0; + var rotationSize = 0; + var scaleSize = 0; + var lastPosition = 0; +#endif // Synchronize State Flags and Network Tick { if (isWriting) @@ -671,6 +700,14 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade } } +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + if (isWriting) + { + bitSetAndTickSize = m_Writer.Position - positionStart; + lastPosition = m_Writer.Position; + } +#endif + // If debugging states and track by state identifier is enabled, serialize the current state identifier if (TrackByStateId) { @@ -740,6 +777,14 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade } } +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + if (isWriting) + { + positionSize = m_Writer.Position - lastPosition; + lastPosition = m_Writer.Position; + } +#endif + // Synchronize Rotation if (HasRotAngleChange) { @@ -850,6 +895,14 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade } } +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + if (isWriting) + { + rotationSize = m_Writer.Position - lastPosition; + lastPosition = m_Writer.Position; + } +#endif + // Synchronize Scale if (HasScaleChange) { @@ -920,6 +973,14 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade } } +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + if (isWriting) + { + scaleSize = m_Writer.Position - lastPosition; + lastPosition = m_Writer.Position; + } +#endif + // Only if we are receiving state if (!isWriting) { @@ -930,6 +991,9 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade else { LastSerializedSize = m_Writer.Position - positionStart; +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + Debug.Log($"[NT-WriteSize][BitsAndTick: {bitSetAndTickSize}][position: {positionSize}][rotation: {rotationSize}][scale: {scaleSize}]"); +#endif } } } @@ -1256,7 +1320,7 @@ public enum AuthorityModes /// [Tooltip("When set, NetworkTransform will send common state updates using unreliable network delivery " + "to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates are " + - "sent using reliable fragmented sequenced network delivery.")] + "sent using reliable fragmented sequenced network delivery. Note: This will change the order of operations between transform state updates and other messages sent reliably.")] public bool UseUnreliableDeltas = false; /// @@ -1433,9 +1497,12 @@ internal bool SynchronizeScale /// Sets whether the transform should be treated as local (true) or world (false) space. /// /// - /// This is synchronized by authority. During runtime, this should only be changed by the + /// This is synchronized by the authority. During runtime, this should only be changed by the /// authoritative side. Non-authoritative instances will be overridden by the next - /// authoritative state update. + /// authoritative state update.
+ /// Note:
+ /// When is enabled, this field will be automatically adjusted. + /// Adjusting this field during runtime and when isn't enabled. ///
[Tooltip("Sets whether this transform should sync in local space or in world space")] public bool InLocalSpace = false; @@ -1449,19 +1516,22 @@ internal bool SynchronizeScale /// /// Only works with components that are not paired with a or component that is configured to use the rigid body for motion.
/// will automatically be set when this is enabled. - /// Does not auto-synchronize clients if changed on the authority instance during runtime (i.e. apply this setting in-editor). + /// This field doesn't auto-synchronize with non-authority clients if changed on the authority instance during runtime (so you should apply this setting in-Editor). + /// Read the NetworkTransform documentation for more information and to avoid improper use. ///
+ [Tooltip("When enabled, NetworkTransform controls world or local space settings while also providing smooth parenting transitions." + + "When disabled, world or local space settings have to be adjusted by script or in the inspector view.")] public bool SwitchTransformSpaceWhenParented = false; /// /// Returns true if position is currently in local space and false if it is in world space. /// - protected bool PositionInLocalSpace => (!SwitchTransformSpaceWhenParented && InLocalSpace) || (m_PositionInterpolator != null && m_PositionInterpolator.InLocalSpace && SwitchTransformSpaceWhenParented); + protected bool PositionInLocalSpace => InLocalSpace; /// /// Returns true if rotation is currently in local space and false if it is in world space. /// - protected bool RotationInLocalSpace => (!SwitchTransformSpaceWhenParented && InLocalSpace) || (m_RotationInterpolator != null && m_RotationInterpolator.InLocalSpace && SwitchTransformSpaceWhenParented); + protected bool RotationInLocalSpace => InLocalSpace; /// /// When enabled (default) interpolation is applied. @@ -1650,7 +1720,15 @@ internal NetworkTransformState LocalAuthoritativeNetworkState // Non-Authoritative's current position, scale, and rotation that is used to assure the non-authoritative side cannot make adjustments to // the portions of the transform being synchronized. private Vector3 m_InternalCurrentPosition; - private Vector3 m_TargetPosition; + + /// + /// Used primarily to track the last state received that had a change in position. + /// When interpolation is disabled, this value is applied immediately to the transform. + /// When interpolation is enabled, this value is only updated in the event that if + /// interpolation is disabled the last known state position update will be continually applied. + /// This might not be the exact + /// + private Vector3 m_LastStateTargetPosition; private Vector3 m_InternalCurrentScale; private Vector3 m_TargetScale; private Quaternion m_InternalCurrentRotation; @@ -1764,10 +1842,14 @@ protected override void OnSynchronize(ref BufferSerializer serializer) // for the non-authority side to be able to properly synchronize delta position updates. CheckForStateChange(ref SynchronizeState, ref transformToCommit, true, targetClientId); SynchronizeState.NetworkSerialize(serializer); + LastTickSync = SynchronizeState.GetNetworkTick(); + OnAuthorityPushTransformState(ref SynchronizeState); } else { SynchronizeState.NetworkSerialize(serializer); + LastTickSync = SynchronizeState.GetNetworkTick(); + OnNetworkTransformStateUpdated(ref SynchronizeState, ref SynchronizeState); } } @@ -1879,7 +1961,6 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz m_UseRigidbodyForMotion = m_NetworkRigidbodyInternal.UseRigidBodyForMotion; } #endif - // If the transform has deltas (returns dirty) or if an explicitly set state is pending if (m_LocalAuthoritativeNetworkState.ExplicitSet || CheckForStateChange(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize, forceState: settingState)) { @@ -1889,6 +1970,12 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz // For explicit set, we use the current ServerTime.Tick and not CurrentTick since this is a SetState specific flow // that is outside of the normal internal tick flow. m_LocalAuthoritativeNetworkState.NetworkTick = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick; + + if (SwitchTransformSpaceWhenParented && m_LocalAuthoritativeNetworkState.ExplicitSet && m_LocalAuthoritativeNetworkState.IsDirty && transform.parent != null && !m_LocalAuthoritativeNetworkState.InLocalSpace) + { + InLocalSpace = true; + CheckForStateChange(ref m_LocalAuthoritativeNetworkState, ref transformToCommit, synchronize, forceState: true); + } } // Send the state update @@ -1897,6 +1984,9 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz // Mark the last tick and the old state (for next ticks) m_OldState = m_LocalAuthoritativeNetworkState; + // Preserve our teleporting flag in order to know if the state pushed was a teleport + m_LocalAuthoritativeNetworkState.WasTeleported = m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame; + // Reset the teleport and explicit state flags after we have sent the state update. // These could be set again in the below OnAuthorityPushTransformState virtual method m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false; @@ -2040,6 +2130,31 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra var isPositionDirty = isTeleportingAndNotSynchronizing ? networkState.HasPositionChange : false; var isRotationDirty = isTeleportingAndNotSynchronizing ? networkState.HasRotAngleChange : false; var isScaleDirty = isTeleportingAndNotSynchronizing ? networkState.HasScaleChange : false; + networkState.SwitchTransformSpaceWhenParented = SwitchTransformSpaceWhenParented; + + // All of the checks below, up to the delta position checking portion, are to determine if the + // authority changed a property during runtime that requires a full synchronizing. +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D + if ((InLocalSpace != networkState.InLocalSpace || isSynchronization) && !m_UseRigidbodyForMotion) +#else + if (InLocalSpace != networkState.InLocalSpace) +#endif + { + // When SwitchTransformSpaceWhenParented is set we automatically set our local space based on whether + // we are parented or not. + networkState.InLocalSpace = SwitchTransformSpaceWhenParented ? transform.parent != null : InLocalSpace; + if (SwitchTransformSpaceWhenParented) + { + InLocalSpace = networkState.InLocalSpace; + } + isDirty = true; + + // Otherwise, if SwitchTransformSpaceWhenParented is set we force a full state update. + // If interpolation is enabled, then any non-authority instance will update any pending + // buffered values to the correct world or local space values. + forceState = SwitchTransformSpaceWhenParented; + } + #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : InLocalSpace ? transformToUse.localPosition : transformToUse.position; @@ -2065,28 +2180,8 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra var scale = transformToUse.localScale; networkState.IsSynchronizing = isSynchronization; - // All of the checks below, up to the delta position checking portion, are to determine if the - // authority changed a property during runtime that requires a full synchronizing. -#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D - if ((InLocalSpace != networkState.InLocalSpace || isSynchronization) && !m_UseRigidbodyForMotion) -#else - if (InLocalSpace != networkState.InLocalSpace) -#endif - { - // When SwitchTransformSpaceWhenParented is set we automatically set our local space based on whether - // we are parented or not. - networkState.InLocalSpace = SwitchTransformSpaceWhenParented ? transform.parent != null : InLocalSpace; - isDirty = true; - // If SwitchTransformSpaceWhenParented is not set, then we will want to teleport - networkState.IsTeleportingNextFrame = !SwitchTransformSpaceWhenParented; - // Otherwise, if SwitchTransformSpaceWhenParented is set we force a full state update. - // If interpolation is enabled, then any non-authority instance will update any pending - // buffered values to the correct world or local space values. - forceState = SwitchTransformSpaceWhenParented; - } - // Check for parenting when synchronizing and/or teleporting - if (isSynchronization || networkState.IsTeleportingNextFrame) + if (isSynchronization || networkState.IsTeleportingNextFrame || forceState) { // This all has to do with complex nested hierarchies and how it impacts scale // when set for the first time or teleporting and depends upon whether the @@ -2467,10 +2562,12 @@ private void OnNetworkTick(bool isCalledFromParent = false) var transformSource = transform; OnUpdateAuthoritativeState(ref transformSource, isCalledFromParent); #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D - m_InternalCurrentPosition = m_TargetPosition = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); + m_InternalCurrentPosition = m_LastStateTargetPosition = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); + m_InternalCurrentRotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : GetSpaceRelativeRotation(); + m_TargetRotation = m_InternalCurrentRotation.eulerAngles; #else m_InternalCurrentPosition = GetSpaceRelativePosition(); - m_TargetPosition = GetSpaceRelativePosition(); + m_LastStateTargetPosition = GetSpaceRelativePosition(); #endif } else // If we are no longer authority, unsubscribe to the tick event @@ -2489,11 +2586,13 @@ internal void UpdatePositionInterpolator(Vector3 position, double time, bool res { if (resetInterpolator) { - m_PositionInterpolator.ResetTo(position, time); + m_PositionInterpolator.AutoConvertTransformSpace = SwitchTransformSpaceWhenParented; + m_PositionInterpolator.InLocalSpace = InLocalSpace; + m_PositionInterpolator.ResetTo(transform.parent, position, time); } else { - m_PositionInterpolator.AddMeasurement(position, time); + m_PositionInterpolator.AddMeasurement(transform.parent, position, time); } } } @@ -2526,10 +2625,10 @@ protected internal void ApplyAuthoritativeState() // at the end of this method and assure that when not interpolating the non-authoritative side // cannot make adjustments to any portions the transform not being synchronized. var adjustedPosition = m_InternalCurrentPosition; - var currentPosistion = GetSpaceRelativePosition(); - adjustedPosition.x = SyncPositionX ? m_InternalCurrentPosition.x : currentPosistion.x; - adjustedPosition.y = SyncPositionY ? m_InternalCurrentPosition.y : currentPosistion.y; - adjustedPosition.z = SyncPositionZ ? m_InternalCurrentPosition.z : currentPosistion.z; + var currentPosition = GetSpaceRelativePosition(); + adjustedPosition.x = SyncPositionX ? m_InternalCurrentPosition.x : currentPosition.x; + adjustedPosition.y = SyncPositionY ? m_InternalCurrentPosition.y : currentPosition.y; + adjustedPosition.z = SyncPositionZ ? m_InternalCurrentPosition.z : currentPosition.z; var adjustedRotation = m_InternalCurrentRotation; var adjustedRotAngles = adjustedRotation.eulerAngles; @@ -2546,8 +2645,15 @@ protected internal void ApplyAuthoritativeState() adjustedScale.y = SyncScaleY ? adjustedScale.y : currentScale.y; adjustedScale.z = SyncScaleZ ? adjustedScale.z : currentScale.z; + // Only if SwitchTransformSpaceWhenParented is not enabled should + // non-authority instances preserve the current state's local space + // setting. + if (!SwitchTransformSpaceWhenParented) + { + InLocalSpace = networkState.InLocalSpace; + } + // Non-Authority Preservers the authority's transform state update modes - InLocalSpace = networkState.InLocalSpace; Interpolate = networkState.UseInterpolation; UseHalfFloatPrecision = networkState.UseHalfFloatPrecision; UseQuaternionSynchronization = networkState.QuaternionSync; @@ -2620,7 +2726,7 @@ protected internal void ApplyAuthoritativeState() { if (networkState.HasPositionChange && SynchronizePosition) { - adjustedPosition = m_TargetPosition; + adjustedPosition = m_LastStateTargetPosition; } if (networkState.HasScaleChange && SynchronizeScale) @@ -2695,21 +2801,7 @@ protected internal void ApplyAuthoritativeState() { if (PositionInLocalSpace) { - // This handles the edge case of transitioning from local to world space where applying a local - // space value to a non-parented transform will be applied in world space. Since parenting is not - // tick synchronized, there can be one or two ticks between a state update with the InLocalSpace - // state update which can cause the body to seemingly "teleport" when it is just applying a local - // space value relative to world space 0,0,0. - if (SwitchTransformSpaceWhenParented && m_IsFirstNetworkTransform && Interpolate && m_PreviousNetworkObjectParent != null - && transform.parent == null) - { - m_InternalCurrentPosition = m_PreviousNetworkObjectParent.transform.TransformPoint(m_InternalCurrentPosition); - transform.position = m_InternalCurrentPosition; - } - else - { - transform.localPosition = m_InternalCurrentPosition; - } + transform.localPosition = m_InternalCurrentPosition; } else { @@ -2751,20 +2843,7 @@ protected internal void ApplyAuthoritativeState() { if (RotationInLocalSpace) { - // This handles the edge case of transitioning from local to world space where applying a local - // space value to a non-parented transform will be applied in world space. Since parenting is not - // tick synchronized, there can be one or two ticks between a state update with the InLocalSpace - // state update which can cause the body to rotate world space relative and cause a slight rotation - // of the body in-between this transition period. - if (SwitchTransformSpaceWhenParented && m_IsFirstNetworkTransform && Interpolate && m_PreviousNetworkObjectParent != null && transform.parent == null) - { - m_InternalCurrentRotation = m_PreviousNetworkObjectParent.transform.rotation * m_InternalCurrentRotation; - transform.rotation = m_InternalCurrentRotation; - } - else - { - transform.localRotation = m_InternalCurrentRotation; - } + transform.localRotation = m_InternalCurrentRotation; } else { @@ -2875,7 +2954,7 @@ private void ApplyTeleportingState(NetworkTransformState newState) } m_InternalCurrentPosition = currentPosition; - m_TargetPosition = currentPosition; + m_LastStateTargetPosition = currentPosition; // Apply the position if (newState.InLocalSpace) @@ -2986,7 +3065,9 @@ private void ApplyTeleportingState(NetworkTransformState newState) if (Interpolate) { - m_RotationInterpolator.ResetTo(currentRotation, sentTime); + m_RotationInterpolator.AutoConvertTransformSpace = SwitchTransformSpaceWhenParented; + m_RotationInterpolator.InLocalSpace = newState.InLocalSpace; + m_RotationInterpolator.ResetTo(transform.parent, currentRotation, sentTime); } } @@ -2999,6 +3080,8 @@ private void ApplyTeleportingState(NetworkTransformState newState) OnTransformUpdated(); } + + internal int LastTickSync = 0; /// /// Adds the new state's values to their respective interpolator /// @@ -3015,6 +3098,8 @@ internal void ApplyUpdatedState(NetworkTransformState newState) UseHalfFloatPrecision = newState.UseHalfFloatPrecision; UseUnreliableDeltas = newState.UseUnreliableDeltas; + SwitchTransformSpaceWhenParented = newState.SwitchTransformSpaceWhenParented; + if (SlerpPosition != newState.UsePositionSlerp) { SlerpPosition = newState.UsePositionSlerp; @@ -3024,9 +3109,14 @@ internal void ApplyUpdatedState(NetworkTransformState newState) m_LocalAuthoritativeNetworkState = newState; if (m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame) { + LastTickSync = m_LocalAuthoritativeNetworkState.GetNetworkTick(); ApplyTeleportingState(m_LocalAuthoritativeNetworkState); return; } + else if (m_LocalAuthoritativeNetworkState.IsSynchronizing) + { + LastTickSync = m_LocalAuthoritativeNetworkState.GetNetworkTick(); + } var sentTime = newState.SentTime; var currentRotation = GetSpaceRelativeRotation(); @@ -3034,6 +3124,7 @@ internal void ApplyUpdatedState(NetworkTransformState newState) // Only if using half float precision and our position had changed last update then if (UseHalfFloatPrecision && m_LocalAuthoritativeNetworkState.HasPositionChange) { + // Do a full precision synchronization to apply the base position and offset. if (m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat) { m_HalfPositionState = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition; @@ -3047,9 +3138,10 @@ internal void ApplyUpdatedState(NetworkTransformState newState) // This is to assure when you get the position of the state it is the correct position m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.ToVector3(0); } - // Update our target position - m_TargetPosition = m_HalfPositionState.ToVector3(newState.NetworkTick); - m_LocalAuthoritativeNetworkState.CurrentPosition = m_TargetPosition; + // Update the target position for this incoming state. + // This becomes the last known received state position (unlike interpolators that will have a queue). + m_LastStateTargetPosition = m_HalfPositionState.ToVector3(newState.NetworkTick); + m_LocalAuthoritativeNetworkState.CurrentPosition = m_LastStateTargetPosition; } if (!Interpolate) @@ -3058,17 +3150,17 @@ internal void ApplyUpdatedState(NetworkTransformState newState) return; } - AdjustForChangeInTransformSpace(); - // Apply axial changes from the new state // Either apply the delta position target position or the current state's delta position // depending upon whether UsePositionDeltaCompression is enabled if (m_LocalAuthoritativeNetworkState.HasPositionChange) { + // If interpolating, get the current value as the final next position or current position + // depending upon if the interpolator is still processing a state or not. if (!m_LocalAuthoritativeNetworkState.UseHalfFloatPrecision) { + var newTargetPosition = (Interpolate && SwitchTransformSpaceWhenParented) ? m_PositionInterpolator.GetInterpolatedValue() : m_LastStateTargetPosition; var position = m_LocalAuthoritativeNetworkState.GetPosition(); - var newTargetPosition = m_TargetPosition; if (m_LocalAuthoritativeNetworkState.HasPositionX) { newTargetPosition.x = position.x; @@ -3083,9 +3175,10 @@ internal void ApplyUpdatedState(NetworkTransformState newState) { newTargetPosition.z = position.z; } - m_TargetPosition = newTargetPosition; + m_LastStateTargetPosition = newTargetPosition; } - UpdatePositionInterpolator(m_TargetPosition, sentTime); + + UpdatePositionInterpolator(m_LastStateTargetPosition, sentTime); } if (m_LocalAuthoritativeNetworkState.HasScaleChange) @@ -3119,7 +3212,7 @@ internal void ApplyUpdatedState(NetworkTransformState newState) } } m_TargetScale = currentScale; - m_ScaleInterpolator.AddMeasurement(currentScale, sentTime); + m_ScaleInterpolator.AddMeasurement(transform.parent, currentScale, sentTime); } // With rotation, we check if there are any changes first and @@ -3154,7 +3247,7 @@ internal void ApplyUpdatedState(NetworkTransformState newState) currentRotation.eulerAngles = currentEulerAngles; } - m_RotationInterpolator.AddMeasurement(currentRotation, sentTime); + m_RotationInterpolator.AddMeasurement(transform.parent, currentRotation, sentTime); } } @@ -3182,11 +3275,6 @@ protected virtual void OnBeforeUpdateTransformState() /// private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransformState newState) { - if (!NetworkObject.IsSpawned || CanCommitToTransform) - { - return; - } - // If we are using UseUnreliableDeltas and our old state tick is greater than the new state tick, // then just ignore the newstate. This avoids any scenario where the new state is out of order // from the old state (with unreliable traffic and/or mixed unreliable and reliable) @@ -3483,6 +3571,12 @@ protected virtual void Awake() m_RotationInterpolator = new BufferedLinearInterpolatorQuaternion(); m_PositionInterpolator = new BufferedLinearInterpolatorVector3(); m_ScaleInterpolator = new BufferedLinearInterpolatorVector3(); + + // Always start in world space until spawned and initialized. + if (SwitchTransformSpaceWhenParented) + { + InLocalSpace = false; + } } /// @@ -3493,7 +3587,7 @@ public override void OnNetworkSpawn() Initialize(); - if (CanCommitToTransform) + if (CanCommitToTransform && !SwitchTransformSpaceWhenParented) { SetState(GetSpaceRelativePosition(), GetSpaceRelativeRotation(), GetScale(), false); } @@ -3563,14 +3657,17 @@ private void ResetInterpolatedStateToCurrentAuthoritativeState() var position = GetSpaceRelativePosition(); var rotation = GetSpaceRelativeRotation(); #endif + // Reset interpolators to the current state of the NetworkTransform + m_PositionInterpolator.AutoConvertTransformSpace = SwitchTransformSpaceWhenParented; m_PositionInterpolator.InLocalSpace = InLocalSpace; - m_RotationInterpolator.InLocalSpace = InLocalSpace; - UpdatePositionInterpolator(position, serverTime, true); UpdatePositionSlerp(); - m_ScaleInterpolator.ResetTo(transform.localScale, serverTime); - m_RotationInterpolator.ResetTo(rotation, serverTime); + m_RotationInterpolator.AutoConvertTransformSpace = SwitchTransformSpaceWhenParented; + m_RotationInterpolator.InLocalSpace = InLocalSpace; + m_RotationInterpolator.ResetTo(transform.parent, rotation, serverTime); + + m_ScaleInterpolator.ResetTo(transform.parent, transform.localScale, serverTime); } private NetworkObject m_CachedNetworkObject; /// @@ -3602,7 +3699,12 @@ private void InternalInitialization(bool isOwnershipChange = false) { InLocalSpace = true; } + else + { + InLocalSpace = false; + } } + // Always apply this if SwitchTransformSpaceWhenParented is set. TickSyncChildren = true; } @@ -3647,7 +3749,7 @@ private void InternalInitialization(bool isOwnershipChange = false) } m_InternalCurrentPosition = currentPosition; - m_TargetPosition = currentPosition; + m_LastStateTargetPosition = currentPosition; RegisterForTickUpdate(this); @@ -3672,7 +3774,7 @@ private void InternalInitialization(bool isOwnershipChange = false) DeregisterForTickUpdate(this); ResetInterpolatedStateToCurrentAuthoritativeState(); m_InternalCurrentPosition = currentPosition; - m_TargetPosition = currentPosition; + m_LastStateTargetPosition = currentPosition; m_InternalCurrentScale = transform.localScale; m_TargetScale = transform.localScale; m_InternalCurrentRotation = currentRotation; @@ -3691,51 +3793,6 @@ protected void Initialize() #endregion #region PARENTING AND OWNERSHIP - // This might seem aweful, but when transitioning between two parents in local space we need to - // catch the moment the transition happens and only apply the special case parenting from one parent - // to another parent once. Keeping track of the "previous previous" allows us to detect the - // back and fourth scenario: - // - No parent (world space) - // - Parent under NetworkObjectA (world to local) - // - Parent under NetworkObjectB (local to local) (catch with "previous previous") - // - Parent under NetworkObjectA (local to local) (catch with "previous previous") - // - Parent under NetworkObjectB (local to local) (catch with "previous previous") - private NetworkObject m_PreviousCurrentParent; - private NetworkObject m_PreviousPreviousParent; - private void AdjustForChangeInTransformSpace() - { - if (SwitchTransformSpaceWhenParented && m_IsFirstNetworkTransform && (m_PositionInterpolator.InLocalSpace != InLocalSpace || - m_RotationInterpolator.InLocalSpace != InLocalSpace || - (InLocalSpace && m_CurrentNetworkObjectParent && m_PreviousNetworkObjectParent && m_PreviousCurrentParent != m_CurrentNetworkObjectParent && m_PreviousPreviousParent != m_PreviousNetworkObjectParent))) - { - var parent = m_CurrentNetworkObjectParent ? m_CurrentNetworkObjectParent : m_PreviousNetworkObjectParent; - if (parent) - { - // In the event it is a NetworkObject to NetworkObject parenting transfer, we will need to migrate our interpolators - // and our current position and rotation to world space relative to the previous parent before converting them to local - // space relative to the new parent - if (InLocalSpace && m_CurrentNetworkObjectParent && m_PreviousNetworkObjectParent) - { - m_PreviousCurrentParent = m_CurrentNetworkObjectParent; - m_PreviousPreviousParent = m_PreviousNetworkObjectParent; - // Convert our current postion and rotation to world space based on the previous parent's transform - m_InternalCurrentPosition = m_PreviousNetworkObjectParent.transform.TransformPoint(m_InternalCurrentPosition); - m_InternalCurrentRotation = m_PreviousNetworkObjectParent.transform.rotation * m_InternalCurrentRotation; - // Convert our current postion and rotation to local space based on the current parent's transform - m_InternalCurrentPosition = m_CurrentNetworkObjectParent.transform.InverseTransformPoint(m_InternalCurrentPosition); - m_InternalCurrentRotation = Quaternion.Inverse(m_CurrentNetworkObjectParent.transform.rotation) * m_InternalCurrentRotation; - // Convert both interpolators to world space based on the previous parent's transform - m_PositionInterpolator.ConvertTransformSpace(m_PreviousNetworkObjectParent.transform, false); - m_RotationInterpolator.ConvertTransformSpace(m_PreviousNetworkObjectParent.transform, false); - // Next, fall into normal transform space conversion of both interpolators to local space based on the current parent's transform - } - - m_PositionInterpolator.ConvertTransformSpace(parent.transform, InLocalSpace); - m_RotationInterpolator.ConvertTransformSpace(parent.transform, InLocalSpace); - } - } - } - /// public override void OnLostOwnership() { @@ -3763,9 +3820,6 @@ protected override void OnOwnershipChanged(ulong previous, ulong current) private List m_ParentedChildren = new List(); private bool m_IsFirstNetworkTransform; - private NetworkObject m_CurrentNetworkObjectParent = null; - private NetworkObject m_PreviousNetworkObjectParent = null; - internal void ChildRegistration(NetworkObject child, bool isAdding) { if (isAdding) @@ -3804,66 +3858,117 @@ public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObj base.OnNetworkObjectParentChanged(parentNetworkObject); } - - internal override void InternalOnNetworkObjectParentChanged(NetworkObject parentNetworkObject) + private void DefaultParentChanged(NetworkObject parentNetworkObject) { - // The root NetworkTransform handles tracking any NetworkObject parenting since nested NetworkTransforms (of the same NetworkObject) - // will never (or rather should never) change their world space once spawned. #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D - // Handling automatic transform space switching can only be applied to NetworkTransforms that don't use the Rigidbody for motion - if (!m_UseRigidbodyForMotion && SwitchTransformSpaceWhenParented) + var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); + var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : GetSpaceRelativeRotation(); #else - if (SwitchTransformSpaceWhenParented) + var position = GetSpaceRelativePosition(); + var rotation = GetSpaceRelativeRotation(); #endif + m_LastStateTargetPosition = m_InternalCurrentPosition = position; + m_InternalCurrentRotation = rotation; + m_TargetRotation = m_InternalCurrentRotation.eulerAngles; + m_TargetScale = m_InternalCurrentScale = GetScale(); + + if (Interpolate) { - m_PreviousNetworkObjectParent = m_CurrentNetworkObjectParent; - m_CurrentNetworkObjectParent = parentNetworkObject; - if (m_IsFirstNetworkTransform) + m_ScaleInterpolator.Clear(); + m_PositionInterpolator.Clear(); + m_RotationInterpolator.Clear(); + + // Always use NetworkManager here as this can be invoked prior to spawning + var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time; + UpdatePositionInterpolator(m_InternalCurrentPosition, tempTime, true); + m_ScaleInterpolator.ResetTo(m_InternalCurrentScale, tempTime); + m_RotationInterpolator.ResetTo(m_InternalCurrentRotation, tempTime); + } + } + + internal bool IsFirstTransform() + { + return m_IsFirstNetworkTransform; + } + + internal override void InternalOnNetworkObjectParentChanged(NetworkObject parentNetworkObject) + { + if (!SwitchTransformSpaceWhenParented) + { + // Motion authority doesn't need to adjust anything + if (CanCommitToTransform) { - if (CanCommitToTransform) - { - InLocalSpace = m_CurrentNetworkObjectParent != null; - } - if (m_PreviousNetworkObjectParent && m_PreviousNetworkObjectParent.NetworkTransforms != null && m_PreviousNetworkObjectParent.NetworkTransforms.Count > 0) - { - // Always deregister with the first NetworkTransform in the list - m_PreviousNetworkObjectParent.NetworkTransforms[0].ChildRegistration(NetworkObject, false); - } - if (m_CurrentNetworkObjectParent && m_CurrentNetworkObjectParent.NetworkTransforms != null && m_CurrentNetworkObjectParent.NetworkTransforms.Count > 0) - { - // Always register with the first NetworkTransform in the list - m_CurrentNetworkObjectParent.NetworkTransforms[0].ChildRegistration(NetworkObject, true); - } + return; } + // Keep the same legacy behaviour for compatibility purposes + DefaultParentChanged(parentNetworkObject); } else { - // Keep the same legacy behaviour for compatibility purposes - if (!CanCommitToTransform) + InLocalSpace = parentNetworkObject != null; + + if (SynchronizePosition) { -#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D - var position = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetPosition() : GetSpaceRelativePosition(); - var rotation = m_UseRigidbodyForMotion ? m_NetworkRigidbodyInternal.GetRotation() : GetSpaceRelativeRotation(); -#else - var position = GetSpaceRelativePosition(); - var rotation = GetSpaceRelativeRotation(); -#endif - m_TargetPosition = m_InternalCurrentPosition = position; - m_InternalCurrentRotation = rotation; - m_TargetRotation = m_InternalCurrentRotation.eulerAngles; - m_TargetScale = m_InternalCurrentScale = GetScale(); + m_PositionInterpolator.AutoConvertTransformSpace = SwitchTransformSpaceWhenParented; + m_PositionInterpolator.InLocalSpace = InLocalSpace; + m_PositionInterpolator.Parent = InLocalSpace ? parentNetworkObject.transform : null; - if (Interpolate) + if (LastTickSync == m_LocalAuthoritativeNetworkState.GetNetworkTick()) { - m_ScaleInterpolator.Clear(); - m_PositionInterpolator.Clear(); - m_RotationInterpolator.Clear(); - - // Always use NetworkManager here as this can be invoked prior to spawning - var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time; - UpdatePositionInterpolator(m_InternalCurrentPosition, tempTime, true); - m_ScaleInterpolator.ResetTo(m_InternalCurrentScale, tempTime); - m_RotationInterpolator.ResetTo(m_InternalCurrentRotation, tempTime); + m_InternalCurrentPosition = m_LastStateTargetPosition = GetSpaceRelativePosition(); + m_PositionInterpolator.ResetTo(m_PositionInterpolator.Parent, m_InternalCurrentPosition, NetworkManager.ServerTime.Time); + if (InLocalSpace) + { + transform.localPosition = m_InternalCurrentPosition; + } + else + { + transform.position = m_InternalCurrentPosition; + } + } + else + { + if (CanCommitToTransform) + { + m_InternalCurrentPosition = GetSpaceRelativePosition(); + } + else + { + m_InternalCurrentPosition = m_LastStateTargetPosition = Interpolate ? m_PositionInterpolator.GetInterpolatedValue() : GetSpaceRelativePosition(); + } + } + } + + if (SynchronizeRotation) + { + m_RotationInterpolator.AutoConvertTransformSpace = SwitchTransformSpaceWhenParented; + m_RotationInterpolator.InLocalSpace = InLocalSpace; + m_RotationInterpolator.Parent = InLocalSpace ? parentNetworkObject.transform : null; + if (LastTickSync == m_LocalAuthoritativeNetworkState.GetNetworkTick()) + { + m_InternalCurrentRotation = GetSpaceRelativeRotation(); + m_TargetRotation = m_InternalCurrentRotation.eulerAngles; + m_RotationInterpolator.ResetTo(m_RotationInterpolator.Parent, m_InternalCurrentRotation, NetworkManager.ServerTime.Time); + if (InLocalSpace) + { + transform.localRotation = m_InternalCurrentRotation; + } + else + { + transform.rotation = m_InternalCurrentRotation; + } + } + else + { + if (CanCommitToTransform) + { + m_InternalCurrentRotation = GetSpaceRelativeRotation(); + } + else + { + m_InternalCurrentRotation = Interpolate ? m_RotationInterpolator.GetInterpolatedValue() : GetSpaceRelativeRotation(); + } + m_TargetRotation = m_InternalCurrentRotation.eulerAngles; } } } @@ -3878,7 +3983,8 @@ internal override void InternalOnNetworkObjectParentChanged(NetworkObject parent /// This will override any changes made previously to the transform /// This isn't resistant to network jitter. Server side changes due to this method won't be interpolated. /// The parameters are broken up into pos / rot / scale on purpose so that the caller can perturb - /// just the desired one(s) + /// just the desired one(s). + /// Using this method during the spawn sequence isn't recommended. Refer to the NetworkTransform documentation for more information on the recommended usage. /// /// new position to move to. Can be null /// new rotation to rotate to. Can be null @@ -4014,9 +4120,10 @@ private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool /// /// Teleport an already spawned object to the given values without interpolating. + /// Using this method during the spawn sequence isn't recommended. Refer to the NetworkTransform documentation for more information on the recommended usage. /// /// - /// This is intended to be used on already spawned objects, for setting the position of a dynamically spawned object just apply the transform values prior to spawning.
+ /// This is intended to be used on already spawned objects, for setting the position of a dynamically spawned object just apply the transform values prior to spawning.
/// With player objects, override the method and have the authority make adjustments to the transform prior to invoking base.OnNetworkSpawn. ///
/// new position to move to. @@ -4123,9 +4230,8 @@ internal BufferedLinearInterpolatorQuaternion GetRotationInterpolator() // Non-Authority private void UpdateInterpolation() { - AdjustForChangeInTransformSpace(); // Select the time system relative to the type of NetworkManager instance. - var timeSystem = m_CachedNetworkManager.IsServer ? m_CachedNetworkManager.ServerTime : m_CachedNetworkManager.LocalTime; + var timeSystem = m_CachedNetworkManager.IsServer ? m_CachedNetworkManager.LocalTime : m_CachedNetworkManager.ServerTime; var currentTime = timeSystem.Time; #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var cachedDeltaTime = m_UseRigidbodyForMotion ? m_CachedNetworkManager.RealTimeProvider.FixedDeltaTime : m_CachedNetworkManager.RealTimeProvider.DeltaTime; @@ -4405,7 +4511,6 @@ public virtual void OnFixedUpdate() m_FixedFrameCount++; m_FixedUpdatesPerFrameCount++; #endif - // Update interpolation when enabled if (Interpolate) { @@ -4468,10 +4573,11 @@ internal NetworkTransformState OutboundState /// internal void TransformStateUpdate(ulong senderId) { - if (CanCommitToTransform) + if (!IsSpawned || CanCommitToTransform) { // TODO: Investigate where this state should be applied or just discarded. // For now, discard the state if we assumed ownership. + // Debug.Log($"[Client-{NetworkManager.LocalClientId}] Ignoring inbound update from Client-{0} and parentUpdated:{isParentingDirective}!"); return; } // Store the previous/old state @@ -4487,30 +4593,6 @@ internal void TransformStateUpdate(ulong senderId) // Used to send outbound messages private NetworkTransformMessage m_OutboundMessage = new NetworkTransformMessage(); - - internal int SerializeMessage(FastBufferWriter writer, int targetVersion) - { - var networkObject = NetworkObject; - var position = writer.Position; - BytePacker.WriteValueBitPacked(writer, NetworkObjectId); - BytePacker.WriteValueBitPacked(writer, (int)NetworkBehaviourId); - writer.WriteNetworkSerializable(m_LocalAuthoritativeNetworkState); - if (m_CachedNetworkManager.DistributedAuthorityMode) - { - BytePacker.WriteValuePacked(writer, networkObject.Observers.Count - 1); - - foreach (var targetId in networkObject.Observers) - { - if (OwnerClientId == targetId) - { - continue; - } - BytePacker.WriteValuePacked(writer, targetId); - } - } - return writer.Position - position; - } - /// /// Invoked by the authoritative instance to sends a containing the /// @@ -4538,9 +4620,9 @@ private void UpdateTransformState() // - If UsUnrealiable is not enabled // - If teleporting or synchronizing // - If sending an UnrealiableFrameSync or synchronizing the base position of the NetworkDeltaPosition - var networkDelivery = !UseUnreliableDeltas || m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame || m_LocalAuthoritativeNetworkState.IsSynchronizing - || m_LocalAuthoritativeNetworkState.UnreliableFrameSync || m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat - ? NetworkDelivery.ReliableSequenced : NetworkDelivery.UnreliableSequenced; + var networkDelivery = !UseUnreliableDeltas | m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame | m_LocalAuthoritativeNetworkState.IsSynchronizing + | m_LocalAuthoritativeNetworkState.UnreliableFrameSync | m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat + ? MessageDeliveryType.DefaultDelivery : NetworkDelivery.UnreliableSequenced; // Server-host-dahost always sends updates to all clients (but itself) if (IsServer) @@ -4669,6 +4751,11 @@ internal void TickUpdate() { return; } + if (m_NetworkManager == null || m_NetworkManager.ShutdownInProgress || !m_NetworkManager.IsListening) + { + Remove(); + return; + } foreach (var networkTransform in NetworkTransforms) { if (networkTransform.IsSpawned) @@ -4752,6 +4839,6 @@ private static void DeregisterForTickUpdate(NetworkTransform networkTransform) internal interface INetworkTransformLogStateEntry { - void AddLogEntry(NetworkTransform.NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false); + public void AddLogEntry(NetworkTransform.NetworkTransformState networkTransformState, ulong targetClient, bool preUpdate = false); } } diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index ccaab404e4..ccfa6a2905 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -635,7 +635,7 @@ private void SendConnectionRequest() } } - SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId); + SendMessage(ref message, MessageDeliveryType.DefaultDelivery, NetworkManager.ServerClientId); message.MessageVersions.Dispose(); } @@ -803,7 +803,7 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, response.Position ?? null, response.Rotation ?? null); // Spawn the player NetworkObject locally - NetworkManager.SpawnManager.SpawnNetworkObjectLocally( + NetworkManager.SpawnManager.AuthorityLocalSpawn( playerObject, NetworkManager.SpawnManager.GetNetworkObjectId(), sceneObject: false, @@ -857,7 +857,7 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne } if (!MockSkippingApproval) { - SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ownerClientId); + SendMessage(ref message, MessageDeliveryType.DefaultDelivery, ownerClientId); } else { @@ -987,7 +987,7 @@ internal void ApprovedPlayerSpawn(ulong clientId, uint playerPrefabHash) message.ObjectInfo.HasParent = false; message.ObjectInfo.IsPlayerObject = true; message.ObjectInfo.OwnerClientId = clientId; - var size = SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientPair.Key); + var size = SendMessage(ref message, MessageDeliveryType.DefaultDelivery, clientPair.Key); NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientPair.Key, ConnectedClients[clientId].PlayerObject, size); } } @@ -1021,14 +1021,14 @@ internal NetworkClient AddClient(ulong clientId) { ConnectedClientsList.Add(networkClient); } - + var networkDelivery = MessageDeliveryType.DefaultDelivery; if (NetworkManager.LocalClientId != clientId) { if ((!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer) || (NetworkManager.DistributedAuthorityMode && NetworkManager.NetworkConfig.EnableSceneManagement && NetworkManager.DAHost && NetworkManager.LocalClient.IsSessionOwner)) { var message = new ClientConnectedMessage { ClientId = clientId }; - NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, ConnectedClientIds.Where((c) => c != NetworkManager.LocalClientId).ToArray()); + NetworkManager.MessageManager.SendMessage(ref message, networkDelivery, ConnectedClientIds.Where((c) => c != NetworkManager.LocalClientId).ToArray()); } else if (NetworkManager.DistributedAuthorityMode && NetworkManager.NetworkConfig.EnableSceneManagement && NetworkManager.DAHost && !NetworkManager.LocalClient.IsSessionOwner) { @@ -1037,7 +1037,7 @@ internal NetworkClient AddClient(ulong clientId) ShouldSynchronize = true, ClientId = clientId }; - NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.CurrentSessionOwner); + NetworkManager.MessageManager.SendMessage(ref message, networkDelivery, NetworkManager.CurrentSessionOwner); } } if (!ConnectedClientIds.Contains(clientId)) @@ -1286,7 +1286,7 @@ internal void OnClientDisconnectFromServer(ulong clientId) ConnectedClientIds.Remove(clientId); var message = new ClientDisconnectedMessage { ClientId = clientId }; - MessageManager?.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, ConnectedClientIds); + MessageManager?.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, ConnectedClientIds); // Used for testing/validation purposes only #if ENABLE_DAHOST_AUTOPROMOTE_SESSION_OWNER diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 332694af7d..debf52ce3f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -104,7 +104,7 @@ internal void __endSendServerRpc(ref FastBufferWriter bufferWriter, uint rpcMeth { default: case RpcDelivery.Reliable: - networkDelivery = NetworkDelivery.ReliableFragmentedSequenced; + networkDelivery = MessageDeliveryType.DefaultDelivery; break; case RpcDelivery.Unreliable: if (bufferWriter.Length > networkManager.MessageManager.NonFragmentedMessageMaxSize) @@ -190,7 +190,7 @@ internal void __endSendClientRpc(ref FastBufferWriter bufferWriter, uint rpcMeth { default: case RpcDelivery.Reliable: - networkDelivery = NetworkDelivery.ReliableFragmentedSequenced; + networkDelivery = MessageDeliveryType.DefaultDelivery; break; case RpcDelivery.Unreliable: if (bufferWriter.Length > networkManager.MessageManager.NonFragmentedMessageMaxSize) @@ -358,7 +358,7 @@ internal void __endSendRpc(ref FastBufferWriter bufferWriter, uint rpcMethodId, { default: case RpcDelivery.Reliable: - networkDelivery = NetworkDelivery.ReliableFragmentedSequenced; + networkDelivery = MessageDeliveryType.DefaultDelivery; break; case RpcDelivery.Unreliable: if (bufferWriter.Length > NetworkManager.MessageManager.NonFragmentedMessageMaxSize) @@ -1014,7 +1014,7 @@ internal void InitializeVariables() for (int i = 0; i < NetworkVariableFields.Count; i++) { - var networkDelivery = NetworkVariableBase.Delivery; + var networkDelivery = MessageDeliveryType.DefaultDelivery; if (!firstLevelIndex.ContainsKey(networkDelivery)) { firstLevelIndex.Add(networkDelivery, secondLevelCounter); diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs index 943257dd74..4dc1257d10 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using Unity.Profiling; namespace Unity.Netcode @@ -10,23 +11,185 @@ public class NetworkBehaviourUpdater { private NetworkManager m_NetworkManager; private NetworkConnectionManager m_ConnectionManager; + + /// + /// Contains the current dirty s that are proccessed each new network tick. + /// Under most cases, dirty s are fully processed on the next network tick. + /// Under certain conditions, like user script invoking + /// to define , a can remain in the + /// list until the configured traits' conditions have been met. + /// private HashSet m_DirtyNetworkObjects = new HashSet(); + + /// + /// Contains any dirty s that will be added to the + /// list on the next network tick (). + /// private HashSet m_PendingDirtyNetworkObjects = new HashSet(); #if DEVELOPMENT_BUILD || UNITY_EDITOR private ProfilerMarker m_NetworkBehaviourUpdate = new ProfilerMarker($"{nameof(NetworkBehaviour)}.{nameof(NetworkBehaviourUpdate)}"); #endif + /// + /// Adds a to the prending dirty list. + /// The list is merged into the list + /// when processed. + /// internal void AddForUpdate(NetworkObject networkObject) { // Since this is a HashSet, we don't need to worry about duplicate entries m_PendingDirtyNetworkObjects.Add(networkObject); } + /// + /// (Client-server network topology only) + /// The server handles processing network variables the same way as a client + /// with the primary difference being that the server sends updates to all + /// observers. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ProcessDirtyObjectServer(NetworkObject dirtyObj, bool forceSend) + { + for (int i = 0; i < m_ConnectionManager.ConnectedClientsList.Count; i++) + { + var client = m_ConnectionManager.ConnectedClientsList[i]; + if (m_NetworkManager.DistributedAuthorityMode || dirtyObj.IsNetworkVisibleTo(client.ClientId)) + { + // Sync just the variables for just the objects this client sees + for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) + { + dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId, forceSend); + } + } + } + } + + /// + /// Clients handle processing dirty objects relative to the client. + /// The is client to server. + /// With distributed authority live service sessions, this is sent to + /// the Rust server. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ProcessDirtyObjectClient(NetworkObject dirtyObj, bool forceSend) + { + for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) + { + dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId, forceSend); + } + } + + /// + /// Handle house cleaning on the child s. + /// This includes some collections specific checks and updates. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void PostProcessDirtyObject(NetworkObject dirtyObj) + { + for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) + { + var behaviour = dirtyObj.ChildNetworkBehaviours[k]; + for (int i = 0; i < behaviour.NetworkVariableFields.Count; i++) + { + // Set to true for NetworkVariable to ignore duplication of the + // "internal original value" for collections support. + behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = true; + if (behaviour.NetworkVariableFields[i].IsDirty() && + !behaviour.NetworkVariableIndexesToResetSet.Contains(i)) + { + behaviour.NetworkVariableIndexesToResetSet.Add(i); + behaviour.NetworkVariableIndexesToReset.Add(i); + } + // Reset back to false when done + behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = false; + } + } + } + + /// + /// Invokes on all child s. + /// + /// + /// Refer to the definition. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ResetDirtyObject(NetworkObject dirtyObj, bool forceSend) + { + foreach (var behaviour in dirtyObj.ChildNetworkBehaviours) + { + behaviour.PostNetworkVariableWrite(forceSend); + } + } + + /// + /// Temporary work-around for assuring any pending dirty states are pushed out prior to showing the object + /// TODO: We need to send all messages that are specific to a NetworkObject along with a NetworkObject event header + /// and grouped together such that all directed messages will be processed after spawned. + /// + /// + internal void ForceSendIfDirtyOnNetworkShow(NetworkObject networkObject) + { + // Exit early if no pending dirty NetworkVariables. + if (!m_PendingDirtyNetworkObjects.Contains(networkObject) && !m_DirtyNetworkObjects.Contains(networkObject)) + { + return; + } + + ProcessDirtyObject(networkObject, true); + + // Remove it from the pending and queued dirty objects lists + m_PendingDirtyNetworkObjects.Remove(networkObject); + m_DirtyNetworkObjects.Remove(networkObject); + } + + /// + /// The primary "dirty" processor. + /// Invokes: + /// - on all properties that derive from . + /// - (if the server). + /// - (if the client). + /// - to handle the post processing of network variables. + /// - which cleans up and removes the from the dirty list. + /// + /// The to process. + /// When enabled, any dirty network variables will be added to a + /// and added to the outbound queue. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void ProcessDirtyObject(NetworkObject networkObject, bool forceSend) + { + // Only the server or the owner of the NetworkObject will send + // delta state updates. Otherwise, if neither are true we exit early. + if (!(m_NetworkManager.IsServer || networkObject.IsOwner)) + { + return; + } + + // Pre-variable update + for (int k = 0; k < networkObject.ChildNetworkBehaviours.Count; k++) + { + networkObject.ChildNetworkBehaviours[k].PreVariableUpdate(); + } + + // Server sends updates to all clients where a client sends updates + // to the server or DA service. + if (m_NetworkManager.IsServer) + { + ProcessDirtyObjectServer(networkObject, forceSend); + } + else + { + ProcessDirtyObjectClient(networkObject, forceSend); + } + + // Handle post processing and resetting of the NetworkObject + PostProcessDirtyObject(networkObject); + ResetDirtyObject(networkObject, forceSend); + } + /// /// Sends NetworkVariable deltas /// - /// internal only, when changing ownership we want to send this before the change in ownership message + /// Refer to the definition. internal void NetworkBehaviourUpdate(bool forceSend = false) { #if DEVELOPMENT_BUILD || UNITY_EDITOR @@ -44,78 +207,11 @@ internal void NetworkBehaviourUpdate(bool forceSend = false) // trying to process them, even if they were previously marked as dirty. m_DirtyNetworkObjects.RemoveWhere((sobj) => sobj == null); - if (m_ConnectionManager.LocalClient.IsServer) - { - foreach (var dirtyObj in m_DirtyNetworkObjects) - { - for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) - { - dirtyObj.ChildNetworkBehaviours[k].PreVariableUpdate(); - } - - for (int i = 0; i < m_ConnectionManager.ConnectedClientsList.Count; i++) - { - var client = m_ConnectionManager.ConnectedClientsList[i]; - if (m_NetworkManager.DistributedAuthorityMode || dirtyObj.IsNetworkVisibleTo(client.ClientId)) - { - // Sync just the variables for just the objects this client sees - for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) - { - dirtyObj.ChildNetworkBehaviours[k].NetworkVariableUpdate(client.ClientId, forceSend); - } - } - } - } - } - else - { - // when client updates the server, it tells it about all its objects - foreach (var sobj in m_DirtyNetworkObjects) - { - if (sobj.IsOwner) - { - for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) - { - sobj.ChildNetworkBehaviours[k].PreVariableUpdate(); - } - for (int k = 0; k < sobj.ChildNetworkBehaviours.Count; k++) - { - sobj.ChildNetworkBehaviours[k].NetworkVariableUpdate(NetworkManager.ServerClientId, forceSend); - } - } - } - } - foreach (var dirtyObj in m_DirtyNetworkObjects) { - for (int k = 0; k < dirtyObj.ChildNetworkBehaviours.Count; k++) - { - var behaviour = dirtyObj.ChildNetworkBehaviours[k]; - for (int i = 0; i < behaviour.NetworkVariableFields.Count; i++) - { - // Set to true for NetworkVariable to ignore duplication of the - // "internal original value" for collections support. - behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = true; - if (behaviour.NetworkVariableFields[i].IsDirty() && - !behaviour.NetworkVariableIndexesToResetSet.Contains(i)) - { - behaviour.NetworkVariableIndexesToResetSet.Add(i); - behaviour.NetworkVariableIndexesToReset.Add(i); - } - // Reset back to false when done - behaviour.NetworkVariableFields[i].NetworkUpdaterCheck = false; - } - } + ProcessDirtyObject(dirtyObj, forceSend); } - // Now, reset all the no-longer-dirty variables - foreach (var dirtyObj in m_DirtyNetworkObjects) - { - foreach (var behaviour in dirtyObj.ChildNetworkBehaviours) - { - behaviour.PostNetworkVariableWrite(forceSend); - } - } m_DirtyNetworkObjects.Clear(); } finally @@ -130,25 +226,21 @@ internal void Initialize(NetworkManager networkManager) { m_NetworkManager = networkManager; m_ConnectionManager = networkManager.ConnectionManager; - m_NetworkManager.NetworkTickSystem.Tick += NetworkBehaviourUpdater_Tick; + m_NetworkManager.NetworkTickSystem.Tick += OnNetworkTick; } internal void Shutdown() { - m_NetworkManager.NetworkTickSystem.Tick -= NetworkBehaviourUpdater_Tick; + m_NetworkManager.NetworkTickSystem.Tick -= OnNetworkTick; } - // Order of operations requires NetworkVariable updates first then showing NetworkObjects - private void NetworkBehaviourUpdater_Tick() + /// + /// Process any dirty s on each new + /// network tick. + /// + private void OnNetworkTick() { - // First update NetworkVariables NetworkBehaviourUpdate(); - - // Then show any NetworkObjects queued to be made visible/shown - m_NetworkManager.SpawnManager.HandleNetworkObjectShow(); - - // Handle object redistribution (DA + disabled scene management only) - m_NetworkManager.HandleRedistributionToClients(); } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 5fe071ce0e..3da1da9045 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -188,11 +188,6 @@ public bool DAHost ///
internal void HandleRedistributionToClients() { - if (!DistributedAuthorityMode || !RedistributeToClients || NetworkConfig.EnableSceneManagement || ShutdownInProgress) - { - return; - } - foreach (var clientId in ClientsToRedistribute) { SpawnManager.DistributeNetworkObjects(clientId); @@ -261,7 +256,7 @@ internal void PromoteSessionOwner(ulong clientId) var clients = ConnectionManager.ConnectedClientIds.Where(c => c != LocalClientId).ToArray(); foreach (var targetClient in clients) { - ConnectionManager.SendMessage(ref sessionOwnerMessage, NetworkDelivery.ReliableSequenced, targetClient); + ConnectionManager.SendMessage(ref sessionOwnerMessage, MessageDeliveryType.DefaultDelivery, targetClient); } } @@ -441,6 +436,17 @@ public void NetworkUpdate(NetworkUpdateStage updateStage) SpawnManager.DeferredDespawnUpdate(ServerTime); } + // Send any pending objects to be shown (in-between ticks) + SpawnManager.HandleNetworkObjectShow(true); + + // Handles object redistribution when scene management is disabled and + // using a distributed authority network topology. Only set specific to + // this configuration and when a client connects. + if (RedistributeToClients) + { + HandleRedistributionToClients(); + } + // Update any NetworkObject's registered to notify of scene migration changes. SpawnManager.UpdateNetworkObjectSceneChanges(); @@ -1540,13 +1546,27 @@ internal void ShutdownInternal() #if UNITY_EDITOR EndNetworkSession(); #endif - if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(ShutdownInternal)); } - OnPreShutdown?.Invoke(); + // Always wrap events that can invoke user script in a + // try-catch to assure any proceeding script is still + // executed. + // Example: + // In editor some script registered to OnPreShutdown + // throws and exception. The UnregisterAllNetworkUpdates + // will never be invoked which means it will continue to + // be invoked outside of play mode. + try + { + OnPreShutdown?.Invoke(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } this.UnregisterAllNetworkUpdates(); @@ -1633,30 +1653,59 @@ internal void ShutdownInternal() NetworkTickSystem = null; } - // Ensures that the NetworkManager is cleaned up before OnDestroy is run on NetworkObjects and NetworkBehaviours when quitting the application. private void OnApplicationQuit() { + // Abrupt shutdown (or immediate exit of play mode). + // Assure we unregister from network updates. + this.UnregisterAllNetworkUpdates(); + // Make sure ShutdownInProgress returns true during this time m_ShuttingDown = true; + // Exit early if this is invoked and the Singleton has yet to be set. + if (Singleton == null && !IsListening) + { + return; + } OnDestroy(); +#if UNITY_EDITOR + if (Singleton != null) + { + Debug.LogWarning($"[nameof({nameof(OnApplicationQuit)}][{nameof(NetworkManager)}][{name}] Singleton is not null after invoking OnDestroy. Singleton instance name is {Singleton.name}. Do you have more than one {nameof(NetworkManager)} instance in the DDOL scene?"); + } +#endif } // Note that this gets also called manually by OnSceneUnloaded and OnApplicationQuit private void OnDestroy() { - ShutdownInternal(); + try + { + ShutdownInternal(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded; - // Notify we are destroying NetworkManager - OnDestroying?.Invoke(this); + // try-catch to assure we reset the Singleton and, if in the editor, + // unscubscribe from playModeStateChanged. + try + { + // Notify we are destroying NetworkManager + OnDestroying?.Invoke(this); + } + catch (Exception ex) + { + Debug.LogException(ex); + } if (Singleton == this) { Singleton = null; } - #if UNITY_EDITOR EditorApplication.playModeStateChanged -= ModeChanged; #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 9282578f5a..109566a307 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -798,7 +798,7 @@ public OwnershipRequestStatus RequestOwnership() }; var sendTarget = NetworkManager.DAHost ? OwnerClientId : NetworkManager.ServerClientId; - NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, NetworkDelivery.Reliable, sendTarget); + NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, MessageDeliveryType.DefaultDelivery, sendTarget); return OwnershipRequestStatus.RequestSent; } @@ -885,7 +885,7 @@ internal void OwnershipRequest(ulong clientRequestingOwnership) }; var sendTarget = NetworkManager.DAHost ? clientRequestingOwnership : NetworkManager.ServerClientId; - NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, NetworkDelivery.Reliable, sendTarget); + NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, MessageDeliveryType.DefaultDelivery, sendTarget); } } @@ -1080,14 +1080,14 @@ internal void SendOwnershipStatusUpdate() { continue; } - NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, NetworkDelivery.Reliable, clientId); + NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, MessageDeliveryType.DefaultDelivery, clientId); } } else { changeOwnership.ClientIdCount = Observers.Count; changeOwnership.ClientIds = Observers.ToArray(); - NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, NetworkDelivery.Reliable, NetworkManager.ServerClientId); + NetworkManager.ConnectionManager.SendMessage(ref changeOwnership, MessageDeliveryType.DefaultDelivery, NetworkManager.ServerClientId); } } @@ -1611,12 +1611,12 @@ public void NetworkHide(ulong clientId) if (!NetworkManager.DAHost) { // Send destroy call to service or DAHost - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ServerClientId); + size = NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, NetworkManager.ServerClientId); } else // DAHost mocking service { // Send destroy call - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); + size = NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, clientId); // Broadcast the destroy to all clients so they can update their observers list foreach (var client in NetworkManager.ConnectionManager.ConnectedClientIds) { @@ -1624,14 +1624,14 @@ public void NetworkHide(ulong clientId) { continue; } - size += NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, client); + size += NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, client); } } } else { // Send destroy call - size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); + size = NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, clientId); } NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, this, size); } @@ -1738,18 +1738,28 @@ private void OnDestroy() if (!NetworkManager.ShutdownInProgress) { // Since we still have a session connection, log locally and on the server to inform user of this issue. - if (NetworkManager.LogLevel <= LogLevel.Error) + // If the NetworkObject's GaaeObject is not valid or the scene is no longer valid or loaded, then this was due to the + // unloading of a scene which is done by the authority... + if (gameObject != null && gameObject.scene.IsValid() && gameObject.scene.isLoaded) { - if (NetworkManager.DistributedAuthorityMode) + if (NetworkManager.LogLevel <= LogLevel.Error && gameObject != null && gameObject.scene.IsValid() && gameObject.scene.isLoaded) { - NetworkLog.LogError($"[Invalid Destroy][{gameObject.name}][NetworkObjectId:{NetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-owner client is not valid during a distributed authority session. Call {nameof(Destroy)} or {nameof(Despawn)} on the client-owner instead."); - } - else - { - NetworkLog.LogErrorServer($"[Invalid Destroy][{gameObject.name}][NetworkObjectId:{NetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead."); + if (NetworkManager.DistributedAuthorityMode) + { + NetworkLog.LogError($"[Invalid Destroy][{gameObject.name}][NetworkObjectId:{NetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-owner client is not valid during a distributed authority session. Call {nameof(Destroy)} or {nameof(Despawn)} on the client-owner instead."); + } + else + { + NetworkLog.LogErrorServer($"[Invalid Destroy][{gameObject.name}][NetworkObjectId:{NetworkObjectId}] Destroy a spawned {nameof(NetworkObject)} on a non-host client is not valid. Call {nameof(Destroy)} or {nameof(Despawn)} on the server/host instead."); + } } + return; + } + else + { + // If the destroy was authority scene event triggered, then mark this destroy as authority triggered. + isAuthorityDestroy = true; } - return; } // Otherwise, clients can despawn NetworkObjects while shutting down and should not generate any messages when this happens } @@ -1825,7 +1835,7 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } } - NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); + NetworkManager.SpawnManager.AuthorityLocalSpawn(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); if ((NetworkManager.DistributedAuthorityMode && NetworkManager.DAHost) || (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer)) { @@ -1943,6 +1953,12 @@ public void Spawn(bool destroyWithScene = false) /// /// Spawns a across the network with a given owner. Can only be called from server /// + /// + /// When using a client-server or distributed authority network topology, you should take into consideration any components + /// that might require ownership checks while running through the spawn process. To avoid issues that could arise by initially spawning + /// without ownership, it is recommended to use first, so it is spawned as both the owner and the authority, and then use + /// to change the ownership to the intended client.
+ ///
/// The clientId to own the object /// Should the object be destroyed when the scene is changed public void SpawnWithOwnership(ulong clientId, bool destroyWithScene = false) @@ -2303,9 +2319,10 @@ private void OnTransformParentChanged() } var removeParent = false; var parentTransform = transform.parent; + var parentObject = (NetworkObject)null; if (parentTransform != null) { - if (!transform.parent.TryGetComponent(out var parentObject)) + if (!transform.parent.TryGetComponent(out parentObject)) { transform.parent = m_CachedParent; AuthorityAppliedParenting = false; @@ -2358,7 +2375,7 @@ private void OnTransformParentChanged() { if (!NetworkManager.DAHost) { - NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, 0); + NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, 0); return; } else @@ -2369,7 +2386,7 @@ private void OnTransformParentChanged() { continue; } - NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); + NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, clientId); } } } @@ -2393,7 +2410,7 @@ private void OnTransformParentChanged() clientIds[idx++] = clientId; } } - NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientIds, idx); + NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, clientIds, idx); } } } @@ -3287,8 +3304,9 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf throw new SpawnStateException($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); } - // Do not invoke Pre spawn here (SynchronizeNetworkBehaviours needs to be invoked prior to this) - networkManager.SpawnManager.SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.NetworkObjectId, sceneObject.IsSceneObject, sceneObject.IsPlayerObject, sceneObject.OwnerClientId, sceneObject.DestroyWithScene); + // Invoke the non-authority local spawn method + // (It also invokes post spawn and handles processing derferred messages) + networkManager.SpawnManager.NonAuthorityLocalSpawn(networkObject, sceneObject, sceneObject.DestroyWithScene); if (sceneObject.SyncObservers) { diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs index cd29c6a651..d9319bcfb8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs @@ -108,8 +108,7 @@ private static void LogServer(string message, LogType logType) Message = message, SenderId = localId }; - var size = networkManager.ConnectionManager.SendMessage(ref networkMessage, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId); - + var size = networkManager.ConnectionManager.SendMessage(ref networkMessage, MessageDeliveryType.DefaultDelivery, NetworkManager.ServerClientId); networkManager.NetworkMetrics.TrackServerLogSent(NetworkManager.ServerClientId, (uint)logType, size); } } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs index 4e697acc0d..ec460cc821 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs @@ -6,6 +6,44 @@ namespace Unity.Netcode { + /// + /// Enum representing the different types of messages that can be sent over the network. + /// The values cannot be changed, as they are used to serialize and deserialize messages. + /// Adding new messages should be done by adding new values to the end of the enum + /// using the next free value. + /// + /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + /// Add any new Message types to this table at the END with incremented index value + /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + internal enum NetworkMessageTypes : uint + { + ConnectionApproved = 0, + ConnectionRequest = 1, + ChangeOwnership = 2, + ClientConnected = 3, + ClientDisconnected = 4, + ClientRpc = 5, + CreateObject = 6, + DestroyObject = 7, + DisconnectReason = 8, + ForwardClientRpc = 9, + ForwardServerRpc = 10, + NamedMessage = 11, + NetworkTransformMessage = 12, + NetworkVariableDelta = 13, + ParentSync = 14, + Proxy = 15, + Rpc = 16, + SceneEvent = 17, + ServerLog = 18, + ServerRpc = 19, + SessionOwner = 20, + TimeSync = 21, + Unnamed = 22, + AnticipationCounterSyncPingMessage = 23, + AnticipationCounterSyncPongMessage = 24, + } + internal struct ILPPMessageProvider : INetworkMessageProvider { #pragma warning disable IDE1006 // disable naming rule violation check @@ -13,70 +51,15 @@ internal struct ILPPMessageProvider : INetworkMessageProvider internal static readonly List __network_message_types = new List(); #pragma warning restore IDE1006 // restore naming rule violation check - /// - /// Enum representing the different types of messages that can be sent over the network. - /// The values cannot be changed, as they are used to serialize and deserialize messages. - /// Adding new messages should be done by adding new values to the end of the enum - /// using the next free value. - /// - /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - /// Add any new Message types to this table at the END with incremented index value - /// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - internal enum NetworkMessageTypes : uint - { - ConnectionApproved = 0, - ConnectionRequest = 1, - ChangeOwnership = 2, - ClientConnected = 3, - ClientDisconnected = 4, - ClientRpc = 5, - CreateObject = 6, - DestroyObject = 7, - DisconnectReason = 8, - ForwardClientRpc = 9, - ForwardServerRpc = 10, - NamedMessage = 11, - NetworkTransformMessage = 12, - NetworkVariableDelta = 13, - ParentSync = 14, - Proxy = 15, - Rpc = 16, - SceneEvent = 17, - ServerLog = 18, - ServerRpc = 19, - SessionOwner = 20, - TimeSync = 21, - Unnamed = 22, - AnticipationCounterSyncPingMessage = 23, - AnticipationCounterSyncPongMessage = 24, - } - - // Enable this for integration tests that need no message types defined internal static bool IntegrationTestNoMessages; - public List GetMessages() + /// + /// Returns a table of message type to NetworkMessageTypes enum value + /// + /// Dictionary + internal static Dictionary GetMessageTypesMap() { - // return no message types when defined for integration tests - if (IntegrationTestNoMessages) - { - return new List(); - } - var messageTypeCount = Enum.GetValues(typeof(NetworkMessageTypes)).Length; - // Assure the allowed types count is the same as our NetworkMessageType enum count - if (__network_message_types.Count != messageTypeCount) - { - throw new Exception($"Allowed types is not equal to the number of message type indices! Allowed Count: {__network_message_types.Count} | Index Count: {messageTypeCount}"); - } - - // Populate with blanks to be replaced later - var adjustedMessageTypes = new List(); - var blank = new NetworkMessageManager.MessageWithHandler(); - for (int i = 0; i < messageTypeCount; i++) - { - adjustedMessageTypes.Add(blank); - } - // Create a type to enum index lookup table // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // Add new Message types to this table paired with its new NetworkMessageTypes enum @@ -109,6 +92,33 @@ internal enum NetworkMessageTypes : uint { typeof(AnticipationCounterSyncPingMessage), NetworkMessageTypes.AnticipationCounterSyncPingMessage}, { typeof(AnticipationCounterSyncPongMessage), NetworkMessageTypes.AnticipationCounterSyncPongMessage}, }; + return messageTypes; + } + + public List GetMessages() + { + // return no message types when defined for integration tests + if (IntegrationTestNoMessages) + { + return new List(); + } + var messageTypeCount = Enum.GetValues(typeof(NetworkMessageTypes)).Length; + // Assure the allowed types count is the same as our NetworkMessageType enum count + if (__network_message_types.Count != messageTypeCount) + { + throw new Exception($"Allowed types is not equal to the number of message type indices! Allowed Count: {__network_message_types.Count} | Index Count: {messageTypeCount}"); + } + + // Populate with blanks to be replaced later + var adjustedMessageTypes = new List(); + var blank = new NetworkMessageManager.MessageWithHandler(); + for (int i = 0; i < messageTypeCount; i++) + { + adjustedMessageTypes.Add(blank); + } + + // Get the message type to NetworkMessageTypes enum value table + var messageTypes = GetMessageTypesMap(); // Assure the type to lookup table count and NetworkMessageType enum count matches (i.e. to catch human error when adding new messages) if (messageTypes.Count != messageTypeCount) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs index 46267d5220..fc1d41d372 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs @@ -44,4 +44,14 @@ internal interface INetworkMessage void Handle(ref NetworkContext context); int Version { get; } } + + + internal static class MessageDeliveryType where T : INetworkMessage + { + internal static NetworkDelivery DefaultDelivery { get; private set; } + internal static void Initialize() + { + DefaultDelivery = MessageDelivery.GetDelivery(typeof(T)); + } + } } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs new file mode 100644 index 0000000000..e8cf2ca6db --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using Unity.Netcode; +using UnityEditor; +using UnityEngine; + +internal static class MessageDelivery +{ + private static Dictionary s_MessageToDelivery = new Dictionary(); + + private static Dictionary s_MessageToMessageType = new Dictionary(); + + /// + /// - Skip named and unnamed since they inherently can have their network delivery type adjusted + /// when sending the message via public API. + /// - Skip the time sync messages since it has always used unreliable network delivery. + /// + private static HashSet s_SkipMessageTypes = new HashSet(){ + NetworkMessageTypes.NamedMessage, NetworkMessageTypes.Unnamed}; + + [RuntimeInitializeOnLoadMethod] + private static void OnApplicationStart() + { + UpdateMessageTypes(); + } + + /// + /// FIrst pass at providing an easier path to configuring the network + /// delivery type for the message type. + /// TODO: Once coalesces all reliable messages + /// and/or organizes by a more unified order of operation tracking built into the + /// buffer and/or converts all places that would normally generate a message to + /// commands that will, eventually, generate messages. + /// For now, we are sending all reliable fragmented sequenced. + /// + private static void UpdateMessageTypes() + { + s_MessageToDelivery.Clear(); + var networkMessageTypes = Enum.GetValues(typeof(NetworkMessageTypes)); + foreach (var messageTypeObject in networkMessageTypes) + { + var messageType = (NetworkMessageTypes)messageTypeObject; + if (s_SkipMessageTypes.Contains(messageType)) + { + continue; + } + s_MessageToDelivery.Add(messageType, NetworkDelivery.ReliableFragmentedSequenced); + } + s_MessageToMessageType = ILPPMessageProvider.GetMessageTypesMap(); + + // Fast path look-ups + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + // RpcMessage.cs + { + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + } + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + } + +#if UNITY_EDITOR + [InitializeOnLoadMethod] + [InitializeOnEnterPlayMode] + private static void OnEnterPlayMode() + { + UpdateMessageTypes(); + } +#endif + internal static NetworkDelivery GetDelivery(Type type) + { + // Return the default if not registered or null + if (type == null || s_SkipMessageTypes.Contains(s_MessageToMessageType[type])) + { + return NetworkDelivery.ReliableFragmentedSequenced; + } + return GetDelivery(s_MessageToMessageType[type]); + } + + internal static NetworkDelivery GetDelivery(NetworkMessageTypes messageType) + { + if (s_SkipMessageTypes.Contains(messageType)) + { + throw new Exception($"{messageType} is not registered in the message type to network delivery map!"); + } + return s_MessageToDelivery[messageType]; + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs.meta b/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs.meta new file mode 100644 index 0000000000..2742d4ab10 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1259b14a36330714f8bbcd688260b782 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs index 85cfce4f58..569b0349c8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ChangeOwnershipMessage.cs @@ -266,7 +266,7 @@ private bool HandleDAHostMessageForwarding(ref NetworkManager networkManager, ul { var clientList = ClientIdCount > 0 ? ClientIds : networkManager.ConnectedClientsIds; - var message = new ChangeOwnershipMessage() + var message = new ChangeOwnershipMessage { NetworkObjectId = NetworkObjectId, OwnerClientId = OwnerClientId, @@ -276,14 +276,14 @@ private bool HandleDAHostMessageForwarding(ref NetworkManager networkManager, ul ClientIdCount = 0, ChangeMessageType = ChangeMessageType, }; - + var networkDelivery = MessageDeliveryType.DefaultDelivery; if (ChangeMessageType == ChangeType.RequestDenied) { // If the local DAHost's client is not the target, then forward to the target if (RequestClientId != networkManager.LocalClientId) { message.OwnershipRequestResponseStatus = OwnershipRequestResponseStatus; - networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, RequestClientId); + networkManager.ConnectionManager.SendMessage(ref message, networkDelivery, RequestClientId); // We don't want the local DAHost's client to process this message return false; @@ -294,7 +294,7 @@ private bool HandleDAHostMessageForwarding(ref NetworkManager networkManager, ul // If the DAHost client is not authority, just forward the message to the authority if (OwnerClientId != networkManager.LocalClientId) { - networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, OwnerClientId); + networkManager.ConnectionManager.SendMessage(ref message, networkDelivery, OwnerClientId); // We don't want the local DAHost's client to process this message return false; @@ -310,7 +310,7 @@ private bool HandleDAHostMessageForwarding(ref NetworkManager networkManager, ul continue; } - networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, clientId); + networkManager.ConnectionManager.SendMessage(ref message, networkDelivery, clientId); } } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs index 1fa9a93e53..c55d4df0da 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ClientConnectedMessage.cs @@ -49,7 +49,7 @@ public void Handle(ref NetworkContext context) networkManager.ConnectionManager.InvokeOnPeerConnectedCallback(ClientId); } - // DANGO-TODO: Remove the session owner object distribution check once the service handles object distribution + // This handles object redistribution when scene management is disabled if (networkManager.DistributedAuthorityMode && networkManager.CMBServiceConnection && !networkManager.NetworkConfig.EnableSceneManagement) { // Don't redistribute for the local instance diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/DestroyObjectMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/DestroyObjectMessage.cs index be037e7b90..1316defab8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/DestroyObjectMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/DestroyObjectMessage.cs @@ -192,12 +192,12 @@ private void HandleDAHostForwardMessage(ulong senderId, ref NetworkManager netwo }; var ownerClientId = networkObject == null ? senderId : networkObject.OwnerClientId; var clientIds = networkObject == null ? networkManager.ConnectionManager.ConnectedClientIds : networkObject.Observers.ToList(); - + var networkDelivery = MessageDeliveryType.DefaultDelivery; foreach (var clientId in clientIds) { if (clientId != networkManager.LocalClientId && clientId != ownerClientId) { - networkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); + networkManager.ConnectionManager.SendMessage(ref message, networkDelivery, clientId); } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NetworkTransformMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NetworkTransformMessage.cs index cb01fcf7b4..5ac5a2a069 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NetworkTransformMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NetworkTransformMessage.cs @@ -32,7 +32,28 @@ public void Serialize(FastBufferWriter writer, int targetVersion) } else { - BytesWritten = NetworkTransform.SerializeMessage(writer, targetVersion); + var position = writer.Position; + // Provides the source of the message (NetworkObject-->NetworkTransform : NetworkBehaviour). + BytePacker.WriteValueBitPacked(writer, NetworkTransform.NetworkObjectId); +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + var networkObjectIdSize = writer.Position - position; +#endif + BytePacker.WriteValueBitPacked(writer, (int)NetworkTransform.NetworkBehaviourId); +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + var networkBehaviourIdSize = writer.Position - position - networkObjectIdSize; +#endif + + // Serialize the current local state. + writer.WriteNetworkSerializable(NetworkTransform.LocalAuthoritativeNetworkState); +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + var networkTransformStateSize = writer.Position - position - networkObjectIdSize - networkBehaviourIdSize; +#endif + BytesWritten = writer.Position - position; + +#if NGO_NETWORKTRANSFORMSTATE_LOGWRITESIZE + var parentInfo = writer.Position - position - networkObjectIdSize - networkBehaviourIdSize - networkTransformStateSize; + Debug.Log($"[NO-ID: {networkObjectIdSize}][NB-ID: {networkBehaviourIdSize}][NTState: {networkTransformStateSize}][PINFO: {parentInfo}][Total: {BytesWritten}]"); +#endif } } @@ -44,6 +65,10 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int Debug.LogError($"[{nameof(NetworkTransformMessage)}] System owner context was not of type {nameof(NetworkManager)}!"); return false; } + if (networkManager.ShutdownInProgress) + { + return false; + } var currentPosition = reader.Position; var networkObjectId = (ulong)0; var networkBehaviourId = 0; @@ -89,7 +114,9 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int isServerAuthoritative = NetworkTransform.IsServerAuthoritative(); ownerAuthoritativeServerSide = !isServerAuthoritative && networkManager.IsServer; + // Deserialize the inbound NetworkTransformState reader.ReadNetworkSerializableInPlace(ref NetworkTransform.InboundState); + NetworkTransform.InboundState.LastSerializedSize = reader.Position - currentPosition; } else @@ -114,22 +141,22 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int { if (ownerAuthoritativeServerSide) { - var targetCount = 1; - - if (networkManager.DistributedAuthorityMode && networkManager.DAHost) - { - ByteUnpacker.ReadValueBitPacked(reader, out targetCount); - } + var targetCount = networkObject.Observers.Count; var targetIds = stackalloc ulong[targetCount]; if (networkManager.DistributedAuthorityMode && networkManager.DAHost) { - var targetId = (ulong)0; - for (int i = 0; i < targetCount; i++) + var count = 0; + foreach (var targetId in networkObject.Observers) { - ByteUnpacker.ReadValueBitPacked(reader, out targetId); - targetIds[i] = targetId; + targetIds[count] = targetId; + // Sanity check, this should never happen. + if (count >= targetCount) + { + Debug.LogError($"[{nameof(NetworkTransformMessage)}] Exceeded total number of observers!"); + } + count++; } } @@ -208,6 +235,8 @@ public void Handle(ref NetworkContext context) Debug.LogError($"[{nameof(NetworkTransformMessage)}][Dropped] Reciever {nameof(NetworkTransform)} was not set!"); return; } + + // Update the state NetworkTransform.TransformStateUpdate(context.SenderId); } } diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs index fa93c04937..2c7276365e 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/NetworkVariableBase.cs @@ -33,11 +33,6 @@ public abstract class NetworkVariableBase : IDisposable [NonSerialized] internal double LastUpdateSent; - /// - /// The delivery type (QoS) to send data with - /// - internal const NetworkDelivery Delivery = NetworkDelivery.ReliableFragmentedSequenced; - /// /// Maintains a link to the associated NetworkBehaviour /// diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 134cc98ae8..92906a0406 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -146,9 +146,8 @@ public class SceneEvent ///
public class NetworkSceneManager : IDisposable { - private const NetworkDelivery k_DeliveryType = NetworkDelivery.ReliableFragmentedSequenced; internal const int InvalidSceneNameOrPath = -1; - + private NetworkDelivery m_NetworkDelivery; // Used to be able to turn re-synchronization off internal static bool DisableReSynchronization; @@ -815,6 +814,7 @@ internal NetworkSceneManager(NetworkManager networkManager) { NetworkManager = networkManager; SceneEventDataStore = new Dictionary(); + m_NetworkDelivery = MessageDeliveryType.DefaultDelivery; // Generates the scene name to hash value GenerateScenesInBuild(); @@ -1080,7 +1080,7 @@ private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds) { EventData = sceneEvent, }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ServerClientId); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, NetworkManager.ServerClientId); NetworkManager.NetworkMetrics.TrackSceneEventSent(NetworkManager.ServerClientId, (uint)sceneEvent.SceneEventType, SceneNameFromHash(sceneEvent.SceneHash), size); } @@ -1094,7 +1094,7 @@ private void SendSceneEventData(uint sceneEventId, ulong[] targetClientIds) EventData = sceneEvent, }; var sendTarget = distributedAuthority && !NetworkManager.DAHost ? NetworkManager.ServerClientId : clientId; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, sendTarget); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, sendTarget); NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEvent.SceneEventType, SceneNameFromHash(sceneEvent.SceneHash), size); } } @@ -1227,7 +1227,7 @@ private bool OnSceneEventProgressCompleted(SceneEventProgress sceneEventProgress EventData = sceneEventData }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ConnectedClientsIds); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, NetworkManager.ConnectedClientsIds); NetworkManager.NetworkMetrics.TrackSceneEventSent( NetworkManager.ConnectedClientsIds, (uint)sceneEventProgress.SceneEventType, @@ -1434,7 +1434,7 @@ private void OnSceneUnloaded(uint sceneEventId) // This might seem like it needs more logic to determine the target, but the only scenario where we send to the session owner is if the // current instance is the DAHost. var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, target); NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size); } EndSceneEvent(sceneEventId); @@ -1823,7 +1823,7 @@ private void OnSessionOwnerLoadedScene(uint sceneEventId, Scene scene) if (!keyValuePairBySceneHandle.Value.IsPlayerObject) { // All in-scene placed NetworkObjects default to being owned by the server - NetworkManager.SpawnManager.SpawnNetworkObjectLocally(keyValuePairBySceneHandle.Value, + NetworkManager.SpawnManager.AuthorityLocalSpawn(keyValuePairBySceneHandle.Value, NetworkManager.SpawnManager.GetNetworkObjectId(), true, false, NetworkManager.LocalClientId, true); } } @@ -1889,7 +1889,7 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene) EventData = sceneEventData, }; var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, target); NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size); } else @@ -2054,11 +2054,11 @@ internal void SynchronizeNetworkObjects(ulong clientId, bool synchronizingServic var size = 0; if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) { - size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, NetworkManager.ServerClientId); + size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, NetworkManager.ServerClientId); } else { - size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, clientId); + size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, clientId); } NetworkManager.NetworkMetrics.TrackSceneEventSent(clientId, (uint)sceneEventData.SceneEventType, "", size); @@ -2211,7 +2211,7 @@ private void ClientLoadedSynchronization(uint sceneEventId) { EventData = responseSceneEventData }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, target); NetworkManager.NetworkMetrics.TrackSceneEventSent(NetworkManager.ServerClientId, (uint)responseSceneEventData.SceneEventType, sceneName, size); @@ -2344,7 +2344,7 @@ private void HandleClientSceneEvent(uint sceneEventId) EventData = sceneEventData, }; var target = NetworkManager.DAHost ? NetworkManager.CurrentSessionOwner : NetworkManager.ServerClientId; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, k_DeliveryType, target); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, m_NetworkDelivery, target); NetworkManager.NetworkMetrics.TrackSceneEventSent(target, (uint)sceneEventData.SceneEventType, SceneNameFromHash(sceneEventData.SceneHash), size); } else @@ -2599,7 +2599,7 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader) { continue; } - NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, client); + NetworkManager.MessageManager.SendMessage(ref message, m_NetworkDelivery, client); } } } @@ -2618,7 +2618,7 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader) { EventData = sceneEventData, }; - NetworkManager.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, sceneEventData.TargetClientId); + NetworkManager.MessageManager.SendMessage(ref message, m_NetworkDelivery, sceneEventData.TargetClientId); EndSceneEvent(sceneEventData.SceneEventId); return; } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 5cba446a1e..1ae90338fb 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using UnityEngine; @@ -653,7 +654,7 @@ internal void ChangeOwnership(NetworkObject networkObject, ulong clientId, bool /// /// the client to check /// the to check if it is pending show - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool IsObjectVisibilityPending(ulong clientId, ref NetworkObject networkObject) { if (NetworkManager.DistributedAuthorityMode && ClientsToShowObject.ContainsKey(networkObject)) @@ -1024,6 +1025,7 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO } /// + /// Only spawn authority instances should invoke this. /// Invoked from: /// - ConnectionManager after instantiating a player prefab when running in client-server. /// - NetworkObject when spawning a newly instantiated NetworkObject for the first time. @@ -1036,7 +1038,7 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO /// Distributed Authority: /// DAHost client and standard DA clients invoke this method. /// - internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + internal void AuthorityLocalSpawn(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) { if (networkObject == null) { @@ -1101,6 +1103,41 @@ internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong netwo } SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); + + // When done spawning invoke post spawn + networkObject.InvokeBehaviourNetworkPostSpawn(); + + // No need to check for deferred messages since this method is used for authority spawning. + } + + /// + /// Only spawn non-authority instances should invoke this. + /// This is invoked to instantiate an authority spawned , and + /// is only invoked by: + /// + /// + /// IMPORTANT: Pre spawn methods need to be invoked from within . + /// + internal void NonAuthorityLocalSpawn(NetworkObject networkObject, in NetworkObject.SceneObject sceneObject, bool destroyWithScene) + { + if (networkObject == null) + { + throw new ArgumentNullException(nameof(networkObject), "Cannot spawn null object"); + } + + if (networkObject.IsSpawned) + { + throw new SpawnStateException($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); + } + + // Do not invoke Pre spawn here (SynchronizeNetworkBehaviours needs to be invoked prior to this) + SpawnNetworkObjectLocallyCommon(networkObject, sceneObject.NetworkObjectId, sceneObject.IsSceneObject, sceneObject.IsPlayerObject, sceneObject.OwnerClientId, destroyWithScene); + + // It is ok to invoke NetworkBehaviour.OnPostSpawn methods + networkObject.InvokeBehaviourNetworkPostSpawn(); + + // Process any deferred messages once the object is 100% finished spawning, + NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkObject.NetworkObjectId); } internal void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) @@ -1177,8 +1214,6 @@ internal void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkObject.InvokeBehaviourNetworkSpawn(); - NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkId); - // propagate the IsSceneObject setting to child NetworkObjects var children = networkObject.GetComponentsInChildren(); foreach (var childObject in children) @@ -1208,9 +1243,6 @@ internal void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong { networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; } - - // It is now ok to invoke NetworkBehaviour.OnPostSpawn methods - networkObject.InvokeBehaviourNetworkPostSpawn(); } internal Dictionary NetworkObjectsToSynchronizeSceneChanges = new Dictionary(); @@ -1267,7 +1299,6 @@ internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject { return; } - var message = new CreateObjectMessage { ObjectInfo = networkObject.GetMessageSceneObject(clientId, NetworkManager.DistributedAuthorityMode), @@ -1275,7 +1306,7 @@ internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject UpdateObservers = NetworkManager.DistributedAuthorityMode, ObserverIds = NetworkManager.DistributedAuthorityMode ? networkObject.Observers.ToArray() : null, }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, clientId); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, clientId); NetworkManager.NetworkMetrics.TrackObjectSpawnSent(clientId, networkObject, size); } @@ -1299,7 +1330,7 @@ internal void SendSpawnCallForObserverUpdate(ulong[] newObservers, NetworkObject UpdateObservers = true, UpdateNewObservers = true, }; - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableFragmentedSequenced, NetworkManager.ServerClientId); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, MessageDeliveryType.DefaultDelivery, NetworkManager.ServerClientId); foreach (var clientId in newObservers) { // TODO: We might want to track observer update sent as well? @@ -1495,7 +1526,7 @@ internal void ServerSpawnSceneObjectsOnStartSweep() ownerId = NetworkManager.LocalClientId; } - SpawnNetworkObjectLocally(networkObjects[i], GetNetworkObjectId(), true, false, ownerId, true); + AuthorityLocalSpawn(networkObjects[i], GetNetworkObjectId(), true, false, ownerId, true); networkObjectsToSpawn.Add(networkObjects[i]); } } @@ -1677,9 +1708,10 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec IsTargetedDestroy = false, IsDistributedAuthority = distributedAuthority, }; + var networkDelivery = MessageDeliveryType.DefaultDelivery; foreach (var clientId in m_TargetClientIds) { - var size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientId); + var size = NetworkManager.ConnectionManager.SendMessage(ref message, networkDelivery, clientId); NetworkManager.NetworkMetrics.TrackObjectDestroySent(clientId, networkObject, size); } } @@ -1749,21 +1781,33 @@ internal void UpdateObservedNetworkObjects(ulong clientId) } /// - /// See + /// This is only invoked by during the stage. /// - internal void HandleNetworkObjectShow() + internal void HandleNetworkObjectShow(bool forceSend = false) { + // Covers any distributed authority client that is not the DAHost + var isDistributedAuthorityClient = NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost; + // Exit early if there is nothing to be shown + if ((isDistributedAuthorityClient && ClientsToShowObject.Count == 0) || (!isDistributedAuthorityClient && ObjectsToShowToClient.Count == 0)) + { + return; + } + // In distributed authority mode, we send a single message that is broadcasted to all clients // that will be shown the object (i.e. 1 message to service that then broadcasts that to the // targeted clients). When using a DAHost, we skip this and send like we do in client-server - if (NetworkManager.DistributedAuthorityMode && !NetworkManager.DAHost) + if (isDistributedAuthorityClient) { + var behaviourUpdater = NetworkManager.BehaviourUpdater; foreach (var entry in ClientsToShowObject) { if (entry.Key != null && entry.Key.IsSpawned) { try { + // Always push the most recent deltas when showing a NetworkObject + // to another client. + behaviourUpdater.ForceSendIfDirtyOnNetworkShow(entry.Key); SendSpawnCallForObserverUpdate(entry.Value.ToArray(), entry.Key); } catch (Exception ex) @@ -1790,6 +1834,10 @@ internal void HandleNetworkObjectShow() { try { + if (forceSend) + { + NetworkManager.BehaviourUpdater.ForceSendIfDirtyOnNetworkShow(networkObject); + } SendSpawnCallForObject(clientId, networkObject); } catch (Exception ex) diff --git a/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs b/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs index 652b59cc09..3bc0422494 100644 --- a/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs +++ b/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs @@ -108,6 +108,8 @@ public class NetworkTimeSystem /// private int m_TimeSyncFrequencyTicks; + private NetworkDelivery m_NetworkDelivery; + /// /// The constructor class for /// @@ -122,6 +124,7 @@ public NetworkTimeSystem(double localBufferSec, double serverBufferSec = k_Defau HardResetThresholdSec = hardResetThresholdSec; AdjustmentRatio = adjustmentRatio; m_TickLatencyAverage = 2; + m_NetworkDelivery = MessageDeliveryType.DefaultDelivery; } /// @@ -189,7 +192,7 @@ private void OnTickSyncTime() { Tick = m_NetworkTickSystem.ServerTime.Tick }; - m_ConnectionManager.SendMessage(ref message, NetworkDelivery.Unreliable, m_ConnectionManager.ConnectedClientIds); + m_ConnectionManager.SendMessage(ref message, m_NetworkDelivery, m_ConnectionManager.ConnectedClientIds); } #if DEVELOPMENT_BUILD || UNITY_EDITOR diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/DistributedAuthorityCodecTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/DistributedAuthorityCodecTests.cs index 61b75f49eb..a501a6403d 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/DistributedAuthorityCodecTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/DistributedAuthorityCodecTests.cs @@ -608,11 +608,12 @@ public IEnumerator SceneEventMessageObjectSceneChanged() private IEnumerator SendMessage(ref T message) where T : INetworkMessage { + var delivery = MessageDelivery.GetDelivery(typeof(T)); m_Client.MessageManager.SetVersion(k_ClientId, XXHash.Hash32(typeof(T).FullName), message.Version); var clientIds = new NativeArray(1, Allocator.Temp); clientIds[0] = k_ClientId; - m_Client.MessageManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, clientIds); + m_Client.MessageManager.SendMessage(ref message, delivery, clientIds); m_Client.MessageManager.ProcessSendQueues(); return m_ClientCodecHook.WaitForMessageReceived(message); } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/InvalidConnectionEventsTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/InvalidConnectionEventsTest.cs index d849bc216a..eafe1b5cd1 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/InvalidConnectionEventsTest.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/InvalidConnectionEventsTest.cs @@ -83,7 +83,7 @@ public IEnumerator WhenSendingConnectionApprovedToAlreadyConnectedClient_Connect { ConnectedClientIds = new NativeArray(0, Allocator.Temp) }; - m_ServerNetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, m_ClientNetworkManagers[0].LocalClientId); + m_ServerNetworkManager.ConnectionManager.SendMessage(ref message, MessageDelivery.GetDelivery(NetworkMessageTypes.ConnectionApproved), m_ClientNetworkManagers[0].LocalClientId); // Unnamed message is something to wait for. When this one is received, // we know the above one has also reached its destination. @@ -105,7 +105,7 @@ public IEnumerator WhenSendingConnectionApprovedToAlreadyConnectedClient_Connect public IEnumerator WhenSendingConnectionRequestToAnyClient_ConnectionRequestMessageIsRejected() { var message = new ConnectionRequestMessage(); - m_ServerNetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, m_ClientNetworkManagers[0].LocalClientId); + m_ServerNetworkManager.ConnectionManager.SendMessage(ref message, MessageDelivery.GetDelivery(NetworkMessageTypes.ConnectionRequest), m_ClientNetworkManagers[0].LocalClientId); // Unnamed message is something to wait for. When this one is received, // we know the above one has also reached its destination. @@ -127,7 +127,7 @@ public IEnumerator WhenSendingConnectionRequestToAnyClient_ConnectionRequestMess public IEnumerator WhenSendingConnectionRequestFromAlreadyConnectedClient_ConnectionRequestMessageIsRejected() { var message = new ConnectionRequestMessage(); - m_ClientNetworkManagers[0].ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, m_ServerNetworkManager.LocalClientId); + m_ClientNetworkManagers[0].ConnectionManager.SendMessage(ref message, MessageDelivery.GetDelivery(NetworkMessageTypes.ConnectionRequest), m_ServerNetworkManager.LocalClientId); // Unnamed message is something to wait for. When this one is received, // we know the above one has also reached its destination. @@ -152,7 +152,7 @@ public IEnumerator WhenSendingConnectionApprovedFromAnyClient_ConnectionApproved { ConnectedClientIds = new NativeArray(0, Allocator.Temp) }; - m_ClientNetworkManagers[0].ConnectionManager.SendMessage(ref message, NetworkDelivery.Reliable, m_ServerNetworkManager.LocalClientId); + m_ClientNetworkManagers[0].ConnectionManager.SendMessage(ref message, MessageDelivery.GetDelivery(NetworkMessageTypes.ConnectionApproved), m_ServerNetworkManager.LocalClientId); // Unnamed message is something to wait for. When this one is received, // we know the above one has also reached its destination. diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformSpawnSequences.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformSpawnSequences.cs new file mode 100644 index 0000000000..790b45b8ef --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformSpawnSequences.cs @@ -0,0 +1,359 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using Unity.Netcode.Components; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + + [TestFixture(HostOrServer.DAHost)] + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.Server)] + internal class NetworkTransformAutoParenting : IntegrationTestWithApproximation + { + public enum TransformSpace + { + World, + Local + } + + protected override int NumberOfClients => 4; + + private List m_PrefabsToSpawn = new List(); + private NetworkObject m_ParentToSpawn; + + private List m_ParentInstances = new List(); + private List m_ChildInstances = new List(); + private NetworkObject m_ChildInstance; + private NetworkObject m_FinalParent; + private ulong m_NetworkObjectIdToValidate; + + private TransformSpace m_ParentWorldOrLocal; + + + public NetworkTransformAutoParenting(HostOrServer host) : base(host) + { + } + + + public class SpawnSequenceController : NetworkBehaviour + { + public NetworkObject ObjectToParentUnder; + public Vector3 Offset; + + public bool ApplyParentAndOffset; + + private NetworkTransform m_NetworkTransform; + + protected override void OnNetworkPreSpawn(ref NetworkManager networkManager) + { + m_NetworkTransform = GetComponent(); + base.OnNetworkPreSpawn(ref networkManager); + } + + protected override void OnNetworkPostSpawn() + { + if (ApplyParentAndOffset && m_NetworkTransform.CanCommitToTransform) + { + + } + base.OnNetworkPostSpawn(); + } + } + + public class NetworkTransformStateMonitor : NetworkTransform + { + public static bool VerboseDebug; + public int DetectedMotionCount { get; private set; } + + private bool m_HasTeleported; + private Vector3 m_LastKnownPosition; + + private void Log(string msg) + { + if (VerboseDebug) + { + Debug.Log(msg); + } + } + + protected override void OnNetworkTransformStateUpdated(ref NetworkTransformState oldState, ref NetworkTransformState newState) + { + if (newState.IsTeleportingNextFrame) + { + DetectedMotionCount = 0; + m_HasTeleported = true; + NetworkManager.NetworkTickSystem.Tick += OnNetworkTick; + m_LastKnownPosition = transform.position; + } + base.OnNetworkTransformStateUpdated(ref oldState, ref newState); + } + + private void OnNetworkTick() + { + NetworkManager.NetworkTickSystem.Tick -= OnNetworkTick; + Log("Teleporting tick completed."); + } + + protected string GetVector3Values(Vector3 vector3) + { + return $"({vector3.x:F6},{vector3.y:F6},{vector3.z:F6})"; + } + + + protected bool Approximately(Vector3 a, Vector3 b) + { + var deltaVariance = 0.0001f; + return System.Math.Round(Mathf.Abs(a.x - b.x), 4) <= deltaVariance && + System.Math.Round(Mathf.Abs(a.y - b.y), 4) <= deltaVariance && + System.Math.Round(Mathf.Abs(a.z - b.z), 4) <= deltaVariance; + } + + private void Update() + { + if (CanCommitToTransform) + { + return; + } + + if (m_HasTeleported) + { + if (Approximately(transform.position, m_LastKnownPosition)) + { + DetectedMotionCount++; + Log($"[{DetectedMotionCount}] Moving from {GetVector3Values(m_LastKnownPosition)} to {GetVector3Values(transform.position)}"); + } + } + } + } + + protected override IEnumerator OnTearDown() + { + m_PrefabsToSpawn.Clear(); + return base.OnTearDown(); + } + + private NetworkObject CreatePrefabToSpawn(TransformSpace transformSpace, bool useHalfPrecision, bool useQuaternion, bool compressQuaternion) + { + var prefabToSpawn = CreateNetworkObjectPrefab($"SeqObj[{m_PrefabsToSpawn.Count}]").GetComponent(); + var networkTransform = prefabToSpawn.gameObject.AddComponent(); + networkTransform.SwitchTransformSpaceWhenParented = true; + // Validates that even if you try to set local space it will be reset to world when 1st spawned + networkTransform.InLocalSpace = transformSpace == TransformSpace.Local; + networkTransform.UseHalfFloatPrecision = useHalfPrecision; + networkTransform.UseQuaternionSynchronization = useQuaternion; + networkTransform.UseQuaternionCompression = compressQuaternion; + var spawnSequenceController = prefabToSpawn.gameObject.AddComponent(); + spawnSequenceController.Offset = GetRandomVector3(-20.0f, 20.0f); + return prefabToSpawn; + } + + /// + /// Generates objects to spawn. + /// + protected override void OnServerAndClientsCreated() + { + m_ParentToSpawn = CreateNetworkObjectPrefab("SeqParent").GetComponent(); + + m_PrefabsToSpawn.Add(CreatePrefabToSpawn(TransformSpace.World, false, false, false)); + m_PrefabsToSpawn.Add(CreatePrefabToSpawn(TransformSpace.Local, false, false, false)); + + m_PrefabsToSpawn.Add(CreatePrefabToSpawn(TransformSpace.World, true, false, false)); + m_PrefabsToSpawn.Add(CreatePrefabToSpawn(TransformSpace.Local, true, false, false)); + + m_PrefabsToSpawn.Add(CreatePrefabToSpawn(TransformSpace.World, true, true, false)); + m_PrefabsToSpawn.Add(CreatePrefabToSpawn(TransformSpace.Local, true, true, false)); + + m_PrefabsToSpawn.Add(CreatePrefabToSpawn(TransformSpace.World, true, true, true)); + m_PrefabsToSpawn.Add(CreatePrefabToSpawn(TransformSpace.Local, true, true, true)); + + base.OnServerAndClientsCreated(); + } + + private bool AllClientsSpawnedParentObject(StringBuilder errorLog) + { + var hadError = false; + foreach (var networkManager in m_NetworkManagers) + { + foreach (var parent in m_ParentInstances) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(parent.NetworkObjectId)) + { + errorLog.AppendLine($"Client-{networkManager.LocalClientId}] Has not spawned {parent.name}!"); + hadError = true; + } + } + } + return !hadError; + } + + private bool AllClientsSpawnedObject() + { + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_NetworkObjectIdToValidate)) + { + return false; + } + } + return true; + } + + private bool AllClientsDespawnedObject() + { + foreach (var networkManager in m_NetworkManagers) + { + if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_NetworkObjectIdToValidate)) + { + return false; + } + } + return true; + } + + private bool AllClientsParented(StringBuilder errorLog) + { + var hadError = false; + foreach (var networkManager in m_NetworkManagers) + { + var localNetworkObject = networkManager.SpawnManager.SpawnedObjects[m_NetworkObjectIdToValidate]; + if (localNetworkObject.transform.parent == null) + { + errorLog.AppendLine($"Client-{networkManager.LocalClientId}] {localNetworkObject.name} Has no parent when it should!"); + hadError = true; + continue; + } + var parent = localNetworkObject.transform.parent.GetComponent(); + if (parent.NetworkObjectId != m_FinalParent.NetworkObjectId) + { + errorLog.AppendLine($"Client-{networkManager.LocalClientId}] {localNetworkObject.name} Should be parented under {m_FinalParent.name}-{m_FinalParent.NetworkObjectId} but is parented under {parent.name}-{parent.NetworkObjectId}!"); + hadError = true; + } + } + return !hadError; + } + + private const int k_ParentsToSpawn = 7; + private const int k_ParentingIterations = 3; + + /// + /// Validates that when SwitchTransformSpaceWhenParented is enabled and parenting occurs multiple times + /// that all non-authority instances are properly synchronized with parenting and their final transform values. + /// + [UnityTest] + public IEnumerator SwitchTransformSpaceWhenParented() + { + var authority = GetAuthorityNetworkManager(); + for (int i = 0; i < k_ParentsToSpawn; i++) + { + m_ParentInstances.Add(SpawnObject(m_ParentToSpawn.gameObject, authority).GetComponent()); + } + yield return WaitForConditionOrTimeOut(AllClientsSpawnedParentObject); + AssertOnTimeout($"Timed out waiting for all clients to spawn parent instances!"); + + foreach (var prefabToSpawn in m_PrefabsToSpawn) + { + yield return SpawnAndTest(prefabToSpawn, true); + yield return SpawnAndTest(prefabToSpawn, false); + } + } + + /// + /// This runs through the entire spawn and parenting validation tests + /// for the prefab passed in while also adjusting whether to parent + /// with world position stays enabled or disabled. + /// + private IEnumerator SpawnAndTest(NetworkObject prefabToSpawn, bool worldPositionStays) + { + var authority = GetAuthorityNetworkManager(); + m_ChildInstance = SpawnObject(prefabToSpawn.gameObject, authority).GetComponent(); + var networkTransform = m_ChildInstance.GetComponent(); + m_ParentWorldOrLocal = worldPositionStays ? TransformSpace.World : TransformSpace.Local; + Assert.False(networkTransform.InLocalSpace, $"{m_ChildInstance.name} should never be in local space when not parented and SwitchTransformSpaceWhenParented is enabled!"); + + m_EnableVerboseDebug = true; + VerboseDebug($"[Testing][Parenting: {m_ParentWorldOrLocal}][HalfFloat: {networkTransform.UseHalfFloatPrecision}][Quaternion: {networkTransform.UseQuaternionSynchronization}][Compressed Quaternion: {networkTransform.UseQuaternionCompression}]"); + m_EnableVerboseDebug = false; + m_NetworkObjectIdToValidate = m_ChildInstance.NetworkObjectId; + + var startingParentIndex = Random.Range(0, k_ParentsToSpawn - 1); + + // Iterate several times setting the parent, handling parent-to-parent, and removing the parent + // in order to validate we can handle back-to-back world to local, local to local, and local to + // world transformations in the same frame (and that it synchronizes properly). + for (int i = 0; i < k_ParentingIterations; i++) + { + if (m_ChildInstance.transform.parent) + { + m_ChildInstance.TryRemoveParent(); + } + for (int j = 0; j < k_ParentsToSpawn; j++) + { + var parentIndex = (j + startingParentIndex) % k_ParentsToSpawn; + var parent = m_ParentInstances[parentIndex]; + m_ChildInstance.TrySetParent(parent, m_ParentWorldOrLocal == TransformSpace.World); + m_FinalParent = parent; + } + } + yield return WaitForConditionOrTimeOut(AllClientsSpawnedObject); + AssertOnTimeout($"Timed out waiting for all clients to spawn {m_ChildInstance.name} instance!"); + + yield return WaitForConditionOrTimeOut(AllClientsParented); + AssertOnTimeout($"Timed out waiting for all clients to parent {m_ChildInstance.name} under the final parent instance {m_FinalParent.name}!"); + + yield return WaitForConditionOrTimeOut(TransformsMatch); + AssertOnTimeout($"Timed out waiting for all non-authority transforms of the child to match the authority transform of the child {m_ChildInstance.name}!"); + + var name = m_ChildInstance.name; + m_ChildInstance.Despawn(); + + yield return WaitForConditionOrTimeOut(AllClientsDespawnedObject); + AssertOnTimeout($"Timed out waiting for all clients to despawn {name}!"); + } + + + protected bool TransformsMatch(StringBuilder errorLog) + { + return InternalTransformsMatch(errorLog, TransformSpace.World) && InternalTransformsMatch(errorLog, TransformSpace.Local); + } + + private bool InternalTransformsMatch(StringBuilder errorLog, TransformSpace transformSpace) + { + var hasErrors = false; + var useWorldSpace = transformSpace == TransformSpace.World ? true : false; + var authorityEulerRotation = useWorldSpace ? m_ChildInstance.transform.eulerAngles : m_ChildInstance.transform.localEulerAngles; + var authorityPosition = useWorldSpace ? m_ChildInstance.transform.position : m_ChildInstance.transform.localPosition; + + foreach (var networkManager in m_NetworkManagers) + { + + var nonAuthorityInstance = networkManager.SpawnManager.SpawnedObjects[m_ChildInstance.NetworkObjectId]; + var nonAuthorityEulerRotation = useWorldSpace ? nonAuthorityInstance.transform.eulerAngles : nonAuthorityInstance.transform.localEulerAngles; + + var xIsEqual = ApproximatelyEuler(authorityEulerRotation.x, nonAuthorityEulerRotation.x); + var yIsEqual = ApproximatelyEuler(authorityEulerRotation.y, nonAuthorityEulerRotation.y); + var zIsEqual = ApproximatelyEuler(authorityEulerRotation.z, nonAuthorityEulerRotation.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + errorLog.AppendLine($"[Client-{nonAuthorityInstance.NetworkManager.LocalClientId}][{nonAuthorityInstance.gameObject.name}] Rotation {GetVector3Values(nonAuthorityEulerRotation)} does not match the authority rotation {GetVector3Values(authorityEulerRotation)}!"); + hasErrors = true; + } + var nonAuthorityPosition = useWorldSpace ? nonAuthorityInstance.transform.position : nonAuthorityInstance.transform.localPosition; + xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x); + yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y); + zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z); + + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + errorLog.AppendLine($"[Client-{nonAuthorityInstance.NetworkManager.LocalClientId}][{nonAuthorityInstance.gameObject.name}] Position {GetVector3Values(nonAuthorityPosition)} does not match the authority position {GetVector3Values(authorityPosition)}!"); + hasErrors = true; + } + } + return !hasErrors; + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformSpawnSequences.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformSpawnSequences.cs.meta new file mode 100644 index 0000000000..4d1ee7820a --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformSpawnSequences.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e3b84df777511084f86e2c255f226532 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsChangingTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsChangingTests.cs index b54e874c38..ce0b7d378c 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsChangingTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsChangingTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -377,6 +378,7 @@ public override void AddItem() protected override void OnNetworkPostSpawn() { + Log($"[Client-{NetworkManager.LocalClientId}] Running post spawn."); m_DictionaryCollection.OnValueChanged += OnValueChanged; base.OnNetworkPostSpawn(); } @@ -488,6 +490,8 @@ public enum HelperStates } private HelperStates HelperState { get; set; } + private Dictionary m_StateToAction = new Dictionary(); + private int m_SendClearForOwnershipOnTick; private ulong m_NextClient = 0; @@ -495,9 +499,15 @@ public enum HelperStates public void SetState(HelperStates helperState) { + Log($"[StateUpdate] Previous: {HelperState} New: {helperState}"); HelperState = helperState; } + private void Awake() + { + InitializeStateUpdates(); + } + protected virtual bool OnValidateAgainst(BaseCollectionUpdateHelper otherHelper) { return true; @@ -653,24 +663,36 @@ private void OnNetworkTick() { return; } + m_StateToAction[HelperState]?.Invoke(); + } - if (HelperState == HelperStates.ChangingOwner) - { - NetworkObject.ChangeOwnership(m_NextClient); - Log($"Local Change ownership to Client-{m_NextClient} complete! New Owner is {NetworkObject.OwnerClientId} | Expected {m_NextClient}"); - } - else - { - ChangingOwnershipClearRpc(RpcTarget.Single(m_ClientToSendClear, RpcTargetUse.Temp)); - } - HelperState = HelperStates.Stop; + private void InitializeStateUpdates() + { + m_StateToAction.Add(HelperStates.Start, null); + m_StateToAction.Add(HelperStates.Stop, null); + m_StateToAction.Add(HelperStates.Pause, null); + m_StateToAction.Add(HelperStates.ClearToChangeOwner, ClearToChangeOwnerStateUpdate); + m_StateToAction.Add(HelperStates.ChangingOwner, ChangingOwnerStateUpdate); + } + + private void ChangingOwnerStateUpdate() + { + NetworkObject.ChangeOwnership(m_NextClient); + Log($"Local Change ownership to Client-{m_NextClient} complete! New Owner is {NetworkObject.OwnerClientId} | Expected {m_NextClient}"); + SetState(HelperStates.Stop); + } + + private void ClearToChangeOwnerStateUpdate() + { + ChangingOwnershipClearRpc(RpcTarget.Single(m_ClientToSendClear, RpcTargetUse.Temp)); + SetState(HelperStates.Stop); } protected void Log(string msg) { if (VerboseMode) { - Debug.Log($"[Client-{NetworkManager.LocalClientId}] {msg}"); + Debug.Log($"[Frame: {Time.frameCount}][Tick: {((uint)(NetworkManager.LocalTime.TickWithPartial * 1000)) * 0.001f}][{name}] {msg}"); } } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs index 224b82cf32..4bad45e088 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableCollectionsTests.cs @@ -781,7 +781,6 @@ public IEnumerator TestDictionaryCollections() } m_CurrentKey = 1000; - if (m_EnableDebug) { VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>> Init Values <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); @@ -808,12 +807,11 @@ public IEnumerator TestDictionaryCollections() count++; m_Stage = 0; } - Assert.IsTrue(m_IsInitialized, $"Not all clients synchronized properly!\n {m_InitializedStatus.ToString()}"); VerboseDebug(m_InitializedStatus.ToString()); + VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BEGIN <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); } - VerboseDebug(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> BEGIN <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); foreach (var client in m_Clients) { /////////////////////////////////////////////////////////////////////////// diff --git a/testproject/Assets/Tests/Runtime/MessageOrdering.cs b/testproject/Assets/Tests/Runtime/MessageOrdering.cs index 8295b20940..f4ef5a7fbd 100644 --- a/testproject/Assets/Tests/Runtime/MessageOrdering.cs +++ b/testproject/Assets/Tests/Runtime/MessageOrdering.cs @@ -9,6 +9,9 @@ namespace TestProject.RuntimeTests { + /// + /// TODO: This test needs to be converted to an integration test + /// public class MessageOrderingTests { private GameObject m_Prefab; @@ -117,7 +120,7 @@ public IEnumerator SpawnRpcDespawn() //server.PrefabHandler.AddHandler(networkObject.GlobalObjectIdHash, handler); foreach (var client in m_ClientNetworkManagers) { - var clientHandler = new SpawnRpcDespawnInstanceHandler(networkObject.GlobalObjectIdHash); + var clientHandler = new SpawnRpcDespawnInstanceHandler(networkObject.GlobalObjectIdHash, client); client.PrefabHandler.AddHandler(networkObject, clientHandler); clientHandlers.Add(clientHandler); } @@ -196,6 +199,8 @@ public IEnumerator SpawnRpcDespawn() Debug.Log($"It took {Time.frameCount - frameCountStart} frames to process the MessageOrdering.SpawnRpcDespawn integration test."); } + private ulong m_SpawnedNetworkObjectId; + [UnityTest] public IEnumerator RpcOnNetworkSpawn() { @@ -211,7 +216,7 @@ public IEnumerator RpcOnNetworkSpawn() // Make it a prefab NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); var handlers = new List(); - var handler = new SpawnRpcDespawnInstanceHandler(networkObject.GlobalObjectIdHash); + var handler = new SpawnRpcDespawnInstanceHandler(networkObject.GlobalObjectIdHash, server); // We *must* always add a unique handler to both the server and the clients server.PrefabHandler.AddHandler(networkObject, handler); @@ -219,7 +224,7 @@ public IEnumerator RpcOnNetworkSpawn() foreach (var client in clients) { // Create a unique SpawnRpcDespawnInstanceHandler per client - handler = new SpawnRpcDespawnInstanceHandler(networkObject.GlobalObjectIdHash); + handler = new SpawnRpcDespawnInstanceHandler(networkObject.GlobalObjectIdHash, client); handlers.Add(handler); client.PrefabHandler.AddHandler(networkObject, handler); } @@ -249,10 +254,30 @@ public IEnumerator RpcOnNetworkSpawn() // [Host-Side] Check to make sure all clients are connected yield return NetcodeIntegrationTestHelpers.WaitForClientsConnectedToServer(server, clients.Length, null, 512); - var serverObject = Object.Instantiate(m_Prefab, Vector3.zero, Quaternion.identity); - NetworkObject serverNetworkObject = serverObject.GetComponent(); - serverNetworkObject.NetworkManagerOwner = server; - serverNetworkObject.Spawn(); + var serverNetworkObject = NetworkObject.InstantiateAndSpawn(m_Prefab, server); + + m_SpawnedNetworkObjectId = serverNetworkObject.GlobalObjectIdHash; + + // Make sure everyone spawns the object + var allClientsSpawnedObject = false; + var waitPeriod = new WaitForSeconds(1.0f / server.NetworkConfig.TickRate); + var timeout = Time.realtimeSinceStartup + 4.0f; + while (!allClientsSpawnedObject) + { + if (timeout < Time.realtimeSinceStartup) + { + Assert.Fail($"Timed out waiting for all clients to spawn {serverNetworkObject.name}!"); + } + foreach (var client in clients) + { + if (!client.SpawnManager.SpawnedObjects.ContainsKey(m_SpawnedNetworkObjectId)) + { + yield return waitPeriod; + continue; + } + } + allClientsSpawnedObject = true; + } // Wait until all objects have spawned. const int maxFrames = 240; diff --git a/testproject/Assets/Tests/Runtime/Support/SpawnRpcDespawn.cs b/testproject/Assets/Tests/Runtime/Support/SpawnRpcDespawn.cs index 7b38239d41..b23f95fb98 100644 --- a/testproject/Assets/Tests/Runtime/Support/SpawnRpcDespawn.cs +++ b/testproject/Assets/Tests/Runtime/Support/SpawnRpcDespawn.cs @@ -1,4 +1,3 @@ -using System; using NUnit.Framework; using Unity.Netcode; using UnityEngine; @@ -7,7 +6,16 @@ namespace TestProject.RuntimeTests.Support { public class SpawnRpcDespawn : NetworkBehaviour, INetworkUpdateSystem { - public static NetworkUpdateStage TestStage; + public static bool VerboseLogging; + private static NetworkUpdateStage s_TestStage; + public static NetworkUpdateStage TestStage + { + get { return s_TestStage; } + set + { + s_TestStage = value; + } + } public static int ClientUpdateCount; public static int ServerUpdateCount; public static bool ClientNetworkSpawnRpcCalled; @@ -17,6 +25,24 @@ public class SpawnRpcDespawn : NetworkBehaviour, INetworkUpdateSystem private bool m_Active = false; + private void Log(string header, string msg) + { + if (!VerboseLogging) + { + return; + } + Debug.Log($"[{nameof(SpawnRpcDespawn)}][Client-{NetworkManager.LocalClientId}]{header} {msg}"); + } + + private void Log(string msg) + { + if (!VerboseLogging) + { + return; + } + Log(string.Empty, msg); + } + [ClientRpc] public void SendIncrementUpdateCountClientRpc() { @@ -24,19 +50,19 @@ public void SendIncrementUpdateCountClientRpc() StageExecutedByReceiver = NetworkUpdateLoop.UpdateStage; ++ClientUpdateCount; - Debug.Log($"Client RPC executed at {NetworkUpdateLoop.UpdateStage}; client count to {ClientUpdateCount.ToString()}"); + Log($"Client RPC executed at {NetworkUpdateLoop.UpdateStage}; client count to {ClientUpdateCount.ToString()}"); } public void IncrementUpdateCount() { ++ServerUpdateCount; - Debug.Log($"Server count to {ServerUpdateCount.ToString()}"); + Log($"Server count to {ServerUpdateCount.ToString()}"); SendIncrementUpdateCountClientRpc(); } public void Activate() { - Debug.Log("Activated"); + Log("Activated"); m_Active = true; } @@ -44,20 +70,23 @@ public override void OnNetworkSpawn() { if (!IsServer) { + Log("Client instance spawning!"); // Asserting that the RPC is not called before OnNetworkSpawn Assert.IsFalse(ClientNetworkSpawnRpcCalled); return; } - + Log($"[Should execute: {ExecuteClientRpc}]", "Server instance spawning"); if (ExecuteClientRpc) { - TestClientRpc(); + Log($"[Executing]", $"Server invoking {nameof(ClientTestRpc)}."); + ClientTestRpc(); } } - [ClientRpc] - private void TestClientRpc() + [Rpc(SendTo.NotMe)] + private void ClientTestRpc() { + Log($"Received {nameof(ClientTestRpc)} message and processed it!"); ClientNetworkSpawnRpcCalled = true; if (ShutdownInClientRpc) { @@ -65,21 +94,15 @@ private void TestClientRpc() } } - public void Awake() + protected override void OnNetworkPreSpawn(ref NetworkManager networkManager) { - foreach (NetworkUpdateStage stage in Enum.GetValues(typeof(NetworkUpdateStage))) - { - NetworkUpdateLoop.RegisterNetworkUpdate(this, stage); - } + NetworkUpdateLoop.RegisterAllNetworkUpdates(this); + base.OnNetworkPreSpawn(ref networkManager); } public override void OnDestroy() { - foreach (NetworkUpdateStage stage in Enum.GetValues(typeof(NetworkUpdateStage))) - { - NetworkUpdateLoop.UnregisterNetworkUpdate(this, stage); - } - + NetworkUpdateLoop.UnregisterAllNetworkUpdates(this); base.OnDestroy(); } diff --git a/testproject/Assets/Tests/Runtime/Support/SpawnRpcDespawnInstanceHandler.cs b/testproject/Assets/Tests/Runtime/Support/SpawnRpcDespawnInstanceHandler.cs index 74c1ca7db8..5f4e9b2dcf 100644 --- a/testproject/Assets/Tests/Runtime/Support/SpawnRpcDespawnInstanceHandler.cs +++ b/testproject/Assets/Tests/Runtime/Support/SpawnRpcDespawnInstanceHandler.cs @@ -10,31 +10,36 @@ public class SpawnRpcDespawnInstanceHandler : INetworkPrefabInstanceHandler public bool WasSpawned = false; public bool WasDestroyed = false; + private NetworkManager m_NetworkManager; - public SpawnRpcDespawnInstanceHandler(uint prefabHash) + public SpawnRpcDespawnInstanceHandler(uint prefabHash, NetworkManager networkManager) { + m_NetworkManager = networkManager; m_PrefabHash = prefabHash; } public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) { WasSpawned = true; - Assert.AreEqual(NetworkUpdateStage.EarlyUpdate, NetworkUpdateLoop.UpdateStage); + if (ownerClientId != NetworkManager.ServerClientId) + { + Assert.AreEqual(NetworkUpdateStage.EarlyUpdate, NetworkUpdateLoop.UpdateStage); + } // See if there is a valid registered NetworkPrefabOverrideLink associated with the provided prefabHash GameObject networkPrefabReference = null; - if (NetworkManager.Singleton.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(m_PrefabHash)) + if (m_NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks.ContainsKey(m_PrefabHash)) { - switch (NetworkManager.Singleton.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[m_PrefabHash].Override) + switch (m_NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[m_PrefabHash].Override) { default: case NetworkPrefabOverride.None: - networkPrefabReference = NetworkManager.Singleton.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[m_PrefabHash].Prefab; + networkPrefabReference = m_NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[m_PrefabHash].Prefab; break; case NetworkPrefabOverride.Hash: case NetworkPrefabOverride.Prefab: - networkPrefabReference = NetworkManager.Singleton.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[m_PrefabHash].OverridingTargetPrefab; + networkPrefabReference = m_NetworkManager.NetworkConfig.Prefabs.NetworkPrefabOverrideLinks[m_PrefabHash].OverridingTargetPrefab; break; } } @@ -50,19 +55,20 @@ public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaterni } // Otherwise, instantiate an instance of the NetworkPrefab linked to the prefabHash - var networkObject = Object.Instantiate(networkPrefabReference, position, rotation).GetComponent(); - - return networkObject; + return Object.Instantiate(networkPrefabReference, position, rotation).GetComponent(); } public void Destroy(NetworkObject networkObject) { + if (m_NetworkManager.ShutdownInProgress) + { + return; + } WasDestroyed = true; - if (networkObject.NetworkManager.IsClient) + if (!networkObject.NetworkManager.IsServer) { Assert.AreEqual(NetworkUpdateStage.EarlyUpdate, NetworkUpdateLoop.UpdateStage); } - Object.Destroy(networkObject.gameObject); } }