글로벌 오브젝트 추가
This commit is contained in:
@@ -1,9 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// 노드의 역할. 서로 배타적이므로 플래그 여러 개가 아니라 하나의 종류로 둔다.
|
||||
public enum DialogNodeKind
|
||||
{
|
||||
[InspectorName("대사")] Line, // 평범한 대사 노드
|
||||
[InspectorName("호감도 분기")] AffectionCheck, // 대사 없이 조건만 검사해 즉시 라우팅
|
||||
[InspectorName("종료")] End, // 대화를 끝낸다 (BGM 처리만 정함)
|
||||
}
|
||||
|
||||
[CreateAssetMenu(menuName = "Communication/Dialog Node")]
|
||||
public class DialogNode : ScriptableObject
|
||||
{
|
||||
[Tooltip("이 노드의 역할. 그래프에서 어떤 노드를 썼는지에 따라 임포터가 정한다")]
|
||||
public DialogNodeKind Kind = DialogNodeKind.Line;
|
||||
|
||||
[Header("Speaker")]
|
||||
public CharacterData Speaker;
|
||||
|
||||
@@ -18,7 +29,13 @@ public class DialogNode : ScriptableObject
|
||||
// 진행은 항상 플레이어 입력(OnDialogNext)으로 넘어간다.
|
||||
|
||||
[Header("Presentation")]
|
||||
public AudioClip Bgm; // 있으면 이 대사부터 전용 BGM 재생, 비어있으면 기본 BGM으로 복귀
|
||||
[Tooltip("있으면 이 대사부터 이 BGM으로 갈아탄다. 비우면 변경 없음 — " +
|
||||
"다시 바꾸기 전까지 계속 이어진다")]
|
||||
public AudioClip Bgm;
|
||||
|
||||
[Tooltip("이 대사가 시작될 때 한 번 재생할 효과음 (문 여는 소리, 쿵 소리 등). " +
|
||||
"BGM과 달리 이어지지 않고 1회성이다")]
|
||||
public AudioClip Sfx;
|
||||
|
||||
[Tooltip("이 대사만의 타이핑 연출(속도·글자색·타이핑 사운드). " +
|
||||
"비우면 DialogHud의 기본 스타일을 쓴다")]
|
||||
@@ -36,17 +53,18 @@ public class DialogNode : ScriptableObject
|
||||
public List<HiddenBranch> HiddenBranches = new();
|
||||
|
||||
[Header("Affection Branch")]
|
||||
[Tooltip("켜면 이 노드는 대사 없이 호감도 조건만 검사해 즉시 라우팅한다: " +
|
||||
"조건을 만족하면 AffectionPassBranch로, 아니면 Next로")]
|
||||
public bool AffectionCheck;
|
||||
|
||||
[Tooltip("검사할 호감도 조건들 (캐릭터·연산자·값 + 다음 조건과의 and/or). " +
|
||||
[Tooltip("(Kind = 호감도 분기 전용) 검사할 호감도 조건들 (캐릭터·연산자·값 + 다음 조건과의 and/or). " +
|
||||
"캐릭터를 비우면 대화 주인 NPC. AND가 OR보다 우선. 비어 있으면 무조건 통과")]
|
||||
public List<AffectionRequirement> AffectionRequirements = new();
|
||||
|
||||
[Tooltip("조건을 만족했을 때 갈 노드 (실패하면 Next로)")]
|
||||
public DialogNode AffectionPassBranch;
|
||||
|
||||
[Header("End")]
|
||||
[Tooltip("(Kind = 종료 전용) 켜면 대화가 끝나도 마지막에 재생 중이던 BGM을 유지한다. " +
|
||||
"끄면 장소 BGM으로 복귀. 종료 노드마다 따로 정할 수 있어서 분기별로 다르게 끝낼 수 있다")]
|
||||
public bool KeepBgmOnEnd;
|
||||
|
||||
[Header("ChoiceQuestion")]
|
||||
[TextArea(2,5)] public string ChoiceQuestion;
|
||||
|
||||
|
||||
@@ -185,14 +185,25 @@ private async Awaitable<StoryBeat> SelectBeat(List<StoryBeat> playable)
|
||||
|
||||
private async Awaitable PlayBeat(StoryBeat beat)
|
||||
{
|
||||
// 도달한 종료 노드가 정한다. 중간에 끊기면(씬 전환 등) false로 남아 장소 BGM으로 복귀한다.
|
||||
bool keepBgmOnEnd = false;
|
||||
|
||||
try
|
||||
{
|
||||
var node = beat.Group.StartNode;
|
||||
int routingHops = 0; // 연속 라우팅 횟수 — 라우팅 노드끼리 순환하면 대기 없는 무한 루프가 되므로 차단
|
||||
while (node != null)
|
||||
{
|
||||
// 종료 노드 — 대화를 끝낸다. 분기마다 다른 종료 노드를 둘 수 있어서
|
||||
// "이 루트는 BGM 유지, 저 루트는 복귀"가 각각 정해진다.
|
||||
if (node.Kind == DialogNodeKind.End)
|
||||
{
|
||||
keepBgmOnEnd = node.KeepBgmOnEnd;
|
||||
break;
|
||||
}
|
||||
|
||||
// 호감도 라우팅 노드 — 대사 없이 즉시 분기 (플레이어에겐 분기 자체가 보이지 않는다)
|
||||
if (node.AffectionCheck)
|
||||
if (node.Kind == DialogNodeKind.AffectionCheck)
|
||||
{
|
||||
if (++routingHops > 100)
|
||||
{
|
||||
@@ -254,8 +265,12 @@ private async Awaitable PlayBeat(StoryBeat beat)
|
||||
{
|
||||
if (DialogHud.Instance != null)
|
||||
DialogHud.Instance.Hide();
|
||||
if (SoundManager.Instance != null)
|
||||
SoundManager.Instance.ClearOverrideBGM(); // 대화가 끝나면 기본 BGM으로 복귀
|
||||
|
||||
// 대화 BGM 처리 — 도달한 종료 노드가 Keep BGM이면 마지막 곡을 그대로 유지하고,
|
||||
// 아니면 장소 BGM으로 복귀한다.
|
||||
if (SoundManager.Instance != null && !keepBgmOnEnd)
|
||||
SoundManager.Instance.ClearOverrideBGM();
|
||||
|
||||
RestoreDefaultAnimations();
|
||||
}
|
||||
}
|
||||
@@ -303,14 +318,14 @@ private async Awaitable<bool> PlayNode(DialogNode node)
|
||||
if (node.Progress != 0)
|
||||
StoryManager.Instance.MainProgress += node.Progress;
|
||||
|
||||
// 전용 BGM: 설정돼 있으면 교체, 비어 있으면 기본 BGM으로 복귀
|
||||
if (SoundManager.Instance != null)
|
||||
{
|
||||
if (node.Bgm != null)
|
||||
SoundManager.Instance.PlayOverrideBGM(node.Bgm);
|
||||
else
|
||||
SoundManager.Instance.ClearOverrideBGM();
|
||||
}
|
||||
// BGM: 지정된 노드에서만 갈아타고, 다시 바꾸기 전까지 계속 이어진다.
|
||||
// 비어 있으면 "변경 없음" — 모든 노드에 같은 곡을 넣어줄 필요가 없다.
|
||||
if (node.Bgm != null && SoundManager.Instance != null)
|
||||
SoundManager.Instance.PlayOverrideBGM(node.Bgm);
|
||||
|
||||
// 1회성 효과음 — 이 대사가 뜨는 순간 한 번 재생된다
|
||||
if (node.Sfx != null && SoundManager.Instance != null)
|
||||
SoundManager.Instance.PlaySFX(node.Sfx);
|
||||
|
||||
// 보이스 재생
|
||||
if (node.Voice != null && node.Speaker != null)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
namespace DinoLove.Dialog.GraphTool.Editor
|
||||
{
|
||||
// 호감도 분기 노드. 대사 없이 호감도 조건식만 검사해 두 경로 중 하나로 즉시 라우팅한다.
|
||||
// DialogNode(AffectionCheck=true) 하나로 변환된다.
|
||||
// DialogNode(Kind = AffectionCheck) 하나로 변환된다.
|
||||
//
|
||||
// Condition Count로 조건 줄을 늘린다. 조건 하나는 [Target / 연산자 / 값] 세 줄이고,
|
||||
// 조건 사이마다 [and · or] 연결자 줄이 하나씩 생긴다:
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using Unity.GraphToolkit.Editor;
|
||||
|
||||
namespace DinoLove.Dialog.GraphTool.Editor
|
||||
{
|
||||
// 대화의 종료점. 진행 경로의 끝을 이 노드에 연결하면 "여기서 대화가 끝난다"가 그래프에 드러난다.
|
||||
// Start와 달리 **여러 개 둘 수 있다** — 분기로 갈린 루트마다 종료 노드를 따로 두고
|
||||
// BGM 처리를 다르게 정할 수 있다.
|
||||
//
|
||||
// 대화가 끝날 때의 BGM 처리를 여기서 정한다:
|
||||
// Keep BGM 꺼짐(기본) → 장소 BGM(LocationData.Bgm)으로 복귀
|
||||
// Keep BGM 켜짐 → 마지막에 재생 중이던 BGM을 그대로 유지
|
||||
//
|
||||
// 임포터는 이 노드를 DialogNode(Kind = End)로 변환한다. 대사가 없으므로 재생 시
|
||||
// 아무것도 표시하지 않고 BGM 처리만 정한 뒤 대화를 끝낸다.
|
||||
[Serializable]
|
||||
internal class DialogEndNode : DialogGraphNode
|
||||
{
|
||||
public const string PORT_KEEP_BGM = "KeepBgm";
|
||||
|
||||
protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
{
|
||||
AddExecInput(context);
|
||||
|
||||
// 옵션(AddOption)이 아니라 입력 포트를 쓴다 — 이 프로젝트에서 값 렌더링이
|
||||
// 여러 타입으로 검증된 경로이고, 임포터의 GetInputPortValue로 그대로 읽힌다.
|
||||
context.AddInputPort<bool>(PORT_KEEP_BGM).WithDisplayName("Keep BGM")
|
||||
.WithTooltip("켜면 대화가 끝나도 마지막 BGM을 그대로 유지한다. " +
|
||||
"끄면(기본) 장소 BGM으로 돌아간다").Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96e836626b396b144a706f8e4e8e611e
|
||||
@@ -42,6 +42,11 @@ public override void OnGraphChanged(GraphLogger infos)
|
||||
infos.LogWarning("Start 노드는 하나만 사용됩니다. 가장 먼저 생성된 노드만 적용됩니다.", extra);
|
||||
break;
|
||||
}
|
||||
|
||||
// End 노드도 필수. 단 Start와 달리 여러 개 둘 수 있다 —
|
||||
// 분기로 갈린 루트마다 종료 노드를 따로 두고 BGM 처리를 다르게 정할 수 있다.
|
||||
if (!GetNodes().OfType<DialogEndNode>().Any())
|
||||
infos.LogError("End 노드가 필요합니다. (Dialog End Node를 추가해 마지막 대사에 연결하세요)", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace DinoLove.Dialog.GraphTool.Editor
|
||||
// .dlg 그래프 에셋을 기존 런타임 타입(DialogGroup / DialogNode / DialogChoice)으로 변환한다.
|
||||
// 생성된 DialogNode들은 서브에셋으로, DialogGroup이 메인 에셋으로 등록된다.
|
||||
// 따라서 DialogPlayer는 수정 없이 임포트된 .dlg 에셋(= DialogGroup)을 그대로 사용한다.
|
||||
[ScriptedImporter(17, DialogGraph.AssetExtension)] // 버전 올리면 기존 .dlg 에셋이 재임포트됨
|
||||
[ScriptedImporter(20, DialogGraph.AssetExtension)] // 버전 올리면 기존 .dlg 에셋이 재임포트됨
|
||||
internal class DialogGraphImporter : ScriptedImporter
|
||||
{
|
||||
public override void OnImportAsset(AssetImportContext ctx)
|
||||
@@ -38,7 +38,9 @@ public override void OnImportAsset(AssetImportContext ctx)
|
||||
if (firstGraphNode == null)
|
||||
return; // Start만 있고 연결 없음
|
||||
|
||||
// 1패스: 도달 가능한 모든 라인 노드 → DialogNode 인스턴스 생성 (중복 제거)
|
||||
// 1패스: 도달 가능한 모든 노드 → DialogNode 인스턴스 생성 (중복 제거).
|
||||
// End 노드도 DialogNode가 된다 — 그래야 분기마다 다른 종료 노드를 두고
|
||||
// 각자 다른 BGM 처리를 갖게 할 수 있다 (Next로 자연히 연결되므로 별도 배선이 없다).
|
||||
var map = new Dictionary<INode, DialogNode>();
|
||||
var order = new List<INode>();
|
||||
var queue = new Queue<INode>();
|
||||
@@ -48,7 +50,8 @@ public override void OnImportAsset(AssetImportContext ctx)
|
||||
{
|
||||
var gn = queue.Dequeue();
|
||||
if (gn == null || map.ContainsKey(gn)
|
||||
|| (gn is not DialogLineNode && gn is not DialogAffectionNode))
|
||||
|| (gn is not DialogLineNode && gn is not DialogAffectionNode
|
||||
&& gn is not DialogEndNode))
|
||||
continue;
|
||||
|
||||
var dn = ScriptableObject.CreateInstance<DialogNode>();
|
||||
@@ -75,10 +78,19 @@ public override void OnImportAsset(AssetImportContext ctx)
|
||||
{
|
||||
var dn = map[gn];
|
||||
|
||||
// 종료 노드 — 대사 없이 대화를 끝내며, BGM 처리만 담는다
|
||||
if (gn is DialogEndNode)
|
||||
{
|
||||
dn.Kind = DialogNodeKind.End;
|
||||
dn.KeepBgmOnEnd = GetInputPortValue<bool>(
|
||||
gn.GetInputPortByName(DialogEndNode.PORT_KEEP_BGM));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 호감도 분기 노드 — 대사 없이 라우팅만: 조건 통과 → AffectionPassBranch, 실패 → Next
|
||||
if (gn is DialogAffectionNode affectionNode)
|
||||
{
|
||||
dn.AffectionCheck = true;
|
||||
dn.Kind = DialogNodeKind.AffectionCheck;
|
||||
|
||||
int conditionCount = 1;
|
||||
var countOption = affectionNode.GetNodeOptionByName(DialogAffectionNode.OPTION_CONDITION_COUNT);
|
||||
@@ -117,6 +129,7 @@ public override void OnImportAsset(AssetImportContext ctx)
|
||||
dn.Expression = GetInputPortValue<ExpressionData>(gn.GetInputPortByName(DialogLineNode.PORT_EXPRESSION));
|
||||
dn.Voice = GetInputPortValue<VoiceClip>(gn.GetInputPortByName(DialogLineNode.PORT_VOICE));
|
||||
dn.Bgm = GetInputPortValue<AudioClip>(gn.GetInputPortByName(DialogLineNode.PORT_BGM));
|
||||
dn.Sfx = GetInputPortValue<AudioClip>(gn.GetInputPortByName(DialogLineNode.PORT_SFX));
|
||||
dn.Typewriter = GetInputPortValue<TypewriterStyle>(gn.GetInputPortByName(DialogLineNode.PORT_TYPEWRITER));
|
||||
dn.Affection = GetInputPortValue<int>(gn.GetInputPortByName(DialogLineNode.PORT_AFFECTION));
|
||||
dn.Progress = GetInputPortValue<int>(gn.GetInputPortByName(DialogLineNode.PORT_PROGRESS));
|
||||
|
||||
@@ -21,6 +21,7 @@ internal class DialogLineNode : DialogGraphNode
|
||||
public const string PORT_EXPRESSION = "Expression";
|
||||
public const string PORT_VOICE = "Voice";
|
||||
public const string PORT_BGM = "Bgm";
|
||||
public const string PORT_SFX = "Sfx";
|
||||
public const string PORT_TYPEWRITER = "Typewriter";
|
||||
public const string PORT_AFFECTION = "Affection";
|
||||
public const string PORT_PROGRESS = "Progress";
|
||||
@@ -69,7 +70,11 @@ protected override void OnDefinePorts(IPortDefinitionContext context)
|
||||
context.AddInputPort<ExpressionData>(PORT_EXPRESSION).WithDisplayName("Expression").Build();
|
||||
context.AddInputPort<VoiceClip>(PORT_VOICE).WithDisplayName("Voice").Build();
|
||||
context.AddInputPort<AudioClip>(PORT_BGM).WithDisplayName("BGM")
|
||||
.WithTooltip("있으면 이 대사부터 전용 BGM 재생, 비우면 기본 BGM으로 복귀").Build();
|
||||
.WithTooltip("있으면 이 대사부터 이 BGM으로 갈아탄다. " +
|
||||
"비우면 변경 없음 — 다시 바꾸기 전까지 계속 이어진다").Build();
|
||||
context.AddInputPort<AudioClip>(PORT_SFX).WithDisplayName("SFX")
|
||||
.WithTooltip("이 대사가 시작될 때 한 번 재생할 효과음 (문 여는 소리, 쿵 소리 등). " +
|
||||
"BGM과 달리 이어지지 않고 1회성이다").Build();
|
||||
context.AddInputPort<TypewriterStyle>(PORT_TYPEWRITER).WithDisplayName("Typewriter")
|
||||
.WithTooltip("이 대사만의 타이핑 연출(속도·글자색·타이핑 사운드). " +
|
||||
"비우면 DialogHud의 기본 스타일").Build();
|
||||
|
||||
18
Assets/02_Scripts/GlobalObject.cs
Normal file
18
Assets/02_Scripts/GlobalObject.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class GlobalObject : MonoBehaviour
|
||||
{
|
||||
private static GlobalObject _instance;
|
||||
void Awake()
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/GlobalObject.cs.meta
Normal file
2
Assets/02_Scripts/GlobalObject.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 945cc6d1ec9db6446b7c06f420117c84
|
||||
@@ -55,6 +55,12 @@ public void MoveTo(LocationData location)
|
||||
if (location == null || DialogPlayer.IsAnyActive) return;
|
||||
|
||||
Current = location;
|
||||
|
||||
// 장소 BGM = SoundManager의 기본 BGM 층. 대화용 BGM(Override)이 걷히면 이 곡으로 돌아온다.
|
||||
// 비어 있으면 변경하지 않는다 — 노드 BGM과 같은 규칙(비면 변경 없음)이다.
|
||||
if (location.Bgm != null && SoundManager.Instance != null)
|
||||
SoundManager.Instance.SetDefaultBGM(location.Bgm);
|
||||
|
||||
if (_currentInstance != null) Destroy(_currentInstance);
|
||||
_currentInstance = location.Prefab != null
|
||||
? Instantiate(location.Prefab, _locationRoot)
|
||||
|
||||
@@ -45,7 +45,7 @@ private void Awake()
|
||||
}
|
||||
|
||||
// 씬이 바뀌면 이전 씬에서 남은 전용(Override) BGM을 정리한다 (ISceneInitializable).
|
||||
// 새 씬의 SceneBgm.Start가 SetDefaultBGM으로 그 씬의 곡을 넘겨주면 그대로 재생된다.
|
||||
// 기본 BGM 층은 LocationManager.MoveTo가 장소 입장 시 SetDefaultBGM으로 넘겨준다.
|
||||
public void OnSceneLoaded() => ClearOverrideBGM();
|
||||
|
||||
private void Initialize()
|
||||
|
||||
@@ -11,4 +11,8 @@ public class LocationData : ScriptableObject
|
||||
|
||||
[Tooltip("이 장소의 프리팹 — 배경 + 캐릭터 슬롯(CharacterSlot)들 + 이동 버튼 등")]
|
||||
public GameObject Prefab;
|
||||
|
||||
[Tooltip("이 장소의 기본 BGM. 대화용 BGM이 걷히면 이 곡으로 돌아온다. " +
|
||||
"비우면 변경하지 않는다(이전 곡 유지) — 같은 곡을 쓰는 장소마다 넣지 않아도 된다")]
|
||||
public AudioClip Bgm;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -36,10 +37,38 @@ public sealed class Typewriter
|
||||
// 지금 적용할 스타일 (글자 색 오버라이드 판단용). 연출 없이 표시했으면 null
|
||||
public TypewriterStyle Style => _style;
|
||||
|
||||
// Label.text에 그대로 넣을 문자열. 아직 안 드러난 뒷부분은 투명 처리돼 있다
|
||||
public string Composed => _cursor >= _full.Length
|
||||
? _full
|
||||
: _full.Substring(0, _cursor) + HIDE_TAG + _full.Substring(_cursor);
|
||||
// Label.text에 그대로 넣을 문자열. 아직 안 드러난 뒷부분은 투명 처리돼 있다.
|
||||
//
|
||||
// 뒷부분의 태그마다 <alpha=#00>을 다시 선언하는 게 핵심이다:
|
||||
// <color=...>나 </color>는 색을 (재)지정하면서 알파까지 같이 덮으므로, 앞에 한 번만 걸어둔
|
||||
// <alpha=#00>이 풀려 아직 안 나온 글자가 보여 버린다. 뒷부분은 전부 숨겨야 하는 구간이니
|
||||
// 태그가 끝날 때마다 투명도를 다시 못박는 게 항상 옳다.
|
||||
public string Composed
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_cursor >= _full.Length) return _full;
|
||||
|
||||
var sb = new StringBuilder(_full.Length + 32);
|
||||
sb.Append(_full, 0, _cursor);
|
||||
sb.Append(HIDE_TAG);
|
||||
|
||||
for (int i = _cursor; i < _full.Length; i++)
|
||||
{
|
||||
char c = _full[i];
|
||||
sb.Append(c);
|
||||
if (c != '<') continue;
|
||||
|
||||
int close = _full.IndexOf('>', i + 1);
|
||||
if (close < 0) continue; // 닫히지 않은 '<' — 평범한 글자로 취급
|
||||
|
||||
sb.Append(_full, i + 1, close - i); // 태그 나머지 + '>'
|
||||
sb.Append(HIDE_TAG); // 색이 되돌아갔을 수 있으니 다시 숨긴다
|
||||
i = close;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// 새 텍스트로 타이핑을 시작한다. style이 null이면 연출 없이 즉시 전체 표시.
|
||||
// token은 소유자(MonoBehaviour)의 destroyCancellationToken을 넘길 것.
|
||||
|
||||
Reference in New Issue
Block a user