142 lines
9.2 KiB
C#
142 lines
9.2 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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<TextAsset>(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<WLSkillYawRunner>();
|
||
}
|
||
|
||
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; }
|
||
}
|
||
|
||
/// <summary>#975 두 허벅지를 제 관절 둘레 위 축으로 돌려 두 발 정면 평균이 「골반 정면 − legNeutral」 쪽을 보게(k = 진행 가중).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>발 정면 = 발 → 발가락 · 발가락 뼈가 없으면 무릎이 향한 쪽(무릎 − 허벅지 · 발 가운데).</summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|