2026-07-05 대화시스템 수정
This commit is contained in:
11
Assets/02_Scripts/Communication/Dialog/AffectionModifier.cs
Normal file
11
Assets/02_Scripts/Communication/Dialog/AffectionModifier.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using UnityEngine;
|
||||
|
||||
// 인스펙터 이벤트에서 호감도를 조작하기 위한 헬퍼.
|
||||
// 예: 선택지 뒤 노드에 EventKey를 걸고 → DialogPlayer의 NodeEvent →
|
||||
// 이 컴포넌트의 Add(5)를 연결하면 그 선택을 했을 때 호감도가 오른다.
|
||||
public class AffectionModifier : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private CharacterData _character;
|
||||
|
||||
public void Add(int delta) => StoryState.AddAffection(_character, delta);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04fdb10aa14325a4c8767f7d3cce49de
|
||||
41
Assets/02_Scripts/Communication/Dialog/DialogCondition.cs
Normal file
41
Assets/02_Scripts/Communication/Dialog/DialogCondition.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// DialogGroup 하나가 활성화되기 위한 조건 묶음. 모든 항목을 만족해야 한다(AND).
|
||||
// 값이 0이거나 목록이 비어있으면 그 항목은 조건 없음으로 통과된다.
|
||||
[Serializable]
|
||||
public class DialogCondition
|
||||
{
|
||||
[Tooltip("메인 진행도가 이 값 이상이어야 함 (0 = 조건 없음)")]
|
||||
[Min(0)] public int MinMainProgress;
|
||||
|
||||
[Tooltip("이 NPC의 호감도가 이 값 이상이어야 함 (0 = 조건 없음)")]
|
||||
[Min(0)] public int MinAffection;
|
||||
|
||||
[Tooltip("먼저 완료했어야 하는 대화들 (전부 완료 필요)")]
|
||||
public List<DialogGroup> RequiredDialogs = new();
|
||||
|
||||
[Tooltip("골랐어야 하는 선택지 Code들 (전부 필요)")]
|
||||
public List<string> RequiredChoiceCodes = new();
|
||||
|
||||
// affectionTarget: 호감도 조건을 검사할 캐릭터 (보통 대화를 거는 NPC 자신)
|
||||
public bool IsMet(CharacterData affectionTarget)
|
||||
{
|
||||
if (StoryState.MainProgress < MinMainProgress)
|
||||
return false;
|
||||
|
||||
if (MinAffection > 0 && StoryState.GetAffection(affectionTarget) < MinAffection)
|
||||
return false;
|
||||
|
||||
foreach (var group in RequiredDialogs)
|
||||
if (group != null && !StoryState.IsDialogCompleted(group.name))
|
||||
return false;
|
||||
|
||||
foreach (var code in RequiredChoiceCodes)
|
||||
if (!string.IsNullOrEmpty(code) && !StoryState.HasChosen(code))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f307619d54e9a74990a68cda19bff5b
|
||||
@@ -7,30 +7,31 @@
|
||||
[RequireComponent(typeof(CharacterVoiceObject))]
|
||||
public class DialogPlayer : MonoBehaviour
|
||||
{
|
||||
// 대화 후보 하나. 리스트에서 위에 있을수록 우선순위가 높다.
|
||||
// (스토리 대화를 위에, 조건 없는 기본 잡담을 맨 아래에 두는 것을 권장)
|
||||
[System.Serializable]
|
||||
public struct RegionGroup
|
||||
public struct DialogEntry
|
||||
{
|
||||
public string Region; // 영역 이름 (NPC마다 자유롭게 지정 — 그룹 이름과 무관)
|
||||
public DialogGroup Group;
|
||||
public DialogCondition Condition;
|
||||
|
||||
[Tooltip("켜면 완료 후에도 반복 재생 가능(잡담용). 끄면 1회성(스토리 대화)")]
|
||||
public bool Repeatable;
|
||||
|
||||
[Tooltip("이 대화를 처음 완료하면 메인 진행도 +N (필수 대화가 아니면 0)")]
|
||||
[Min(0)] public int ProgressOnComplete;
|
||||
}
|
||||
|
||||
// 마지막 선택지 코드(LastChoiceCode)를 인자로 넘기는 UnityEvent (인스펙터 노출용 구체 타입)
|
||||
[System.Serializable]
|
||||
public class ChoiceCodeEvent : UnityEvent<string> { }
|
||||
|
||||
// 노드의 EventKey ↔ 그 노드 재생 시 호출할 이벤트. 인자로 LastChoiceCode가 전달됨.
|
||||
// 노드의 EventKey ↔ 그 노드 재생 시 호출할 이벤트.
|
||||
[System.Serializable]
|
||||
public struct NodeEvent
|
||||
{
|
||||
public string Key;
|
||||
public ChoiceCodeEvent Event;
|
||||
public UnityEvent Event;
|
||||
}
|
||||
|
||||
[Tooltip("영역 이름 ↔ 그 영역에서 재생할 DialogGroup")]
|
||||
[SerializeField] private List<RegionGroup> _regionGroups;
|
||||
|
||||
[Header("Region")]
|
||||
[SerializeField] private string _currentRegion; // 현재 영역 이름. DialogRegion 트리거가 갱신
|
||||
[Tooltip("이 NPC의 대화 후보들. 위에서부터 조건을 검사해 첫 번째로 만족하는 대화를 재생")]
|
||||
[SerializeField] private List<DialogEntry> _dialogs = new();
|
||||
|
||||
[Header("Dialog HUD Placement")] // 씬에서 캐릭터 위치/주변(벽 등)에 맞춰 조절
|
||||
[SerializeField] private float _hudChestHeight = 1.2f; // 화자 발 기준 가슴 높이
|
||||
@@ -41,7 +42,7 @@ public struct NodeEvent
|
||||
[Tooltip("노드의 Event Key와 같은 Key가 그 노드 재생 시 호출됨")]
|
||||
[SerializeField] private List<NodeEvent> _nodeEvents = new();
|
||||
|
||||
private Dictionary<string, DialogGroup> _regionMap;
|
||||
private CharacterVoiceObject _voice; // 이 NPC의 캐릭터 정보 (호감도 조건 대상)
|
||||
private Animator _animator;
|
||||
private int _initialGestureHash;
|
||||
private int _initialExpressionHash;
|
||||
@@ -49,17 +50,9 @@ public struct NodeEvent
|
||||
private readonly Dictionary<Transform, Quaternion> _originalRotations = new();
|
||||
public bool IsPlaying { get; private set; }
|
||||
|
||||
// 마지막으로 고른 선택지 (인덱스/코드). DialogVariables에도 lastChoiceIndex / lastChoiceCode 로 저장됨
|
||||
public int LastChoiceIndex { get; private set; } = -1;
|
||||
public string LastChoiceCode { get; private set; }
|
||||
public event Action<int, string> OnChoiceSelected; // (index, code)
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_regionMap = new Dictionary<string, DialogGroup>();
|
||||
foreach (var e in _regionGroups)
|
||||
if (e.Group != null) _regionMap[e.Region] = e.Group;
|
||||
|
||||
_voice = GetComponent<CharacterVoiceObject>();
|
||||
_animator = GetComponentInChildren<Animator>();
|
||||
|
||||
if (_animator != null)
|
||||
@@ -74,38 +67,38 @@ private void Awake()
|
||||
}
|
||||
|
||||
public async Awaitable Play()
|
||||
{
|
||||
var region = ResolveRegion();
|
||||
if (region != null)
|
||||
await Play(region);
|
||||
}
|
||||
|
||||
// 현재 영역. 영역이 없거나 매칭 그룹이 없으면 리스트 첫 항목으로 폴백.
|
||||
private string ResolveRegion()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_currentRegion) && _regionMap.ContainsKey(_currentRegion))
|
||||
return _currentRegion;
|
||||
return _regionGroups.Count > 0 ? _regionGroups[0].Region : null;
|
||||
}
|
||||
|
||||
// 영역 전환 (DialogRegion 트리거가 호출). 다음 Play()부터 해당 영역 대화가 재생됨.
|
||||
public void SetRegion(string region) => _currentRegion = region;
|
||||
|
||||
public string CurrentRegion => _currentRegion;
|
||||
|
||||
public async Awaitable Play(string region)
|
||||
{
|
||||
if (IsPlaying) return;
|
||||
if (!_regionMap.TryGetValue(region, out var group))
|
||||
|
||||
int index = FindPlayableIndex();
|
||||
if (index < 0)
|
||||
{
|
||||
Debug.LogWarning($"[DialogPlayer] 영역 대화 없음: {region}");
|
||||
Debug.Log($"[DialogPlayer] 조건에 맞는 대화가 없음: {name}");
|
||||
return;
|
||||
}
|
||||
await PlayEntry(_dialogs[index]);
|
||||
}
|
||||
|
||||
// 리스트 순서 = 우선순위. 조건을 만족하고 (반복 가능하거나 아직 안 한) 첫 대화.
|
||||
private int FindPlayableIndex()
|
||||
{
|
||||
for (int i = 0; i < _dialogs.Count; i++)
|
||||
{
|
||||
var entry = _dialogs[i];
|
||||
if (entry.Group == null) continue;
|
||||
if (!entry.Repeatable && StoryState.IsDialogCompleted(entry.Group.name)) continue;
|
||||
if (entry.Condition != null && !entry.Condition.IsMet(_voice.Character)) continue;
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private async Awaitable PlayEntry(DialogEntry entry)
|
||||
{
|
||||
IsPlaying = true;
|
||||
try
|
||||
{
|
||||
var node = group.StartNode;
|
||||
var node = entry.Group.StartNode;
|
||||
while (node != null)
|
||||
{
|
||||
await PlayNode(node);
|
||||
@@ -121,6 +114,15 @@ public async Awaitable Play(string region)
|
||||
node = node.Next;
|
||||
}
|
||||
}
|
||||
|
||||
// 여기까지 왔으면 자연 종료(끝까지 재생) — 이때만 완료로 기록한다.
|
||||
// (중간에 오브젝트 파괴 등으로 끊기면 예외로 빠져나가 기록되지 않음)
|
||||
bool firstTime = StoryState.MarkDialogCompleted(entry.Group.name);
|
||||
if (firstTime && entry.ProgressOnComplete > 0)
|
||||
StoryState.MainProgress += entry.ProgressOnComplete;
|
||||
StoryState.Save();
|
||||
|
||||
Debug.Log($"[DialogPlayer] 대화 종료: {entry.Group.name}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -130,8 +132,6 @@ public async Awaitable Play(string region)
|
||||
RestoreDefaultAnimations();
|
||||
RestoreRotations();
|
||||
}
|
||||
|
||||
Debug.Log("[DialogPlayer] 대화 종료");
|
||||
}
|
||||
|
||||
private void RestoreDefaultAnimations()
|
||||
@@ -242,37 +242,31 @@ private void RaiseNodeEvent(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
foreach (var e in _nodeEvents)
|
||||
if (e.Key == key) e.Event?.Invoke(LastChoiceCode); // 마지막 선택지 코드를 인자로 전달
|
||||
if (e.Key == key) e.Event?.Invoke();
|
||||
}
|
||||
|
||||
// 선택 결과 기록: 인덱스/코드를 프로퍼티 + DialogVariables에 저장하고 이벤트 발행
|
||||
// 선택 기록: 선택지 Code를 StoryState에 영구 기록 (대화 활성화 조건 판정에 사용)
|
||||
private void RecordChoice(DialogNode node, int index)
|
||||
{
|
||||
string code = (node.Choices != null && index >= 0 && index < node.Choices.Count)
|
||||
? node.Choices[index].Code : null;
|
||||
code = DialogVariables.Format(code); // {token} 치환 → 동적으로 생성된 코드 반영
|
||||
|
||||
LastChoiceIndex = index;
|
||||
LastChoiceCode = code;
|
||||
|
||||
DialogVariables.Set("lastChoiceIndex", index.ToString());
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
DialogVariables.Set("lastChoiceCode", code);
|
||||
|
||||
OnChoiceSelected?.Invoke(index, code);
|
||||
StoryState.RecordChoice(code);
|
||||
}
|
||||
|
||||
private async Awaitable<int> WaitForChoice(DialogNode node)
|
||||
{
|
||||
//선택을 기다리는 함수 수정해서 사용할것
|
||||
|
||||
|
||||
if (ChoiceHud.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[DialogPlayer] ChoiceHud 없음 — 0번 자동 선택");
|
||||
return 0;
|
||||
}
|
||||
return await ChoiceHud.Instance.Show(node.ChoiceQuestion, node.Choices);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 대화 진행 입력(OnDialogNext = VR B버튼) 한 번을 대기
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
// 영역 트리거. 이 콜라이더(isTrigger) 안으로 NPC(DialogPlayer 보유)가 들어오면
|
||||
// 그 NPC의 현재 영역을 _regionKey로 전환한다.
|
||||
// _regionKey는 해당 영역에서 재생할 DialogGroup의 이름과 일치해야 한다 (예: "Coast", "Hill").
|
||||
//
|
||||
// 주의: OnTriggerEnter가 동작하려면 들어오는 쪽(또는 트리거 쪽)에 Rigidbody가 있어야 하고,
|
||||
// 이 오브젝트의 Collider는 Is Trigger여야 한다.
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class DialogRegion : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string _regionKey; // DialogGroup 이름과 일치
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
// 컴포넌트 추가 시 편의상 트리거로 설정
|
||||
var col = GetComponent<Collider>();
|
||||
if (col != null) col.isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
var player = other.GetComponentInParent<DialogPlayer>();
|
||||
if (player != null)
|
||||
player.SetRegion(_regionKey);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fb6dce8c4da85e478d7da770f057cf3
|
||||
@@ -22,7 +22,7 @@ public override void OnImportAsset(AssetImportContext ctx)
|
||||
return;
|
||||
}
|
||||
|
||||
// 메인 에셋: DialogGroup (이름은 파일명 기준 — DialogPlayer가 이름으로 조회)
|
||||
// 메인 에셋: DialogGroup (이름은 파일명 기준 — StoryState 대화 이력의 키로 쓰임)
|
||||
var groupName = Path.GetFileNameWithoutExtension(ctx.assetPath);
|
||||
var group = ScriptableObject.CreateInstance<DialogGroup>();
|
||||
group.name = groupName;
|
||||
|
||||
Reference in New Issue
Block a user