대화중 표정 수정
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
// 표정에 따라 캐릭터의 "커스텀 연출"을 켜고 끄는 범용 컴포넌트. 캐릭터 루트에 붙인다.
|
||||
// 눈 하이라이트, 볼 홍조, 땀방울, 눈동자 색 등 표정별로 달라지는 것들을 Key로 등록해 두고,
|
||||
// ExpressionData.CustomKeys에 그 Key를 넣으면 그 표정이 재생되는 동안 활성화된다. (DialogPlayer가 호출)
|
||||
//
|
||||
// 각 Effect는 아래 중 필요한 것만 채운다 (여러 개 동시 가능):
|
||||
// - 텍스처 스왑 : Texture Target + Texture Property + Active/Inactive Texture (MaterialPropertyBlock — 가볍고 원본 머티리얼 안 건드림)
|
||||
// - 오브젝트 on/off : Toggle Object (+ Invert)
|
||||
// - 그 외 무엇이든 : On Set (UnityEvent<bool>) — 활성 시 true, 비활성 시 false
|
||||
public class CharacterExpressionCustom : MonoBehaviour
|
||||
{
|
||||
[Serializable] public class BoolEvent : UnityEvent<bool> { }
|
||||
|
||||
[Serializable]
|
||||
public class Effect
|
||||
{
|
||||
[Tooltip("표정(ExpressionData.CustomKeys)에서 참조할 키. 예: eyeHighlightOff, blush, sweat")]
|
||||
public string Key;
|
||||
|
||||
[Header("텍스처 스왑 (선택)")]
|
||||
[Tooltip("텍스처를 바꿀 렌더러. 비우면 텍스처 스왑 안 함")]
|
||||
public Renderer TextureTarget;
|
||||
[Tooltip("바꿀 텍스처 프로퍼티. 이 프로젝트 BMAC_Toon 알베도=_Texture2D (URP Lit=_BaseMap)")]
|
||||
public string TextureProperty = "_Texture2D";
|
||||
[Tooltip("활성 시 텍스처. 비우면 원본 유지")]
|
||||
public Texture ActiveTexture;
|
||||
[Tooltip("비활성(기본) 시 텍스처. 비우면 시작 시점의 원본으로 복원")]
|
||||
public Texture InactiveTexture;
|
||||
|
||||
[Header("오브젝트 on/off (선택)")]
|
||||
public GameObject ToggleObject;
|
||||
[Tooltip("켜면 반전 — 활성 시 오브젝트를 '끈다' (예: 하이라이트 오브젝트 숨기기)")]
|
||||
public bool Invert;
|
||||
|
||||
[Header("그 외 (선택)")]
|
||||
[Tooltip("활성 시 true, 비활성 시 false로 호출 — 인스펙터에서 무엇이든 연결")]
|
||||
public BoolEvent OnSet;
|
||||
|
||||
// 런타임 내부 상태
|
||||
[NonSerialized] public bool Active;
|
||||
[NonSerialized] public bool Captured;
|
||||
[NonSerialized] public int PropId;
|
||||
[NonSerialized] public Texture CapturedDefault; // 시작 시점의 원본 텍스처
|
||||
}
|
||||
|
||||
[SerializeField] private List<Effect> _effects = new();
|
||||
|
||||
private MaterialPropertyBlock _mpb;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_mpb = new MaterialPropertyBlock();
|
||||
foreach (var e in _effects)
|
||||
Capture(e);
|
||||
}
|
||||
|
||||
private static void Capture(Effect e)
|
||||
{
|
||||
if (e.Captured) return;
|
||||
e.PropId = Shader.PropertyToID(e.TextureProperty);
|
||||
if (e.TextureTarget != null && e.TextureTarget.sharedMaterial != null)
|
||||
e.CapturedDefault = e.TextureTarget.sharedMaterial.GetTexture(e.PropId);
|
||||
e.Captured = true;
|
||||
}
|
||||
|
||||
// 주어진 Key 집합에 맞춰 모든 Effect를 켜고 끈다 (집합에 없는 Effect는 비활성/기본으로).
|
||||
public void SetActiveKeys(ICollection<string> keys)
|
||||
{
|
||||
foreach (var e in _effects)
|
||||
{
|
||||
bool want = keys != null && keys.Count > 0
|
||||
&& !string.IsNullOrEmpty(e.Key) && keys.Contains(e.Key);
|
||||
Apply(e, want);
|
||||
}
|
||||
}
|
||||
|
||||
// 전부 기본(비활성)으로 되돌린다 — 대화 종료 시
|
||||
public void ResetAll()
|
||||
{
|
||||
foreach (var e in _effects)
|
||||
Apply(e, false);
|
||||
}
|
||||
|
||||
private void Apply(Effect e, bool active)
|
||||
{
|
||||
Capture(e);
|
||||
if (e.Active == active) return; // 상태 동일하면 스킵
|
||||
e.Active = active;
|
||||
|
||||
// 텍스처 스왑
|
||||
if (e.TextureTarget != null)
|
||||
{
|
||||
var tex = active
|
||||
? (e.ActiveTexture != null ? e.ActiveTexture : e.CapturedDefault)
|
||||
: (e.InactiveTexture != null ? e.InactiveTexture : e.CapturedDefault);
|
||||
e.TextureTarget.GetPropertyBlock(_mpb); // 기존 MPB 값 보존
|
||||
_mpb.SetTexture(e.PropId, tex);
|
||||
e.TextureTarget.SetPropertyBlock(_mpb);
|
||||
}
|
||||
|
||||
// 오브젝트 on/off
|
||||
if (e.ToggleObject != null)
|
||||
e.ToggleObject.SetActive(active ^ e.Invert);
|
||||
|
||||
// 그 외
|
||||
e.OnSet?.Invoke(active);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d65345b00d8e0a84294e67be64e0842d
|
||||
@@ -56,9 +56,17 @@ public struct NodeEvent
|
||||
private Animator _animator;
|
||||
private readonly Dictionary<Transform, Quaternion> _originalRotations = new();
|
||||
|
||||
// 대화 중 제스처/표정을 재생한 Animator들의 원래 상태 — 대화 종료 시 전부 복원.
|
||||
// 대화 중 제스처/표정으로 건드린 (Animator, 레이어)의 원래 state — 대화 종료 시 그 레이어만 복원.
|
||||
// (끼어든 다른 NPC의 Animator도 포함되므로 딕셔너리로 추적한다)
|
||||
private readonly Dictionary<Animator, (int gestureHash, int expressionHash, bool hasExpression)> _touchedAnimators = new();
|
||||
private readonly Dictionary<(Animator anim, int layer), int> _restoreStates = new();
|
||||
|
||||
// 클립 교체 재생용 — Animator별 오버라이드 컨트롤러(최초 1회만 래핑), 레이어별 슬롯 유무 캐시, 핑퐁 커서
|
||||
private readonly Dictionary<Animator, AnimatorOverrideController> _overrides = new();
|
||||
private readonly Dictionary<(Animator anim, int layer), (bool hasA, bool hasB)> _slotCache = new();
|
||||
private readonly Dictionary<(Animator anim, int layer), int> _slotCursor = new(); // 0 = 직전에 A 슬롯 사용
|
||||
|
||||
// 표정으로 커스텀 연출을 켠 캐릭터들 — 대화 종료 시 전부 원복
|
||||
private readonly HashSet<CharacterExpressionCustom> _touchedCustoms = new();
|
||||
public bool IsPlaying { get; private set; }
|
||||
|
||||
// 지금 실제 대사를 재생 중인 DialogPlayer (전역 1개) — 이때 다른 NPC의 Play()는 무시된다.
|
||||
@@ -355,29 +363,126 @@ private async Awaitable PlayEntry(DialogEntry entry)
|
||||
}
|
||||
}
|
||||
|
||||
// 대화 중 제스처/표정을 재생했던 모든 Animator(끼어든 NPC 포함)를 원래 상태로 복원
|
||||
// 대화 중 제스처/표정으로 건드린 (Animator, 레이어)를 원래 state로 복원 (끼어든 NPC 포함)
|
||||
private void RestoreDefaultAnimations()
|
||||
{
|
||||
foreach (var kvp in _touchedAnimators)
|
||||
foreach (var kvp in _restoreStates)
|
||||
{
|
||||
var anim = kvp.Key;
|
||||
if (anim == null) continue;
|
||||
anim.CrossFade(kvp.Value.gestureHash, 0.3f, 0, normalizedTimeOffset: 0f);
|
||||
if (kvp.Value.hasExpression)
|
||||
anim.CrossFade(kvp.Value.expressionHash, 0.3f, 1, normalizedTimeOffset: 0f);
|
||||
var (anim, layer) = kvp.Key;
|
||||
if (anim == null || layer >= anim.layerCount) continue;
|
||||
anim.CrossFade(kvp.Value, 0.3f, layer, normalizedTimeOffset: 0f);
|
||||
}
|
||||
_touchedAnimators.Clear();
|
||||
_restoreStates.Clear();
|
||||
|
||||
// 표정으로 켰던 커스텀 연출 전부 원복
|
||||
foreach (var c in _touchedCustoms)
|
||||
if (c != null) c.ResetAll();
|
||||
_touchedCustoms.Clear();
|
||||
}
|
||||
|
||||
// 처음 건드리는 Animator면 현재 상태를 기억해 둔다 (대화 종료 시 복원 기준)
|
||||
private void CaptureInitialAnimState(Animator anim)
|
||||
// 이 레이어를 처음 건드리면 현재 state를 기억해 둔다 (대화 종료 시 이 레이어만 복원)
|
||||
private void CaptureLayerState(Animator anim, int layer)
|
||||
{
|
||||
if (_touchedAnimators.ContainsKey(anim)) return;
|
||||
bool hasExpression = anim.layerCount > 1;
|
||||
_touchedAnimators[anim] = (
|
||||
anim.GetCurrentAnimatorStateInfo(0).fullPathHash,
|
||||
hasExpression ? anim.GetCurrentAnimatorStateInfo(1).fullPathHash : 0,
|
||||
hasExpression);
|
||||
if (layer < 0 || layer >= anim.layerCount) return;
|
||||
var key = (anim, layer);
|
||||
if (_restoreStates.ContainsKey(key)) return;
|
||||
_restoreStates[key] = anim.GetCurrentAnimatorStateInfo(layer).fullPathHash;
|
||||
}
|
||||
|
||||
// ── 표정/제스처 재생 (슬롯 클립 교체 우선, 없으면 StateName 폴백) ──────────────
|
||||
// AnimClip이 있으면: 그 레이어의 Dlg 슬롯 state(플레이스홀더 클립)를 실제 클립으로 교체해 재생.
|
||||
// 슬롯 2개(A·B) → 번갈아 CrossFade(부드러운 블렌드), 1개 → Play(즉시 전환), 0개 → 아래 StateName 폴백.
|
||||
// AnimClip이 없으면(구식 자산): 컨트롤러에 미리 만들어 둔 StateName state로 CrossFade.
|
||||
private const string SlotStatePrefix = "DlgSlot"; // 슬롯 state 이름: DlgSlotA / DlgSlotB
|
||||
private static string SlotStateName(char slot) => SlotStatePrefix + slot;
|
||||
private static string SlotClipName(int layer, char slot) => $"__DlgSlot_{layer}_{slot}"; // 슬롯이 무는 플레이스홀더 클립 이름
|
||||
|
||||
private void PlayAnimData(Animator anim, AnimationClip clip, string stateName, float crossFade, int layer)
|
||||
{
|
||||
if (layer < 0 || layer >= anim.layerCount) layer = 0;
|
||||
CaptureLayerState(anim, layer);
|
||||
|
||||
// 슬롯 방식 (클립 교체) — 이 레이어에 Dlg 슬롯이 있을 때만
|
||||
if (clip != null && TryGetSlots(anim, layer, out bool hasA, out bool hasB))
|
||||
{
|
||||
bool pingpong = hasA && hasB;
|
||||
char slot;
|
||||
if (pingpong)
|
||||
{
|
||||
var key = (anim, layer);
|
||||
_slotCursor.TryGetValue(key, out int last); // last == 0 → 직전에 A 사용
|
||||
slot = last == 0 ? 'B' : 'A';
|
||||
_slotCursor[key] = slot == 'A' ? 0 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
slot = hasA ? 'A' : 'B';
|
||||
}
|
||||
|
||||
var ovr = GetOverride(anim);
|
||||
ovr[SlotClipName(layer, slot)] = clip; // 슬롯의 플레이스홀더를 실제 클립으로 교체
|
||||
if (pingpong)
|
||||
anim.CrossFade(SlotStateName(slot), crossFade, layer, normalizedTimeOffset: 0f);
|
||||
else
|
||||
anim.Play(SlotStateName(slot), layer, 0f); // 슬롯 1개 → 스냅
|
||||
return;
|
||||
}
|
||||
|
||||
// 폴백 — 컨트롤러에 이름으로 미리 만들어 둔 state
|
||||
if (!string.IsNullOrEmpty(stateName))
|
||||
anim.CrossFade(stateName, crossFade, layer);
|
||||
else if (clip != null)
|
||||
Debug.LogWarning($"[DialogPlayer] '{anim.name}' 레이어 {layer}에 Dlg 슬롯도 없고 StateName도 비어 클립을 재생할 수 없습니다: {clip.name}");
|
||||
}
|
||||
|
||||
// 이 Animator를 (아직 아니면) 오버라이드 컨트롤러로 래핑해 캐시 — 최초 1회만 rebind 발생
|
||||
private AnimatorOverrideController GetOverride(Animator anim)
|
||||
{
|
||||
if (_overrides.TryGetValue(anim, out var o) && o != null) return o;
|
||||
if (anim.runtimeAnimatorController is AnimatorOverrideController existing)
|
||||
o = existing;
|
||||
else
|
||||
{
|
||||
o = new AnimatorOverrideController(anim.runtimeAnimatorController)
|
||||
{ name = anim.runtimeAnimatorController.name + " (Dlg)" };
|
||||
anim.runtimeAnimatorController = o;
|
||||
}
|
||||
_overrides[anim] = o;
|
||||
return o;
|
||||
}
|
||||
|
||||
// 이 레이어에 Dlg 슬롯 플레이스홀더 클립(__DlgSlot_{layer}_A/B)이 존재하는지. 결과는 캐시.
|
||||
private bool TryGetSlots(Animator anim, int layer, out bool hasA, out bool hasB)
|
||||
{
|
||||
var key = (anim, layer);
|
||||
if (_slotCache.TryGetValue(key, out var c)) { hasA = c.hasA; hasB = c.hasB; return hasA || hasB; }
|
||||
|
||||
string an = SlotClipName(layer, 'A'), bn = SlotClipName(layer, 'B');
|
||||
hasA = false; hasB = false;
|
||||
var rac = anim.runtimeAnimatorController;
|
||||
if (rac is AnimatorOverrideController ao)
|
||||
{
|
||||
// 이미 래핑돼 슬롯이 실제 클립으로 교체됐어도, 원본(Key) 클립 이름은 플레이스홀더 그대로다
|
||||
var list = new List<KeyValuePair<AnimationClip, AnimationClip>>();
|
||||
ao.GetOverrides(list);
|
||||
foreach (var p in list)
|
||||
{
|
||||
if (p.Key == null) continue;
|
||||
if (p.Key.name == an) hasA = true;
|
||||
else if (p.Key.name == bn) hasB = true;
|
||||
}
|
||||
}
|
||||
else if (rac != null)
|
||||
{
|
||||
foreach (var cl in rac.animationClips)
|
||||
{
|
||||
if (cl == null) continue;
|
||||
if (cl.name == an) hasA = true;
|
||||
else if (cl.name == bn) hasB = true;
|
||||
}
|
||||
}
|
||||
_slotCache[key] = (hasA, hasB);
|
||||
return hasA || hasB;
|
||||
}
|
||||
|
||||
// ── 대화 중 캐릭터 회전 ────────────────────────────────────────
|
||||
@@ -586,11 +691,30 @@ private async Awaitable<bool> PlayNode(DialogNode node)
|
||||
|
||||
if (anim != null)
|
||||
{
|
||||
CaptureInitialAnimState(anim); // 대화 종료 시 복원할 원래 상태 기억
|
||||
// 클립 교체가 최초 1회 rebind를 유발할 수 있으니, 건드릴 레이어의 원래 state를 먼저 기억
|
||||
if (node.Gesture != null) CaptureLayerState(anim, node.Gesture.AnimationLayer);
|
||||
if (node.Expression != null) CaptureLayerState(anim, node.Expression.AnimationLayer);
|
||||
|
||||
if (node.Gesture != null)
|
||||
anim.CrossFade(node.Gesture.StateName, node.Gesture.CrossFadeDuration, node.Gesture.AnimationLayer);
|
||||
PlayAnimData(anim, node.Gesture.AnimClip, node.Gesture.StateName,
|
||||
node.Gesture.CrossFadeDuration, node.Gesture.AnimationLayer);
|
||||
if (node.Expression != null)
|
||||
anim.CrossFade(node.Expression.StateName, node.Expression.CrossFadeDuration, node.Expression.AnimationLayer);
|
||||
PlayAnimData(anim, node.Expression.AnimClip, null, // 표정은 StateName 폴백 없음 — 슬롯 전용
|
||||
node.Expression.CrossFadeDuration, node.Expression.AnimationLayer);
|
||||
}
|
||||
|
||||
// 표정에 딸린 커스텀 연출(눈 하이라이트·홍조 등) 적용 — 화자(못 찾으면 대화 주인 NPC)의 CharacterExpressionCustom에.
|
||||
// CustomKeys에 있는 Key만 활성, 나머지는 비활성 → 다른 표정으로 바뀌면 자동으로 이전 연출이 꺼진다.
|
||||
if (node.Expression != null)
|
||||
{
|
||||
var custom = gestureObj != null
|
||||
? gestureObj.GetComponentInChildren<CharacterExpressionCustom>()
|
||||
: GetComponentInChildren<CharacterExpressionCustom>();
|
||||
if (custom != null)
|
||||
{
|
||||
_touchedCustoms.Add(custom);
|
||||
custom.SetActiveKeys(node.Expression.CustomKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
Assets/02_Scripts/Communication/Dialog/Editor.meta
Normal file
8
Assets/02_Scripts/Communication/Dialog/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c3577dafc249894086578d0ca46ab32
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,114 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
// 선택한 AnimatorController에 대화용 슬롯 state(DlgSlotA / DlgSlotB)와 플레이스홀더 클립을 심는다.
|
||||
// 슬롯 2개 = 부드러운 블렌드(핑퐁). 이후 ExpressionData / GestureData의 AnimClip만 채우면
|
||||
// 컨트롤러를 다시 열 필요 없이 그 클립이 해당 레이어의 슬롯에서 재생된다. (DialogPlayer 참고)
|
||||
//
|
||||
// Setup은 매번 기존 Dlg 슬롯을 싹 지우고 새로 만든다 → 몇 번을 돌려도 정확히 (레이어당 2개)만 남는다.
|
||||
// 사용법: 프로젝트 창에서 .controller를 하나 이상 선택 → Tools/Dialog/Setup ....
|
||||
public static class DialogAnimSlotSetup
|
||||
{
|
||||
private const string SlotStatePrefix = "DlgSlot"; // 슬롯 state 이름: DlgSlotA / DlgSlotB
|
||||
private const string ClipPrefix = "__DlgSlot_"; // 플레이스홀더 클립 이름 접두사
|
||||
private static string SlotClipName(int layer, char slot) => $"{ClipPrefix}{layer}_{slot}";
|
||||
|
||||
// 슬롯을 심을 레이어: 0(Body/제스처), 1(Face/표정). 레이어가 없으면 건너뜀.
|
||||
private static readonly int[] TargetLayers = { 0, 1 };
|
||||
private static readonly char[] Slots = { 'A', 'B' }; // 2개 = 블렌드(핑퐁)
|
||||
|
||||
[MenuItem("Tools/Dialog/Setup Expression·Gesture Slots (Selected Controllers)")]
|
||||
private static void SetupSelected()
|
||||
{
|
||||
var controllers = Selection.GetFiltered<AnimatorController>(SelectionMode.Assets);
|
||||
if (controllers.Length == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Dialog Slots",
|
||||
"프로젝트 창에서 AnimatorController(.controller)를 하나 이상 선택한 뒤 실행하세요.", "확인");
|
||||
return;
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
foreach (var ac in controllers)
|
||||
{
|
||||
RemoveSlots(ac); // 기존 슬롯/고아 클립 정리
|
||||
total += CreateSlots(ac); // 새로 생성
|
||||
EditorUtility.SetDirty(ac);
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
EditorUtility.DisplayDialog("Dialog Slots",
|
||||
$"{controllers.Length}개 컨트롤러 정리 후 재생성 — 슬롯 state {total}개.", "확인");
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Dialog/Remove Expression·Gesture Slots (Selected Controllers)")]
|
||||
private static void RemoveSelected()
|
||||
{
|
||||
var controllers = Selection.GetFiltered<AnimatorController>(SelectionMode.Assets);
|
||||
if (controllers.Length == 0) return;
|
||||
|
||||
foreach (var ac in controllers)
|
||||
{
|
||||
RemoveSlots(ac);
|
||||
EditorUtility.SetDirty(ac);
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
EditorUtility.DisplayDialog("Dialog Slots", $"{controllers.Length}개 컨트롤러에서 Dlg 슬롯 제거 완료.", "확인");
|
||||
}
|
||||
|
||||
// Dlg 슬롯 state(모든 레이어)와 __DlgSlot_* 플레이스홀더 클립(중복·고아 포함)을 전부 제거
|
||||
private static void RemoveSlots(AnimatorController ac)
|
||||
{
|
||||
foreach (var layer in ac.layers)
|
||||
{
|
||||
var sm = layer.stateMachine;
|
||||
if (sm == null) continue;
|
||||
foreach (var cs in sm.states) // sm.states는 복사본 → 순회 중 제거 안전
|
||||
{
|
||||
if (cs.state != null && cs.state.name.StartsWith(SlotStatePrefix))
|
||||
sm.RemoveState(cs.state);
|
||||
}
|
||||
}
|
||||
|
||||
string path = AssetDatabase.GetAssetPath(ac);
|
||||
foreach (var obj in AssetDatabase.LoadAllAssetsAtPath(path))
|
||||
{
|
||||
if (obj is AnimationClip clip && clip.name.StartsWith(ClipPrefix))
|
||||
{
|
||||
AssetDatabase.RemoveObjectFromAsset(clip);
|
||||
Object.DestroyImmediate(clip, allowDestroyingAssets: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int CreateSlots(AnimatorController ac)
|
||||
{
|
||||
int added = 0;
|
||||
var layers = ac.layers;
|
||||
foreach (int layer in TargetLayers)
|
||||
{
|
||||
if (layer >= layers.Length) continue;
|
||||
var sm = layers[layer].stateMachine;
|
||||
|
||||
// 레이어의 WriteDefaultValues 관습을 그대로 따라간다 (한 레이어에서 WD 혼용 시 경고 방지)
|
||||
bool wd = sm.states.Length == 0 || sm.states[0].state.writeDefaultValues;
|
||||
|
||||
foreach (char slot in Slots)
|
||||
{
|
||||
// 슬롯마다 고유한 빈 클립을 물려야 개별적으로 오버라이드된다 (컨트롤러의 서브 에셋으로 저장)
|
||||
var clip = new AnimationClip { name = SlotClipName(layer, slot) };
|
||||
AssetDatabase.AddObjectToAsset(clip, ac);
|
||||
|
||||
var st = sm.AddState(SlotStatePrefix + slot);
|
||||
st.motion = clip;
|
||||
st.writeDefaultValues = wd;
|
||||
added++;
|
||||
}
|
||||
}
|
||||
return added;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ecafb1ca75b469f4cae493c26c20a725
|
||||
@@ -1,10 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Communication/Expression")]
|
||||
public class ExpressionData : ScriptableObject
|
||||
{
|
||||
[HideInInspector] public int AnimationLayer = 1;
|
||||
public string StateName;
|
||||
public float CrossFadeDuration = 0.2f;
|
||||
[HideInInspector] public int AnimationLayer = 1; // 표정: 보통 Face 레이어(1)
|
||||
|
||||
[Tooltip("이 레이어의 Dlg 슬롯에 꽂아 재생할 표정 클립. " +
|
||||
"슬롯은 Tools/Dialog 메뉴로 컨트롤러에 자동 생성")]
|
||||
public AnimationClip AnimClip;
|
||||
}
|
||||
|
||||
public float CrossFadeDuration = 0.2f;
|
||||
|
||||
[Tooltip("이 표정 동안 활성화할 커스텀 연출 Key들 (화자의 CharacterExpressionCustom에 등록된 Key). " +
|
||||
"예: eyeHighlightOff, blush. 대화가 끝나거나 다른 표정으로 바뀌면 자동 해제")]
|
||||
public List<string> CustomKeys = new();
|
||||
}
|
||||
|
||||
@@ -3,8 +3,15 @@
|
||||
[CreateAssetMenu(menuName = "Communication/Gesture")]
|
||||
public class GestureData : ScriptableObject
|
||||
{
|
||||
[HideInInspector] public int AnimationLayer = 0;
|
||||
[HideInInspector] public int AnimationLayer = 0; // 제스처: 보통 Body 레이어(0)
|
||||
|
||||
[Tooltip("구식 폴백 — Dlg 슬롯이 없는 컨트롤러에서 이 이름의 state로 CrossFade한다. " +
|
||||
"AnimClip 방식을 쓰면 비워도 됨")]
|
||||
public string StateName;
|
||||
|
||||
public float CrossFadeDuration = 0.2f;
|
||||
|
||||
[Tooltip("권장 — 채우면 이 레이어의 Dlg 슬롯에 이 클립을 꽂아 재생한다(컨트롤러 편집 불필요). " +
|
||||
"슬롯은 Tools/Dialog 메뉴로 컨트롤러에 자동 생성")]
|
||||
public AnimationClip AnimClip;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user