using UnityEngine; using System.Collections.Generic; /// /// Height scaling for GKC player characters. /// NEW APPROACH: Instead of scaling COM/capsule/camera/IK down, /// simply SCALES THE MODEL UP (or down) to match GKC's standard character height. /// This way capsule, camera, IK, weapons, ground detection — everything stays at /// GKC default values and just works. Only the model transform changes. /// public class characterHeightScaler : MonoBehaviour { [Header ("Auto-Detect Settings")] [Space] [Tooltip ("When enabled, the system automatically measures your FBX model height " + "and scales it to match the GKC standard height. No manual slider needed.")] public bool autoDetectHeight = true; [Tooltip ("The standard/reference height in Unity units for the default GKC character. " + "Typically 1.8 units (average humanoid).")] public float gkcStandardHeight = 1.8f; [Tooltip ("The detected height of the current FBX model in Unity units (before scaling).")] public float detectedModelHeight; [Tooltip ("The calculated model scale to match GKC standard height.\n" + "= gkcStandardHeight / detectedModelHeight")] public float modelScaleMultiplier = 1.0f; [Space] [Header ("Manual Scale (when Auto-Detect is OFF)")] [Space] [Range (0.3f, 3.0f)] [Tooltip ("Manual scale multiplier for the model. Used only when autoDetectHeight is OFF.")] public float characterHeightScale = 1.0f; public float minHeightScale = 0.3f; public float maxHeightScale = 3.0f; [Space] [Header ("State")] [Space] public bool heightBaseValuesCaptured; public bool showDebugLog; [Space] [Header ("Captured Base Values")] [Space] public Vector3 baseModelLocalScale; public Vector3 baseModelLocalPosition; public Transform modelTransform; [Tooltip ("Small vertical offset to fine-tune foot alignment (in Unity units). " + "Positive = raise model, Negative = lower model.")] public float footAlignmentOffset = 0.0f; // Internal references playerComponentsManager componentsManager; playerController playerControllerManager; IKSystem IKManager; IKFootSystem IKFootManager; Transform COMTransform; // ==================== AUTO-DETECT HEIGHT ==================== /// /// Measures the FBX model height using multiple methods. /// Returns the height in Unity units. /// public float detectModelHeight (GameObject model) { if (model == null) { Debug.LogWarning ("characterHeightScaler: No model provided for height detection."); return gkcStandardHeight; } float measuredHeight = 0f; string method = "none"; Animator animator = model.GetComponentInChildren (); // --- Method 1: Animator humanoid bones (head to foot) --- if (animator != null && animator.isHuman && animator.avatar != null && animator.avatar.isHuman) { Transform head = animator.GetBoneTransform (HumanBodyBones.Head); Transform leftFoot = animator.GetBoneTransform (HumanBodyBones.LeftFoot); Transform rightFoot = animator.GetBoneTransform (HumanBodyBones.RightFoot); if (head != null && (leftFoot != null || rightFoot != null)) { Transform foot = leftFoot != null ? leftFoot : rightFoot; measuredHeight = head.position.y - foot.position.y; // Head bone is at eye level, add ~10% for top of head measuredHeight *= 1.1f; method = "Animator bones (head-to-foot)"; } } // --- Method 2: Animator humanScale --- if (measuredHeight <= 0.01f && animator != null && animator.isHuman) { measuredHeight = animator.humanScale * 1.8f; method = "Animator humanScale"; } // --- Method 3: SkinnedMeshRenderer bounds --- if (measuredHeight <= 0.01f) { SkinnedMeshRenderer[] skinnedRenderers = model.GetComponentsInChildren (); if (skinnedRenderers.Length > 0) { Bounds combinedBounds = new Bounds (skinnedRenderers [0].bounds.center, Vector3.zero); for (int i = 0; i < skinnedRenderers.Length; i++) { combinedBounds.Encapsulate (skinnedRenderers [i].bounds); } measuredHeight = combinedBounds.size.y; method = "SkinnedMeshRenderer bounds"; } } // --- Method 4: Regular Renderer bounds --- if (measuredHeight <= 0.01f) { Renderer[] renderers = model.GetComponentsInChildren (); if (renderers.Length > 0) { Bounds combinedBounds = new Bounds (renderers [0].bounds.center, Vector3.zero); for (int i = 0; i < renderers.Length; i++) { combinedBounds.Encapsulate (renderers [i].bounds); } measuredHeight = combinedBounds.size.y; method = "Renderer bounds"; } } // --- Fallback --- if (measuredHeight <= 0.01f) { measuredHeight = gkcStandardHeight; method = "fallback (assuming standard)"; } if (showDebugLog) { Debug.Log ("characterHeightScaler: DETECT '" + method + "'" + " | measured=" + measuredHeight.ToString ("F3") + " | standard=" + gkcStandardHeight.ToString ("F3")); } return measuredHeight; } /// /// Auto-detects model height and calculates the scale multiplier /// to make the model match GKC standard height. /// scale = gkcStandardHeight / detectedModelHeight /// public float autoDetectAndSetScale () { resolveModelTransform (); if (modelTransform == null) { Debug.LogWarning ("characterHeightScaler: Cannot auto-detect — no model transform found."); return 1.0f; } detectedModelHeight = detectModelHeight (modelTransform.gameObject); if (detectedModelHeight > 0.01f) { modelScaleMultiplier = gkcStandardHeight / detectedModelHeight; modelScaleMultiplier = Mathf.Clamp (modelScaleMultiplier, minHeightScale, maxHeightScale); } else { modelScaleMultiplier = 1.0f; } characterHeightScale = modelScaleMultiplier; if (showDebugLog) { Debug.Log ("characterHeightScaler: AUTO-DETECT RESULT" + " | modelHeight=" + detectedModelHeight.ToString ("F3") + " | standardHeight=" + gkcStandardHeight.ToString ("F3") + " | scaleMultiplier=" + modelScaleMultiplier.ToString ("F3") + " (model will be scaled UP by " + modelScaleMultiplier.ToString ("F2") + "x)"); } return modelScaleMultiplier; } /// /// Full automatic: capture base values, detect height, scale model, align feet. /// public void autoDetectAndApply () { captureBaseValues (); autoDetectAndSetScale (); applyHeightScale (); if (showDebugLog) { Debug.Log ("characterHeightScaler: FULL AUTO complete." + " Model scaled " + characterHeightScale.ToString ("F3") + "x" + " (from " + detectedModelHeight.ToString ("F3") + "m to ~" + gkcStandardHeight.ToString ("F3") + "m)"); } } // ==================== CAPTURE / APPLY / RESET ==================== /// /// Captures the model's current localScale and localPosition as the base (unscaled) values. /// Call after building the character, before applying any scale. /// public void captureBaseValues () { resolveReferences (); resolveModelTransform (); if (modelTransform != null) { baseModelLocalScale = modelTransform.localScale; baseModelLocalPosition = modelTransform.localPosition; } heightBaseValuesCaptured = true; characterHeightScale = 1.0f; modelScaleMultiplier = 1.0f; if (showDebugLog) { Debug.Log ("characterHeightScaler: CAPTURED" + " | model='" + (modelTransform != null ? modelTransform.name : "NULL") + "'" + " | baseScale=" + baseModelLocalScale + " | basePos=" + baseModelLocalPosition); } updateComponent (); } /// /// Scales the model transform to match GKC standard height. /// No COM, capsule, camera, IK, or weapon changes needed — /// those all stay at GKC default values since we're fitting the model to the system, /// not the system to the model. /// public void applyHeightScale () { if (!heightBaseValuesCaptured) { Debug.LogWarning ("characterHeightScaler: No base values captured. Click 'Capture Base Values' first."); return; } resolveReferences (); float scale = characterHeightScale; if (showDebugLog) { Debug.Log ("characterHeightScaler: ===== APPLYING MODEL SCALE = " + scale.ToString ("F3") + "x ====="); } // 1. Scale the model if (modelTransform != null) { modelTransform.localScale = baseModelLocalScale * scale; if (showDebugLog) { Debug.Log (" [Model] '" + modelTransform.name + "' localScale = " + modelTransform.localScale); } } // 2. Align feet to ground (model origin may not be at feet) alignFeetToGround (); // 3. Recalculate IK foot initial values (bone positions changed due to model scale) if (IKFootManager != null) { IKFootManager.calculateInitialFootValues (); if (showDebugLog) { Debug.Log (" [IKFoot] Recalculated initial foot values"); } } updateComponent (); markComponentsDirty (); if (showDebugLog) { Debug.Log ("characterHeightScaler: ===== APPLY COMPLETE ====="); } } /// /// After scaling the model, ensure feet are aligned with ground level. /// Measures the lowest foot bone Y (or mesh bottom) and adjusts model localPosition. /// void alignFeetToGround () { if (modelTransform == null) { return; } Animator animator = modelTransform.GetComponentInChildren (); float footWorldY = float.MaxValue; bool footFound = false; // Try humanoid foot bones (LeftToes / RightToes first, then LeftFoot / RightFoot) if (animator != null && animator.isHuman) { Transform leftToes = animator.GetBoneTransform (HumanBodyBones.LeftToes); Transform rightToes = animator.GetBoneTransform (HumanBodyBones.RightToes); Transform leftFoot = animator.GetBoneTransform (HumanBodyBones.LeftFoot); Transform rightFoot = animator.GetBoneTransform (HumanBodyBones.RightFoot); // Prefer toes (closest to ground), fall back to foot bone if (leftToes != null || rightToes != null) { if (leftToes != null) footWorldY = Mathf.Min (footWorldY, leftToes.position.y); if (rightToes != null) footWorldY = Mathf.Min (footWorldY, rightToes.position.y); footFound = true; } else if (leftFoot != null || rightFoot != null) { if (leftFoot != null) footWorldY = Mathf.Min (footWorldY, leftFoot.position.y); if (rightFoot != null) footWorldY = Mathf.Min (footWorldY, rightFoot.position.y); footFound = true; } } // Fallback: SkinnedMeshRenderer bounds bottom if (!footFound) { SkinnedMeshRenderer[] renderers = modelTransform.GetComponentsInChildren (); if (renderers.Length > 0) { Bounds combinedBounds = renderers [0].bounds; for (int i = 1; i < renderers.Length; i++) { combinedBounds.Encapsulate (renderers [i].bounds); } footWorldY = combinedBounds.min.y; footFound = true; } } if (!footFound) { if (showDebugLog) { Debug.LogWarning ("characterHeightScaler: Could not find foot bones or renderers for alignment."); } return; } // Ground level = Player Controller transform.position.y (capsule bottom) float groundWorldY = transform.position.y; float offsetY = footWorldY - groundWorldY; if (showDebugLog) { Debug.Log (" [FootAlign] footWorldY=" + footWorldY.ToString ("F4") + " groundY=" + groundWorldY.ToString ("F4") + " offset=" + offsetY.ToString ("F4") + " userAdjust=" + footAlignmentOffset.ToString ("F4")); } // Adjust model localPosition to put feet on ground + user fine-tune offset if (Mathf.Abs (offsetY) > 0.005f || Mathf.Abs (footAlignmentOffset) > 0.001f) { Vector3 modelPos = modelTransform.localPosition; modelPos.y = baseModelLocalPosition.y - offsetY + footAlignmentOffset; modelTransform.localPosition = modelPos; if (showDebugLog) { Debug.Log (" [FootAlign] model localPos.y = " + modelPos.y.ToString ("F4")); } } } /// /// Resets model to original scale and position (1:1 with FBX import). /// public void resetHeightScale () { if (modelTransform != null) { modelTransform.localScale = baseModelLocalScale; modelTransform.localPosition = baseModelLocalPosition; } characterHeightScale = 1.0f; modelScaleMultiplier = 1.0f; if (IKFootManager != null) { IKFootManager.calculateInitialFootValues (); } updateComponent (); markComponentsDirty (); } // ==================== INTERNAL ==================== void resolveReferences () { if (componentsManager == null) { GameObject playerObject = null; buildPlayer builder = GetComponent (); if (builder != null) { playerObject = builder.player; } if (playerObject == null) { playerObject = transform.root.gameObject; } componentsManager = playerObject.GetComponent (); } if (componentsManager != null) { if (playerControllerManager == null) { playerControllerManager = componentsManager.getPlayerController (); } if (IKManager == null) { IKManager = componentsManager.getIKSystem (); } if (IKFootManager == null) { IKFootManager = GetComponentInChildren (); } if (COMTransform == null && IKManager != null) { COMTransform = IKManager.getIKBodyCOM (); } } resolveModelTransform (); } void resolveModelTransform () { if (modelTransform == null) { buildPlayer builder = GetComponent (); if (builder != null && builder.currentCharacterModel != null) { modelTransform = builder.currentCharacterModel.transform; } } } void markComponentsDirty () { #if UNITY_EDITOR if (modelTransform != null) { UnityEditor.EditorUtility.SetDirty (modelTransform); UnityEditor.EditorUtility.SetDirty (modelTransform.gameObject); } if (IKFootManager != null) { UnityEditor.EditorUtility.SetDirty (IKFootManager); } UnityEditor.EditorUtility.SetDirty (this); #endif } void updateComponent () { #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty (this); #endif } }