MiniGames/Assets/Scripts/Shared/PlatformParenting.cs
2025-09-06 23:30:06 +05:00

193 lines
8.1 KiB
C#

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<Transform, Transform> _prevParents = new();
void OnEnable() { Cache(); FitToParentCollider(); }
void Reset() { Cache(); FitToParentCollider(); }
//void OnValidate() { if (!isActiveAndEnabled) return; Cache(); FitToParentCollider(); }
void Cache()
{
if (_topBox == null) _topBox = GetComponent<BoxCollider>();
_parentCollider = null;
if (transform.parent != null)
{
// Prefer exact colliders on the parent itself
_parentCollider = transform.parent.GetComponent<BoxCollider>();
if (_parentCollider == null) _parentCollider = transform.parent.GetComponent<MeshCollider>();
if (_parentCollider == null) _parentCollider = transform.parent.GetComponent<Collider>();
}
}
[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<BoxCollider>();
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
}