using UnityEngine; using System.Text; namespace OnlyScove.Scripts.GameSetup { [RequireComponent(typeof(CharacterController))] public class CharacterAutoSetup : MonoBehaviour { [Header("Manual Overrides (If Detection Fails)")] [SerializeField] private float defaultHeight = 1.8f; [SerializeField] private float defaultShoulderWidth = 0.4f; [Header("Settings")] [SerializeField] private Transform modelRoot; [SerializeField] private bool autoDetectOnStart = true; [SerializeField] private float zCenterOffset = 0.05f; private void Start() { if (autoDetectOnStart) { ApplyAutoSetup(); } } [ContextMenu("Apply Auto Setup")] public void ApplyAutoSetup() { CharacterController controller = GetComponent(); PlayerStateMachine stateMachine = GetComponent(); Animator animator = GetComponentInChildren(); StringBuilder sb = new StringBuilder(); sb.AppendLine($"[AUTO-SETUP REPORT] Character: {gameObject.name}"); sb.AppendLine("------------------------------------------------------------"); // 1. HEIGHT DETECTION float finalHeight = defaultHeight; string heightMethod = "Default Fallback"; if (animator != null && animator.GetBoneTransform(HumanBodyBones.Head) != null) { heightMethod = "Humanoid Bones (Feet to Head)"; Transform head = animator.GetBoneTransform(HumanBodyBones.Head); // We measure from the local Y=0 (feet) to the head bone and add 10% for the skull/hair float headHeight = transform.InverseTransformPoint(head.position).y; finalHeight = headHeight * 1.12f; // 12% extra for the top of the skull } else { heightMethod = "Local Mesh Bounds"; Bounds localBounds = GetRelativeBounds(); if (localBounds.size.y > 0) finalHeight = localBounds.size.y; } sb.AppendLine(string.Format("1. HEIGHT: {0:F3}m ({1}) ➔ {2:F3}m", finalHeight, heightMethod, finalHeight)); // 2. RADIUS DETECTION float shoulderWidth = defaultShoulderWidth; string radiusMethod = "Default Fallback"; if (animator != null && animator.GetBoneTransform(HumanBodyBones.Hips) != null) { radiusMethod = "Humanoid Bones (Shoulders)"; Transform leftArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm); Transform rightArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm); if (leftArm != null && rightArm != null) { float distance = Vector3.Distance(leftArm.position, rightArm.position); shoulderWidth = distance * 1.2f; // Add 20% for arm/shoulder thickness } } else { radiusMethod = "Bounds Width Fallback"; Bounds b = GetRelativeBounds(); shoulderWidth = b.size.x * 0.25f; } float finalRadius = shoulderWidth / 2f; sb.AppendLine(string.Format("2. RADIUS: {0:F3}m ({1}) [/ 2] ➔ {2:F3}m", shoulderWidth, radiusMethod, finalRadius)); // 3. COLLISION PRECISION float skinWidth = finalRadius * 0.10f; sb.AppendLine(string.Format("3. SKIN WIDTH: {0:F3}m (Radius) [x 0.10] ➔ {1:F3}m", finalRadius, skinWidth)); // 4. CAPSULE CENTER // Center Y = (Height / 2) + SkinWidth float centerY = (finalHeight / 2f) + skinWidth; sb.AppendLine(string.Format("4. CENTER Y: ({0:F3}m / 2) + {1:F3}m (Skin) ➔ {2:F3}m", finalHeight, skinWidth, centerY)); sb.AppendLine(string.Format("5. CENTER Z: [Fixed Offset] ➔ {0:F3}m", zCenterOffset)); // 5. MOVEMENT CONSTRAINTS float stepOffset = finalHeight * 0.15f; sb.AppendLine(string.Format("6. STEP OFFSET: {0:F3}m (Height) [x 0.15] ➔ {1:F3}m", finalHeight, stepOffset)); // 6. GROUND CHECK (STATE MACHINE) sb.AppendLine("------------------------------------------------------------"); if (stateMachine != null) { float groundCheckRadius = finalRadius * 0.6f; float groundCheckOffsetY = finalHeight / 24f; Vector3 groundCheckOffset = new Vector3(0, groundCheckOffsetY, zCenterOffset); stateMachine.SetGroundCheck(groundCheckRadius, groundCheckOffset); sb.AppendLine("7. GROUND CHECK SETUP:"); sb.AppendLine(string.Format(" - Radius: {0:F3}m (Radius) [x 0.60] ➔ {1:F3}m", finalRadius, groundCheckRadius)); sb.AppendLine(string.Format(" - Offset Y: {0:F3}m (Height) [/ 24] ➔ {1:F3}m", finalHeight, groundCheckOffsetY)); sb.AppendLine(string.Format(" - Offset Z: [Sync Center Z] ➔ {0:F3}m", zCenterOffset)); } sb.AppendLine("------------------------------------------------------------"); // Apply to Controller controller.height = finalHeight; controller.radius = finalRadius; controller.skinWidth = skinWidth; controller.center = new Vector3(0, centerY, zCenterOffset); controller.slopeLimit = 45f; controller.stepOffset = stepOffset; controller.minMoveDistance = 0.001f; Debug.Log(sb.ToString()); } private Bounds GetRelativeBounds() { Transform targetRoot = modelRoot != null ? modelRoot : transform; Renderer[] renderers = targetRoot.GetComponentsInChildren(); if (renderers.Length == 0) return new Bounds(Vector3.zero, Vector3.zero); // Using local bounds of SkinnedMeshRenderers for better accuracy on animated characters Bounds combinedLocalBounds = new Bounds(); bool first = true; foreach (Renderer renderer in renderers) { if (renderer is ParticleSystemRenderer) continue; Bounds localB; if (renderer is SkinnedMeshRenderer smr) { localB = smr.localBounds; } else { // For static meshes, convert world bounds back to local root space Vector3 min = transform.InverseTransformPoint(renderer.bounds.min); Vector3 max = transform.InverseTransformPoint(renderer.bounds.max); localB = new Bounds((min + max) / 2f, max - min); } if (first) { combinedLocalBounds = localB; first = false; } else { combinedLocalBounds.Encapsulate(localB); } } return combinedLocalBounds; } } }