diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers.meta b/com.unity.netcode.gameobjects/Runtime/Components/Helpers.meta new file mode 100644 index 0000000000..28b2944c3d --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fbc8738cd4ff119499520131b7aa7232 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableBehaviour.cs new file mode 100644 index 0000000000..260257bfa4 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableBehaviour.cs @@ -0,0 +1,373 @@ +using System; +#if UNITY_EDITOR +using UnityEditor; +#endif +using UnityEngine; + +namespace Unity.Netcode.Components +{ + /// + /// Attachable NetworkBehaviours
+ /// This component handles the parenting synchronization of the that this component is attached to.
+ /// under another 's .
+ /// The to be parented must have this component attached to it and must be nested on any child under the 's .
+ /// The target parent must have an component attached to it and must belong to a + /// different than that of the 's. + ///
+ /// + /// The term "attach" is used in place of parenting in order to distinguish between parenting and + /// parenting ("attaching" and "detaching").
+ /// This component can be used along with one or more in order to enable or disable different components depending + /// upon the instance's current state. + ///
+ public class AttachableBehaviour : NetworkBehaviour + { +#if UNITY_EDITOR + /// + /// + /// In the event an is placed on the same + /// as the , this will automatically create a child and add an + /// to that. + /// + protected virtual void OnValidate() + { + var networkObject = gameObject.GetComponentInParent(); + if (!networkObject) + { + networkObject = gameObject.GetComponent(); + } + if (networkObject && networkObject.gameObject == gameObject) + { + Debug.LogWarning($"[{name}][{nameof(AttachableBehaviour)}] Cannot be placed on the same {nameof(GameObject)} as the {nameof(NetworkObject)}!"); + // Wait for the next editor update to create a nested child and add the AttachableBehaviour + EditorApplication.update += CreatedNestedChild; + } + } + + private void CreatedNestedChild() + { + EditorApplication.update -= CreatedNestedChild; + var childGameObject = new GameObject($"{name}-Child"); + childGameObject.transform.parent = transform; + childGameObject.AddComponent(); + Debug.Log($"[{name}][Created Child] Adding {nameof(AttachableBehaviour)} to newly created child {childGameObject.name}."); + DestroyImmediate(this); + } +#endif + + /// + /// Invoked when the of this instance has changed. + /// + public event Action AttachStateChange; + + /// + /// The various states of . + /// + public enum AttachState + { + /// + /// The instance is not attached to anything. + /// When not attached to anything, the instance will be parented under the original + /// . + /// + Detached, + /// + /// The instance is attaching to an . + /// + /// + /// One example usage:
+ /// When using an with one or more component(s), + /// this would be a good time to enable or disable components. + ///
+ Attaching, + /// + /// The instance is attached to an . + /// + /// + /// This would be a good time to apply any additional local position or rotation values to this instance. + /// + Attached, + /// + /// The instance is detaching from an . + /// + /// + /// One example usage:
+ /// When using an with one or more component(s), + /// this would be a good time to enable or disable components. + ///
+ Detaching + } + + /// + /// The current instance's . + /// + protected AttachState m_AttachState { get; private set; } + + /// + /// The original parent of this instance. + /// + protected GameObject m_DefaultParent { get; private set; } + + /// + /// If attached, attaching, or detaching this will be the this instance is attached to. + /// + protected AttachableNode m_AttachableNode { get; private set; } + + private NetworkVariable m_AttachedNodeReference = new NetworkVariable(new NetworkBehaviourReference(null)); + private Vector3 m_OriginalLocalPosition; + private Quaternion m_OriginalLocalRotation; + + /// + /// If you create a custom and override this method, you must invoke + /// this base instance of . + /// + protected virtual void Awake() + { + m_DefaultParent = transform.parent == null ? gameObject : transform.parent.gameObject; + m_OriginalLocalPosition = transform.localPosition; + m_OriginalLocalRotation = transform.localRotation; + m_AttachState = AttachState.Detached; + m_AttachableNode = null; + } + + /// + /// + /// If you create a custom and override this method, you must invoke + /// this base instance of . + /// + protected override void OnNetworkPostSpawn() + { + if (HasAuthority) + { + m_AttachedNodeReference.Value = new NetworkBehaviourReference(null); + } + m_AttachedNodeReference.OnValueChanged += OnAttachedNodeReferenceChanged; + base.OnNetworkPostSpawn(); + } + + /// + /// + /// If you create a custom and override this method, you will want to + /// invoke this base instance of if you want the current + /// state to have been applied before executing the derived class's + /// script. + /// + protected override void OnNetworkSessionSynchronized() + { + UpdateAttachedState(); + base.OnNetworkSessionSynchronized(); + } + + /// + public override void OnNetworkDespawn() + { + m_AttachedNodeReference.OnValueChanged -= OnAttachedNodeReferenceChanged; + InternalDetach(); + if (NetworkManager && !NetworkManager.ShutdownInProgress) + { + // Notify of the changed attached state + UpdateAttachState(m_AttachState, m_AttachableNode); + } + base.OnNetworkDespawn(); + } + + private void OnAttachedNodeReferenceChanged(NetworkBehaviourReference previous, NetworkBehaviourReference current) + { + UpdateAttachedState(); + } + + private void UpdateAttachedState() + { + var attachableNode = (AttachableNode)null; + var shouldParent = m_AttachedNodeReference.Value.TryGet(out attachableNode, NetworkManager); + var preState = shouldParent ? AttachState.Attaching : AttachState.Detaching; + var preNode = shouldParent ? attachableNode : m_AttachableNode; + shouldParent = shouldParent && attachableNode != null; + + if (shouldParent && m_AttachableNode != null && m_AttachState == AttachState.Attached) + { + // If we are attached to some other AttachableNode, then detach from that before attaching to a new one. + if (m_AttachableNode != attachableNode) + { + // Run through the same process without being triggerd by a NetVar update. + UpdateAttachState(AttachState.Detaching, m_AttachableNode); + m_AttachableNode.Detach(this); + transform.parent = null; + UpdateAttachState(AttachState.Detached, null); + } + } + + // Change the state to attaching or detaching + UpdateAttachState(preState, preNode); + + if (shouldParent) + { + InternalAttach(attachableNode); + } + else + { + InternalDetach(); + } + + // Notify of the changed attached state + UpdateAttachState(m_AttachState, m_AttachableNode); + } + + /// + /// For customized/derived s, override this method to receive notifications + /// when the has changed. + /// + /// The new . + /// The being attached to or from. Will be null when completely detatched. + protected virtual void OnAttachStateChanged(AttachState attachState, AttachableNode attachableNode) + { + + } + + /// + /// Update the attached state. + /// + private void UpdateAttachState(AttachState attachState, AttachableNode attachableNode) + { + try + { + AttachStateChange?.Invoke(attachState, attachableNode); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + + try + { + OnAttachStateChanged(attachState, attachableNode); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + + /// + /// Internal attach method that just handles changing state, parenting, and sending the a + /// notification that an has attached. + /// + internal void InternalAttach(AttachableNode attachableNode) + { + if (attachableNode.NetworkManager != NetworkManager) + { + Debug.Log("Blam!"); + } + m_AttachState = AttachState.Attached; + m_AttachableNode = attachableNode; + // Attachables are always local space relative + transform.SetParent(m_AttachableNode.transform, false); + m_AttachableNode.Attach(this); + } + + /// + /// Attaches the of this instance to the of the . + /// + /// + /// This effectively applies a new parent to a nested and all children + /// of the nested .
+ /// Both the and this instances should be in the spawned state before this + /// is invoked. + ///
+ /// The to attach this instance to. + public void Attach(AttachableNode attachableNode) + { + if (!IsSpawned) + { + NetworkLog.LogError($"[{name}][Attach][Not Spawned] Cannot attach before being spawned!"); + return; + } + + if (!HasAuthority) + { + NetworkLog.LogError($"[{name}][Attach][Not Authority] Client-{NetworkManager.LocalClientId} is not the authority!"); + return; + } + + if (attachableNode.NetworkObject == NetworkObject) + { + NetworkLog.LogError($"[{name}][Attach] Cannot attach to the original {NetworkObject} instance!"); + return; + } + + if (m_AttachableNode != null && m_AttachState == AttachState.Attached && m_AttachableNode == attachableNode) + { + NetworkLog.LogError($"[{name}][Attach] Cannot attach! {name} is already attached to {attachableNode.name}!"); + return; + } + + // Update the attached node reference to the new attachable node. + m_AttachedNodeReference.Value = new NetworkBehaviourReference(attachableNode); + } + + /// + /// Internal detach method that just handles changing state, parenting, and sending the a + /// notification that an has detached. + /// + internal void InternalDetach() + { + if (m_AttachableNode) + { + m_AttachableNode.Detach(this); + m_AttachableNode = null; + if (m_DefaultParent) + { + // Set the original parent and origianl local position and rotation + transform.SetParent(m_DefaultParent.transform, false); + transform.localPosition = m_OriginalLocalPosition; + transform.localRotation = m_OriginalLocalRotation; + } + m_AttachState = AttachState.Detached; + } + } + + /// + /// Invoke to detach from a . + /// + public void Detach() + { + if (!IsSpawned) + { + NetworkLog.LogError($"[{name}][Detach][Not Spawned] Cannot detach if not spawned!"); + return; + } + + if (!HasAuthority) + { + NetworkLog.LogError($"[{name}][Detach][Not Authority] Client-{NetworkManager.LocalClientId} is not the authority!"); + return; + } + + if (m_AttachState != AttachState.Attached || m_AttachableNode == null) + { + // Check for the unlikely scenario that an instance has mismatch between the state and assigned attachable node. + if (!m_AttachableNode && m_AttachState == AttachState.Attached) + { + NetworkLog.LogError($"[{name}][Detach] Invalid state detected! {name}'s state is still {m_AttachState} but has no {nameof(AttachableNode)} assigned!"); + } + + // Developer only notification for the most likely scenario where this method is invoked but the instance is not attached to anything. + if (NetworkManager && NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogWarning($"[{name}][Detach] Cannot detach! {name} is not attached to anything!"); + } + + // If we have the attachable node set and we are not in the middle of detaching, then log an error and note + // this could potentially occur if inoked more than once for the same instance in the same frame. + if (m_AttachableNode && m_AttachState != AttachState.Detaching) + { + NetworkLog.LogError($"[{name}][Detach] Invalid state detected! {name} is still referencing {nameof(AttachableNode)} {m_AttachableNode.name}! Could {nameof(Detach)} be getting invoked more than once for the same instance?"); + } + return; + } + + // Update the attached node reference to nothing-null. + m_AttachedNodeReference.Value = new NetworkBehaviourReference(null); + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableBehaviour.cs.meta b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableBehaviour.cs.meta new file mode 100644 index 0000000000..ade010ae67 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7aa87489bfc51d448940c66c2a2cf840 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableNode.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableNode.cs new file mode 100644 index 0000000000..8c5ad47cf5 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableNode.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using Unity.Netcode; +using Unity.Netcode.Components; + + +/// +/// This component is used in conjunction with and is used to +/// denote a specific child that an +/// can attach itself to. +/// +/// +/// Primarily, the can be used as it is or can be extended to perform additional +/// logical operations when something attaches to or detaches from the instance. +/// +public class AttachableNode : NetworkBehaviour +{ + /// + /// A of the currently attached s. + /// + protected readonly List m_AttachedBehaviours = new List(); + + /// + /// + /// If the this belongs to is despawned, + /// then any attached will be detached during . + /// + public override void OnNetworkDespawn() + { + for (int i = m_AttachedBehaviours.Count - 1; i > 0; i--) + { + m_AttachedBehaviours[i].InternalDetach(); + } + base.OnNetworkDespawn(); + } + + /// + /// Override this method to be notified when an has attached to this node. + /// + /// The that has been attached. + protected virtual void OnAttached(AttachableBehaviour attachableBehaviour) + { + + } + + internal void Attach(AttachableBehaviour attachableBehaviour) + { + if (m_AttachedBehaviours.Contains(attachableBehaviour)) + { + NetworkLog.LogError($"[{nameof(AttachableNode)}][{name}][Attach] {nameof(AttachableBehaviour)} {attachableBehaviour.name} is already attached!"); + return; + } + + m_AttachedBehaviours.Add(attachableBehaviour); + OnAttached(attachableBehaviour); + } + + /// + /// Override this method to be notified when an has detached from this node. + /// + /// The that has been detached. + protected virtual void OnDetached(AttachableBehaviour attachableBehaviour) + { + + } + + internal void Detach(AttachableBehaviour attachableBehaviour) + { + if (!m_AttachedBehaviours.Contains(attachableBehaviour)) + { + NetworkLog.LogError($"[{nameof(AttachableNode)}][{name}][Detach] {nameof(AttachableBehaviour)} {attachableBehaviour.name} is not attached!"); + return; + } + + m_AttachedBehaviours.Remove(attachableBehaviour); + OnDetached(attachableBehaviour); + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableNode.cs.meta b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableNode.cs.meta new file mode 100644 index 0000000000..dda530aca5 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/AttachableNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b870dfbc4cccf6244b4ed1a9b379c701 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/ComponentController.cs b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/ComponentController.cs new file mode 100644 index 0000000000..cc87d6f732 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/ComponentController.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.Components +{ + /// + /// This is a serializable contianer class for entries. + /// + [Serializable] + public class ComponentControllerEntry + { + /// + /// When true, this component's enabled state will be the inverse of + /// the value passed into . + /// + public bool InvertEnabled; + + /// + /// The component to control. + /// + /// + /// You can assign an entire to this property which will + /// add all components attached to the . The + /// and properties will be applied to all components found on the . + /// + public Object Component; + + internal PropertyInfo PropertyInfo; + } + + /// + /// Handles enabling or disabling commonly used components, behaviours, RenderMeshes, etc.
+ /// Anything that derives from and has an enabled property can be added + /// to the list of objects.
+ /// derived components are not allowed and will be automatically removed. + ///
+ /// + /// This will synchronize the enabled or disabled state of the s with + /// connected and late joining clients.
+ /// This class provides the basic functionality to synchronizing components' enabled state.
+ /// It is encouraged to create custom derived versions of this class to provide any additional + /// functionality required for your project specific needs. + ///
+ public class ComponentController : NetworkBehaviour + { + /// + /// Determines whether the selected s will start enabled or disabled when spawned. + /// + [Tooltip("The initial state of the component controllers enabled status when instnatiated.")] + public bool StartEnabled = true; + + /// + /// The list of s to be enabled and disabled. + /// + [Tooltip("The list of components to control. You can drag and drop an entire GameObject on this to include all components.")] + public List Components; + + /// + /// Returns the current enabled state of the . + /// + public bool EnabledState => m_IsEnabled.Value; + + internal List ValidComponents = new List(); + private NetworkVariable m_IsEnabled = new NetworkVariable(); + +#if UNITY_EDITOR + /// + /// + /// Checks for invalid entries. + /// + protected virtual void OnValidate() + { + if (Components == null || Components.Count == 0) + { + return; + } + + var gameObjectsToScan = new List(); + for (int i = Components.Count - 1; i >= 0; i--) + { + if (Components[i] == null) + { + continue; + } + + if (Components[i].Component == null) + { + continue; + } + var componentType = Components[i].Component.GetType(); + if (componentType == typeof(GameObject)) + { + gameObjectsToScan.Add(Components[i]); + Components.RemoveAt(i); + continue; + } + + if (componentType.IsSubclassOf(typeof(NetworkBehaviour))) + { + Debug.LogWarning($"Removing {Components[i].Component.name} since {nameof(NetworkBehaviour)}s are not allowed to be controlled by this component."); + Components.RemoveAt(i); + continue; + } + + var propertyInfo = Components[i].Component.GetType().GetProperty("enabled", BindingFlags.Instance | BindingFlags.Public); + if (propertyInfo == null && propertyInfo.PropertyType != typeof(bool)) + { + Debug.LogWarning($"{Components[i].Component.name} does not contain a public enabled property! (Removing)"); + Components.RemoveAt(i); + } + } + + foreach (var entry in gameObjectsToScan) + { + var asGameObject = entry.Component as GameObject; + var components = asGameObject.GetComponents(); + foreach (var component in components) + { + // Ignore any NetworkBehaviour derived components + if (component.GetType().IsSubclassOf(typeof(NetworkBehaviour))) + { + continue; + } + + var propertyInfo = component.GetType().GetProperty("enabled", BindingFlags.Instance | BindingFlags.Public); + if (propertyInfo != null && propertyInfo.PropertyType == typeof(bool)) + { + var componentEntry = new ComponentControllerEntry() + { + Component = component, + PropertyInfo = propertyInfo, + }; + Components.Add(componentEntry); + } + } + } + gameObjectsToScan.Clear(); + } +#endif + + /// + /// This checks to make sure that all entries are valid and will create a final + /// list of valid entries. + /// + protected virtual void Awake() + { + ValidComponents.Clear(); + + // If no components then don't try to initialize. + if (Components == null) + { + return; + } + + var emptyEntries = 0; + + foreach (var entry in Components) + { + if (entry == null) + { + emptyEntries++; + continue; + } + var propertyInfo = entry.Component.GetType().GetProperty("enabled", BindingFlags.Instance | BindingFlags.Public); + if (propertyInfo != null && propertyInfo.PropertyType == typeof(bool)) + { + entry.PropertyInfo = propertyInfo; + ValidComponents.Add(entry); + } + else + { + NetworkLog.LogWarning($"{name} does not contain a public enable property! (Ignoring)"); + } + } + if (emptyEntries > 0) + { + NetworkLog.LogWarning($"{name} has {emptyEntries} emtpy(null) entries in the {nameof(Components)} list!"); + } + + // Apply the initial state of all components this instance is controlling. + InitializeComponents(); + } + + /// + public override void OnNetworkSpawn() + { + if (HasAuthority) + { + m_IsEnabled.Value = StartEnabled; + } + base.OnNetworkSpawn(); + } + + /// + /// + /// Assures all instances subscribe to the internal of type + /// that synchronizes all instances when s are enabled + /// or disabled. + /// + protected override void OnNetworkPostSpawn() + { + m_IsEnabled.OnValueChanged += OnEnabledChanged; + ApplyEnabled(m_IsEnabled.Value); + base.OnNetworkPostSpawn(); + } + + /// + public override void OnNetworkDespawn() + { + m_IsEnabled.OnValueChanged -= OnEnabledChanged; + base.OnNetworkDespawn(); + } + + private void OnEnabledChanged(bool previous, bool current) + { + ApplyEnabled(current); + } + + /// + /// Initializes each component entry to its initial state. + /// + private void InitializeComponents() + { + foreach (var entry in ValidComponents) + { + // If invert enabled is true, then use the inverted value passed in. + // Otherwise, directly apply the value passed in. + var isEnabled = entry.InvertEnabled ? !StartEnabled : StartEnabled; + entry.PropertyInfo.SetValue(entry.Component, isEnabled); + } + } + + /// + /// Applies states changes to all components being controlled by this instance. + /// + /// the state update to apply + private void ApplyEnabled(bool enabled) + { + foreach (var entry in ValidComponents) + { + // If invert enabled is true, then use the inverted value passed in. + // Otherwise, directly apply the value passed in. + var isEnabled = entry.InvertEnabled ? !enabled : enabled; + entry.PropertyInfo.SetValue(entry.Component, isEnabled); + } + } + + /// + /// Invoke on the authority side to enable or disable components assigned to this instance. + /// + /// + /// If any component entry has the set to true, + /// then the inverse of the isEnabled property passed in will be used. If the component entry has the + /// set to false (default), then the value of the + /// isEnabled property will be applied. + /// + /// true = enabled | false = disabled + public void SetEnabled(bool isEnabled) + { + if (!IsSpawned) + { + Debug.Log($"[{name}] Must be spawned to use {nameof(SetEnabled)}!"); + return; + } + + if (!HasAuthority) + { + Debug.Log($"[Client-{NetworkManager.LocalClientId}] Attempting to invoke {nameof(SetEnabled)} without authority!"); + return; + } + m_IsEnabled.Value = isEnabled; + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Helpers/ComponentController.cs.meta b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/ComponentController.cs.meta new file mode 100644 index 0000000000..e7482640fa --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Components/Helpers/ComponentController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4d0b25b3f95e5324abc80c09cb29f271 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs index 96cd6b79e0..d368465cf3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs @@ -1121,7 +1121,7 @@ private void ApplyFixedJoint(NetworkRigidbodyBase bodyToConnectTo, Vector3 posit /// - This instance can be viewed as the child. /// - The can be viewed as the parent. ///
- /// This is the recommended way, as opposed to parenting, to attached/detatch two rigid bodies to one another when is enabled. + /// This is the recommended way, as opposed to parenting, to attached/detach two rigid bodies to one another when is enabled. /// For more details on using and . ///
/// This provides a simple joint solution between two rigid bodies and serves as an example. You can add different joint types by creating a customized/derived @@ -1187,7 +1187,7 @@ private void RemoveFromParentBody() #if COM_UNITY_MODULES_PHYSICS2D [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DetatchFromFixedJoint2D() + private void DetachFromFixedJoint2D() { if (FixedJoint2D == null) { @@ -1206,7 +1206,7 @@ private void DetatchFromFixedJoint2D() #endif #if COM_UNITY_MODULES_PHYSICS [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DetatchFromFixedJoint3D() + private void DetachFromFixedJoint3D() { if (FixedJoint == null) { @@ -1227,7 +1227,7 @@ private void DetatchFromFixedJoint3D() /// this will detach from the fixed joint and destroy the fixed joint component. /// /// - /// This is the recommended way, as opposed to parenting, to attached/detatch two rigid bodies to one another when is enabled. + /// This is the recommended way, as opposed to parenting, to attached/detach two rigid bodies to one another when is enabled. /// public void DetachFromFixedJoint() { @@ -1240,18 +1240,18 @@ public void DetachFromFixedJoint() #if COM_UNITY_MODULES_PHYSICS && COM_UNITY_MODULES_PHYSICS2D if (m_IsRigidbody2D) { - DetatchFromFixedJoint2D(); + DetachFromFixedJoint2D(); } else { - DetatchFromFixedJoint3D(); + DetachFromFixedJoint3D(); } #endif #if COM_UNITY_MODULES_PHYSICS && !COM_UNITY_MODULES_PHYSICS2D - DetatchFromFixedJoint3D(); + DetachFromFixedJoint3D(); #endif #if !COM_UNITY_MODULES_PHYSICS && COM_UNITY_MODULES_PHYSICS2D - DetatchFromFixedJoint2D(); + DetachFromFixedJoint2D(); #endif } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/AttachableBehaviourTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/AttachableBehaviourTests.cs new file mode 100644 index 0000000000..0efc6b6654 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/AttachableBehaviourTests.cs @@ -0,0 +1,381 @@ +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.Host)] + [TestFixture(HostOrServer.Server)] + [TestFixture(HostOrServer.DAHost)] + internal class AttachableBehaviourTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + public AttachableBehaviourTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + private GameObject m_SourcePrefab; + private GameObject m_TargetPrefabA; + private GameObject m_TargetPrefabB; + + /// + /// All of the below instances belong to the authority + /// + private NetworkObject m_SourceInstance; + private NetworkObject m_TargetInstance; + private NetworkObject m_TargetInstanceB; + private TestAttachable m_AttachableBehaviourInstance; + private TestNode m_AttachableNodeInstance; + private TestNode m_AttachableNodeInstanceB; + + private bool m_UseTargetB; + + private StringBuilder m_ErrorLog = new StringBuilder(); + + protected override IEnumerator OnSetup() + { + m_ErrorLog.Clear(); + return base.OnSetup(); + } + + protected override void OnServerAndClientsCreated() + { + // The source prefab contains the nested NetworkBehaviour that + // will be parented under the target prefab. + m_SourcePrefab = CreateNetworkObjectPrefab("Source"); + // The target prefab that the source prefab will attach + // will be parented under the target prefab. + m_TargetPrefabA = CreateNetworkObjectPrefab("TargetA"); + m_TargetPrefabB = CreateNetworkObjectPrefab("TargetB"); + var sourceChild = new GameObject("SourceChild"); + var targetChildA = new GameObject("TargetChildA"); + var targetChildB = new GameObject("TargetChildB"); + sourceChild.transform.parent = m_SourcePrefab.transform; + targetChildA.transform.parent = m_TargetPrefabA.transform; + targetChildB.transform.parent = m_TargetPrefabB.transform; + + sourceChild.AddComponent(); + targetChildA.AddComponent(); + targetChildB.AddComponent(); + base.OnServerAndClientsCreated(); + } + + private NetworkObject GetTargetInstance() + { + return m_UseTargetB ? m_TargetInstanceB : m_TargetInstance; + } + + private bool AllClientsSpawnedInstances() + { + m_ErrorLog.Clear(); + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_SourceInstance.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has not spawned {m_SourceInstance.name} yet!"); + } + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_TargetInstance.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has not spawned {m_TargetInstance.name} yet!"); + } + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_TargetInstanceB.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has not spawned {m_TargetInstanceB.name} yet!"); + } + } + return m_ErrorLog.Length == 0; + } + + private bool ResetAllStates() + { + m_ErrorLog.Clear(); + var target = GetTargetInstance(); + // The attachable can move between the two spawned instances. + var currentAttachableRoot = m_AttachableBehaviourInstance.State == AttachableBehaviour.AttachState.Attached ? target : m_SourceInstance; + foreach (var networkManager in m_NetworkManagers) + { + // Source + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_SourceInstance.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has no spawned instance of {currentAttachableRoot.name}!"); + } + else + { + var attachable = networkManager.SpawnManager.SpawnedObjects[currentAttachableRoot.NetworkObjectId].GetComponentInChildren(); + attachable.ResetStates(); + } + + // Target + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_TargetInstance.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has no spawned instance of {m_TargetInstance.name}!"); + } + else + { + var node = networkManager.SpawnManager.SpawnedObjects[m_TargetInstance.NetworkObjectId].GetComponentInChildren(); + node.ResetStates(); + } + + // Target B + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_TargetInstanceB.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has no spawned instance of {m_TargetInstanceB.name}!"); + } + else + { + var node = networkManager.SpawnManager.SpawnedObjects[m_TargetInstanceB.NetworkObjectId].GetComponentInChildren(); + node.ResetStates(); + } + } + return m_ErrorLog.Length == 0; + } + + private bool AllInstancesAttachedStateChanged(bool checkAttached) + { + m_ErrorLog.Clear(); + var target = GetTargetInstance(); + // The attachable can move between the two spawned instances so we have to use the appropriate one depending upon the authority's current state. + var currentAttachableRoot = m_AttachableBehaviourInstance.State == AttachableBehaviour.AttachState.Attached ? target : m_SourceInstance; + var attachable = (TestAttachable)null; + var node = (TestNode)null; + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(currentAttachableRoot.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has no spawned instance of {currentAttachableRoot.name}!"); + continue; + } + else + { + attachable = networkManager.SpawnManager.SpawnedObjects[currentAttachableRoot.NetworkObjectId].GetComponentInChildren(); + } + + if (!attachable) + { + attachable = networkManager.SpawnManager.SpawnedObjects[m_TargetInstance.NetworkObjectId].GetComponentInChildren(); + if (!attachable) + { + attachable = networkManager.SpawnManager.SpawnedObjects[m_TargetInstanceB.NetworkObjectId].GetComponentInChildren(); + if (!attachable) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}][Attachable] Attachable was not found!"); + } + } + continue; + } + + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(target.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has no spawned instance of {target.name}!"); + continue; + } + else + { + node = networkManager.SpawnManager.SpawnedObjects[target.NetworkObjectId].GetComponentInChildren(); + } + + if (!attachable.CheckStateChangedOverride(checkAttached, false, node)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}][{attachable.name}] Did not have its override invoked!"); + } + if (!attachable.CheckStateChangedOverride(checkAttached, true, node)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}][{attachable.name}] Did not have its event invoked!"); + } + if ((checkAttached && !node.OnAttachedInvoked) || (!checkAttached && !node.OnDetachedInvoked)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}][{node.name}] Did not have its override invoked!"); + } + if (checkAttached && attachable.transform.parent != node.transform) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}][{attachable.name}] {node.name} is not the parent of {attachable.name}!"); + } + else if (!checkAttached && attachable.transform.parent != attachable.DefaultParent.transform) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}][{attachable.name}] {attachable.DefaultParent.name} is not the parent of {attachable.name}!"); + } + } + return m_ErrorLog.Length == 0; + } + + [UnityTest] + public IEnumerator AttachAndDetachTests() + { + var authority = GetAuthorityNetworkManager(); + m_SourceInstance = SpawnObject(m_SourcePrefab, authority).GetComponent(); + m_TargetInstance = SpawnObject(m_TargetPrefabA, authority).GetComponent(); + m_TargetInstanceB = SpawnObject(m_TargetPrefabB, authority).GetComponent(); + yield return WaitForConditionOrTimeOut(AllClientsSpawnedInstances); + AssertOnTimeout($"Timed out waiting for all clients to spawn {m_SourceInstance.name} and {m_TargetInstance.name}!\n {m_ErrorLog}"); + + m_AttachableBehaviourInstance = m_SourceInstance.GetComponentInChildren(); + Assert.NotNull(m_AttachableBehaviourInstance, $"{m_SourceInstance.name} does not have a nested child {nameof(AttachableBehaviour)}!"); + + m_AttachableNodeInstance = m_TargetInstance.GetComponentInChildren(); + Assert.NotNull(m_AttachableNodeInstance, $"{m_TargetInstance.name} does not have a nested child {nameof(AttachableNode)}!"); + + m_AttachableNodeInstanceB = m_TargetInstanceB.GetComponentInChildren(); + Assert.NotNull(m_AttachableNodeInstanceB, $"{m_TargetInstanceB.name} does not have a nested child {nameof(AttachableNode)}!"); + + Assert.True(ResetAllStates(), $"Failed to reset all states!\n {m_ErrorLog}"); + m_AttachableBehaviourInstance.Attach(m_AttachableNodeInstance); + + yield return WaitForConditionOrTimeOut(() => AllInstancesAttachedStateChanged(true)); + AssertOnTimeout($"Timed out waiting for all clients to attach {m_AttachableBehaviourInstance.name} to {m_AttachableNodeInstance.name}!\n {m_ErrorLog}"); + + // Wait a brief period of time + yield return s_DefaultWaitForTick; + + // Now late join a client to make sure it synchronizes properly + yield return CreateAndStartNewClient(); + yield return WaitForConditionOrTimeOut(() => AllInstancesAttachedStateChanged(true)); + AssertOnTimeout($"Timed out waiting for all clients to attach {m_AttachableBehaviourInstance.name} to {m_AttachableNodeInstance.name}!\n {m_ErrorLog}"); + + // Wait a brief period of time + yield return s_DefaultWaitForTick; + + // Reset all states and prepare for 2nd attach test + Assert.True(ResetAllStates(), $"Failed to reset all states!\n {m_ErrorLog}"); + + // Now, while attached, attach to another attachable node which should detach from the current and attach to the new. + m_AttachableBehaviourInstance.Attach(m_AttachableNodeInstanceB); + + // The attachable should detach from the current AttachableNode first + yield return WaitForConditionOrTimeOut(() => AllInstancesAttachedStateChanged(false)); + AssertOnTimeout($"Timed out waiting for all clients to detach {m_AttachableBehaviourInstance.name} from {m_AttachableNodeInstance.name}!\n {m_ErrorLog}"); + + // Switch the conditional to check the target B attachable node + m_UseTargetB = true; + + // Then the attachable should attach to the target B attachable node + yield return WaitForConditionOrTimeOut(() => AllInstancesAttachedStateChanged(true)); + AssertOnTimeout($"Timed out waiting for all clients to attach {m_AttachableBehaviourInstance.name} to {m_AttachableNodeInstanceB.name}!\n {m_ErrorLog}"); + + // Reset all states and prepare for final detach test + Assert.True(ResetAllStates(), $"Failed to reset all states!\n {m_ErrorLog}"); + + // Now verify complete detaching works + m_AttachableBehaviourInstance.Detach(); + yield return WaitForConditionOrTimeOut(() => AllInstancesAttachedStateChanged(false)); + AssertOnTimeout($"Timed out waiting for all clients to detach {m_AttachableBehaviourInstance.name} from {m_AttachableNodeInstance.name}!\n {m_ErrorLog}"); + } + + /// + /// Helps to validate that the overrides and events are invoked when an attachable attaches or detaches from the instance. + /// This also helps to validate that the appropriate instance is passed in as a parameter. + /// + public class TestAttachable : AttachableBehaviour + { + private Dictionary m_StateUpdates = new Dictionary(); + + private Dictionary m_StateUpdateEvents = new Dictionary(); + + public GameObject DefaultParent => m_DefaultParent; + public AttachState State => m_AttachState; + + public override void OnNetworkSpawn() + { + AttachStateChange += OnAttachStateChangeEvent; + name = $"{name}-{NetworkManager.LocalClientId}"; + base.OnNetworkSpawn(); + } + + public override void OnNetworkDespawn() + { + AttachStateChange -= OnAttachStateChangeEvent; + base.OnNetworkDespawn(); + } + + private void OnAttachStateChangeEvent(AttachState attachState, AttachableNode attachableNode) + { + m_StateUpdateEvents.Add(attachState, attachableNode); + } + + protected override void OnAttachStateChanged(AttachState attachState, AttachableNode attachableNode) + { + m_StateUpdates.Add(attachState, attachableNode); + base.OnAttachStateChanged(attachState, attachableNode); + } + + public void ResetStates() + { + m_StateUpdates.Clear(); + m_StateUpdateEvents.Clear(); + } + + private void Log(string message) + { + Debug.Log($"[{name}] {message}"); + } + + public bool CheckStateChangedOverride(bool checkAttached, bool checkEvent, AttachableNode attachableNode) + { + var tableToCheck = checkEvent ? m_StateUpdateEvents : m_StateUpdates; + var checkStatus = checkAttached ? (tableToCheck.ContainsKey(AttachState.Attaching) && tableToCheck.ContainsKey(AttachState.Attached)) : + (tableToCheck.ContainsKey(AttachState.Detaching) && tableToCheck.ContainsKey(AttachState.Detached)); + + if (checkStatus) + { + foreach (var entry in tableToCheck) + { + // Ignore any states that don't match what is being checked + if ((checkStatus && (entry.Key == AttachState.Detaching || entry.Key == AttachState.Detached)) || + (!checkStatus && (entry.Key == AttachState.Attaching || entry.Key == AttachState.Attached))) + { + continue; + } + + // Special case for completely detached + if (entry.Key == AttachState.Detached) + { + if (entry.Value != null) + { + Log($"[Value] The value {entry.Value.name} is not null!"); + checkStatus = false; + break; + } + } + else if (entry.Value != attachableNode) + { + Log($"[{entry.Key}][Value] The value {entry.Value.name} is not the same as {attachableNode.name}!"); + checkStatus = false; + break; + } + } + } + return checkStatus; + } + } + + /// + /// Helps to validate that the overrides are invoked when an attachable attaches or detaches from the instance. + /// + public class TestNode : AttachableNode + { + public bool OnAttachedInvoked { get; private set; } + public bool OnDetachedInvoked { get; private set; } + + public void ResetStates() + { + OnAttachedInvoked = false; + OnDetachedInvoked = false; + } + + protected override void OnAttached(AttachableBehaviour attachableBehaviour) + { + OnAttachedInvoked = true; + base.OnAttached(attachableBehaviour); + } + + protected override void OnDetached(AttachableBehaviour attachableBehaviour) + { + OnDetachedInvoked = true; + base.OnDetached(attachableBehaviour); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/AttachableBehaviourTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/AttachableBehaviourTests.cs.meta new file mode 100644 index 0000000000..5e7eb6db85 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/AttachableBehaviourTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 016a03eb97e603345a44bca4defacf24 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/ComponentControllerTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/ComponentControllerTests.cs new file mode 100644 index 0000000000..7d2d6e86cb --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/ComponentControllerTests.cs @@ -0,0 +1,191 @@ +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.Host)] + [TestFixture(HostOrServer.Server)] + [TestFixture(HostOrServer.DAHost)] + internal class ComponentControllerTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + private StringBuilder m_ErrorLog = new StringBuilder(); + private GameObject m_TestPrefab; + + private NetworkManager m_Authority; + private ComponentController m_AuthorityController; + + public ComponentControllerTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + protected override IEnumerator OnSetup() + { + m_ErrorLog.Clear(); + yield return base.OnSetup(); + } + + protected override void OnServerAndClientsCreated() + { + // The source prefab contains the nested NetworkBehaviour that + // will be parented under the target prefab. + m_TestPrefab = CreateNetworkObjectPrefab("TestObject"); + var sourceChild = new GameObject("Child"); + sourceChild.transform.parent = m_TestPrefab.transform; + var meshRenderer = sourceChild.AddComponent(); + var light = sourceChild.AddComponent(); + var controller = m_TestPrefab.AddComponent(); + controller.Components = new List + { + new ComponentControllerEntry() + { + Component = meshRenderer, + }, + new ComponentControllerEntry() + { + InvertEnabled = true, + Component = light, + } + }; + base.OnServerAndClientsCreated(); + } + + private bool AllClientsSpawnedInstances() + { + m_ErrorLog.Clear(); + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityController.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Has not spawned {m_AuthorityController.name} yet!"); + } + } + return m_ErrorLog.Length == 0; + } + + private void ControllerStateMatches(ComponentController controller) + { + if (m_AuthorityController.EnabledState != controller.EnabledState) + { + m_ErrorLog.AppendLine($"[Client-{controller.NetworkManager.LocalClientId}] The authority controller state ({m_AuthorityController.EnabledState})" + + $" does not match the local controller state ({controller.EnabledState})!"); + return; + } + + if (m_AuthorityController.ValidComponents.Count != controller.ValidComponents.Count) + { + m_ErrorLog.AppendLine($"[Client-{controller.NetworkManager.LocalClientId}] The authority controller has {m_AuthorityController.ValidComponents.Count} valid components but " + + $"the local instance has {controller.ValidComponents.Count}!"); + return; + } + + for (int i = 0; i < m_AuthorityController.ValidComponents.Count; i++) + { + var authorityEntry = m_AuthorityController.ValidComponents[i]; + var nonAuthorityEntry = controller.ValidComponents[i]; + if (authorityEntry.InvertEnabled != nonAuthorityEntry.InvertEnabled) + { + m_ErrorLog.AppendLine($"[Client-{controller.NetworkManager.LocalClientId}] The authority controller's component entry ({i}) " + + $"has an inverted state of {authorityEntry.InvertEnabled} but the local instance has a value of " + + $"{nonAuthorityEntry.InvertEnabled}!"); + } + + var authorityIsEnabled = (bool)authorityEntry.PropertyInfo.GetValue(authorityEntry.Component); + var nonAuthorityIsEnabled = (bool)nonAuthorityEntry.PropertyInfo.GetValue(authorityEntry.Component); + if (authorityIsEnabled != nonAuthorityIsEnabled) + { + m_ErrorLog.AppendLine($"[Client-{controller.NetworkManager.LocalClientId}] The authority controller's component ({i}) " + + $"entry's enabled state is {authorityIsEnabled} but the local instance's value is {nonAuthorityIsEnabled}!"); + } + } + } + + private bool AllComponentStatesMatch() + { + m_ErrorLog.Clear(); + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityController.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Does not have a spawned instance of {m_AuthorityController.name}!"); + } + var controller = networkManager.SpawnManager.SpawnedObjects[m_AuthorityController.NetworkObjectId].GetComponent(); + ControllerStateMatches(controller); + } + return m_ErrorLog.Length == 0; + } + + private bool AllComponentStatesAreCorrect(bool isEnabled) + { + m_ErrorLog.Clear(); + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityController.NetworkObjectId)) + { + m_ErrorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Does not have a spawned instance of {m_AuthorityController.name}!"); + } + var controller = networkManager.SpawnManager.SpawnedObjects[m_AuthorityController.NetworkObjectId].GetComponent(); + for (int i = 0; i < controller.ValidComponents.Count; i++) + { + var componentEntry = controller.ValidComponents[i]; + + var componentEntryIsEnabled = (bool)componentEntry.PropertyInfo.GetValue(componentEntry.Component); + var valueToCheck = componentEntry.InvertEnabled ? !isEnabled : isEnabled; + + if (valueToCheck != componentEntryIsEnabled) + { + m_ErrorLog.AppendLine($"[Client-{controller.NetworkManager.LocalClientId}] The enabled state for entry ({i}) " + + $"should be {valueToCheck} but is {componentEntryIsEnabled}!"); + } + } + } + return m_ErrorLog.Length == 0; + } + + [UnityTest] + public IEnumerator EnabledDisabledSynchronizationTests() + { + m_Authority = GetAuthorityNetworkManager(); + + m_AuthorityController = SpawnObject(m_TestPrefab, m_Authority).GetComponent(); + + yield return WaitForConditionOrTimeOut(AllClientsSpawnedInstances); + AssertOnTimeout($"All clients did not spawn an instance of {m_AuthorityController.name}!\n {m_ErrorLog}"); + + // Validate that clients start off with matching states. + yield return WaitForConditionOrTimeOut(AllComponentStatesMatch); + AssertOnTimeout($"Not all client instances matched the authority instance {m_AuthorityController.name}! \n {m_ErrorLog}"); + + // Validate that all controllers have the correct enabled value for the current authority controller instance's value. + yield return WaitForConditionOrTimeOut(() => AllComponentStatesAreCorrect(m_AuthorityController.EnabledState)); + AssertOnTimeout($"Not all client instances have the correct enabled state!\n {m_ErrorLog}"); + + // Toggle the enabled state of the authority controller + m_AuthorityController.SetEnabled(!m_AuthorityController.EnabledState); + + // Validate that all controllers' states match + yield return WaitForConditionOrTimeOut(AllComponentStatesMatch); + AssertOnTimeout($"Not all client instances matched the authority instance {m_AuthorityController.name}! \n {m_ErrorLog}"); + + // Validate that all controllers have the correct enabled value for the current authority controller instance's value. + yield return WaitForConditionOrTimeOut(() => AllComponentStatesAreCorrect(m_AuthorityController.EnabledState)); + AssertOnTimeout($"Not all client instances have the correct enabled state!\n {m_ErrorLog}"); + + // Late join a client to assure the late joining client's values are synchronized properly + yield return CreateAndStartNewClient(); + + // Validate that all controllers' states match + yield return WaitForConditionOrTimeOut(AllComponentStatesMatch); + AssertOnTimeout($"Not all client instances matched the authority instance {m_AuthorityController.name}! \n {m_ErrorLog}"); + + // Validate that all controllers have the correct enabled value for the current authority controller instance's value. + yield return WaitForConditionOrTimeOut(() => AllComponentStatesAreCorrect(m_AuthorityController.EnabledState)); + AssertOnTimeout($"Not all client instances have the correct enabled state!\n {m_ErrorLog}"); + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/ComponentControllerTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/ComponentControllerTests.cs.meta new file mode 100644 index 0000000000..c5cb8ed883 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/ComponentControllerTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 22e1625a8e8c9a24ab0408e95a5250a9 \ No newline at end of file