88 lines
3.7 KiB
C#
88 lines
3.7 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Unity Animator 로 몸짓을 재생한다. Humanoid 클립을 담은 컨트롤러가 필요하다.
|
|
///
|
|
/// VRM 과 어떻게 맞물리는가:
|
|
/// 모델을 불러올 때 Vrm10Instance.Runtime 에 접근하면서 컨트롤 리그가 만들어지고,
|
|
/// 그때 Animator.avatar 가 컨트롤 리그용으로 교체된다. 그래서 이 Animator 에
|
|
/// Humanoid 클립을 틀면 컨트롤 리그의 본이 움직이고, Vrm10Runtime.Process() 가
|
|
/// LateUpdate 에서 그걸 실제 메시 본으로 옮긴다. ProceduralIdle 이 팔을 내리는 것과
|
|
/// 똑같은 경로다 — 그래서 둘은 같은 본을 두고 싸운다. 이 구현이 살아 있는 동안
|
|
/// ProceduralIdle 은 꺼야 한다(CharacterMotionDirector 가 처리한다).
|
|
///
|
|
/// 클립은 반드시 임포트 설정에서 Rig > Animation Type = Humanoid 여야 한다.
|
|
/// Generic 으로 들어온 클립은 본 이름으로 묶여서 다른 모델에 리타게팅되지 않는다.
|
|
///
|
|
/// MonoBehaviour 가 아닌 이유: 씬 수명주기가 필요 없고, 캐릭터가 새로 로드될 때마다
|
|
/// 새로 만들어 갈아끼우는 편이 상태가 남지 않아 깔끔하다.
|
|
/// </summary>
|
|
public class AnimatorCharacterMotion : ICharacterMotion
|
|
{
|
|
readonly Animator animator;
|
|
readonly IReadOnlyDictionary<CharacterMotionKind, string> stateNames;
|
|
readonly float crossFade;
|
|
|
|
CharacterMotionKind? current;
|
|
|
|
/// <summary>이름이 틀린 상태를 매 프레임 경고하지 않도록, 한 번 알린 것은 기억한다.</summary>
|
|
readonly HashSet<string> warned = new HashSet<string>();
|
|
|
|
public bool IsReady { get; }
|
|
|
|
public AnimatorCharacterMotion(Animator animator, RuntimeAnimatorController controller,
|
|
IReadOnlyDictionary<CharacterMotionKind, string> 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);
|
|
}
|
|
}
|