108 lines
6.5 KiB
C#
108 lines
6.5 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// WLBowPoseIK.cs — 활 쏘는 자세에서 두 손을 머리 바깥으로 벌린다 (#967 · 2026-10-22)
|
||
|
|
//
|
||
|
|
// PD 원문: 「활 기본 공격시에도 한쪽 손이 가만히 있는 동작처럼 보이는 현상을 수정해줘. (위에서 아래로 내려다 보는 각도일 때 특히 두드러지며, 아래에서 위를 볼때도 한손만 쓰는 것처럼 보임)」
|
||
|
|
// ■ 원인(편집 모드 손 실측 · 조직 레포 scripts/wl/probes/967_bowhands.cs) — 활 1·2타(Archer RangeAttack1 · BowShot1/2_S) 두 손이 머리 높이(0.6~0.86 m · 머리 0.70 m)에서
|
||
|
|
// 앞뒤 일직선(활 손 앞 0.29 m · 당기는 손 뒤 0.36 m · 좌우 0.00~0.15 m) → 위 카메라에서 화면 위/아래로 쏘면 큰 머리가 한 손을 가린다(위를 보면 활 손 · 아래를 보면 당기는 손).
|
||
|
|
// ■ 활 직업 PC 애니메이터의 IK(OnAnimatorIK · 컨트롤러 기본 층 IK Pass) — states 의 상태를 재생하는 동안 두 손 목표 = 애니메이션 손 위치 + PC 기준 오프셋
|
||
|
|
// (rightOffset = 당기는 손 · leftOffset = 활 손 · (오른쪽, 위, 앞) m) · 손 방향(회전)은 애니메이션 그대로 · 가중치 = 상태 정규화 시각 rampIn 까지 0 → weight · rampOut 부터 weight → 0 · 전환 중엔 두 상태 가중치를 섞는다.
|
||
|
|
// ■ 붙이기 = WLBowChargeBoot 가 0.5 s 마다 MyValue.MyPC 의 애니메이터 오브젝트에 보장(Ensure).
|
||
|
|
// ■ 값 = Resources/WL/WLBowPoseIK.json (C45) · enabled 0 = 끔 · RuntimeDisabled(프로브 전후 비교용)
|
||
|
|
// 🔴 Debug.LogError 금지(원본 팝업).
|
||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using UnityEngine;
|
||
|
|
|
||
|
|
namespace WL.Combat.Weapon
|
||
|
|
{
|
||
|
|
[System.Serializable]
|
||
|
|
public class WLBowPoseIKJson
|
||
|
|
{
|
||
|
|
public int enabled = 0;
|
||
|
|
public int[] classes = new int[0];
|
||
|
|
public string[] states = new string[0]; // 기본 층 상태 이름(활 기본 공격 · 속사가 쓰는 상태)
|
||
|
|
public float[] rightOffset = { 0f, 0f, 0f }; // 당기는 손(오른손) 목표 오프셋 — PC 기준 (오른쪽, 위, 앞) m
|
||
|
|
public float[] leftOffset = { 0f, 0f, 0f }; // 활 손(왼손)
|
||
|
|
public float weight = 1f;
|
||
|
|
public float rampIn = 0.15f, rampOut = 0.85f; // 상태 정규화 시각 — 0 → rampIn 가중치 오름 · rampOut → 1 내림
|
||
|
|
public string note = "";
|
||
|
|
}
|
||
|
|
|
||
|
|
public sealed class WLBowPoseIK : MonoBehaviour
|
||
|
|
{
|
||
|
|
public const string ResourcesPath = "WL/WLBowPoseIK";
|
||
|
|
public static bool RuntimeDisabled;
|
||
|
|
public static int Attached, Frames;
|
||
|
|
public static float LastWeight;
|
||
|
|
public static string LastLog = "";
|
||
|
|
|
||
|
|
static WLBowPoseIKJson s_cfg;
|
||
|
|
static readonly HashSet<int> s_hashes = new HashSet<int>();
|
||
|
|
|
||
|
|
Animator _an; PCActor _pc;
|
||
|
|
|
||
|
|
static WLBowPoseIKJson Cfg
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
if (s_cfg != null) return s_cfg;
|
||
|
|
s_cfg = new WLBowPoseIKJson(); s_hashes.Clear();
|
||
|
|
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; }
|
||
|
|
if (s_cfg.states != null) foreach (var s in s_cfg.states) if (!string.IsNullOrEmpty(s)) s_hashes.Add(Animator.StringToHash(s));
|
||
|
|
return s_cfg;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||
|
|
static void Reset() { s_cfg = null; s_hashes.Clear(); RuntimeDisabled = false; Attached = Frames = 0; LastWeight = 0f; LastLog = ""; }
|
||
|
|
|
||
|
|
/// <summary>활 직업이면 PC 애니메이터 오브젝트에 붙인다(이미 있으면 그대로).</summary>
|
||
|
|
public static void Ensure(PCActor pc)
|
||
|
|
{
|
||
|
|
if (pc == null || pc.m_animation == null) return;
|
||
|
|
var c = Cfg; if (c.enabled == 0) return;
|
||
|
|
var go = pc.m_animation.gameObject;
|
||
|
|
if (go.GetComponent<WLBowPoseIK>() != null) return;
|
||
|
|
go.AddComponent<WLBowPoseIK>(); Attached++;
|
||
|
|
}
|
||
|
|
|
||
|
|
static Vector3 V(float[] a) { return a != null && a.Length >= 3 ? new Vector3(a[0], a[1], a[2]) : Vector3.zero; }
|
||
|
|
|
||
|
|
float StateWeight(AnimatorStateInfo st, WLBowPoseIKJson c)
|
||
|
|
{
|
||
|
|
if (!s_hashes.Contains(st.shortNameHash)) return 0f;
|
||
|
|
float n = Mathf.Clamp01(st.normalizedTime);
|
||
|
|
float w = 1f;
|
||
|
|
if (c.rampIn > 0.001f && n < c.rampIn) w = n / c.rampIn;
|
||
|
|
if (c.rampOut < 0.999f && n > c.rampOut) w = Mathf.Min(w, (1f - n) / Mathf.Max(0.001f, 1f - c.rampOut));
|
||
|
|
return Mathf.Clamp01(w) * Mathf.Clamp01(c.weight);
|
||
|
|
}
|
||
|
|
|
||
|
|
void OnAnimatorIK(int layer)
|
||
|
|
{
|
||
|
|
if (layer != 0 || RuntimeDisabled) return;
|
||
|
|
var c = Cfg; if (c.enabled == 0 || s_hashes.Count == 0) return;
|
||
|
|
if (_an == null) _an = GetComponent<Animator>();
|
||
|
|
if (_pc == null) _pc = GetComponentInParent<PCActor>();
|
||
|
|
if (_an == null || _pc == null || c.classes == null || System.Array.IndexOf(c.classes, _pc.Get_ID()) < 0) return;
|
||
|
|
float w;
|
||
|
|
if (_an.IsInTransition(0))
|
||
|
|
{
|
||
|
|
float tr = Mathf.Clamp01(_an.GetAnimatorTransitionInfo(0).normalizedTime);
|
||
|
|
w = Mathf.Lerp(StateWeight(_an.GetCurrentAnimatorStateInfo(0), c), StateWeight(_an.GetNextAnimatorStateInfo(0), c), tr);
|
||
|
|
}
|
||
|
|
else w = StateWeight(_an.GetCurrentAnimatorStateInfo(0), c);
|
||
|
|
LastWeight = w;
|
||
|
|
if (w <= 0.001f) return;
|
||
|
|
var rot = _pc.transform.rotation;
|
||
|
|
var r = _an.GetIKPosition(AvatarIKGoal.RightHand) + rot * V(c.rightOffset);
|
||
|
|
var l = _an.GetIKPosition(AvatarIKGoal.LeftHand) + rot * V(c.leftOffset);
|
||
|
|
_an.SetIKPositionWeight(AvatarIKGoal.RightHand, w); _an.SetIKPosition(AvatarIKGoal.RightHand, r);
|
||
|
|
_an.SetIKPositionWeight(AvatarIKGoal.LeftHand, w); _an.SetIKPosition(AvatarIKGoal.LeftHand, l);
|
||
|
|
Frames++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|