using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [ExecuteAlways] [RequireComponent(typeof(BoxCollider))] public class PlatformParenting : MonoBehaviour { public enum TopFaceMode { ParentUp, WorldUpAuto } [Header("Fit Settings")] [Range(0.01f, 0.5f)] public float heightPercent = 0.05f; public float surfaceOffset = 0.002f; public bool isTrigger = false; [Tooltip("How to choose the platform's 'top' face.")] public TopFaceMode topFace = TopFaceMode.WorldUpAuto; [Tooltip("Align this child so its local Y points along the chosen top normal.")] public bool alignToTopFace = true; private BoxCollider _topBox; private Collider _parentCollider; private readonly Dictionary _prevParents = new(); void OnEnable() { Cache(); FitToParentCollider(); } void Reset() { Cache(); FitToParentCollider(); } //void OnValidate() { if (!isActiveAndEnabled) return; Cache(); FitToParentCollider(); } void Cache() { if (_topBox == null) _topBox = GetComponent(); _parentCollider = null; if (transform.parent != null) { // Prefer exact colliders on the parent itself _parentCollider = transform.parent.GetComponent(); if (_parentCollider == null) _parentCollider = transform.parent.GetComponent(); if (_parentCollider == null) _parentCollider = transform.parent.GetComponent(); } } [ContextMenu("Fit To Parent Collider")] public void FitToParentCollider() { if (transform.parent == null) { Debug.LogWarning("[PlatformParenting] Must be a child of the platform."); return; } if (_topBox == null) _topBox = GetComponent(); if (_parentCollider == null) { Debug.LogWarning("[PlatformParenting] No collider found on parent."); return; } // --- Use local bounds from BoxCollider or MeshCollider for oriented math --- if (_parentCollider is BoxCollider pb) { FitUsingLocalBounds(pb.transform, pb.size, pb.center); return; } if (_parentCollider is MeshCollider pm && pm.sharedMesh != null) { Bounds m = pm.sharedMesh.bounds; // local bounds of the mesh FitUsingLocalBounds(pm.transform, m.size, m.center); return; } // Fallback: world AABB (least precise) Bounds b = _parentCollider.bounds; float thinY = Mathf.Max(0.001f, b.size.y * Mathf.Clamp01(heightPercent)); Vector3 worldCtr = new Vector3(b.center.x, b.max.y + surfaceOffset + thinY * 0.5f, b.center.z); if (alignToTopFace) transform.rotation = Quaternion.LookRotation(Vector3.forward, Vector3.up); transform.position = worldCtr; Vector3 lossy = Abs(transform.lossyScale); _topBox.size = new Vector3(b.size.x / lossy.x, thinY / lossy.y, b.size.z / lossy.z); _topBox.center = Vector3.zero; _topBox.isTrigger = isTrigger; } // Fits a thin box on the parent's true top face (relative to world-up) private void FitUsingLocalBounds(Transform src, Vector3 localSize, Vector3 localCenter) { // Parent local axes in world space Vector3 wr = src.right, wu = src.up, wf = src.forward; Vector3[] worldAxes = { wr, wu, wf }; Vector3[] localAxes = { Vector3.right, Vector3.up, Vector3.forward }; // Choose which local axis is most "up" in world space int topAxis = 1; // default Y int sign = 1; if (topFace == TopFaceMode.WorldUpAuto) { float best = -1f; for (int i = 0; i < 3; i++) { float d = Vector3.Dot(worldAxes[i], Vector3.up); float ad = Mathf.Abs(d); if (ad > best) { best = ad; topAxis = i; sign = (d >= 0f) ? 1 : -1; } } } else // ParentUp: use local Y, pick sign so it's closest to world up { float d = Vector3.Dot(wu, Vector3.up); topAxis = 1; sign = (d >= 0f) ? 1 : -1; } Vector3 localNormal = sign * localAxes[topAxis]; Vector3 worldNormal = sign * worldAxes[topAxis]; // World sizes (account for non-uniform scale) Vector3 srcLossy = Abs(src.lossyScale); Vector3 worldSize = new Vector3(localSize.x * srcLossy.x, localSize.y * srcLossy.y, localSize.z * srcLossy.z); float axisWorldSize = (topAxis == 0 ? worldSize.x : topAxis == 1 ? worldSize.y : worldSize.z); float thin = Mathf.Max(0.001f, axisWorldSize * Mathf.Clamp01(heightPercent)); // Top face world center Vector3 topWorld = src.TransformPoint(localCenter + 0.5f * ComponentAlong(localSize, topAxis) * localNormal); Vector3 worldCenter = topWorld + worldNormal * (surfaceOffset + thin * 0.5f); transform.position = worldCenter; // Align this child so its local Y points along the top normal; keep forward projected on the plane if (alignToTopFace) { Vector3 planeForward = Vector3.ProjectOnPlane(src.forward, worldNormal); if (planeForward.sqrMagnitude < 1e-6f) planeForward = Vector3.ProjectOnPlane(src.right, worldNormal); transform.rotation = Quaternion.LookRotation(planeForward.normalized, worldNormal.normalized); } // Decide which of the remaining two parent axes becomes our local X vs Z by checking alignment to child.right int a = OtherIndex(topAxis, false); int b = OtherIndex(topAxis, true); float sizeA = ComponentAlong(worldSize, a); float sizeB = ComponentAlong(worldSize, b); Vector3 axisAWorld = worldAxes[a].normalized * Mathf.Sign(Vector3.Dot(worldAxes[a], transform.right)); float dotA = Mathf.Abs(Vector3.Dot(axisAWorld.normalized, transform.right)); float sizeXWorld = dotA >= 0.5f ? sizeA : sizeB; float sizeZWorld = dotA >= 0.5f ? sizeB : sizeA; Vector3 childLossy = Abs(transform.lossyScale); _topBox.size = new Vector3( sizeXWorld / childLossy.x, thin / childLossy.y, sizeZWorld / childLossy.z ); _topBox.center = Vector3.zero; _topBox.isTrigger = isTrigger; } static float ComponentAlong(Vector3 v, int axis) => axis == 0 ? v.x : axis == 1 ? v.y : v.z; static int OtherIndex(int topAxis, bool second) { // returns the two indices other than topAxis; order stable if (topAxis == 0) return second ? 2 : 1; if (topAxis == 1) return second ? 2 : 0; return second ? 1 : 0; } static Vector3 Abs(Vector3 v) => new(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); // ---------- Parenting behavior ---------- private void ParentRider(Transform rider) { if (!_prevParents.ContainsKey(rider)) _prevParents[rider] = rider.parent; rider.parent = transform.parent != null ? transform.parent : transform; } private void UnparentRider(Transform rider) { if (_prevParents.TryGetValue(rider, out var orig)) rider.parent = orig; else rider.parent = null; _prevParents.Remove(rider); } private void OnCollisionEnter(Collision c) { if (!isTrigger && c.gameObject.CompareTag("Player")) ParentRider(c.transform); } private void OnCollisionExit(Collision c) { if (!isTrigger && c.gameObject.CompareTag("Player")) UnparentRider(c.transform); } private void OnTriggerEnter(Collider o) { if (isTrigger && o.CompareTag("Player")) ParentRider(o.transform); } private void OnTriggerExit(Collider o) { if (isTrigger && o.CompareTag("Player")) UnparentRider(o.transform); } #if UNITY_EDITOR void OnDrawGizmosSelected() { if (_topBox == null) return; Gizmos.matrix = transform.localToWorldMatrix; Gizmos.DrawWireCube(_topBox.center, _topBox.size); } [CustomEditor(typeof(PlatformParenting))] class PlatformParentingEditor : Editor { public override void OnInspectorGUI() { DrawDefaultInspector(); if (GUILayout.Button("Fit To Parent Collider")) ((PlatformParenting)target).FitToParentCollider(); } } #endif }