using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace Streamingle.Utils.RoughConstraints { public enum RoughConstraintUpdateMode { Update, LateUpdate, FixedUpdate, Manual } [ExecuteAlways] public abstract class RoughConstraintBase : MonoBehaviour { [Header("Constraint")] public bool constraintActive = true; public bool locked = true; [Range(0f, 1f)] public float weight = 1f; public List sources = new List(); [Header("Rough Follow")] [Min(0f)] public float damping = 12f; public RoughConstraintUpdateMode updateMode = RoughConstraintUpdateMode.LateUpdate; public bool useUnscaledTime; public bool snapOnEnable = true; #if UNITY_EDITOR private bool editorUpdateRegistered; #endif protected float CurrentDeltaTime { get { if (!Application.isPlaying) { return 1f / 60f; } if (updateMode == RoughConstraintUpdateMode.FixedUpdate) { return useUnscaledTime ? Time.fixedUnscaledDeltaTime : Time.fixedDeltaTime; } return useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; } } protected float FollowT { get { if (damping <= 0f) { return 1f; } return 1f - Mathf.Exp(-damping * Mathf.Max(0f, CurrentDeltaTime)); } } protected virtual void OnEnable() { #if UNITY_EDITOR RegisterEditorUpdate(); #endif if (!snapOnEnable) { return; } SnapToSources(); } protected virtual void OnDisable() { #if UNITY_EDITOR UnregisterEditorUpdate(); #endif } private void Update() { if (!Application.isPlaying) { if (updateMode != RoughConstraintUpdateMode.Manual) { Evaluate(); } return; } if (updateMode == RoughConstraintUpdateMode.Update) { Evaluate(); } } private void LateUpdate() { if (!Application.isPlaying) { return; } if (updateMode == RoughConstraintUpdateMode.LateUpdate) { Evaluate(); } } private void FixedUpdate() { if (!Application.isPlaying) { return; } if (updateMode == RoughConstraintUpdateMode.FixedUpdate) { Evaluate(); } } #if UNITY_EDITOR private void RegisterEditorUpdate() { if (editorUpdateRegistered) { return; } EditorApplication.update += EditorUpdate; editorUpdateRegistered = true; } private void UnregisterEditorUpdate() { if (!editorUpdateRegistered) { return; } EditorApplication.update -= EditorUpdate; editorUpdateRegistered = false; } private void EditorUpdate() { if (this == null) { UnregisterEditorUpdate(); return; } if (Application.isPlaying || updateMode == RoughConstraintUpdateMode.Manual) { return; } Evaluate(); } #endif public void Evaluate() { if (!CanEvaluate()) { return; } ExecuteConstraint(FollowT); } public void SnapToSources() { if (!CanEvaluate()) { return; } ExecuteConstraint(1f); } public void SetLocked(bool value) { if (value && !locked) { CaptureOffsetsFromCurrentPose(); } locked = value; } public void ActivateConstraint() { constraintActive = true; locked = true; SnapToSources(); } protected abstract void ExecuteConstraint(float followT); protected virtual void CaptureOffsetsFromCurrentPose() { } private bool CanEvaluate() { return constraintActive && locked && weight > 0f; } protected bool TryGetWeightedPosition(out Vector3 position) { position = Vector3.zero; var totalWeight = 0f; for (var i = 0; i < sources.Count; i++) { var source = sources[i]; if (source.sourceTransform == null || source.weight <= 0f) { continue; } position += source.sourceTransform.position * source.weight; totalWeight += source.weight; } if (totalWeight <= 0f) { return false; } position /= totalWeight; return true; } protected bool TryGetWeightedRotation(out Quaternion rotation) { rotation = Quaternion.identity; var hasRotation = false; var accumulatedWeight = 0f; for (var i = 0; i < sources.Count; i++) { var source = sources[i]; if (source.sourceTransform == null || source.weight <= 0f) { continue; } var sourceRotation = source.sourceTransform.rotation; if (!hasRotation) { rotation = sourceRotation; accumulatedWeight = source.weight; hasRotation = true; continue; } var lerpWeight = source.weight / (accumulatedWeight + source.weight); rotation = Quaternion.Slerp(rotation, sourceRotation, lerpWeight); accumulatedWeight += source.weight; } return hasRotation; } protected bool TryGetWeightedScale(out Vector3 scale) { scale = Vector3.zero; var totalWeight = 0f; for (var i = 0; i < sources.Count; i++) { var source = sources[i]; if (source.sourceTransform == null || source.weight <= 0f) { continue; } scale += source.sourceTransform.localScale * source.weight; totalWeight += source.weight; } if (totalWeight <= 0f) { return false; } scale /= totalWeight; return true; } protected static Vector3 ApplyAxisMask(Vector3 current, Vector3 target, Vector3Bool constrainedAxis) { if (!constrainedAxis.x) target.x = current.x; if (!constrainedAxis.y) target.y = current.y; if (!constrainedAxis.z) target.z = current.z; return target; } protected static Vector3 ApplyEulerAxisMask(Quaternion current, Quaternion target, Vector3Bool constrainedAxis) { var currentEuler = current.eulerAngles; var targetEuler = target.eulerAngles; if (!constrainedAxis.x) targetEuler.x = currentEuler.x; if (!constrainedAxis.y) targetEuler.y = currentEuler.y; if (!constrainedAxis.z) targetEuler.z = currentEuler.z; return targetEuler; } protected static Quaternion LookRotationForAxis(Vector3 direction, Vector3 worldUp, Vector3 localForward, Vector3 localUp) { var safeDirection = SafeNormal(direction, Vector3.forward); var safeWorldUp = SafeUp(safeDirection, worldUp); var safeLocalForward = SafeNormal(localForward, Vector3.forward); var safeLocalUp = SafeUp(safeLocalForward, localUp); var targetWorldRotation = Quaternion.LookRotation(safeDirection, safeWorldUp); var localAxisRotation = Quaternion.LookRotation(safeLocalForward, safeLocalUp); return targetWorldRotation * Quaternion.Inverse(localAxisRotation); } private static Vector3 SafeNormal(Vector3 value, Vector3 fallback) { return value.sqrMagnitude > 0.000001f ? value.normalized : fallback; } private static Vector3 SafeUp(Vector3 forward, Vector3 up) { var safeUp = SafeNormal(up, Vector3.up); if (Mathf.Abs(Vector3.Dot(forward, safeUp)) < 0.999f) { return safeUp; } return Mathf.Abs(Vector3.Dot(forward, Vector3.up)) < 0.999f ? Vector3.up : Vector3.right; } } }