2026-03-26 20:27:19 +07:00
|
|
|
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<CharacterController>();
|
2026-06-04 11:50:09 +07:00
|
|
|
// PlayerStateMachine stateMachine = GetComponent<PlayerStateMachine>();
|
2026-03-26 20:27:19 +07:00
|
|
|
Animator animator = GetComponentInChildren<Animator>();
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
sb.AppendLine($"<color=#4FC3F7><b>[AUTO-SETUP REPORT]</b></color> Character: <b>{gameObject.name}</b>");
|
|
|
|
|
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
|
|
|
|
|
|
|
|
|
// 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("<b>1. HEIGHT:</b> {0:F3}m ({1}) ➔ <color=#81C784>{2:F3}m</color>", 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("<b>2. RADIUS:</b> {0:F3}m ({1}) [/ 2] ➔ <color=#81C784>{2:F3}m</color>", shoulderWidth, radiusMethod, finalRadius));
|
|
|
|
|
|
|
|
|
|
// 3. COLLISION PRECISION
|
|
|
|
|
float skinWidth = finalRadius * 0.10f;
|
|
|
|
|
sb.AppendLine(string.Format("<b>3. SKIN WIDTH:</b> {0:F3}m (Radius) [x 0.10] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, skinWidth));
|
|
|
|
|
|
|
|
|
|
// 4. CAPSULE CENTER
|
|
|
|
|
// Center Y = (Height / 2) + SkinWidth
|
|
|
|
|
float centerY = (finalHeight / 2f) + skinWidth;
|
|
|
|
|
sb.AppendLine(string.Format("<b>4. CENTER Y:</b> ({0:F3}m / 2) + {1:F3}m (Skin) ➔ <color=#81C784>{2:F3}m</color>", finalHeight, skinWidth, centerY));
|
|
|
|
|
sb.AppendLine(string.Format("<b>5. CENTER Z:</b> [Fixed Offset] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
|
|
|
|
|
|
|
|
|
|
// 5. MOVEMENT CONSTRAINTS
|
|
|
|
|
float stepOffset = finalHeight * 0.15f;
|
|
|
|
|
sb.AppendLine(string.Format("<b>6. STEP OFFSET:</b> {0:F3}m (Height) [x 0.15] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, stepOffset));
|
|
|
|
|
|
|
|
|
|
// 6. GROUND CHECK (STATE MACHINE)
|
|
|
|
|
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
2026-06-04 11:50:09 +07:00
|
|
|
// 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("<b>7. GROUND CHECK SETUP:</b>");
|
|
|
|
|
// sb.AppendLine(string.Format(" - Radius: {0:F3}m (Radius) [x 0.60] ➔ <color=#81C784>{1:F3}m</color>", finalRadius, groundCheckRadius));
|
|
|
|
|
// sb.AppendLine(string.Format(" - Offset Y: {0:F3}m (Height) [/ 24] ➔ <color=#81C784>{1:F3}m</color>", finalHeight, groundCheckOffsetY));
|
|
|
|
|
// sb.AppendLine(string.Format(" - Offset Z: [Sync Center Z] ➔ <color=#81C784>{0:F3}m</color>", zCenterOffset));
|
|
|
|
|
// }
|
2026-03-26 20:27:19 +07:00
|
|
|
|
|
|
|
|
sb.AppendLine("<color=#757575>------------------------------------------------------------</color>");
|
|
|
|
|
|
|
|
|
|
// 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<Renderer>();
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|