using System; using UnityEngine; using NWH.Common.Utility; #if UNITY_EDITOR using NWH.NUI; using UnityEditor; #endif namespace NWH.Common.CoM { /// /// Script used for adjusting Rigidbody properties at runtime based on /// attached IMassAffectors. This allows for vehicle center of mass and inertia changes /// as the fuel is depleted, cargo is added, etc. without the need of physically parenting Rigidbodies to the object. /// [DisallowMultipleComponent] [DefaultExecutionOrder(-1000)] [RequireComponent(typeof(Rigidbody))] public class VariableCenterOfMass : MonoBehaviour { /// /// Should the default Rigidbody mass be used? /// public bool useDefaultMass = true; /// /// If true, the script will search for any IMassAffectors attached as a child (recursively) /// of this script and use them when calculating mass, center of mass and inertia tensor. /// public bool useMassAffectors = false; /// /// Base mass of the object, without IMassAffectors. /// [UnityEngine.Tooltip("Base mass of the object, without IMassAffectors.")] public float baseMass = 1400f; /// /// Total mass of the object with masses of IMassAffectors counted in. /// [UnityEngine.Tooltip("Total mass of the object with masses of IMassAffectors counted in.")] public float combinedMass = 1400f; /// /// Object dimensions in [m]. X - width, Y - height, Z - length. /// It is important to set the correct dimensions or otherwise inertia might be calculated incorrectly. /// [UnityEngine.Tooltip("Object dimensions in [m]. X - width, Y - height, Z - length.\r\nIt is important to set the correct dimensions or otherwise inertia might be calculated incorrectly.")] public Vector3 dimensions = new Vector3(1.8f, 1.6f, 4.6f); /// /// When enabled the Unity-calculated center of mass will be used. /// [Tooltip( "When enabled the Unity-calculated center of mass will be used.")] public bool useDefaultCenterOfMass = true; /// /// Center of mass of the object. Auto calculated. To adjust center of mass use centerOfMassOffset. /// [Tooltip( "Center of mass of the rigidbody. Needs to be readjusted when new colliders are added.")] public Vector3 centerOfMass = Vector3.zero; /// /// Combined center of mass, including the Rigidbody and any IMassAffectors. /// public Vector3 combinedCenterOfMass = Vector3.zero; /// /// When true inertia settings will be ignored and default Rigidbody inertia tensor will be used. /// [UnityEngine.Tooltip("When true inertia settings will be ignored and default Rigidbody inertia tensor will be used.")] public bool useDefaultInertia = true; /// /// Vector by which the inertia tensor of the rigidbody will be scaled on Start(). /// Due to the uniform density of the rigidbodies, versus the very non-uniform density of a vehicle, inertia can feel /// off. /// Use this to adjust inertia tensor values. /// [Tooltip( " Vector by which the inertia tensor of the rigidbody will be scaled on Start().\r\n Due to the unform density of the rigidbodies, versus the very non-uniform density of a vehicle, inertia can feel\r\n off.\r\n Use this to adjust inertia tensor values.")] public Vector3 inertiaTensor = new Vector3(1000f, 1000f, 1000f); /// /// Total inertia tensor. Includes Rigidbody and IMassAffectors. /// public Vector3 combinedInertiaTensor; /// /// Objects attached or part of the vehicle affecting its center of mass and inertia. /// [NonSerialized] public IMassAffector[] affectors; private Rigidbody _rigidbody; private void Awake() { Initialize(); } private void Initialize() { _rigidbody = GetComponent(); if (useDefaultMass) baseMass = _rigidbody.mass; if (useDefaultInertia) inertiaTensor = _rigidbody.inertiaTensor; if (useDefaultCenterOfMass) centerOfMass = _rigidbody.centerOfMass; affectors = GetMassAffectors(); UpdateAllProperties(); } private void OnValidate() { _rigidbody = GetComponent(); affectors = GetMassAffectors(); } private void FixedUpdate() { UpdateAllProperties(); } public void UpdateAllProperties() { if (!useDefaultMass) UpdateMass(); if (!useDefaultCenterOfMass) UpdateCoM(); if (!useDefaultInertia) UpdateInertia(); } public void UpdateMass() { if (useMassAffectors) { combinedMass = CalculateMass(); } else { combinedMass = baseMass; } _rigidbody.mass = combinedMass; } /// /// Calculates and applies the CoM to the Rigidbody. /// public void UpdateCoM() { if (useMassAffectors) { combinedCenterOfMass = centerOfMass + CalculateRelativeCenterOfMassOffset(); } else { combinedCenterOfMass = centerOfMass; } _rigidbody.centerOfMass = combinedCenterOfMass; } /// /// Calculates and applies the inertia tensor to the Rigidbody. /// public void UpdateInertia(bool applyUnchanged = false) { if (useMassAffectors) { combinedInertiaTensor = inertiaTensor + CalculateInertiaTensorOffset(dimensions); } else { combinedInertiaTensor = inertiaTensor; } // Inertia tensor of constrained rigidbody will be 0 which causes errors when trying to set. if (combinedInertiaTensor.x > 0 && combinedInertiaTensor.y > 0 && combinedInertiaTensor.z > 0) { _rigidbody.inertiaTensor = combinedInertiaTensor; _rigidbody.inertiaTensorRotation = Quaternion.identity; } } /// /// Updates list of IMassAffectors attached to this object. /// Call after IMassAffector has been added or removed from the object. /// public IMassAffector[] GetMassAffectors() { return GetComponentsInChildren(true); } /// /// Calculates the mass of the Rigidbody and attached mass affectors. /// public float CalculateMass() { float massSum = baseMass; foreach (IMassAffector affector in affectors) { if (affector.GetTransform().gameObject.activeInHierarchy) { massSum += affector.GetMass(); } } return massSum; } /// /// Calculates the center of mass of the Rigidbody and attached mass affectors. /// public Vector3 CalculateRelativeCenterOfMassOffset() { Vector3 offset = Vector3.zero; if (useMassAffectors) { float massSum = CalculateMass(); for (int i = 0; i < affectors.Length; i++) { offset += transform.InverseTransformPoint(affectors[i].GetWorldCenterOfMass()) * (affectors[i].GetMass() / massSum); } } return offset; } /// /// Calculates the inertia tensor of the Rigidbody and attached mass affectors. /// public Vector3 CalculateInertiaTensorOffset(Vector3 dimensions) { Vector3 affectorInertiaSum = Vector3.zero; for (int i = 0; i < affectors.Length; i++) // Skip first (this) { IMassAffector affector = affectors[i]; if (affector.GetTransform().gameObject.activeInHierarchy) { float mass = affector.GetMass(); Vector3 affectorLocalPos = transform.InverseTransformPoint(affector.GetTransform().position); float x = Vector3.ProjectOnPlane(affectorLocalPos, Vector3.right).magnitude * mass; float y = Vector3.ProjectOnPlane(affectorLocalPos, Vector3.up).magnitude * mass; float z = Vector3.ProjectOnPlane(affectorLocalPos, Vector3.forward).magnitude * mass; affectorInertiaSum.x += x * x; affectorInertiaSum.y += y * y; affectorInertiaSum.z += z * z; } } return affectorInertiaSum; } public static Vector3 CalculateInertia(Vector3 dimensions, float mass) { float c = (1f / 12f) * mass; float Ix = c * (dimensions.y * dimensions.y + dimensions.z * dimensions.z); float Iy = c * (dimensions.x * dimensions.x + dimensions.z * dimensions.z); float Iz = c * (dimensions.y * dimensions.y + dimensions.x * dimensions.x); return new Vector3(Ix, Iy, Iz); } private void OnDrawGizmos() { #if UNITY_EDITOR if (!Application.isPlaying) { Initialize(); UpdateAllProperties(); } // CoM Gizmos.color = Color.yellow; Vector3 worldCoM = transform.TransformPoint(centerOfMass); Gizmos.DrawSphere(worldCoM, 0.03f); Handles.Label(worldCoM, "CoM"); // Mass Affectors Gizmos.color = Color.cyan; if (affectors == null) return; for (int i = 0; i < affectors.Length; i++) { if (affectors[i] == null) continue; Gizmos.DrawSphere(affectors[i].GetTransform().position, 0.05f); } // Dimensions if (!useDefaultInertia) { Transform t = transform; Vector3 fwdOffset = t.forward * dimensions.z * 0.5f; Vector3 rightOffset = t.right * dimensions.x * 0.5f; Vector3 upOffset = t.up * dimensions.y * 0.5f; Gizmos.color = Color.blue; Gizmos.DrawLine(worldCoM + fwdOffset, worldCoM - fwdOffset); Gizmos.color = Color.red; Gizmos.DrawLine(worldCoM + rightOffset, worldCoM - rightOffset); Gizmos.color = Color.green; Gizmos.DrawLine(worldCoM + upOffset, worldCoM - upOffset); } #endif } private void Reset() { _rigidbody = GetComponent(); Bounds bounds = gameObject.FindBoundsIncludeChildren(); dimensions = new Vector3(bounds.extents.x * 2f, bounds.extents.y * 2f, bounds.extents.z * 2f); Debug.Log($"Detected dimensions of {name} as {dimensions} [m]. If incorrect, adjust manually."); if (dimensions.x < 0.001f) dimensions.x = 0.001f; if (dimensions.y < 0.001f) dimensions.y = 0.001f; if (dimensions.z < 0.001f) dimensions.z = 0.001f; centerOfMass = _rigidbody.centerOfMass; baseMass = _rigidbody.mass; combinedMass = baseMass; inertiaTensor = _rigidbody.inertiaTensor; } public Vector3 GetWorldCenterOfMass() { return transform.TransformPoint(combinedCenterOfMass); } } } #if UNITY_EDITOR namespace NWH.Common.CoM { [CustomEditor(typeof(VariableCenterOfMass))] public class VariableCenterOfMassEditor : NUIEditor { public override bool OnInspectorNUI() { if (!base.OnInspectorNUI()) { return false; } VariableCenterOfMass vcom = (VariableCenterOfMass)target; if (vcom == null) { drawer.EndEditor(); return false; } Rigidbody parentRigidbody = vcom.gameObject.GetComponentInParent(true); if (parentRigidbody == null) { drawer.EndEditor(); return false; } if (!Application.isPlaying) { foreach (var o in targets) { var t = (VariableCenterOfMass)o; t.affectors = t.GetMassAffectors(); t.UpdateAllProperties(); } } drawer.BeginSubsection("Mass Affectors"); if (drawer.Field("useMassAffectors").boolValue) { if (vcom.affectors != null) { if (!Application.isPlaying) { vcom.affectors = vcom.GetMassAffectors(); } for (int i = 0; i < vcom.affectors.Length; i++) { IMassAffector affector = vcom.affectors[i]; if (affector == null || affector.GetTransform() == null) continue; string positionStr = i == 0 ? "(this)" : $"Position = {affector.GetTransform().localPosition}"; drawer.Label($"{affector.GetTransform().name} | Mass = {affector.GetMass()} | {positionStr}"); } } } drawer.EndSubsection(); // MASS drawer.BeginSubsection("Mass"); if (!drawer.Field("useDefaultMass").boolValue) { float newMass = drawer.Field("baseMass", true, "kg").floatValue; parentRigidbody.mass = newMass; if (vcom.useMassAffectors) { drawer.Field("combinedMass", false, "kg"); } } drawer.EndSubsection(); // CENTER OF MASS drawer.BeginSubsection("Center Of Mass"); if (!drawer.Field("useDefaultCenterOfMass").boolValue) { drawer.Field("centerOfMass", true); if (vcom.useMassAffectors) { drawer.Field("combinedCenterOfMass", false); } } drawer.EndSubsection(); // INERTIA drawer.BeginSubsection("Inertia"); if (!drawer.Field("useDefaultInertia").boolValue) { drawer.Field("inertiaTensor", true, "kg m2"); if (vcom.useMassAffectors) { drawer.Field("combinedInertiaTensor", false, "kg m2"); } drawer.BeginSubsection("Calculate Inertia From Dimensions"); { drawer.Field("dimensions", true, "m"); if (drawer.Button("Calculate")) { vcom.inertiaTensor = VariableCenterOfMass.CalculateInertia(vcom.dimensions, parentRigidbody.mass); EditorUtility.SetDirty(vcom); } } } drawer.EndSubsection(); drawer.EndEditor(this); return true; } } } #endif