193 lines
8.1 KiB
C#
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
|
|
}
|