143 lines
11 KiB
C#
143 lines
11 KiB
C#
|
|
// WL833_FrameTables.cs — 무기 계열별 공격 전진 「프레임 표」를 팩 원본(루트모션) 클립에서 굽는다 (PD 2026-10-10 「제자리 고정 공격 패턴 → 모션에 맞는 전진」)
|
|||
|
|
// unity command run_script --file AgentScripts/WL833_FrameTables.cs --entry WL833_FrameTables.Run --args "[false, 0.6]" (dry · 표만 출력)
|
|||
|
|
// unity command run_script --file AgentScripts/WL833_FrameTables.cs --entry WL833_FrameTables.Run --args "[true, 0.6]" (WLCombatMotionSettings.asset 에 기록)
|
|||
|
|
//
|
|||
|
|
// 구조(실측): 전진은 AttackRootMotion.FrameTable 모드 — 재생 중 클립 이름에 clipContains 가 포함된 표의 프레임별 cm 를 NavMeshAgent.Move 로 민다.
|
|||
|
|
// #791 표는 검(Knight) 3클립뿐이라 망치·단검(이름이 Attack{N}_S_… 라 검 표를 잘못 씀)·창·활·지팡이(표 없음 = 제자리)가 어긋났다.
|
|||
|
|
// 방법: 팩의 루트모션 원본 클립(Knight@Attack1.FBX 등 · _S 아님)의 RootT.z 커브를 30 fps 프레임마다 읽어 Δ × 치비 humanScale(LH_M05 0.4233)
|
|||
|
|
// × 100 = 프레임별 전진 cm(뒤로 가는 프레임은 0). scale 인자(기본 0.6)는 #791 검 표(발 접지 실측 15/30/28 cm)가 원본 루트모션(22/58/54 cm)의
|
|||
|
|
// 약 0.5~0.7 배였던 것에 맞춘 배율 — 발 미끄러짐을 줄인다. 검 3표는 PD 승인값(09-07)이라 **값 그대로** 두고 키만 Attack{N}_S_101 로 좁힌다.
|
|||
|
|
// 활 2·3타(MoveAttack 7.4 m 달리기 사격)는 표를 만들지 않는다(제자리 사격).
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Linq;
|
|||
|
|
using System.Text;
|
|||
|
|
using UnityEditor;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using WL.Combat;
|
|||
|
|
|
|||
|
|
public static class WL833_FrameTables
|
|||
|
|
{
|
|||
|
|
const string SettingsPath = "Assets/WL/Settings/Resources/WL/WLCombatMotionSettings.asset";
|
|||
|
|
const string PcPath = "Assets/Res_Addr/PC/LH_M05.prefab";
|
|||
|
|
const string R = "Assets/ExplosiveLLC/";
|
|||
|
|
const string K = R + "Knight Warrior Mecanim Animation Pack/Animations/Knight@";
|
|||
|
|
const string H = R + "Hammer Warrior Mecanim Animation Pack/Animations/Hammer@";
|
|||
|
|
const string N = R + "Ninja Warrior Mecanim Animation Pack/Animations/Ninja@";
|
|||
|
|
const string A = R + "Archer Warrior Mecanim Animation Pack/Animations/Archer@";
|
|||
|
|
const string M = R + "Mage Warrior Mecanim Animation Pack/Animations/Mage@";
|
|||
|
|
const string S = "Assets/Shinabro/Platform_Animation/Animation/07_Spear/Stander@";
|
|||
|
|
|
|||
|
|
class Spec { public string key; public string fbx; public string note; public float mult = 1f; public bool pass; public bool stepGate; public Spec(string k, string f, string n, float m = 1f, bool p = false, bool g = false) { key = k; fbx = f; note = n; mult = m; pass = p; stepGate = g; } }
|
|||
|
|
// #841 스텝 창: 팩 FBX 임포터 원본 클립의 Foot* 이벤트(발 접지) 앞 0.30 s ~ 뒤 0.05 s 만 전진을 남긴다(발이 고정된 구간 = 0). 총량은 재분배하지 않는다(미끄러짐 제거가 목적).
|
|||
|
|
const float StepGateBefore = 0.30f, StepGateAfter = 0.05f;
|
|||
|
|
static List<float> FootEventTimes(string fbx)
|
|||
|
|
{
|
|||
|
|
var l = new List<float>();
|
|||
|
|
var importer = AssetImporter.GetAtPath(fbx) as ModelImporter;
|
|||
|
|
if (importer == null) return l;
|
|||
|
|
var clips = importer.clipAnimations; if (clips == null || clips.Length == 0) clips = importer.defaultClipAnimations;
|
|||
|
|
if (clips == null || clips.Length == 0) return l;
|
|||
|
|
var c = clips[0];
|
|||
|
|
if (c.events != null) foreach (var e in c.events) if (e.functionName != null && e.functionName.StartsWith("Foot")) l.Add(e.time); // 임포터 이벤트 time = 정규화
|
|||
|
|
return l;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static readonly Spec[] Specs =
|
|||
|
|
{
|
|||
|
|
// #839 해머 재배열(1타 = 팩 Attack2 · 2타 = Attack1 · 3타 = Attack3) · #841 PD 「발 스텝이 나갈 때만 전진 · 발 고정 구간은 이동 금지」 → stepGate(팩 Foot 이벤트 앞 0.3 s 창 밖은 0)
|
|||
|
|
new Spec("Attack1_S_105", H + "Attack2.FBX", "망치 1타(팩 Attack2 · 스텝 창)", 1f, false, true), new Spec("Attack2_S_105", H + "Attack1.FBX", "망치 2타(팩 Attack1 · 스텝 창)", 1f, false, true), new Spec("Attack3_S_105", H + "Attack3.FBX", "망치 3타(팩 Attack3 회전 · 스텝 창)", 1f, false, true),
|
|||
|
|
new Spec("Attack1_S_102", N + "Attack1.FBX", "단검 1타"), new Spec("Attack2_S_102", N + "Attack2.FBX", "단검 2타"),
|
|||
|
|
new Spec("Attack3_S_102", N + "Attack3.FBX", "단검 3타(#834 PD 「마지막 타는 전진하며」 · 배율 ×1.6 = 원본 루트모션 거의 그대로)", 1.6f),
|
|||
|
|
// #839 PD 「단검 마지막 타(4타 DodgeAttack) 공격 시점에 한발짝 전진」 — 원본 힘민지 클립 루트 이동 실측용(0 이면 합성 표로)
|
|||
|
|
// #840 PD 「4타 돌진베기 경로에 적이 있으면 통과하며 공격」 — 멈춤 거리 무시(passThrough) · 배율 1.2 ≈ 116 cm(적 0.6~1 m 를 지나쳐 뒤로)
|
|||
|
|
new Spec("Attack4_S_102", "Assets/Res_Addr/Animations/Animation/Assassin/Stander@DualBlades_DodgeAttack_Attack4.FBX", "단검 4타(#840 통과 돌진 · 원본 루트 1.62 m × 1.2 배율)", 1.2f, true),
|
|||
|
|
// #839 창 = Spearman 팩(Shinabro 표 3개는 더 이상 쓰는 클립이 없어 폐기)
|
|||
|
|
// #840 창 재배열: 1타 = Attack2(찌르기) · 2타 = 찌르기 구간 ×3(표 없음 · 제자리 연타) · 3타 = Attack1(내려찌르기)
|
|||
|
|
new Spec("Attack1_S_107", R + "Spearman Warrior Mecanim Animation Pack/Animations/Spearman@Attack2.FBX", "창 1타(Spearman Attack2 찌르기)"),
|
|||
|
|
new Spec("Attack3_S_107", R + "Spearman Warrior Mecanim Animation Pack/Animations/Spearman@Attack1.FBX", "창 3타(Spearman Attack1 내려찌르기)"),
|
|||
|
|
new Spec("Bow1_S", A + "Attack1.FBX", "활 1타(제자리 사격 · 한 걸음)"),
|
|||
|
|
new Spec("Bow2_S", A + "Attack1.FBX", "활 2타(#834 Attack1 로 통일)"), new Spec("Bow3_S", A + "Attack1.FBX", "활 3타(#834 Attack1 로 통일)"),
|
|||
|
|
new Spec("Cast1_S", M + "Attack2.FBX", "지팡이 1타"), new Spec("Cast2_S", M + "Attack3.FBX", "지팡이 2타"),
|
|||
|
|
// #838 대검 10801 = 2Handed Warrior 팩 루트모션 클립
|
|||
|
|
// #841 대검 재배열: 1타 = 팩 Attack2 · 2타 = Attack3 · 3타 = Attack1
|
|||
|
|
new Spec("Attack1_S_108", R + "2 Handed Warrior Mecanim Animation Pack/Animations/2Handed@Attack2.FBX", "대검 1타(팩 Attack2)"),
|
|||
|
|
new Spec("Attack2_S_108", R + "2 Handed Warrior Mecanim Animation Pack/Animations/2Handed@Attack3.FBX", "대검 2타(팩 Attack3)"),
|
|||
|
|
new Spec("Attack3_S_108", R + "2 Handed Warrior Mecanim Animation Pack/Animations/2Handed@Attack1.FBX", "대검 3타(팩 Attack1)"),
|
|||
|
|
new Spec("Attack4_S_108", R + "Spearman Warrior Mecanim Animation Pack/Animations/Spearman@Attack3.FBX", "대검 4타(#840 Spearman Attack3 점프 찌르기 · 원본 1.16 m × 0.6 ≈ 69 cm)"),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
public static object Run(bool apply, float scale)
|
|||
|
|
{
|
|||
|
|
var sb = new StringBuilder();
|
|||
|
|
var settings = AssetDatabase.LoadAssetAtPath<WLCombatMotionSettings>(SettingsPath);
|
|||
|
|
if (settings == null) return "설정 에셋 없음 " + SettingsPath;
|
|||
|
|
var pc = AssetDatabase.LoadAssetAtPath<GameObject>(PcPath);
|
|||
|
|
var an = pc != null ? pc.GetComponentInChildren<Animator>(true) : null;
|
|||
|
|
float hs = an != null && an.avatar != null ? an.humanScale : 0f;
|
|||
|
|
if (hs <= 0f) return "치비 humanScale 을 못 읽음 " + PcPath;
|
|||
|
|
sb.AppendLine(string.Format("humanScale={0:F4} scale={1:F2} mode={2} frameTableScale={3:F2}", hs, scale, settings.attackMoveMode, settings.frameTableScale));
|
|||
|
|
|
|||
|
|
var result = new List<AttackFrameTable>();
|
|||
|
|
// ① 검 3표(#791 PD 승인값) — 키만 좁힌다
|
|||
|
|
foreach (var old in settings.attackFrameTables)
|
|||
|
|
{
|
|||
|
|
if (old == null) continue;
|
|||
|
|
string k = old.clipContains;
|
|||
|
|
if (k == "Attack1_S" || k == "Attack2_S" || k == "Attack3_S") k += "_101";
|
|||
|
|
if (!k.StartsWith("Attack") || !k.EndsWith("_101")) continue; // 검 3표만 보존(다른 키는 아래에서 새로)
|
|||
|
|
result.Add(new AttackFrameTable { clipContains = k, frames = (int[])old.frames.Clone(), cm = (float[])old.cm.Clone() });
|
|||
|
|
sb.AppendLine(string.Format("[검] {0,-14} 보존 · 합 {1:F1} cm · 프레임 {2}", k, old.Total(), string.Join(",", old.frames.Select(x => x.ToString()).ToArray())));
|
|||
|
|
}
|
|||
|
|
// ② 새 표 — 팩 원본 루트모션
|
|||
|
|
foreach (var s in Specs)
|
|||
|
|
{
|
|||
|
|
var clip = AssetDatabase.LoadAllAssetsAtPath(s.fbx).OfType<AnimationClip>().FirstOrDefault(c => !c.name.StartsWith("__preview"));
|
|||
|
|
if (clip == null) { sb.AppendLine("[" + s.note + "] 클립 없음 " + s.fbx); continue; }
|
|||
|
|
AnimationCurve z = null;
|
|||
|
|
foreach (var b in AnimationUtility.GetCurveBindings(clip)) if (b.propertyName == "RootT.z") { z = AnimationUtility.GetEditorCurve(clip, b); break; }
|
|||
|
|
if (z == null) { sb.AppendLine("[" + s.note + "] RootT.z 없음 " + clip.name); continue; }
|
|||
|
|
float fr = clip.frameRate > 0f ? clip.frameRate : 30f;
|
|||
|
|
int n = Mathf.Max(1, Mathf.RoundToInt(fr * clip.length));
|
|||
|
|
var frames = new List<int>(); var cms = new List<float>();
|
|||
|
|
float prev = z.Evaluate(0f), raw = 0f, fwdRaw = 0f, gatedOut = 0f;
|
|||
|
|
List<float> foot = s.stepGate ? FootEventTimes(s.fbx) : null;
|
|||
|
|
string gateNote = "";
|
|||
|
|
if (s.stepGate)
|
|||
|
|
{
|
|||
|
|
var w = new List<string>();
|
|||
|
|
foreach (var t in foot) w.Add(Mathf.RoundToInt((t - StepGateBefore / clip.length) * n) + "~" + Mathf.RoundToInt((t + StepGateAfter / clip.length) * n));
|
|||
|
|
gateNote = " · 스텝 창 " + (foot.Count == 0 ? "없음(Foot 이벤트 0 → 전부 0)" : string.Join(",", w.ToArray()));
|
|||
|
|
}
|
|||
|
|
for (int f = 1; f <= n; f++)
|
|||
|
|
{
|
|||
|
|
float v = z.Evaluate(Mathf.Min(f / fr, clip.length));
|
|||
|
|
float d = v - prev; prev = v;
|
|||
|
|
raw += d;
|
|||
|
|
if (d <= 0f) continue;
|
|||
|
|
fwdRaw += d;
|
|||
|
|
float cm = d * hs * 100f * scale * Mathf.Max(s.mult, 0f);
|
|||
|
|
if (s.stepGate)
|
|||
|
|
{
|
|||
|
|
bool inWin = false; float tf = f / (float)n;
|
|||
|
|
foreach (var t in foot) if (tf >= t - StepGateBefore / clip.length && tf <= t + StepGateAfter / clip.length) { inWin = true; break; }
|
|||
|
|
if (!inWin) { gatedOut += cm; continue; }
|
|||
|
|
}
|
|||
|
|
if (cm < 0.1f) continue;
|
|||
|
|
frames.Add(f); cms.Add(Mathf.Round(cm * 10f) / 10f);
|
|||
|
|
}
|
|||
|
|
if (s.stepGate) gateNote += " · 제거 " + gatedOut.ToString("0.0") + " cm";
|
|||
|
|
float total = 0f; foreach (var c in cms) total += c;
|
|||
|
|
result.Add(new AttackFrameTable { clipContains = s.key, frames = frames.ToArray(), cm = cms.ToArray(), passThrough = s.pass });
|
|||
|
|
sb.AppendLine(string.Format("[{0}] {1,-14} {2} len={3:F2}s n={4} · 원본 Δz 순={5:F2} 전진={6:F2}(×hs {7:F2} m) → 표 합 {8:F1} cm · 프레임 {9}~{10} ({11}개){12}",
|
|||
|
|
s.note, s.key, clip.name, clip.length, n, raw, fwdRaw, fwdRaw * hs, total, frames.Count > 0 ? frames[0] : 0, frames.Count > 0 ? frames[frames.Count - 1] : 0, frames.Count, gateNote));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!apply) return sb.Append("(dry)").ToString();
|
|||
|
|
settings.attackFrameTables = result;
|
|||
|
|
EditorUtility.SetDirty(settings);
|
|||
|
|
AssetDatabase.SaveAssets();
|
|||
|
|
sb.AppendLine("APPLIED — 표 " + result.Count + "개 기록 (" + SettingsPath + ")");
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
}
|