#if COM_UNITY_MODULES_PHYSICS using System.Runtime.CompilerServices; using UnityEngine; namespace Unity.Netcode.Components { /// /// NetworkRigidbodyBase is a unified and integration that helps to synchronize physics motion, collision, and interpolation /// when used with a . /// /// /// For a customizable netcode Rigidbody, create your own component from this class and use /// during instantiation (i.e. invoked from within the Awake method). You can re-initialize after having initialized but only when the is not spawned. /// public abstract class NetworkRigidbodyBase : NetworkBehaviour { /// /// When enabled, the associated will use the Rigidbody/Rigidbody2D to apply and synchronize changes in position, rotation, and /// allows for the use of Rigidbody interpolation/extrapolation. /// /// /// If is enabled, non-authoritative instances can only use Rigidbody interpolation. If a network prefab is set to /// extrapolation and is enabled, then non-authoritative instances will automatically be adjusted to use Rigidbody /// interpolation while the authoritative instance will still use extrapolation. /// [Tooltip("When enabled and a NetworkTransform component is attached, the NetworkTransform will use the rigid body for motion and detecting changes in state.")] public bool UseRigidBodyForMotion; /// /// When enabled (default), automatically set the Kinematic state of the Rigidbody based on ownership. /// When disabled, Kinematic state needs to be set by external script(s). /// public bool AutoUpdateKinematicState = true; /// /// Primarily applies to the property when disabled but you still want /// the Rigidbody to be automatically set to Kinematic when despawned. /// public bool AutoSetKinematicOnDespawn = true; // Determines if this is a Rigidbody or Rigidbody2D implementation private bool m_IsRigidbody2D => RigidbodyType == RigidbodyTypes.Rigidbody2D; // Used to cache the authority state of this Rigidbody during the last frame private bool m_IsAuthority; private Rigidbody m_Rigidbody; private Rigidbody2D m_Rigidbody2D; internal NetworkTransform NetworkTransform; private float m_TickFrequency; private float m_TickRate; private enum InterpolationTypes { None, Interpolate, Extrapolate } private InterpolationTypes m_OriginalInterpolation; /// /// Used to define the type of Rigidbody implemented. /// /// public enum RigidbodyTypes { Rigidbody, Rigidbody2D, } public RigidbodyTypes RigidbodyType { get; private set; } /// /// Initializes the networked Rigidbody based on the /// passed in as a parameter. /// /// /// Cannot be initialized while the associated is spawned. /// /// type of rigid body being initialized /// (optional) The to be used /// (optional) The to be used protected void Initialize(RigidbodyTypes rigidbodyType, NetworkTransform networkTransform = null, Rigidbody2D rigidbody2D = null, Rigidbody rigidbody = null) { // Don't initialize if already spawned if (IsSpawned) { Debug.LogError($"[{name}] Attempting to initialize while spawned is not allowed."); return; } RigidbodyType = rigidbodyType; m_Rigidbody2D = rigidbody2D; m_Rigidbody = rigidbody; NetworkTransform = networkTransform; if (m_IsRigidbody2D && m_Rigidbody2D == null) { m_Rigidbody2D = GetComponent(); } else if (m_Rigidbody == null) { m_Rigidbody = GetComponent(); } SetOriginalInterpolation(); if (NetworkTransform == null) { NetworkTransform = GetComponent(); } if (NetworkTransform != null) { NetworkTransform.RegisterRigidbody(this); } else { throw new System.Exception($"[Missing {nameof(NetworkTransform)}] No {nameof(NetworkTransform)} is assigned or can be found during initialization!"); } if (AutoUpdateKinematicState) { SetIsKinematic(true); } } internal Vector3 GetAdjustedPositionThreshold() { // Since the threshold is a measurement of unity world space units per tick, we will allow for the maximum threshold // to be no greater than the threshold measured in unity world space units per second var thresholdMax = NetworkTransform.PositionThreshold * m_TickRate; // Get the velocity in unity world space units per tick var perTickVelocity = GetLinearVelocity() * m_TickFrequency; // Since a rigid body can have "micro-motion" when allowed to come to rest (based on friction etc), we will allow for // no less than 1/10th the threshold value. var minThreshold = NetworkTransform.PositionThreshold * 0.1f; // Finally, we adjust the threshold based on the body's current velocity perTickVelocity.x = Mathf.Clamp(Mathf.Abs(perTickVelocity.x), minThreshold, thresholdMax); perTickVelocity.y = Mathf.Clamp(Mathf.Abs(perTickVelocity.y), minThreshold, thresholdMax); // 2D Rigidbody only moves on x & y axis if (!m_IsRigidbody2D) { perTickVelocity.z = Mathf.Clamp(Mathf.Abs(perTickVelocity.z), minThreshold, thresholdMax); } return perTickVelocity; } internal Vector3 GetAdjustedRotationThreshold() { // Since the rotation threshold is a measurement pf degrees per tick, we get the maximum threshold // by calculating the threshold in degrees per second. var thresholdMax = NetworkTransform.RotAngleThreshold * m_TickRate; // Angular velocity is expressed in radians per second where as the rotation being checked is in degrees. // Convert the angular velocity to degrees per second and then convert that to degrees per tick. var rotationPerTick = (GetAngularVelocity() * Mathf.Rad2Deg) * m_TickFrequency; var minThreshold = NetworkTransform.RotAngleThreshold * m_TickFrequency; // 2D Rigidbody only rotates around Z axis if (!m_IsRigidbody2D) { rotationPerTick.x = Mathf.Clamp(Mathf.Abs(rotationPerTick.x), minThreshold, thresholdMax); rotationPerTick.y = Mathf.Clamp(Mathf.Abs(rotationPerTick.y), minThreshold, thresholdMax); } rotationPerTick.z = Mathf.Clamp(Mathf.Abs(rotationPerTick.z), minThreshold, thresholdMax); return rotationPerTick; } /// /// Sets the linear velocity of the Rigidbody. /// /// /// For , only the x and y components of the are applied. /// public void SetLinearVelocity(Vector3 linearVelocity) { if (m_IsRigidbody2D) { m_Rigidbody2D.velocity = linearVelocity; } else { m_Rigidbody.linearVelocity = linearVelocity; } } /// /// Gets the linear velocity of the Rigidbody. /// /// /// For , the velocity returned is only applied to the x and y components. /// /// as the linear velocity [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3 GetLinearVelocity() { if (m_IsRigidbody2D) { return m_Rigidbody2D.velocity; } else { return m_Rigidbody.linearVelocity; } } /// /// Sets the angular velocity for the Rigidbody. /// /// /// For , the z component of is only used to set the angular velocity. /// A quick way to pass in a 2D angular velocity component is: * angularVelocity (where angularVelocity is a float) /// /// the angular velocity to apply to the body [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetAngularVelocity(Vector3 angularVelocity) { if (m_IsRigidbody2D) { m_Rigidbody2D.angularVelocity = angularVelocity.z; } else { m_Rigidbody.angularVelocity = angularVelocity; } } /// /// Gets the angular velocity for the Rigidbody. /// /// /// For , the z component of the returned is the angular velocity of the object. /// /// angular velocity as a [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3 GetAngularVelocity() { if (m_IsRigidbody2D) { return Vector3.forward * m_Rigidbody2D.velocity; } else { return m_Rigidbody.angularVelocity; } } /// /// Gets the position of the Rigidbody /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector3 GetPosition() { if (m_IsRigidbody2D) { return m_Rigidbody2D.position; } else { return m_Rigidbody.position; } } /// /// Gets the rotation of the Rigidbody /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Quaternion GetRotation() { if (m_IsRigidbody2D) { var quaternion = Quaternion.identity; var angles = quaternion.eulerAngles; angles.z = m_Rigidbody2D.rotation; quaternion.eulerAngles = angles; return quaternion; } else { return m_Rigidbody.rotation; } } /// /// Moves the rigid body /// /// The position to move towards [MethodImpl(MethodImplOptions.AggressiveInlining)] public void MovePosition(Vector3 position) { if (m_IsRigidbody2D) { m_Rigidbody2D.MovePosition(position); } else { m_Rigidbody.MovePosition(position); } } /// /// Directly applies a position (like teleporting) /// /// position to apply to the Rigidbody [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetPosition(Vector3 position) { if (m_IsRigidbody2D) { m_Rigidbody2D.position = position; } else { m_Rigidbody.position = position; } } /// /// Applies the rotation and position of the 's /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ApplyCurrentTransform() { if (m_IsRigidbody2D) { m_Rigidbody2D.position = transform.position; m_Rigidbody2D.rotation = transform.eulerAngles.z; } else { m_Rigidbody.position = transform.position; m_Rigidbody.rotation = transform.rotation; } } // Used for Rigidbody only (see info on normalized below) private Vector4 m_QuaternionCheck = Vector4.zero; /// /// Rotatates the Rigidbody towards a specified rotation /// /// The rotation expressed as a [MethodImpl(MethodImplOptions.AggressiveInlining)] public void MoveRotation(Quaternion rotation) { if (m_IsRigidbody2D) { var quaternion = Quaternion.identity; var angles = quaternion.eulerAngles; angles.z = m_Rigidbody2D.rotation; quaternion.eulerAngles = angles; m_Rigidbody2D.MoveRotation(quaternion); } else { // Evidently we need to check to make sure the quaternion is a perfect // magnitude of 1.0f when applying the rotation to a rigid body. m_QuaternionCheck.x = rotation.x; m_QuaternionCheck.y = rotation.y; m_QuaternionCheck.z = rotation.z; m_QuaternionCheck.w = rotation.w; // If the magnitude is greater than 1.0f (even by a very small fractional value), then normalize the quaternion if (m_QuaternionCheck.magnitude != 1.0f) { rotation.Normalize(); } m_Rigidbody.MoveRotation(rotation); } } /// /// Applies a rotation to the Rigidbody /// /// The rotation to apply expressed as a [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetRotation(Quaternion rotation) { if (m_IsRigidbody2D) { m_Rigidbody2D.rotation = rotation.eulerAngles.z; } else { m_Rigidbody.rotation = rotation; } } /// /// Sets the original interpolation of the Rigidbody while taking the Rigidbody type into consideration /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetOriginalInterpolation() { if (m_IsRigidbody2D) { switch (m_Rigidbody2D.interpolation) { case RigidbodyInterpolation2D.None: { m_OriginalInterpolation = InterpolationTypes.None; break; } case RigidbodyInterpolation2D.Interpolate: { m_OriginalInterpolation = InterpolationTypes.Interpolate; break; } case RigidbodyInterpolation2D.Extrapolate: { m_OriginalInterpolation = InterpolationTypes.Extrapolate; break; } } } else { switch (m_Rigidbody.interpolation) { case RigidbodyInterpolation.None: { m_OriginalInterpolation = InterpolationTypes.None; break; } case RigidbodyInterpolation.Interpolate: { m_OriginalInterpolation = InterpolationTypes.Interpolate; break; } case RigidbodyInterpolation.Extrapolate: { m_OriginalInterpolation = InterpolationTypes.Extrapolate; break; } } } } /// /// Wakes the Rigidbody if it is sleeping /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WakeIfSleeping() { if (m_IsRigidbody2D) { if (m_Rigidbody2D.IsSleeping()) { m_Rigidbody2D.WakeUp(); } } else { if (m_Rigidbody.IsSleeping()) { m_Rigidbody.WakeUp(); } } } /// /// Puts the Rigidbody to sleep /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SleepRigidbody() { if (m_IsRigidbody2D) { m_Rigidbody2D.Sleep(); } else { m_Rigidbody.Sleep(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsKinematic() { if (m_IsRigidbody2D) { return m_Rigidbody2D.isKinematic; } else { return m_Rigidbody.isKinematic; } } /// /// Sets the kinematic state of the Rigidbody and handles updating the Rigidbody's /// interpolation setting based on the Kinematic state. /// /// /// When using the Rigidbody for motion, this automatically /// adjusts from extrapolation to interpolation if: /// - The Rigidbody was originally set to extrapolation /// - The NetworkTransform is set to interpolate /// When the two above conditions are true: /// - When switching from non-kinematic to kinematic this will automatically /// switch the Rigidbody from extrapolation to interpolate. /// - When switching from kinematic to non-kinematic this will automatically /// switch the Rigidbody from interpolation back to extrapolation. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetIsKinematic(bool isKinematic) { if (m_IsRigidbody2D) { m_Rigidbody2D.isKinematic = isKinematic; } else { m_Rigidbody.isKinematic = isKinematic; } // If we are not spawned, then exit early if (!IsSpawned) { return; } if (UseRigidBodyForMotion) { // Only if the NetworkTransform is set to interpolate do we need to check for extrapolation if (NetworkTransform.Interpolate && m_OriginalInterpolation == InterpolationTypes.Extrapolate) { if (IsKinematic()) { // If not already set to interpolate then set the Rigidbody to interpolate if (m_Rigidbody.interpolation == RigidbodyInterpolation.Extrapolate) { // Sleep until the next fixed update when switching from extrapolation to interpolation SleepRigidbody(); SetInterpolation(InterpolationTypes.Interpolate); } } else { // Switch it back to the original interpolation if non-kinematic (doesn't require sleep). SetInterpolation(m_OriginalInterpolation); } } } else { SetInterpolation(m_IsAuthority ? m_OriginalInterpolation : (NetworkTransform.Interpolate ? InterpolationTypes.None : m_OriginalInterpolation)); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetInterpolation(InterpolationTypes interpolationType) { switch (interpolationType) { case InterpolationTypes.None: { if (m_IsRigidbody2D) { m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.None; } else { m_Rigidbody.interpolation = RigidbodyInterpolation.None; } break; } case InterpolationTypes.Interpolate: { if (m_IsRigidbody2D) { m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate; } else { m_Rigidbody.interpolation = RigidbodyInterpolation.Interpolate; } break; } case InterpolationTypes.Extrapolate: { if (m_IsRigidbody2D) { m_Rigidbody2D.interpolation = RigidbodyInterpolation2D.Extrapolate; } else { m_Rigidbody.interpolation = RigidbodyInterpolation.Extrapolate; } break; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetInterpolation() { SetInterpolation(m_OriginalInterpolation); } protected override void OnOwnershipChanged(ulong previous, ulong current) { UpdateOwnershipAuthority(); base.OnOwnershipChanged(previous, current); } /// /// Sets the authority based on whether it is server or owner authoritative /// /// /// Distributed authority sessions will always be owner authoritative. /// internal void UpdateOwnershipAuthority() { if (NetworkManager.DistributedAuthorityMode) { // When in distributed authority mode, always use HasAuthority m_IsAuthority = HasAuthority; } else { if (NetworkTransform.IsServerAuthoritative()) { m_IsAuthority = NetworkManager.IsServer; } else { m_IsAuthority = IsOwner; } } if (AutoUpdateKinematicState) { SetIsKinematic(!m_IsAuthority); } } /// public override void OnNetworkSpawn() { m_TickFrequency = 1.0f / NetworkManager.NetworkConfig.TickRate; m_TickRate = NetworkManager.NetworkConfig.TickRate; UpdateOwnershipAuthority(); } /// public override void OnNetworkDespawn() { if (UseRigidBodyForMotion && HasAuthority) { DetachFromFixedJoint(); NetworkRigidbodyConnections.Clear(); } // If we are automatically handling the kinematic state... if (AutoUpdateKinematicState || AutoSetKinematicOnDespawn) { // Turn off physics for the rigid body until spawned, otherwise // non-owners can run fixed updates before the first full // NetworkTransform update and physics will be applied (i.e. gravity, etc) SetIsKinematic(true); } SetInterpolation(m_OriginalInterpolation); } // TODO: Possibly provide a NetworkJoint that allows for more options than fixed. // Rigidbodies do not have the concept of "local space", and as such using a fixed joint will hold the object // in place relative to the parent so jitter/stutter does not occur. // Alternately, users can affix the fixed joint to a child GameObject (without a rigid body) of the parent NetworkObject // and then add a NetworkTransform to that in order to get the parented child NetworkObject to move around in "local space" public FixedJoint FixedJoint { get; private set; } public FixedJoint2D FixedJoint2D { get; private set; } internal System.Collections.Generic.List NetworkRigidbodyConnections = new System.Collections.Generic.List(); internal NetworkRigidbodyBase ParentBody; private bool m_FixedJoint2DUsingGravity; private bool m_OriginalGravitySetting; private float m_OriginalGravityScale; /// /// When using a custom , this virtual method is invoked when the /// is created in the event any additional adjustments are needed. /// protected virtual void OnFixedJointCreated() { } /// /// When using a custom , this virtual method is invoked when the /// is created in the event any additional adjustments are needed. /// protected virtual void OnFixedJoint2DCreated() { } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ApplyFixedJoint2D(NetworkRigidbodyBase bodyToConnect, Vector3 position, float connectedMassScale = 0.0f, float massScale = 1.0f, bool useGravity = false, bool zeroVelocity = true) { transform.position = position; m_Rigidbody2D.position = position; m_OriginalGravitySetting = bodyToConnect.m_Rigidbody.useGravity; m_FixedJoint2DUsingGravity = useGravity; if (!useGravity) { m_OriginalGravityScale = m_Rigidbody2D.gravityScale; m_Rigidbody2D.gravityScale = 0.0f; } if (zeroVelocity) { m_Rigidbody2D.velocity = Vector2.zero; m_Rigidbody2D.angularVelocity = 0.0f; } FixedJoint2D = gameObject.AddComponent(); FixedJoint2D.connectedBody = bodyToConnect.m_Rigidbody2D; OnFixedJoint2DCreated(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ApplyFixedJoint(NetworkRigidbodyBase bodyToConnectTo, Vector3 position, float connectedMassScale = 0.0f, float massScale = 1.0f, bool useGravity = false, bool zeroVelocity = true) { transform.position = position; m_Rigidbody.position = position; if (zeroVelocity) { m_Rigidbody.linearVelocity = Vector3.zero; m_Rigidbody.angularVelocity = Vector3.zero; } m_OriginalGravitySetting = m_Rigidbody.useGravity; m_Rigidbody.useGravity = useGravity; FixedJoint = gameObject.AddComponent(); FixedJoint.connectedBody = bodyToConnectTo.m_Rigidbody; FixedJoint.connectedMassScale = connectedMassScale; FixedJoint.massScale = massScale; OnFixedJointCreated(); } /// /// Authority Only: /// When invoked and not already attached to a fixed joint, this will connect two rigid bodies with enabled. /// Invoke this method on the rigid body you wish to attach to another (i.e. weapon to player, sticky bomb to player/object, etc). /// /// /// /// /// Parenting relative: /// - 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. /// 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 /// version of . ///
/// The target object to attach to. /// The position of the connection (i.e. where you want the object to be affixed). /// The target object's mass scale relative to this object being attached. /// This object's mass scale relative to the target object's. /// Determines if this object will have gravity applied to it along with the object you are connecting this one to (the default is to not use gravity for this object) /// When true (the default), both linear and angular velocities of this object are set to zero. /// When true (the default), this object will teleport itself to the position of connection. /// true (success) false (failed) public bool AttachToFixedJoint(NetworkRigidbodyBase objectToConnectTo, Vector3 positionOfConnection, float connectedMassScale = 0.0f, float massScale = 1.0f, bool useGravity = false, bool zeroVelocity = true, bool teleportObject = true) { if (!UseRigidBodyForMotion) { Debug.LogError($"[{GetType().Name}] {name} does not have {nameof(UseRigidBodyForMotion)} set! Either enable {nameof(UseRigidBodyForMotion)} on this component or do not use a {nameof(FixedJoint)} when parenting under a {nameof(NetworkObject)}."); return false; } if (IsKinematic()) { Debug.LogError($"[{GetType().Name}] {name} is currently kinematic! You cannot use a {nameof(FixedJoint)} with Kinematic bodies!"); return false; } if (objectToConnectTo != null) { if (m_IsRigidbody2D) { ApplyFixedJoint2D(objectToConnectTo, positionOfConnection, connectedMassScale, massScale, useGravity, zeroVelocity); } else { ApplyFixedJoint(objectToConnectTo, positionOfConnection, connectedMassScale, massScale, useGravity, zeroVelocity); } ParentBody = objectToConnectTo; ParentBody.NetworkRigidbodyConnections.Add(this); if (teleportObject) { NetworkTransform.SetState(teleportDisabled: false); } return true; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RemoveFromParentBody() { ParentBody.NetworkRigidbodyConnections.Remove(this); ParentBody = null; } /// /// Authority Only: /// When invoked and already connected to an object via or (depending upon the type of rigid body), /// 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. /// public void DetachFromFixedJoint() { if (!HasAuthority) { Debug.LogError($"[{name}] Only authority can invoke {nameof(DetachFromFixedJoint)}!"); } if (UseRigidBodyForMotion) { if (m_IsRigidbody2D) { if (FixedJoint2D != null) { if (!m_FixedJoint2DUsingGravity) { FixedJoint2D.connectedBody.gravityScale = m_OriginalGravityScale; } FixedJoint2D.connectedBody = null; Destroy(FixedJoint2D); FixedJoint2D = null; ResetInterpolation(); RemoveFromParentBody(); } } else { if (FixedJoint != null) { FixedJoint.connectedBody = null; m_Rigidbody.useGravity = m_OriginalGravitySetting; Destroy(FixedJoint); FixedJoint = null; ResetInterpolation(); RemoveFromParentBody(); } } } } } } #endif // COM_UNITY_MODULES_PHYSICS