글로벌 오브젝트 추가
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();
|
||||
|
||||
Reference in New Issue
Block a user