애니메이션 추가
This commit is contained in:
178
Assets/02_Scripts/Character/CharacterMotionDirector.cs
Normal file
178
Assets/02_Scripts/Character/CharacterMotionDirector.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 이동 상태를 몸짓으로 옮기고, 가만히 있을 때 자세를 바꿔준다. 애니메이션 배선의 유일한 자리.
|
||||
///
|
||||
/// 클립이 없어도 아무것도 깨지지 않는다. 컨트롤러 슬롯이 비어 있으면 지금처럼
|
||||
/// ProceduralIdle 이 계속 대기 자세를 만든다. 모델을 안 넣으면 캐릭터가 안 뜨는 것과
|
||||
/// 같은 방식이다 — 없으면 없는 대로 돌아가고, 넣으면 그때부터 쓰인다.
|
||||
///
|
||||
/// 클립 준비 방법:
|
||||
/// 1. Mixamo 등에서 FBX 를 받는다 (걷기는 In Place 로)
|
||||
/// 2. 임포트 설정에서 Rig > Animation Type = Humanoid (필수. Generic 은 리타게팅 안 됨)
|
||||
/// 3. 메뉴 11 로 컨트롤러를 만들고 각 상태에 클립을 넣는다
|
||||
/// 4. 그 컨트롤러를 이 컴포넌트의 Motion Controller 에 연결한다
|
||||
/// </summary>
|
||||
public class CharacterMotionDirector : MonoBehaviour
|
||||
{
|
||||
[Header("참조 (비우면 씬에서 탐색)")]
|
||||
[SerializeField] VrmCharacterLoader loader;
|
||||
[SerializeField] WindowClimber climber;
|
||||
|
||||
[Header("애니메이션")]
|
||||
[Tooltip("Humanoid 클립을 담은 Animator Controller. 비우면 ProceduralIdle 로 동작한다")]
|
||||
[SerializeField] RuntimeAnimatorController motionController;
|
||||
|
||||
// 짧은 동작일수록 크로스페이드가 차지하는 비중이 커진다. 0.5초짜리 낙하에
|
||||
// 0.15초를 쓰면 30% 가 전환에 묻힌다. 0.1 정도가 부드러움과 또렷함의 절충.
|
||||
[Tooltip("몸짓이 바뀔 때 섞이는 시간(초). 0 이면 뚝 끊긴다. " +
|
||||
"낙하처럼 짧은 동작이 묻히면 더 줄인다")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] float crossFadeSeconds = 0.1f;
|
||||
|
||||
[Header("Animator 상태 이름 (비우면 그 몸짓을 쓰지 않는다)")]
|
||||
[SerializeField] string idleState = "Idle";
|
||||
[SerializeField] string fallState = "Fall";
|
||||
[SerializeField] string walkState = "Walk";
|
||||
[SerializeField] string jumpState = "JumpUp";
|
||||
[SerializeField] string sitState = "Sit";
|
||||
[SerializeField] string lieDownState = "LieDown";
|
||||
|
||||
[Header("쉬는 자세")]
|
||||
[Tooltip("발판에 선 뒤 자세를 바꾸기까지 기다리는 시간의 최소/최대(초). " +
|
||||
"매번 다르게 해야 기계적으로 보이지 않는다")]
|
||||
[SerializeField] float restDelayMin = 5f;
|
||||
[SerializeField] float restDelayMax = 14f;
|
||||
|
||||
[Tooltip("서 있을 때 고를 자세들. Idle 을 섞어둬야 계속 누워만 있지 않는다")]
|
||||
[SerializeField]
|
||||
CharacterMotionKind[] restPoses =
|
||||
{
|
||||
CharacterMotionKind.Idle,
|
||||
CharacterMotionKind.Sit,
|
||||
CharacterMotionKind.LieDown,
|
||||
};
|
||||
|
||||
[Header("동작")]
|
||||
[Tooltip("클립이 준비되면 ProceduralIdle 을 끈다. 둘 다 같은 본을 써서 함께 두면 싸운다")]
|
||||
[SerializeField] bool disableProceduralIdle = true;
|
||||
|
||||
ICharacterMotion motion;
|
||||
ClimbState currentState = ClimbState.Falling;
|
||||
float nextRestChangeTime;
|
||||
|
||||
Dictionary<CharacterMotionKind, string> BuildNames() => new Dictionary<CharacterMotionKind, string>
|
||||
{
|
||||
{ CharacterMotionKind.Idle, idleState },
|
||||
{ CharacterMotionKind.Fall, fallState },
|
||||
{ CharacterMotionKind.Walk, walkState },
|
||||
{ CharacterMotionKind.Jump, jumpState },
|
||||
{ CharacterMotionKind.Sit, sitState },
|
||||
{ CharacterMotionKind.LieDown, lieDownState },
|
||||
};
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (loader == null) loader = FindFirstObjectByType<VrmCharacterLoader>();
|
||||
if (climber == null) climber = FindFirstObjectByType<WindowClimber>();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (loader != null)
|
||||
{
|
||||
loader.Loaded += OnCharacterLoaded;
|
||||
// 이 컴포넌트보다 캐릭터가 먼저 로드됐을 수도 있다.
|
||||
if (loader.Current != null) OnCharacterLoaded(loader.Current);
|
||||
}
|
||||
|
||||
if (climber != null)
|
||||
{
|
||||
climber.StateChanged += OnStateChanged;
|
||||
currentState = climber.State;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (loader != null) loader.Loaded -= OnCharacterLoaded;
|
||||
if (climber != null) climber.StateChanged -= OnStateChanged;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 서 있을 때만 자세를 바꾼다. 걷거나 떨어지는 중에 눕게 할 수는 없다.
|
||||
if (motion == null || currentState != ClimbState.Standing) return;
|
||||
if (Time.unscaledTime < nextRestChangeTime) return;
|
||||
|
||||
ScheduleNextRestChange();
|
||||
|
||||
if (restPoses != null && restPoses.Length > 0)
|
||||
{
|
||||
motion.Play(restPoses[Random.Range(0, restPoses.Length)]);
|
||||
}
|
||||
}
|
||||
|
||||
void OnCharacterLoaded(ICharacterAvatar avatar)
|
||||
{
|
||||
motion = null;
|
||||
if (avatar == null || avatar.Animator == null) return;
|
||||
|
||||
if (motionController == null)
|
||||
{
|
||||
// 클립이 없는 것은 오류가 아니다. 다만 조용히 아무 일도 없으면
|
||||
// "왜 안 움직이지"가 되므로 한 번 알려준다.
|
||||
Debug.Log("[CharacterMotionDirector] Motion Controller 가 비어 있어 " +
|
||||
"ProceduralIdle 로 동작합니다. Humanoid 클립을 넣으면 그때부터 쓰입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
var animatorMotion = new AnimatorCharacterMotion(
|
||||
avatar.Animator, motionController, BuildNames(), crossFadeSeconds);
|
||||
|
||||
if (!animatorMotion.IsReady) return;
|
||||
|
||||
motion = animatorMotion;
|
||||
|
||||
// 같은 본을 매 프레임 덮어쓰는 컴포넌트를 끈다. 남겨두면 실행 순서상
|
||||
// ProceduralIdle 이 Animator 결과를 이겨서 클립이 안 보인다.
|
||||
// HeadLookAt 은 머리만 건드리고, 몸짓 위에 시선을 얹는 쪽이 자연스러워 그대로 둔다.
|
||||
if (disableProceduralIdle && avatar.Root != null)
|
||||
{
|
||||
var idle = avatar.Root.GetComponent<ProceduralIdle>();
|
||||
if (idle != null) idle.enabled = false;
|
||||
}
|
||||
|
||||
currentState = climber != null ? climber.State : ClimbState.Standing;
|
||||
motion.Play(ToMotionKind(currentState));
|
||||
ScheduleNextRestChange();
|
||||
}
|
||||
|
||||
void OnStateChanged(ClimbState state)
|
||||
{
|
||||
currentState = state;
|
||||
motion?.Play(ToMotionKind(state));
|
||||
|
||||
// 서기 시작한 시점부터 다시 센다. 착지하자마자 눕지 않게.
|
||||
if (state == ClimbState.Standing) ScheduleNextRestChange();
|
||||
}
|
||||
|
||||
void ScheduleNextRestChange()
|
||||
{
|
||||
float min = Mathf.Max(0.5f, restDelayMin);
|
||||
float max = Mathf.Max(min, restDelayMax);
|
||||
nextRestChangeTime = Time.unscaledTime + Random.Range(min, max);
|
||||
}
|
||||
|
||||
static CharacterMotionKind ToMotionKind(ClimbState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case ClimbState.Falling: return CharacterMotionKind.Fall;
|
||||
case ClimbState.Walking: return CharacterMotionKind.Walk;
|
||||
case ClimbState.Jumping: return CharacterMotionKind.Jump;
|
||||
default: return CharacterMotionKind.Idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user