132 lines
5.3 KiB
C#
132 lines
5.3 KiB
C#
using System;
|
|
using Live2D.Cubism.Framework.Expression;
|
|
using UnityEngine;
|
|
|
|
// Live2D 모델의 포즈. [[CharacterPoses]](스프라이트 교체)와 같은 자리를 대신한다.
|
|
//
|
|
// 스프라이트판은 포즈마다 오브젝트를 켜고 끄지만, Live2D는 모델이 하나뿐이고
|
|
// 표정(.exp3.json)이 파라미터를 덮어써서 얼굴이 바뀐다. 그래서 매핑 대상이
|
|
// GameObject가 아니라 Expressions List의 인덱스다.
|
|
//
|
|
// 배치법 — 중요: 이 컴포넌트는 모델 오브젝트가 아니라 그 **부모**에 붙일 것.
|
|
// MayaFey CharacterVoiceObject + DialogPlayer + CubismCharacterPoses
|
|
// └── MayaFeyModel CubismModel + CubismExpressionController ← Model Root
|
|
// '숨김'이 Model Root를 SetActive(false)로 끄기 때문에, 같은 오브젝트에 붙이면
|
|
// 자기 자신이 꺼져서 다시 켜지지 못한다.
|
|
//
|
|
// 부위 터치는 콜라이더가 아니라 Live2D의 CubismRaycaster/CubismRaycastable를 쓰는 것이 맞다 —
|
|
// 모델이 변형되면 판정 영역도 같이 따라가므로 포즈마다 손으로 배치할 필요가 없다.
|
|
public class CubismCharacterPoses : MonoBehaviour, ICharacterPoses
|
|
{
|
|
[Serializable]
|
|
private struct Entry
|
|
{
|
|
[Tooltip("공통 감정 축")]
|
|
public Pose Pose;
|
|
|
|
[Tooltip("Expressions List에서 이 포즈에 쓸 표정 번호 (0부터)")]
|
|
public int ExpressionIndex;
|
|
}
|
|
|
|
[Tooltip("표정을 적용할 컨트롤러. 비우면 자식에서 찾는다")]
|
|
[SerializeField] private CubismExpressionController _controller;
|
|
|
|
[Tooltip("'숨김'일 때 끌 오브젝트 (Live2D 모델 루트). 비우면 컨트롤러가 붙은 오브젝트. " +
|
|
"이 컴포넌트가 붙은 오브젝트를 지정하면 안 된다 — 꺼진 뒤 다시 켤 수 없게 된다")]
|
|
[SerializeField] private GameObject _modelRoot;
|
|
|
|
[Tooltip("이 캐릭터가 가진 포즈들")]
|
|
[SerializeField] private Entry[] _entries;
|
|
|
|
[Tooltip("시작할 때 켤 포즈.\n\n" +
|
|
"'숨김'으로 두면 모델은 살아 있되 화면에는 안 나온다 — 대화 중에만 끼어드는 동행자용.\n\n" +
|
|
"'변경 없음'이면 목록에서 실제 포즈인 첫 항목을 쓴다")]
|
|
[SerializeField] private Pose _startPose = Pose.Keep;
|
|
|
|
public Pose Current { get; private set; } = Pose.Hide;
|
|
|
|
private void Awake()
|
|
{
|
|
if (_controller == null)
|
|
_controller = GetComponentInChildren<CubismExpressionController>(includeInactive: true);
|
|
|
|
if (_modelRoot == null && _controller != null)
|
|
_modelRoot = _controller.gameObject;
|
|
|
|
if (_modelRoot == gameObject)
|
|
{
|
|
Debug.LogError($"[CubismCharacterPoses] {name}: Model Root가 자기 자신이다 — " +
|
|
"'숨김'에서 스스로 꺼져 되살아나지 못한다. 모델을 자식으로 두고 그걸 지정할 것");
|
|
_modelRoot = null;
|
|
}
|
|
|
|
Apply(DefaultPose());
|
|
}
|
|
|
|
// Pose.Keep은 "변경 없음"이라 여기까지 오면 안 된다 — 호출하는 쪽에서 걸러낸다.
|
|
public void Apply(Pose pose)
|
|
{
|
|
if (pose == Pose.Keep) return;
|
|
|
|
if (pose == Pose.Hide)
|
|
{
|
|
if (_modelRoot != null) _modelRoot.SetActive(false);
|
|
Current = Pose.Hide;
|
|
return;
|
|
}
|
|
|
|
if (!TryGetExpression(pose, out var index))
|
|
{
|
|
Debug.LogWarning($"[CubismCharacterPoses] {name}에 없는 포즈: {pose} — 무시");
|
|
return;
|
|
}
|
|
|
|
if (_modelRoot != null) _modelRoot.SetActive(true);
|
|
|
|
// 표정은 인덱스 하나만 바꾸면 컨트롤러가 알아서 이전 표정과 블렌딩한다
|
|
if (_controller != null) _controller.CurrentExpressionIndex = index;
|
|
|
|
Current = pose;
|
|
}
|
|
|
|
private bool TryGetExpression(Pose pose, out int index)
|
|
{
|
|
index = -1;
|
|
if (_entries == null) return false;
|
|
|
|
foreach (var e in _entries)
|
|
{
|
|
if (e.Pose != pose) continue;
|
|
|
|
// 표정 목록을 벗어난 번호는 조용히 통과시키면 엉뚱한 얼굴이 뜨거나 아무 일도 안 난다
|
|
int count = _controller != null && _controller.ExpressionsList != null
|
|
&& _controller.ExpressionsList.CubismExpressionObjects != null
|
|
? _controller.ExpressionsList.CubismExpressionObjects.Length : 0;
|
|
|
|
if (e.ExpressionIndex < 0 || e.ExpressionIndex >= count)
|
|
{
|
|
Debug.LogWarning($"[CubismCharacterPoses] {name}: {pose}의 표정 번호 " +
|
|
$"{e.ExpressionIndex}가 목록 범위(0~{count - 1})를 벗어남");
|
|
return false;
|
|
}
|
|
|
|
index = e.ExpressionIndex;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// 시작할 때 켤 포즈. 지정했으면 그대로, '변경 없음'이면 목록의 실제 포즈 첫 항목.
|
|
// 인스펙터에서 배열에 항목을 추가하면 Pose가 0(변경 없음)으로 시작하므로 그건 건너뛴다.
|
|
private Pose DefaultPose()
|
|
{
|
|
if (_startPose != Pose.Keep) return _startPose;
|
|
|
|
if (_entries != null)
|
|
foreach (var e in _entries)
|
|
if (e.Pose is not (Pose.Keep or Pose.Hide)) return e.Pose;
|
|
|
|
return Pose.Hide;
|
|
}
|
|
}
|