using System.Collections.Generic; using UnityEngine; /// /// Unity Animator 로 몸짓을 재생한다. Humanoid 클립을 담은 컨트롤러가 필요하다. /// /// VRM 과 어떻게 맞물리는가: /// 모델을 불러올 때 Vrm10Instance.Runtime 에 접근하면서 컨트롤 리그가 만들어지고, /// 그때 Animator.avatar 가 컨트롤 리그용으로 교체된다. 그래서 이 Animator 에 /// Humanoid 클립을 틀면 컨트롤 리그의 본이 움직이고, Vrm10Runtime.Process() 가 /// LateUpdate 에서 그걸 실제 메시 본으로 옮긴다. ProceduralIdle 이 팔을 내리는 것과 /// 똑같은 경로다 — 그래서 둘은 같은 본을 두고 싸운다. 이 구현이 살아 있는 동안 /// ProceduralIdle 은 꺼야 한다(CharacterMotionDirector 가 처리한다). /// /// 클립은 반드시 임포트 설정에서 Rig > Animation Type = Humanoid 여야 한다. /// Generic 으로 들어온 클립은 본 이름으로 묶여서 다른 모델에 리타게팅되지 않는다. /// /// MonoBehaviour 가 아닌 이유: 씬 수명주기가 필요 없고, 캐릭터가 새로 로드될 때마다 /// 새로 만들어 갈아끼우는 편이 상태가 남지 않아 깔끔하다. /// public class AnimatorCharacterMotion : ICharacterMotion { readonly Animator animator; readonly IReadOnlyDictionary stateNames; readonly float crossFade; CharacterMotionKind? current; /// 이름이 틀린 상태를 매 프레임 경고하지 않도록, 한 번 알린 것은 기억한다. readonly HashSet warned = new HashSet(); public bool IsReady { get; } public AnimatorCharacterMotion(Animator animator, RuntimeAnimatorController controller, IReadOnlyDictionary stateNames, float crossFadeSeconds) { this.animator = animator; this.stateNames = stateNames; crossFade = Mathf.Max(0f, crossFadeSeconds); if (animator == null || controller == null || stateNames == null) { IsReady = false; return; } animator.runtimeAnimatorController = controller; // 컨트롤 리그 본을 직접 만지는 다른 컴포넌트와 섞이므로, 컬링으로 꺼지면 // 화면 밖에서 자세가 굳어버린다. 항상 돌게 둔다. animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; // 위치는 WindowClimber 가 화면 좌표로 정한다. 클립이 캐릭터를 끌고 가면 안 된다. animator.applyRootMotion = false; IsReady = true; } public void Play(CharacterMotionKind kind) { if (!IsReady || animator == null) return; if (current == kind) return; if (!stateNames.TryGetValue(kind, out string stateName) || string.IsNullOrEmpty(stateName)) { return; // 그 몸짓을 쓰지 않기로 한 것. 지금 자세를 유지한다. } // 없는 상태로 CrossFade 하면 Unity 는 조용히 무시한다. 그러면 "왜 안 움직이지"가 // 되므로 한 번은 알려준다. int hash = Animator.StringToHash(stateName); if (!animator.HasState(0, hash)) { if (warned.Add(stateName)) { Debug.LogWarning($"[AnimatorCharacterMotion] 컨트롤러에 '{stateName}' 상태가 없습니다 " + $"({kind} 에 해당). Animator 의 상태 이름을 맞추거나 " + $"CharacterMotionDirector 에서 이름을 바꿔주세요."); } return; } current = kind; animator.CrossFade(hash, crossFade, 0); } }