live2D (sdk가 6.5 미지원으로 sprite로 대응)
This commit is contained in:
@@ -26,7 +26,8 @@ public class DialogPlayer : MonoBehaviour
|
||||
|
||||
// 대화 중 포즈를 바꾼 캐릭터들의 원래 포즈 — 대화 종료 시 전부 복원.
|
||||
// (끼어든 다른 NPC의 포즈도 포함되므로 Animator와 같은 방식으로 딕셔너리에 추적한다)
|
||||
private readonly Dictionary<CharacterPoses, Pose> _touchedPoses = new();
|
||||
// 키가 인터페이스라 스프라이트 인물과 Live2D 인물이 한 딕셔너리에 섞여 들어간다.
|
||||
private readonly Dictionary<ICharacterPoses, Pose> _touchedPoses = new();
|
||||
public bool IsPlaying { get; private set; }
|
||||
|
||||
// 지금 실제 대사를 재생 중인 DialogPlayer (전역 1개) — 이때 다른 NPC의 Play()는 무시된다.
|
||||
@@ -425,19 +426,26 @@ private void ApplyStageVisuals(DialogNode node)
|
||||
poses.Apply(node.Pose);
|
||||
}
|
||||
|
||||
// 포즈를 적용할 대상의 CharacterPoses. 그 캐릭터가 지금 씬에 없으면 null.
|
||||
// 포즈를 적용할 대상의 포즈 구현. 그 캐릭터가 지금 씬에 없으면 null.
|
||||
// 제스처/표정과 달리 화자를 전혀 보지 않는다 — 대상은 노드가 명시한 것 하나뿐이다.
|
||||
private static CharacterPoses FindPoses(CharacterData target)
|
||||
// 스프라이트(CharacterPoses)든 Live2D(CubismCharacterPoses)든 인터페이스로 받으므로
|
||||
// 대화 그래프는 그 인물이 어느 쪽인지 알 필요가 없다.
|
||||
private static ICharacterPoses FindPoses(CharacterData target)
|
||||
{
|
||||
var obj = CharacterVoiceObject.Find(target);
|
||||
return obj != null ? obj.GetComponentInChildren<CharacterPoses>(includeInactive: true) : null;
|
||||
return obj != null ? obj.GetComponentInChildren<ICharacterPoses>(includeInactive: true) : null;
|
||||
}
|
||||
|
||||
// 대화 중 포즈를 바꾼 모든 캐릭터(끼어든 NPC 포함)를 원래 포즈로 복원
|
||||
private void RestoreDefaultPoses()
|
||||
{
|
||||
foreach (var kvp in _touchedPoses)
|
||||
if (kvp.Key != null) kvp.Key.Apply(kvp.Value);
|
||||
{
|
||||
// 인터페이스 참조로는 Unity의 "파괴됨" 검사가 안 걸린다(그냥 C# 널 비교가 된다).
|
||||
// 씬 전환으로 오브젝트가 사라졌을 수 있으므로 Object로 보고 확인한다.
|
||||
if (kvp.Key as UnityEngine.Object != null)
|
||||
kvp.Key.Apply(kvp.Value);
|
||||
}
|
||||
_touchedPoses.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
// CharacterVoiceObject가 등록돼 대사가 찾을 수 있는데, 그렇다고 그림까지 보일 필요는 없다.
|
||||
// Start Pose를 '숨김'으로 두면 살아는 있되 안 보이는 상태가 되고, 이게 동행자(마요이)처럼
|
||||
// 장소에 서 있지 않고 대화 중에만 끼어드는 캐릭터의 기본 상태다.
|
||||
public class CharacterPoses : MonoBehaviour
|
||||
public class CharacterPoses : MonoBehaviour, ICharacterPoses
|
||||
{
|
||||
[Serializable]
|
||||
private struct Entry
|
||||
|
||||
131
Assets/02_Scripts/Story/CubismCharacterPoses.cs
Normal file
131
Assets/02_Scripts/Story/CubismCharacterPoses.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Story/CubismCharacterPoses.cs.meta
Normal file
2
Assets/02_Scripts/Story/CubismCharacterPoses.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f655cdfd6662d54b91c7c63f8812855
|
||||
18
Assets/02_Scripts/Story/ICharacterPoses.cs
Normal file
18
Assets/02_Scripts/Story/ICharacterPoses.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 캐릭터의 포즈를 화면에 반영하는 방법. 구현이 둘이고, 캐릭터마다 골라 붙인다:
|
||||
//
|
||||
// CharacterPoses — 포즈마다 스프라이트 오브젝트를 켜고 끈다.
|
||||
// 단역이나 그림 몇 장으로 끝나는 인물용.
|
||||
// CubismCharacterPoses — Live2D 모델 하나의 표정을 바꾼다.
|
||||
// 모델 제작 비용이 큰 만큼 주요 인물에만 쓴다.
|
||||
//
|
||||
// DialogPlayer는 어느 쪽인지 모른 채 Apply/Current만 쓴다 — 덕분에 대화 그래프는
|
||||
// 그 인물이 스프라이트인지 Live2D인지 신경 쓰지 않고 Pose(공통 감정 축)만 지정하면 된다.
|
||||
// 나중에 스프라이트 인물을 Live2D로 승격시켜도 대화 데이터는 그대로다.
|
||||
public interface ICharacterPoses
|
||||
{
|
||||
// 지금 켜져 있는 포즈. DialogPlayer가 대화 전 상태를 기억했다가 되돌리는 데 쓴다.
|
||||
Pose Current { get; }
|
||||
|
||||
// 포즈를 반영한다. Pose.Keep(변경 없음)은 호출하는 쪽에서 걸러진다.
|
||||
void Apply(Pose pose);
|
||||
}
|
||||
2
Assets/02_Scripts/Story/ICharacterPoses.cs.meta
Normal file
2
Assets/02_Scripts/Story/ICharacterPoses.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1493eee890474344a995464e7311cfc
|
||||
8
Assets/02_Scripts/_Debug/Editor.meta
Normal file
8
Assets/02_Scripts/_Debug/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca8eab6622f39d94198d452a71b598d4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
57
Assets/02_Scripts/_Debug/Editor/CubismDiagnostics.cs
Normal file
57
Assets/02_Scripts/_Debug/Editor/CubismDiagnostics.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Live2D.Cubism.Rendering;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
// Live2D 모델이 화면에 안 나올 때 원인을 좁히는 진단.
|
||||
//
|
||||
// URP 렌더 패스(CubismRenderPassFeature.RecordRenderGraph)는 등록된 컨트롤러가 0개면
|
||||
// 아무것도 그리지 않고 그냥 return한다. 그 등록은 CubismRenderController.OnEnable에서
|
||||
// 일어나는데, 그쪽에도 "Fail silently" 주석과 함께 조기 종료 경로가 둘 있다:
|
||||
//
|
||||
// if (!Model) { return; } // ① 모델 없음
|
||||
// if (GetComponent<ICubismRenderingInterceptor>() != null) { return; } // ② 인터셉터 부착
|
||||
//
|
||||
// 즉 등록에 실패해도 콘솔에 아무 흔적이 남지 않아서, 화면이 비어도 원인을 알 수 없다.
|
||||
// 이 진단은 "렌더 패스가 실제로 몇 개를 보고 있는가"를 드러내서
|
||||
// 등록 문제인지(0개) 그리기 문제인지(1개 이상인데 안 보임) 를 가른다.
|
||||
public static class CubismDiagnostics
|
||||
{
|
||||
[MenuItem("Live2D/진단: 렌더 컨트롤러 등록 확인")]
|
||||
private static void Report()
|
||||
{
|
||||
var registered = CubismRenderControllerGroup.GetInstance().RenderControllers;
|
||||
var registeredCount = registered?.Length ?? 0;
|
||||
|
||||
var inScene = Object.FindObjectsByType<CubismRenderController>(
|
||||
FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
|
||||
Debug.Log($"[Cubism 진단] 렌더 패스가 보는 등록 수: {registeredCount} / 씬의 컨트롤러 수: {inScene.Length}");
|
||||
|
||||
if (inScene.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[Cubism 진단] 씬에 CubismRenderController가 없다 — 모델이 씬에 없거나 프리팹 모드에서 보고 있다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (registeredCount == 0)
|
||||
{
|
||||
Debug.LogWarning("[Cubism 진단] 컨트롤러는 씬에 있는데 등록이 0개다. " +
|
||||
"이 상태면 렌더 패스가 아무것도 그리지 않는다. " +
|
||||
"아래 목록에서 Model이 '없음'이거나 비활성인 항목을 확인할 것. " +
|
||||
"전부 정상인데 0이면 OnEnable이 아직 안 돈 것이므로 " +
|
||||
"해당 오브젝트를 껐다 켜면 재등록된다.");
|
||||
}
|
||||
|
||||
foreach (var c in inScene)
|
||||
{
|
||||
Debug.Log($" · {c.name}\n" +
|
||||
$" enabled = {c.enabled}, activeInHierarchy = {c.gameObject.activeInHierarchy}\n" +
|
||||
$" Model = {(c.Model != null ? "있음" : "없음 → OnEnable이 ①에서 조용히 종료됨")}\n" +
|
||||
$" IsInitialized = {c.IsInitialized}\n" +
|
||||
$" Opacity = {c.Opacity} (0이면 투명해서 안 보인다)\n" +
|
||||
$" SortingOrder = {c.SortingOrder}\n" +
|
||||
$" Position = {c.transform.position}, Scale = {c.transform.lossyScale}",
|
||||
c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56517d896e454c843a164fcccbe18705
|
||||
Reference in New Issue
Block a user