#if !UNITY_2019_3_OR_NEWER #define CINEMACHINE_PHYSICS #define CINEMACHINE_PHYSICS_2D #endif using UnityEngine; namespace Cinemachine { /// An ad-hoc collection of helpers, used by Cinemachine /// or its editor tools in various places [DocumentationSorting(DocumentationSortingAttribute.Level.Undoc)] public static class RuntimeUtility { /// Convenience to destroy an object, using the appropriate method depending /// on whether the game is playing /// The object to destroy public static void DestroyObject(UnityEngine.Object obj) { if (obj != null) { #if UNITY_EDITOR if (Application.isPlaying) UnityEngine.Object.Destroy(obj); else UnityEngine.Object.DestroyImmediate(obj); #else UnityEngine.Object.Destroy(obj); #endif } } /// /// Check whether a GameObject is a prefab. /// For editor only - some things are disallowed if prefab. In runtime, will always return false. /// /// the object to check /// If editor, checks if object is a prefab or prefab instance. /// In runtime, returns false always public static bool IsPrefab(GameObject gameObject) { #if UNITY_EDITOR return UnityEditor.PrefabUtility.GetPrefabInstanceStatus(gameObject) != UnityEditor.PrefabInstanceStatus.NotAPrefab; #else return false; #endif } #if CINEMACHINE_PHYSICS private static RaycastHit[] s_HitBuffer = new RaycastHit[16]; private static int[] s_PenetrationIndexBuffer = new int[16]; /// /// Perform a raycast, but pass through any objects that have a given tag /// /// The ray to cast /// The returned results /// Length of the raycast /// Layers to include /// Tag to ignore /// True if something was hit. Results in hitInfo public static bool RaycastIgnoreTag( Ray ray, out RaycastHit hitInfo, float rayLength, int layerMask, in string ignoreTag) { if (ignoreTag.Length == 0) { if (Physics.Raycast( ray, out hitInfo, rayLength, layerMask, QueryTriggerInteraction.Ignore)) { return true; } } else { int closestHit = -1; int numHits = Physics.RaycastNonAlloc( ray, s_HitBuffer, rayLength, layerMask, QueryTriggerInteraction.Ignore); for (int i = 0; i < numHits; ++i) { if (s_HitBuffer[i].collider.CompareTag(ignoreTag)) continue; if (closestHit < 0 || s_HitBuffer[i].distance < s_HitBuffer[closestHit].distance) closestHit = i; } if (closestHit >= 0) { hitInfo = s_HitBuffer[closestHit]; if (numHits == s_HitBuffer.Length) s_HitBuffer = new RaycastHit[s_HitBuffer.Length * 2]; // full! grow for next time return true; } } hitInfo = new RaycastHit(); return false; } /// /// Perform a sphere cast, but pass through objects with a given tag /// /// Start of the ray /// Radius of the sphere cast /// Normalized direction of the ray /// Results go here /// Length of the ray /// Layers to include /// Tag to ignore /// True if something is hit. Results in hitInfo. public static bool SphereCastIgnoreTag( Vector3 rayStart, float radius, Vector3 dir, out RaycastHit hitInfo, float rayLength, int layerMask, in string ignoreTag) { int closestHit = -1; int numPenetrations = 0; float penetrationDistanceSum = 0; int numHits = Physics.SphereCastNonAlloc( rayStart, radius, dir, s_HitBuffer, rayLength, layerMask, QueryTriggerInteraction.Ignore); for (int i = 0; i < numHits; ++i) { var h = s_HitBuffer[i]; if (ignoreTag.Length > 0 && h.collider.CompareTag(ignoreTag)) continue; // Collect overlapping items if (h.distance == 0 && h.normal == -dir) { // hitInfo for overlapping colliders will have special // values that are not helpful to the caller. Fix that here. var scratchCollider = GetScratchCollider(); scratchCollider.radius = radius; var c = h.collider; if (Physics.ComputePenetration( scratchCollider, rayStart, Quaternion.identity, c, c.transform.position, c.transform.rotation, out var offsetDir, out var offsetDistance)) { h.point = rayStart + offsetDir * (offsetDistance - radius); h.distance = offsetDistance - radius; // will be -ve h.normal = offsetDir; s_HitBuffer[i] = h; if (h.distance < -0.0001f) { penetrationDistanceSum += h.distance; if (s_PenetrationIndexBuffer.Length > numPenetrations + 1) s_PenetrationIndexBuffer[numPenetrations++] = i; } } else { continue; // don't know what's going on, just forget about it } } if (closestHit < 0 || h.distance < s_HitBuffer[closestHit].distance) { closestHit = i; } } // Naively combine penetrating items if (numPenetrations > 1) { hitInfo = new RaycastHit(); for (int i = 0; i < numPenetrations; ++i) { var h = s_HitBuffer[s_PenetrationIndexBuffer[i]]; var t = h.distance / penetrationDistanceSum; hitInfo.point += h.point * t; hitInfo.distance += h.distance * t; hitInfo.normal += h.normal * t; } hitInfo.normal = hitInfo.normal.normalized; return true; } if (closestHit >= 0) { hitInfo = s_HitBuffer[closestHit]; if (numHits == s_HitBuffer.Length) s_HitBuffer = new RaycastHit[s_HitBuffer.Length * 2]; // full! grow for next time return true; } hitInfo = new RaycastHit(); return false; } private static SphereCollider s_ScratchCollider; private static GameObject s_ScratchColliderGameObject; internal static SphereCollider GetScratchCollider() { if (s_ScratchColliderGameObject == null) { s_ScratchColliderGameObject = new GameObject("Cinemachine Scratch Collider"); s_ScratchColliderGameObject.hideFlags = HideFlags.HideAndDontSave; s_ScratchColliderGameObject.transform.position = Vector3.zero; s_ScratchColliderGameObject.SetActive(true); s_ScratchCollider = s_ScratchColliderGameObject.AddComponent(); s_ScratchCollider.isTrigger = true; var rb = s_ScratchColliderGameObject.AddComponent(); rb.detectCollisions = false; rb.isKinematic = true; } return s_ScratchCollider; } internal static void DestroyScratchCollider() { if (s_ScratchColliderGameObject != null) { s_ScratchColliderGameObject.SetActive(false); DestroyObject(s_ScratchColliderGameObject.GetComponent()); } DestroyObject(s_ScratchCollider); DestroyObject(s_ScratchColliderGameObject); s_ScratchColliderGameObject = null; s_ScratchCollider = null; } #endif /// /// Normalize a curve so that its X and Y axes range from 0 to 1 /// /// Curve to normalize /// If true, normalize the X axis /// If true, normalize the Y axis /// The normalized curve public static AnimationCurve NormalizeCurve( AnimationCurve curve, bool normalizeX, bool normalizeY) { if (!normalizeX && !normalizeY) return curve; Keyframe[] keys = curve.keys; if (keys.Length > 0) { float minTime = keys[0].time; float maxTime = minTime; float minVal = keys[0].value; float maxVal = minVal; for (int i = 0; i < keys.Length; ++i) { minTime = Mathf.Min(minTime, keys[i].time); maxTime = Mathf.Max(maxTime, keys[i].time); minVal = Mathf.Min(minVal, keys[i].value); maxVal = Mathf.Max(maxVal, keys[i].value); } float range = maxTime - minTime; float timeScale = range < 0.0001f ? 1 : 1 / range; range = maxVal - minVal; float valScale = range < 1 ? 1 : 1 / range; float valOffset = 0; if (range < 1) { if (minVal > 0 && minVal + range <= 1) valOffset = minVal; else valOffset = 1 - range; } for (int i = 0; i < keys.Length; ++i) { if (normalizeX) keys[i].time = (keys[i].time - minTime) * timeScale; if (normalizeY) keys[i].value = ((keys[i].value - minVal) * valScale) + valOffset; } curve.keys = keys; } return curve; } } }