// ───────────────────────────────────────────────────────────────────────────── // WLSkillYaw.cs — 스킬 동작 상태 동안 보이는 몸(엉덩이 뼈)을 위 축으로 일정 각만큼 돌려 공격 대상 쪽에 맞춘다 (#974 · 2026-10-23) // // PD 원문: 「한손검 주력 스킬 시 회전할 때 공격 대상의 방향과 일치하도록 수정해줘 (현재는 공격 투사체 방향보다 더 회전하고 있어.)」 // ■ 실측(#974 진단 Play · 974_probe Spin): 검기 분출 동작 energyslash(월광 클립 combo09_3)는 몸을 한 바퀴 반 돌리고, // 검기를 쏘는 순간(정규화 0.41~0.45) 몸이 대상 · 검기 방향과 어긋난다(PC 루트 · 검기는 대상 쪽 · 어긋난 각 = 규칙 yaw 의 근거 · JSON 메모) // ■ 처리 = 규칙 상태(지금 상태 또는 넘어가는 중인 다음 상태)일 때 LateUpdate(애니메이션 평가 뒤)에서 엉덩이 뼈를 PC 루트 둘레 위 축으로 yaw° — // 들어갈 때 blendIn · 나갈 때 blendOut 초에 걸쳐 · 애니메이터가 매 프레임 뼈를 다시 쓰므로 누적 없음 · 루트 · 판정 · 투사체 방향 · 카메라는 그대로(보이는 몸만) // (WLImpactFx 비수 3타 회전 #956 과 같은 방식) // ■ #975 PD 「회전 방향은 공격 대상을 보고 있는데 상체만 움직이고 다리 위치가 제대로 바뀌지 않아 몸이 너무 꼬인 느낌」 — 실측(편집 모드 975_legs): // 클립은 한 바퀴 돈 뒤 골반 · 상체만 90~108° 더 돌고 발은 처음 자세 그대로(골반 − 발 = 대기 자세 −17° → 쏘는 순간 +73.5° · 0.68 에 +91°) // → legFollow > 0 이면 정규화 legFrom 부터(legRamp 동안 서서히) 매 프레임 「골반 정면 − 두 발 정면 평균 − legNeutral」 만큼 두 다리(허벅지)를 제 관절 둘레 위 축으로 돌려 // 발이 골반을 따라가게 한다(상체 · 판정 · 투사체는 그대로 · 돌기 중(legFrom 전)은 발이 꼬여 엇갈려 계산하지 않음) // ■ 값 = Resources/WL/WLSkillYaw.json (C45) · 끄기 = enabled 0 // 🔴 Debug.LogError 금지(원본 ErrorLogHook 팝업). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Combat.Weapon { [System.Serializable] public class WLSkillYawRule { public string state = ""; // 애니메이터 0층 상태 이름(예: energyslash) public float yaw; // 보이는 몸을 돌릴 각(°) · + = 위에서 볼 때 시계 방향 public float blendIn = 0.08f, blendOut = 0.15f; // #975 다리 따라 돌리기 — legFollow(0 = 끔 · 1 = 대기 자세만큼만 남기고 다 풂) · legFrom(정규화 시각) · legRamp(정규화 길이) · legNeutral(대기 자세 골반 − 발 각 °) · legMax(한 프레임 상한 °) public float legFollow, legFrom, legRamp = 0.06f, legNeutral, legMax = 130f; public string note = ""; } [System.Serializable] public class WLSkillYawJson { public int enabled = 1; public WLSkillYawRule[] rules = new WLSkillYawRule[0]; public string note = ""; } public static class WLSkillYaw { public const string ResourcesPath = "WL/WLSkillYaw"; public static int Frames, LegFrames; public static float LastYaw, LastLeg, LastLegExcess; // #975 마지막 다리 회전(°) · 그때 골반 − 발 − 대기 자세(°) public static string LastLog = ""; static WLSkillYawJson s_cfg; static GameObject s_runner; internal static WLSkillYawJson Cfg { get { if (s_cfg != null) return s_cfg; s_cfg = new WLSkillYawJson(); var ta = Resources.Load(ResourcesPath); if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; } try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; } return s_cfg; } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] static void Boot() { s_cfg = null; if (Cfg.enabled == 0 || s_runner != null) return; s_runner = new GameObject("[WL974] SkillYaw"); s_runner.hideFlags = HideFlags.HideAndDontSave; Object.DontDestroyOnLoad(s_runner); s_runner.AddComponent(); } internal static WLSkillYawRule Match(AnimatorStateInfo st) { var rules = Cfg.rules; if (rules == null) return null; for (int i = 0; i < rules.Length; i++) { var r = rules[i]; if (r != null && !string.IsNullOrEmpty(r.state) && st.IsName(r.state)) return r; } return null; } } [DefaultExecutionOrder(-100)] // 손 · 무기를 따라가는 궤적 · 이펙트(LateUpdate)보다 먼저 몸을 돌린다 internal sealed class WLSkillYawRunner : MonoBehaviour { float _w, _legW; WLSkillYawRule _rule; void LateUpdate() { try { if (WLSkillYaw.Cfg.enabled == 0) return; var pc = MyValue.MyPC as PCActor; if (pc == null || pc.m_animation == null || !pc.m_animation.isHuman) { _w = 0f; _rule = null; return; } var an = pc.m_animation; var cur = an.GetCurrentAnimatorStateInfo(0); var hit = WLSkillYaw.Match(cur); float n = cur.normalizedTime; if (hit == null && an.IsInTransition(0)) { var nx = an.GetNextAnimatorStateInfo(0); hit = WLSkillYaw.Match(nx); n = nx.normalizedTime; } if (hit != null) { _rule = hit; _legW = hit.legFollow > 0f ? Mathf.Clamp01((n - hit.legFrom) / Mathf.Max(0.001f, hit.legRamp)) : 0f; } // 나가는 중(hit 없음)엔 마지막 값 유지 if (_rule == null) return; float target = hit != null ? 1f : 0f; float dur = target > _w ? _rule.blendIn : _rule.blendOut; _w = Mathf.MoveTowards(_w, target, Time.deltaTime / Mathf.Max(0.01f, dur)); if (_w <= 0f) { _rule = null; WLSkillYaw.LastYaw = 0f; WLSkillYaw.LastLeg = 0f; return; } var hips = an.GetBoneTransform(HumanBodyBones.Hips); if (hips == null) return; float d = _rule.yaw * _w; hips.RotateAround(pc.transform.position, pc.transform.up, d); WLSkillYaw.Frames++; WLSkillYaw.LastYaw = d; if (_rule.legFollow > 0f && _legW > 0f) Legs(an, pc.transform.up, _rule, _legW * _w); } catch (System.Exception ex) { WLSkillYaw.LastLog = "회전 실패 — " + ex.Message; _w = 0f; _rule = null; } } /// #975 두 허벅지를 제 관절 둘레 위 축으로 돌려 두 발 정면 평균이 「골반 정면 − legNeutral」 쪽을 보게(k = 진행 가중). static void Legs(Animator an, Vector3 up, WLSkillYawRule r, float k) { var lul = an.GetBoneTransform(HumanBodyBones.LeftUpperLeg); var rul = an.GetBoneTransform(HumanBodyBones.RightUpperLeg); var lf = an.GetBoneTransform(HumanBodyBones.LeftFoot); var rf = an.GetBoneTransform(HumanBodyBones.RightFoot); if (lul == null || rul == null || lf == null || rf == null) return; Vector3 pelvis = Vector3.ProjectOnPlane(Vector3.Cross(rul.position - lul.position, up), up); Vector3 fl = Vector3.ProjectOnPlane(FootDir(an, HumanBodyBones.LeftToes, HumanBodyBones.LeftLowerLeg, lul, lf), up); Vector3 fr = Vector3.ProjectOnPlane(FootDir(an, HumanBodyBones.RightToes, HumanBodyBones.RightLowerLeg, rul, rf), up); if (pelvis.sqrMagnitude < 1e-8f || fl.sqrMagnitude < 1e-8f || fr.sqrMagnitude < 1e-8f) return; Vector3 feet = fl.normalized + fr.normalized; if (feet.sqrMagnitude < 1e-4f) return; // 두 발이 반대쪽(엇갈림) — 평균 없음 float excess = Mathf.Clamp(Mathf.DeltaAngle(0f, Vector3.SignedAngle(feet, pelvis, up) - r.legNeutral), -r.legMax, r.legMax); float a = excess * r.legFollow * k; lul.RotateAround(lul.position, up, a); rul.RotateAround(rul.position, up, a); WLSkillYaw.LegFrames++; WLSkillYaw.LastLeg = a; WLSkillYaw.LastLegExcess = excess; } /// 발 정면 = 발 → 발가락 · 발가락 뼈가 없으면 무릎이 향한 쪽(무릎 − 허벅지 · 발 가운데). static Vector3 FootDir(Animator an, HumanBodyBones toes, HumanBodyBones knee, Transform hip, Transform foot) { var t = an.GetBoneTransform(toes); if (t != null) return t.position - foot.position; var kn = an.GetBoneTransform(knee); return kn != null ? kn.position - (hip.position + foot.position) * 0.5f : Vector3.zero; } } }