다이얼로그 작업중
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d860f3521bf608c468e4863c7cc232ac
|
||||
TextScriptImporter:
|
||||
guid: 103ef77e0ab79bf4bb00e69a4f3fbb11
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
8
Assets/02_Scripts/Communication/Dialog.meta
Normal file
8
Assets/02_Scripts/Communication/Dialog.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5dd56c374fcdca41927418e4717e370
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/02_Scripts/Communication/Dialog/DialogChoice.cs
Normal file
8
Assets/02_Scripts/Communication/Dialog/DialogChoice.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
[Serializable]
|
||||
public class DialogChoice
|
||||
{
|
||||
public DialogNode DestinationNode;
|
||||
public string ChoiceText;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd5ea8e7c904dd24e967d91b86004efb
|
||||
8
Assets/02_Scripts/Communication/Dialog/DialogGroup.cs
Normal file
8
Assets/02_Scripts/Communication/Dialog/DialogGroup.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Communication/Dialog Group")]
|
||||
public class DialogGroup : ScriptableObject
|
||||
{
|
||||
public string DialogGroupName;
|
||||
public DialogNode StartNode;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72ef984dbd5eb29498ece3d2dae297ea
|
||||
31
Assets/02_Scripts/Communication/Dialog/DialogNode.cs
Normal file
31
Assets/02_Scripts/Communication/Dialog/DialogNode.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Communication/Dialog Node")]
|
||||
public class DialogNode : ScriptableObject
|
||||
{
|
||||
[Header("Speaker")]
|
||||
public CharacterData Speaker;
|
||||
|
||||
[Header("Content")]
|
||||
[TextArea(2,5)] public string TalkText;
|
||||
public GestureData Gesture;
|
||||
public ExpressionData Expression;
|
||||
public VoiceClip Voice;
|
||||
public float LineDuration; //자동 넘김 시간
|
||||
//LineDuration=0 → 플레이어 입력 대기 (수동)
|
||||
//Voice 있음 → 클립 길이만큼 대기
|
||||
//Voice 없음 → LineDuration 대기
|
||||
|
||||
|
||||
[Header("Behavior")]
|
||||
public bool LookAtPlayer;
|
||||
|
||||
[Header("Flow")]
|
||||
public DialogNode Next; // 선택지 없을 때 자동으로 갈 노드
|
||||
public List<DialogChoice> Choices; // 있으면 플레이어 선택 대기
|
||||
|
||||
[Header("ChoiceQuestion")]
|
||||
[TextArea(2,5)] public string ChoiceQuestion;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb30abdb4b5671b458096d48d11c7f27
|
||||
196
Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs
Normal file
196
Assets/02_Scripts/Communication/Dialog/DialogPlayer.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(CharacterVoiceObject))]
|
||||
public class DialogPlayer : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private List<DialogGroup> _dialogGroups;
|
||||
private Dictionary<string, DialogGroup> _dialogGroupMap;
|
||||
private Animator _animator;
|
||||
private int _initialGestureHash;
|
||||
private int _initialExpressionHash;
|
||||
private bool _hasInitialExpression;
|
||||
private readonly Dictionary<Transform, Quaternion> _originalRotations = new();
|
||||
public bool IsPlaying { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_dialogGroupMap = new Dictionary<string, DialogGroup>();
|
||||
foreach (var g in _dialogGroups)
|
||||
_dialogGroupMap[g.DialogGroupName] = g;
|
||||
|
||||
_animator = GetComponentInChildren<Animator>();
|
||||
|
||||
if (_animator != null)
|
||||
{
|
||||
_initialGestureHash = _animator.GetCurrentAnimatorStateInfo(0).fullPathHash;
|
||||
if (_animator.layerCount > 1)
|
||||
{
|
||||
_initialExpressionHash = _animator.GetCurrentAnimatorStateInfo(1).fullPathHash;
|
||||
_hasInitialExpression = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Play()
|
||||
{
|
||||
if(_dialogGroups.Count > 0)
|
||||
_ = Play(_dialogGroups[0].DialogGroupName);
|
||||
|
||||
}
|
||||
|
||||
public async Awaitable Play(string groupName)
|
||||
{
|
||||
if (IsPlaying) return;
|
||||
if (!_dialogGroupMap.TryGetValue(groupName, out var group))
|
||||
{
|
||||
Debug.LogWarning($"[DialogPlayer] 그룹 없음: {groupName}");
|
||||
return;
|
||||
}
|
||||
|
||||
IsPlaying = true;
|
||||
try
|
||||
{
|
||||
var node = group.StartNode;
|
||||
while (node != null)
|
||||
{
|
||||
await PlayNode(node);
|
||||
|
||||
if (node.Choices != null && node.Choices.Count > 0)
|
||||
{
|
||||
int picked = await WaitForChoice(node);
|
||||
node = node.Choices[picked].DestinationNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
node = node.Next;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsPlaying = false;
|
||||
RestoreDefaultAnimations();
|
||||
RestoreRotations();
|
||||
}
|
||||
|
||||
Debug.Log("[DialogPlayer] 대화 종료");
|
||||
}
|
||||
|
||||
private void RestoreDefaultAnimations()
|
||||
{
|
||||
if (_animator == null) return;
|
||||
_animator.CrossFade(_initialGestureHash, 0.3f, 0, normalizedTimeOffset: 0f);
|
||||
if (_hasInitialExpression)
|
||||
_animator.CrossFade(_initialExpressionHash, 0.3f, 1, normalizedTimeOffset: 0f);
|
||||
}
|
||||
|
||||
private async Awaitable RotateTowardPlayer(Transform target)
|
||||
{
|
||||
if (Camera.main == null) return;
|
||||
var playerCam = Camera.main.transform;
|
||||
|
||||
float duration = 0.5f;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
Vector3 dir = playerCam.position - target.position;
|
||||
dir.y = 0f;
|
||||
if (dir.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
var targetRot = Quaternion.LookRotation(dir);
|
||||
target.rotation = Quaternion.Slerp(target.rotation, targetRot, 10f * Time.deltaTime);
|
||||
}
|
||||
elapsed += Time.deltaTime;
|
||||
await Awaitable.NextFrameAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreRotations()
|
||||
{
|
||||
foreach (var kvp in _originalRotations)
|
||||
{
|
||||
if (kvp.Key != null)
|
||||
_ = RotateToRotation(kvp.Key, kvp.Value);
|
||||
}
|
||||
_originalRotations.Clear();
|
||||
}
|
||||
|
||||
private async Awaitable RotateToRotation(Transform target, Quaternion targetRotation)
|
||||
{
|
||||
float duration = 0.5f;
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
if (target == null) return;
|
||||
target.rotation = Quaternion.Slerp(target.rotation, targetRotation, 10f * Time.deltaTime);
|
||||
elapsed += Time.deltaTime;
|
||||
await Awaitable.NextFrameAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Awaitable PlayNode(DialogNode node)
|
||||
{
|
||||
var speakerName = node.Speaker != null ? node.Speaker.Name : "(누구?)";
|
||||
Debug.Log($"[{speakerName}] {node.TalkText}");
|
||||
|
||||
// 보이스 재생
|
||||
if (node.Voice != null && node.Speaker != null)
|
||||
{
|
||||
var voiceObj = CharacterVoiceObject.Find(node.Speaker);
|
||||
if (voiceObj != null && node.Voice.Clip != null)
|
||||
voiceObj.Play(node.Voice.Clip);
|
||||
}
|
||||
|
||||
// 플레이어 향해 회전
|
||||
if (node.LookAtPlayer && node.Speaker != null)
|
||||
{
|
||||
var voiceObj = CharacterVoiceObject.Find(node.Speaker);
|
||||
if (voiceObj != null)
|
||||
{
|
||||
_originalRotations.TryAdd(voiceObj.transform, voiceObj.transform.rotation);
|
||||
_ = RotateTowardPlayer(voiceObj.transform);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.Gesture != null)
|
||||
_animator.CrossFade(node.Gesture.StateName, node.Gesture.CrossFadeDuration, node.Gesture.AnimationLayer);
|
||||
if (node.Expression != null)
|
||||
_animator.CrossFade(node.Expression.StateName, node.Expression.CrossFadeDuration, node.Expression.AnimationLayer);
|
||||
|
||||
// 대기 시간 결정
|
||||
float wait = 0f;
|
||||
if (node.Voice != null && node.Voice.Clip != null)
|
||||
wait = node.Voice.Clip.length;
|
||||
else
|
||||
wait = node.LineDuration;
|
||||
|
||||
if (wait > 0f)
|
||||
await Awaitable.WaitForSecondsAsync(wait);
|
||||
else
|
||||
await WaitForAdvanceInput(); // 수동 진행
|
||||
}
|
||||
|
||||
private async Awaitable<int> WaitForChoice(DialogNode node)
|
||||
{
|
||||
//선택을 기다리는 함수 수정해서 사용할것
|
||||
/*
|
||||
if (ChoiceHud.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[DialogPlayer] ChoiceHud 없음 — 0번 자동 선택");
|
||||
return 0;
|
||||
}
|
||||
return await ChoiceHud.Instance.Show(node.Speaker, node.ChoiceQuestion ,node.Choices);
|
||||
*/
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async Awaitable WaitForAdvanceInput()
|
||||
{
|
||||
// TODO: VR 컨트롤러 버튼 입력 대기. 일단은 1초 대기
|
||||
await Awaitable.WaitForSecondsAsync(1f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9838e0e0a3edefa4e92ddbb98aaa3ce5
|
||||
8
Assets/02_Scripts/Communication/Voice.meta
Normal file
8
Assets/02_Scripts/Communication/Voice.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98959040e708aa04891672a6c21a9479
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class CharacterVoiceObject : MonoBehaviour
|
||||
{
|
||||
public CharacterData Character;
|
||||
public AudioSource VoiceSource;
|
||||
|
||||
private static readonly Dictionary<CharacterData, CharacterVoiceObject> _registry = new();
|
||||
|
||||
private void OnEnable() => _registry[Character] = this;
|
||||
private void OnDisable() => _registry.Remove(Character);
|
||||
|
||||
public static CharacterVoiceObject Find(CharacterData data)
|
||||
=> _registry.TryGetValue(data, out var obj) ? obj : null;
|
||||
|
||||
public void Play(AudioClip clip) => VoiceSource.PlayOneShot(clip);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa8a1a66f46e7ef4f99770437e10ae34
|
||||
109
Assets/02_Scripts/Communication/Voice/LipSync.cs
Normal file
109
Assets/02_Scripts/Communication/Voice/LipSync.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
// 보이스 진폭에 따라 입 관련 블렌드셰이프 그룹의 weight를 직접 제어
|
||||
// LateUpdate에서 갱신해 Animator가 같은 프레임에 0으로 세팅한 값을 덮어씀
|
||||
[RequireComponent(typeof(CharacterVoiceObject))]
|
||||
public class LipSync : MonoBehaviour
|
||||
{
|
||||
[Serializable]
|
||||
private struct LipShape
|
||||
{
|
||||
public string Name;
|
||||
[Range(0f, 100f)] public float MaxWeight; // amplitude=1일 때 도달할 weight
|
||||
}
|
||||
|
||||
[Header("Refs")]
|
||||
[SerializeField] private SkinnedMeshRenderer _meshRenderer;
|
||||
|
||||
// BMAC_OpenMouse_Big 클립의 입 관련 셰이프 프리셋
|
||||
[Header("Mouth Preset (입 최대 시 weight)")]
|
||||
[SerializeField] private LipShape[] _shapes =
|
||||
{
|
||||
new() { Name = "Expression_SurpriesedMouth", MaxWeight = 50f },
|
||||
new() { Name = "Expression_MouthSad_L", MaxWeight = 10f },
|
||||
new() { Name = "Expression_MouthSad_R", MaxWeight = 10f },
|
||||
new() { Name = "Expression_MouthWide_L", MaxWeight = 30f },
|
||||
new() { Name = "Expression_MouthWide_R", MaxWeight = 30f },
|
||||
new() { Name = "Expression_LipsOh", MaxWeight = 100f },
|
||||
new() { Name = "Expression_LipsO", MaxWeight = 5f },
|
||||
};
|
||||
|
||||
[Header("Tuning")]
|
||||
[SerializeField, Range(0f, 20f)] private float _amplitudeScale = 6f; // RMS → 0~1 매핑 배수
|
||||
[SerializeField, Range(0f, 0.05f)] private float _noiseFloor = 0.005f;
|
||||
[SerializeField, Range(0f, 30f)] private float _smoothingSpeed = 15f;
|
||||
[SerializeField] private int _sampleSize = 256;
|
||||
|
||||
private AudioSource _audioSource;
|
||||
private int[] _indices;
|
||||
private float[] _sampleBuffer;
|
||||
private float _currentAmplitude;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
var voiceObj = GetComponent<CharacterVoiceObject>();
|
||||
_audioSource = voiceObj != null ? voiceObj.VoiceSource : null;
|
||||
|
||||
// 메시 자동 탐색 — 첫 번째 셰이프 이름을 가진 SkinnedMeshRenderer 사용
|
||||
if (_meshRenderer == null && _shapes.Length > 0)
|
||||
{
|
||||
string probe = _shapes[0].Name;
|
||||
foreach (var smr in GetComponentsInChildren<SkinnedMeshRenderer>(true))
|
||||
{
|
||||
if (smr.sharedMesh != null && smr.sharedMesh.GetBlendShapeIndex(probe) >= 0)
|
||||
{
|
||||
_meshRenderer = smr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 인덱스 캐시
|
||||
_indices = new int[_shapes.Length];
|
||||
if (_meshRenderer != null && _meshRenderer.sharedMesh != null)
|
||||
{
|
||||
var mesh = _meshRenderer.sharedMesh;
|
||||
for (int i = 0; i < _shapes.Length; i++)
|
||||
{
|
||||
_indices[i] = mesh.GetBlendShapeIndex(_shapes[i].Name);
|
||||
if (_indices[i] < 0)
|
||||
Debug.LogWarning($"[LipSync] 블렌드셰이프 없음: {_shapes[i].Name}", this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < _indices.Length; i++) _indices[i] = -1;
|
||||
}
|
||||
|
||||
if (_audioSource == null)
|
||||
Debug.LogWarning("[LipSync] CharacterVoiceObject.VoiceSource 미할당", this);
|
||||
|
||||
_sampleBuffer = new float[_sampleSize];
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_audioSource == null || _meshRenderer == null) return;
|
||||
|
||||
// PlayOneShot도 잡히도록 항상 샘플링 — 무음은 노이즈 플로어로 컷
|
||||
_audioSource.GetOutputData(_sampleBuffer, 0);
|
||||
|
||||
float sumSq = 0f;
|
||||
for (int i = 0; i < _sampleBuffer.Length; i++)
|
||||
sumSq += _sampleBuffer[i] * _sampleBuffer[i];
|
||||
|
||||
float rms = Mathf.Sqrt(sumSq / _sampleBuffer.Length);
|
||||
rms = Mathf.Max(0f, rms - _noiseFloor);
|
||||
float target = Mathf.Clamp01(rms * _amplitudeScale);
|
||||
|
||||
_currentAmplitude = Mathf.Lerp(_currentAmplitude, target, Time.deltaTime * _smoothingSpeed);
|
||||
|
||||
// Animator가 같은 프레임에 0으로 덮은 값을 LateUpdate에서 다시 씌움
|
||||
for (int i = 0; i < _shapes.Length; i++)
|
||||
{
|
||||
if (_indices[i] < 0) continue;
|
||||
_meshRenderer.SetBlendShapeWeight(_indices[i], _currentAmplitude * _shapes[i].MaxWeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Communication/Voice/LipSync.cs.meta
Normal file
2
Assets/02_Scripts/Communication/Voice/LipSync.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae0d9abbe32fe5b4e8e4886143e1e5e2
|
||||
8
Assets/02_Scripts/Data.meta
Normal file
8
Assets/02_Scripts/Data.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f15371153c317454585018e424b7df5f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/02_Scripts/Data/Character.meta
Normal file
8
Assets/02_Scripts/Data/Character.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7bdce60fa4722c409858d6a5a234ec3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Assets/02_Scripts/Data/Character/CharacterData.cs
Normal file
9
Assets/02_Scripts/Data/Character/CharacterData.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Character/CharacterData")]
|
||||
public class CharacterData : ScriptableObject
|
||||
{
|
||||
public string Id;
|
||||
public string Name;
|
||||
public Sprite Portrait;
|
||||
}
|
||||
2
Assets/02_Scripts/Data/Character/CharacterData.cs.meta
Normal file
2
Assets/02_Scripts/Data/Character/CharacterData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c44a4226e5f3e344a96eac21f829952f
|
||||
8
Assets/02_Scripts/Data/Communication.meta
Normal file
8
Assets/02_Scripts/Data/Communication.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f31eda293b7f19046b229ac17d081f81
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/02_Scripts/Data/Communication/Dialog.meta
Normal file
8
Assets/02_Scripts/Data/Communication/Dialog.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 864a90870193471458445ecc75905891
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Communication/Expression")]
|
||||
public class ExpressionData : ScriptableObject
|
||||
{
|
||||
[HideInInspector] public int AnimationLayer = 1;
|
||||
public string StateName;
|
||||
public float CrossFadeDuration = 0.2f;
|
||||
public AnimationClip AnimClip;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b705522cfca24d141b3d560aeffb2c14
|
||||
10
Assets/02_Scripts/Data/Communication/Dialog/GestureData.cs
Normal file
10
Assets/02_Scripts/Data/Communication/Dialog/GestureData.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Communication/Gesture")]
|
||||
public class GestureData : ScriptableObject
|
||||
{
|
||||
[HideInInspector] public int AnimationLayer = 0;
|
||||
public string StateName;
|
||||
public float CrossFadeDuration = 0.2f;
|
||||
public AnimationClip AnimClip;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7287df3d52d586a4085d412938dae461
|
||||
8
Assets/02_Scripts/Data/Communication/Voice.meta
Normal file
8
Assets/02_Scripts/Data/Communication/Voice.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5b1fe96389ab554db24396abacdc278
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/02_Scripts/Data/Communication/Voice/VoiceClip.cs
Normal file
8
Assets/02_Scripts/Data/Communication/Voice/VoiceClip.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Communication/VoiceClip")]
|
||||
public class VoiceClip : ScriptableObject
|
||||
{
|
||||
public string VoiceCode; //해당 음성 고유코드
|
||||
public AudioClip Clip;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc306ebaeba77b94dbb631e9fdc0e864
|
||||
@@ -1 +0,0 @@
|
||||
지울것
|
||||
Reference in New Issue
Block a user