수정 사항
This commit is contained in:
BIN
Assets/01_Scenes/Chapter1.unity
LFS
BIN
Assets/01_Scenes/Chapter1.unity
LFS
Binary file not shown.
@@ -42,6 +42,11 @@ public class DialogNode : ScriptableObject
|
|||||||
public TypewriterStyle Typewriter;
|
public TypewriterStyle Typewriter;
|
||||||
|
|
||||||
[Header("Stage")]
|
[Header("Stage")]
|
||||||
|
[Tooltip("이 대사 동안 배경 앞·캐릭터 뒤에 띄울 장면 그림 (설명용 컷인, 증언 장면 등). " +
|
||||||
|
"비우면 사라진다 — 여러 대사에 걸쳐 보이려면 그 대사마다 같은 그림을 지정할 것. " +
|
||||||
|
"캐릭터보다 앞에 보여야 하면 Pose를 '숨김'으로 내리면 된다")]
|
||||||
|
public Sprite SceneScreen;
|
||||||
|
|
||||||
[Tooltip("이 대사부터 바꿀 포즈. '변경 없음'이 기본이라 인물이 서 있는 동안 매 대사에 " +
|
[Tooltip("이 대사부터 바꿀 포즈. '변경 없음'이 기본이라 인물이 서 있는 동안 매 대사에 " +
|
||||||
"다시 꽂을 필요가 없고, '숨김'을 고르면 화면에서 내린다. " +
|
"다시 꽂을 필요가 없고, '숨김'을 고르면 화면에서 내린다. " +
|
||||||
"대화가 끝나면 대화 전 포즈로 되돌아간다")]
|
"대화가 끝나면 대화 전 포즈로 되돌아간다")]
|
||||||
|
|||||||
@@ -294,7 +294,8 @@ private async Awaitable PlayBeat(StoryBeat beat)
|
|||||||
if (SoundManager.Instance != null && !keepBgmOnEnd)
|
if (SoundManager.Instance != null && !keepBgmOnEnd)
|
||||||
SoundManager.Instance.ClearOverrideBGM();
|
SoundManager.Instance.ClearOverrideBGM();
|
||||||
|
|
||||||
// 앞/옆 그림은 대화 전용 오버레이라 대화가 끝나면 무조건 걷는다
|
// 장면 그림과 앞/옆 그림은 대화 전용이라 대화가 끝나면 무조건 걷는다
|
||||||
|
if (SceneScreen.Instance != null) SceneScreen.Instance.Hide();
|
||||||
if (FrontSprite.Instance != null) FrontSprite.Instance.Hide();
|
if (FrontSprite.Instance != null) FrontSprite.Instance.Hide();
|
||||||
if (SideSprite.Instance != null) SideSprite.Instance.Hide();
|
if (SideSprite.Instance != null) SideSprite.Instance.Hide();
|
||||||
|
|
||||||
@@ -393,13 +394,15 @@ private async Awaitable<bool> PlayNode(DialogNode node)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 노드가 지정한 무대 연출(포즈 + 앞/옆 그림)을 반영한다. 셋의 수명 규칙이 각각 다르다:
|
// 노드가 지정한 무대 연출을 반영한다. 넷의 수명 규칙이 각각 다르다:
|
||||||
// Pose — 지속. '변경 없음'이 기본이라 인물이 서 있는 동안 다시 꽂을 필요가 없다
|
// Pose — 지속. '변경 없음'이 기본이라 인물이 서 있는 동안 다시 꽂을 필요가 없다
|
||||||
|
// SceneScreen — 지정한 대사에서만. 비우면 사라진다 (배경 앞·캐릭터 뒤 장면 그림)
|
||||||
// FrontSprite — 1회성. 지정 안 한 노드에선 사라진다 ("이의있음!" 같은 번쩍 연출)
|
// FrontSprite — 1회성. 지정 안 한 노드에선 사라진다 ("이의있음!" 같은 번쩍 연출)
|
||||||
// SideSprite — 지정한 대사에서만. 비우면 사라진다
|
// SideSprite — 지정한 대사에서만. 비우면 사라진다
|
||||||
// 뒤의 둘은 Show(null)이 곧 Hide라 그대로 넘기면 두 경우가 한 줄로 처리된다.
|
// Pose 말고 셋은 Show(null)이 곧 Hide라, 노드 값을 그대로 넘기면 두 경우가 한 줄로 처리된다.
|
||||||
private void ApplyStageVisuals(DialogNode node)
|
private void ApplyStageVisuals(DialogNode node)
|
||||||
{
|
{
|
||||||
|
if (SceneScreen.Instance != null) SceneScreen.Instance.Show(node.SceneScreen);
|
||||||
if (FrontSprite.Instance != null) FrontSprite.Instance.Show(node.FrontSprite);
|
if (FrontSprite.Instance != null) FrontSprite.Instance.Show(node.FrontSprite);
|
||||||
if (SideSprite.Instance != null) SideSprite.Instance.Show(node.SideSprite);
|
if (SideSprite.Instance != null) SideSprite.Instance.Show(node.SideSprite);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace DinoLove.Dialog.GraphTool.Editor
|
|||||||
// .dlg 그래프 에셋을 기존 런타임 타입(DialogGroup / DialogNode / DialogChoice)으로 변환한다.
|
// .dlg 그래프 에셋을 기존 런타임 타입(DialogGroup / DialogNode / DialogChoice)으로 변환한다.
|
||||||
// 생성된 DialogNode들은 서브에셋으로, DialogGroup이 메인 에셋으로 등록된다.
|
// 생성된 DialogNode들은 서브에셋으로, DialogGroup이 메인 에셋으로 등록된다.
|
||||||
// 따라서 DialogPlayer는 수정 없이 임포트된 .dlg 에셋(= DialogGroup)을 그대로 사용한다.
|
// 따라서 DialogPlayer는 수정 없이 임포트된 .dlg 에셋(= DialogGroup)을 그대로 사용한다.
|
||||||
[ScriptedImporter(24, DialogGraph.AssetExtension)] // 버전 올리면 기존 .dlg 에셋이 재임포트됨
|
[ScriptedImporter(25, DialogGraph.AssetExtension)] // 버전 올리면 기존 .dlg 에셋이 재임포트됨
|
||||||
internal class DialogGraphImporter : ScriptedImporter
|
internal class DialogGraphImporter : ScriptedImporter
|
||||||
{
|
{
|
||||||
public override void OnImportAsset(AssetImportContext ctx)
|
public override void OnImportAsset(AssetImportContext ctx)
|
||||||
@@ -127,6 +127,7 @@ public override void OnImportAsset(AssetImportContext ctx)
|
|||||||
dn.TalkText = GetInputPortValue<DialogText>(gn.GetInputPortByName(DialogLineNode.PORT_TALK)).Value;
|
dn.TalkText = GetInputPortValue<DialogText>(gn.GetInputPortByName(DialogLineNode.PORT_TALK)).Value;
|
||||||
dn.Gesture = GetInputPortValue<GestureData>(gn.GetInputPortByName(DialogLineNode.PORT_GESTURE));
|
dn.Gesture = GetInputPortValue<GestureData>(gn.GetInputPortByName(DialogLineNode.PORT_GESTURE));
|
||||||
dn.Expression = GetInputPortValue<ExpressionData>(gn.GetInputPortByName(DialogLineNode.PORT_EXPRESSION));
|
dn.Expression = GetInputPortValue<ExpressionData>(gn.GetInputPortByName(DialogLineNode.PORT_EXPRESSION));
|
||||||
|
dn.SceneScreen = GetInputPortValue<Sprite>(gn.GetInputPortByName(DialogLineNode.PORT_SCENE_SCREEN));
|
||||||
dn.Pose = GetInputPortValue<Pose>(gn.GetInputPortByName(DialogLineNode.PORT_POSE));
|
dn.Pose = GetInputPortValue<Pose>(gn.GetInputPortByName(DialogLineNode.PORT_POSE));
|
||||||
dn.PoseTarget = GetInputPortValue<CharacterData>(gn.GetInputPortByName(DialogLineNode.PORT_POSE_TARGET));
|
dn.PoseTarget = GetInputPortValue<CharacterData>(gn.GetInputPortByName(DialogLineNode.PORT_POSE_TARGET));
|
||||||
dn.FrontSprite = GetInputPortValue<Sprite>(gn.GetInputPortByName(DialogLineNode.PORT_FRONT_SPRITE));
|
dn.FrontSprite = GetInputPortValue<Sprite>(gn.GetInputPortByName(DialogLineNode.PORT_FRONT_SPRITE));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ internal class DialogLineNode : DialogGraphNode
|
|||||||
public const string PORT_TALK = "TalkText";
|
public const string PORT_TALK = "TalkText";
|
||||||
public const string PORT_GESTURE = "Gesture";
|
public const string PORT_GESTURE = "Gesture";
|
||||||
public const string PORT_EXPRESSION = "Expression";
|
public const string PORT_EXPRESSION = "Expression";
|
||||||
|
public const string PORT_SCENE_SCREEN = "SceneScreen";
|
||||||
public const string PORT_POSE = "Pose";
|
public const string PORT_POSE = "Pose";
|
||||||
public const string PORT_POSE_TARGET = "PoseTarget";
|
public const string PORT_POSE_TARGET = "PoseTarget";
|
||||||
public const string PORT_FRONT_SPRITE = "FrontSprite";
|
public const string PORT_FRONT_SPRITE = "FrontSprite";
|
||||||
@@ -73,6 +74,13 @@ protected override void OnDefinePorts(IPortDefinitionContext context)
|
|||||||
context.AddInputPort<GestureData>(PORT_GESTURE).WithDisplayName("Gesture").Build();
|
context.AddInputPort<GestureData>(PORT_GESTURE).WithDisplayName("Gesture").Build();
|
||||||
context.AddInputPort<ExpressionData>(PORT_EXPRESSION).WithDisplayName("Expression").Build();
|
context.AddInputPort<ExpressionData>(PORT_EXPRESSION).WithDisplayName("Expression").Build();
|
||||||
|
|
||||||
|
// 무대 연출은 화면 앞뒤 순서대로 늘어놓는다: 장면 그림 → 캐릭터 포즈 → 오버레이.
|
||||||
|
// 그래프에서 위에서 아래로 읽으면 곧 뒤에서 앞 순서가 되도록.
|
||||||
|
context.AddInputPort<Sprite>(PORT_SCENE_SCREEN).WithDisplayName("Scene Screen")
|
||||||
|
.WithTooltip("배경 앞·캐릭터 뒤에 띄울 장면 그림 (설명용 컷인, 증언 장면 등). " +
|
||||||
|
"비우면 사라진다 — 여러 대사에 걸쳐 보이려면 그 대사마다 같은 그림을 지정할 것. " +
|
||||||
|
"캐릭터보다 앞에 보여야 하면 Pose를 '숨김'으로").Build();
|
||||||
|
|
||||||
// 포즈 — 캐릭터 그림 갈아끼우기. 어떤 그림이 뜨는지는 캐릭터 프리팹의 CharacterPoses가 정한다.
|
// 포즈 — 캐릭터 그림 갈아끼우기. 어떤 그림이 뜨는지는 캐릭터 프리팹의 CharacterPoses가 정한다.
|
||||||
//
|
//
|
||||||
// Pose를 Target보다 먼저 두는 것은 읽는 순서 때문이다. "변경 없음 → (대상 없음)"은
|
// Pose를 Target보다 먼저 두는 것은 읽는 순서 때문이다. "변경 없음 → (대상 없음)"은
|
||||||
|
|||||||
55
Assets/02_Scripts/Story/SceneScreen.cs
Normal file
55
Assets/02_Scripts/Story/SceneScreen.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
// 배경보다 앞, 캐릭터보다 뒤에 뜨는 장면 그림 한 장.
|
||||||
|
// 설명용 컷인이나 증인의 증언 장면처럼 "지금 이야기하는 그 장면"을 보여주는 자리다.
|
||||||
|
//
|
||||||
|
// 왜 UXML이 아니라 월드 SpriteRenderer인가:
|
||||||
|
// UXML 패널은 카메라가 그린 화면 **전체 위에** 합성되므로 월드 오브젝트 사이에 끼울 수 없다.
|
||||||
|
// "배경보다 앞, 캐릭터보다 뒤"는 정렬 레이어로만 표현되고, 그건 월드에서만 가능하다.
|
||||||
|
// (FrontSprite·SideSprite가 UXML인 건 그것들이 최상단 오버레이라서다 — 성격이 다르다)
|
||||||
|
//
|
||||||
|
// 캐릭터보다 앞에 보여야 하는 장면이면 캐릭터를 Pose = 숨김으로 내리면 된다.
|
||||||
|
//
|
||||||
|
// 배치: 장소 프리팹이 아니라 **씬**에 둔다. 장소가 아니라 대화가 띄우는 것이라,
|
||||||
|
// 장소 프리팹에 넣으면 장소마다 복제해야 한다 (캐릭터 슬롯과 같은 이유).
|
||||||
|
// Sorting Layer는 Background와 Character 사이로 잡을 것.
|
||||||
|
[RequireComponent(typeof(SpriteRenderer))]
|
||||||
|
public class SceneScreen : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static SceneScreen Instance { get; private set; }
|
||||||
|
|
||||||
|
private SpriteRenderer _renderer;
|
||||||
|
|
||||||
|
// 지금 떠 있는 그림 (없으면 null)
|
||||||
|
public Sprite Current => _renderer != null && _renderer.enabled ? _renderer.sprite : null;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
_renderer = GetComponent<SpriteRenderer>();
|
||||||
|
Hide(); // 대사가 부르기 전까지는 비어 있다
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// null을 넘기면 Hide()와 같다 — 덕분에 호출하는 쪽이 분기 없이 노드 값을 그대로 넘길 수 있다.
|
||||||
|
public void Show(Sprite sprite)
|
||||||
|
{
|
||||||
|
if (_renderer == null) return;
|
||||||
|
|
||||||
|
if (sprite == null) { Hide(); return; }
|
||||||
|
|
||||||
|
_renderer.sprite = sprite;
|
||||||
|
_renderer.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Hide()
|
||||||
|
{
|
||||||
|
if (_renderer != null) _renderer.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/02_Scripts/Story/SceneScreen.cs.meta
Normal file
2
Assets/02_Scripts/Story/SceneScreen.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2903d89d6159cae4083ad6c561b42821
|
||||||
@@ -12,10 +12,11 @@ public class TypewriterStyle : ScriptableObject
|
|||||||
[Tooltip("초당 드러나는 글자 수")]
|
[Tooltip("초당 드러나는 글자 수")]
|
||||||
[Min(1f)] public float CharsPerSecond = 30f;
|
[Min(1f)] public float CharsPerSecond = 30f;
|
||||||
|
|
||||||
[Tooltip("쉼표류(, 、 ·) 뒤에 추가로 쉬는 시간(초)")]
|
[Tooltip("쉼표류(, 、 ·) 뒤에 추가로 쉬는 시간(초). 모든 쉼표에 일정하게 걸리는 바닥값이다. " +
|
||||||
|
"0으로 두면 자동 쉼 없이 대사 안의 <<delay:x>> 표기로만 뜸을 준다")]
|
||||||
[Min(0f)] public float CommaPause = 0.12f;
|
[Min(0f)] public float CommaPause = 0.12f;
|
||||||
|
|
||||||
[Tooltip("문장 끝(. ! ? … 。) 뒤에 추가로 쉬는 시간(초)")]
|
[Tooltip("문장 끝(. ! ? … 。) 뒤에 추가로 쉬는 시간(초). 0으로 두면 자동 쉼 없음")]
|
||||||
[Min(0f)] public float SentencePause = 0.3f;
|
[Min(0f)] public float SentencePause = 0.3f;
|
||||||
|
|
||||||
[Header("Text")]
|
[Header("Text")]
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@@ -18,6 +20,65 @@ public static class DialogMarkup
|
|||||||
public const string OPEN = "[[";
|
public const string OPEN = "[[";
|
||||||
public const string CLOSE = "]]";
|
public const string CLOSE = "]]";
|
||||||
|
|
||||||
|
// 저작자가 원하는 자리에 직접 뜸을 넣는 표기: 그날 밤<<delay:0.4>> 저는 봤습니다
|
||||||
|
//
|
||||||
|
// 문장부호 자동 쉼(TypewriterStyle.CommaPause/SentencePause)은 "어디에나 일정하게" 걸리는
|
||||||
|
// 바닥값이고, 이건 "여기서 이만큼" 을 콕 집는 수단이다. 둘은 대체 관계가 아니라 층이 다르다.
|
||||||
|
public const string DELAY_OPEN = "<<delay:";
|
||||||
|
public const string DELAY_CLOSE = ">>";
|
||||||
|
|
||||||
|
// <<delay:x>> 표기를 텍스트에서 걷어내고, 걷어낸 자리(결과 문자열 기준 인덱스)와 초를 모아 준다.
|
||||||
|
//
|
||||||
|
// 왜 텍스트에서 반드시 제거해야 하는가: Typewriter는 받은 문자열을 그대로 Label에 넣으므로,
|
||||||
|
// 표기가 남아 있으면 화면에 "<<delay:0.4>>"가 그대로 찍힌다.
|
||||||
|
// 인덱스를 결과 문자열 기준으로 잡는 것도 같은 이유다 — Typewriter의 커서와 같은 좌표계여야 한다.
|
||||||
|
//
|
||||||
|
// delays는 인덱스 오름차순으로 채워진다 (앞에서부터 훑으므로 자연히 정렬된다).
|
||||||
|
public static string ExtractDelays(string text, List<(int Index, float Seconds)> delays)
|
||||||
|
{
|
||||||
|
delays?.Clear();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(text) || text.IndexOf(DELAY_OPEN, StringComparison.Ordinal) < 0)
|
||||||
|
return text;
|
||||||
|
|
||||||
|
var sb = new StringBuilder(text.Length);
|
||||||
|
int i = 0;
|
||||||
|
while (i < text.Length)
|
||||||
|
{
|
||||||
|
int open = text.IndexOf(DELAY_OPEN, i, StringComparison.Ordinal);
|
||||||
|
if (open < 0)
|
||||||
|
{
|
||||||
|
sb.Append(text, i, text.Length - i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int close = text.IndexOf(DELAY_CLOSE, open + DELAY_OPEN.Length, StringComparison.Ordinal);
|
||||||
|
if (close < 0)
|
||||||
|
{
|
||||||
|
sb.Append(text, i, text.Length - i); // 닫히지 않은 표기 — 손대지 않고 그대로 둔다
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string raw = text.Substring(open + DELAY_OPEN.Length, close - open - DELAY_OPEN.Length);
|
||||||
|
|
||||||
|
// 숫자가 아니면 저작 실수다. 조용히 지우면 "왜 뜸이 없지"를 한참 찾게 되므로,
|
||||||
|
// 경고를 남기고 표기를 화면에 그대로 남겨 눈에 띄게 한다.
|
||||||
|
if (!float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float seconds))
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[DialogMarkup] 지연 표기의 숫자를 읽지 못함: {DELAY_OPEN}{raw}{DELAY_CLOSE}");
|
||||||
|
sb.Append(text, i, close + DELAY_CLOSE.Length - i);
|
||||||
|
i = close + DELAY_CLOSE.Length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(text, i, open - i);
|
||||||
|
delays?.Add((sb.Length, Mathf.Max(0f, seconds)));
|
||||||
|
i = close + DELAY_CLOSE.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
public static string Expand(string text, TypewriterStyle style)
|
public static string Expand(string text, TypewriterStyle style)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(text) || text.IndexOf(OPEN, StringComparison.Ordinal) < 0)
|
if (string.IsNullOrEmpty(text) || text.IndexOf(OPEN, StringComparison.Ordinal) < 0)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -24,6 +25,11 @@ public sealed class Typewriter
|
|||||||
private bool _running;
|
private bool _running;
|
||||||
private AudioSource _loopSource;
|
private AudioSource _loopSource;
|
||||||
|
|
||||||
|
// 저작자가 <<delay:x>>로 찍어 둔 뜸 — (커서 위치, 초). 인덱스 오름차순.
|
||||||
|
// 표기 자체는 _full에서 제거돼 있으므로 Composed는 이걸 전혀 신경 쓰지 않아도 된다.
|
||||||
|
private readonly List<(int Index, float Seconds)> _delays = new();
|
||||||
|
private int _delayCursor; // 다음에 먹을 지연 (한 번 먹은 것은 다시 먹지 않는다)
|
||||||
|
|
||||||
// Begin/Clear마다 증가. 실행 중인 RunAsync는 자기 세대가 아니면 즉시 물러난다 —
|
// Begin/Clear마다 증가. 실행 중인 RunAsync는 자기 세대가 아니면 즉시 물러난다 —
|
||||||
// 대사를 빠르게 넘기면 이전 루프가 NextFrameAsync에서 깨어나 새 타이핑의 커서를
|
// 대사를 빠르게 넘기면 이전 루프가 NextFrameAsync에서 깨어나 새 타이핑의 커서를
|
||||||
// 같이 전진시켜(속도 2배) 버리는 것을 막는다.
|
// 같이 전진시켜(속도 2배) 버리는 것을 막는다.
|
||||||
@@ -77,7 +83,11 @@ public void Begin(string fullText, TypewriterStyle style, CancellationToken toke
|
|||||||
Finish(); // 이전 타이핑 정리 (사운드 포함)
|
Finish(); // 이전 타이핑 정리 (사운드 포함)
|
||||||
int generation = ++_generation;
|
int generation = ++_generation;
|
||||||
|
|
||||||
_full = fullText ?? string.Empty;
|
// 지연 표기는 여기서 걷어낸다 — 남으면 화면에 그대로 찍힌다.
|
||||||
|
// 연출 없이(style == null) 즉시 표시하는 경로에서도 걷어내야 하므로 분기보다 앞에 둔다.
|
||||||
|
_full = DialogMarkup.ExtractDelays(fullText ?? string.Empty, _delays);
|
||||||
|
_delayCursor = 0;
|
||||||
|
|
||||||
_style = style;
|
_style = style;
|
||||||
_cursor = 0;
|
_cursor = 0;
|
||||||
|
|
||||||
@@ -100,6 +110,7 @@ public void SkipToEnd()
|
|||||||
{
|
{
|
||||||
if (!_running) return;
|
if (!_running) return;
|
||||||
_cursor = _full.Length;
|
_cursor = _full.Length;
|
||||||
|
_delayCursor = _delays.Count; // 건너뛴 구간의 뜸도 같이 소진 처리
|
||||||
Finish();
|
Finish();
|
||||||
_onChanged?.Invoke();
|
_onChanged?.Invoke();
|
||||||
}
|
}
|
||||||
@@ -111,6 +122,8 @@ public void Clear()
|
|||||||
_generation++; // 돌고 있는 루프를 물러나게 한다
|
_generation++; // 돌고 있는 루프를 물러나게 한다
|
||||||
_full = string.Empty;
|
_full = string.Empty;
|
||||||
_cursor = 0;
|
_cursor = 0;
|
||||||
|
_delays.Clear();
|
||||||
|
_delayCursor = 0;
|
||||||
_style = null;
|
_style = null;
|
||||||
_onChanged?.Invoke();
|
_onChanged?.Invoke();
|
||||||
}
|
}
|
||||||
@@ -128,6 +141,16 @@ private async Awaitable RunAsync(int generation, CancellationToken token)
|
|||||||
bool changed = false;
|
bool changed = false;
|
||||||
while (timer <= 0f && _cursor < _full.Length)
|
while (timer <= 0f && _cursor < _full.Length)
|
||||||
{
|
{
|
||||||
|
// 글자를 내기 **전에** 이 자리의 <<delay:x>>를 먼저 먹는다.
|
||||||
|
// 그래야 "<<delay:1>>안녕"이 1초 쉰 뒤에 타이핑을 시작한다 —
|
||||||
|
// 나중에 먹으면 '안'이 먼저 나와 버린다.
|
||||||
|
float authored = ConsumeDelaysAt(_cursor);
|
||||||
|
if (authored > 0f)
|
||||||
|
{
|
||||||
|
timer += authored;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
char revealed = AdvanceOneVisibleChar();
|
char revealed = AdvanceOneVisibleChar();
|
||||||
timer += StepSeconds(revealed);
|
timer += StepSeconds(revealed);
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -181,6 +204,19 @@ private void SkipTags()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 커서 위치까지 밀린 저작 지연을 모아 소비한다.
|
||||||
|
// 같은 자리에 여러 개가 겹쳐 있으면 합산된다.
|
||||||
|
private float ConsumeDelaysAt(int cursor)
|
||||||
|
{
|
||||||
|
float total = 0f;
|
||||||
|
while (_delayCursor < _delays.Count && _delays[_delayCursor].Index <= cursor)
|
||||||
|
{
|
||||||
|
total += _delays[_delayCursor].Seconds;
|
||||||
|
_delayCursor++;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
// 이 글자를 드러낸 뒤 다음 글자까지 기다릴 시간
|
// 이 글자를 드러낸 뒤 다음 글자까지 기다릴 시간
|
||||||
private float StepSeconds(char revealed)
|
private float StepSeconds(char revealed)
|
||||||
=> 1f / Mathf.Max(1f, _style.CharsPerSecond) + PauseAfter(revealed);
|
=> 1f / Mathf.Max(1f, _style.CharsPerSecond) + PauseAfter(revealed);
|
||||||
|
|||||||
Binary file not shown.
@@ -45,7 +45,7 @@ TextureImporter:
|
|||||||
compressionQuality: 50
|
compressionQuality: 50
|
||||||
spriteMode: 1
|
spriteMode: 1
|
||||||
spriteExtrude: 1
|
spriteExtrude: 1
|
||||||
spriteMeshType: 1
|
spriteMeshType: 0
|
||||||
alignment: 0
|
alignment: 0
|
||||||
spritePivot: {x: 0.5, y: 0.5}
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
spritePixelsToUnits: 108
|
spritePixelsToUnits: 108
|
||||||
|
|||||||
8
Assets/04_Models/Locations/LawOffice.meta
Normal file
8
Assets/04_Models/Locations/LawOffice.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4f6c540d9675783428ba2c20efc477fa
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/04_Models/Locations/LawOffice/Prefabs.meta
Normal file
8
Assets/04_Models/Locations/LawOffice/Prefabs.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0919d9b956bd48e469b23a267626e77f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/04_Models/Locations/LawOffice/Prefabs/LawOffice.prefab
LFS
Normal file
BIN
Assets/04_Models/Locations/LawOffice/Prefabs/LawOffice.prefab
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 96b7613d58a79a34f96a9d100735c3e5
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/04_Models/Locations/LawOffice/Textures.meta
Normal file
8
Assets/04_Models/Locations/LawOffice/Textures.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 510ba877f98aa7c4c8fd1a8cc91b8fe6
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/04_Models/Locations/LawOffice/Textures/LawOffice.png
LFS
Normal file
BIN
Assets/04_Models/Locations/LawOffice/Textures/LawOffice.png
LFS
Normal file
Binary file not shown.
169
Assets/04_Models/Locations/LawOffice/Textures/LawOffice.png.meta
Normal file
169
Assets/04_Models/Locations/LawOffice/Textures/LawOffice.png.meta
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 19e7fea455338374990e0696bac64c5f
|
||||||
|
TextureImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 13
|
||||||
|
mipmaps:
|
||||||
|
mipMapMode: 0
|
||||||
|
enableMipMap: 0
|
||||||
|
sRGBTexture: 1
|
||||||
|
linearTexture: 0
|
||||||
|
fadeOut: 0
|
||||||
|
borderMipMap: 0
|
||||||
|
mipMapsPreserveCoverage: 0
|
||||||
|
alphaTestReferenceValue: 0.5
|
||||||
|
mipMapFadeDistanceStart: 1
|
||||||
|
mipMapFadeDistanceEnd: 3
|
||||||
|
bumpmap:
|
||||||
|
convertToNormalMap: 0
|
||||||
|
externalNormalMap: 0
|
||||||
|
heightScale: 0.25
|
||||||
|
normalMapFilter: 0
|
||||||
|
flipGreenChannel: 0
|
||||||
|
isReadable: 0
|
||||||
|
streamingMipmaps: 0
|
||||||
|
streamingMipmapsPriority: 0
|
||||||
|
vTOnly: 0
|
||||||
|
ignoreMipmapLimit: 0
|
||||||
|
grayScaleToAlpha: 0
|
||||||
|
generateCubemap: 6
|
||||||
|
cubemapConvolution: 0
|
||||||
|
seamlessCubemap: 0
|
||||||
|
textureFormat: 1
|
||||||
|
maxTextureSize: 2048
|
||||||
|
textureSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
filterMode: 1
|
||||||
|
aniso: 1
|
||||||
|
mipBias: 0
|
||||||
|
wrapU: 1
|
||||||
|
wrapV: 1
|
||||||
|
wrapW: 0
|
||||||
|
nPOTScale: 0
|
||||||
|
lightmap: 0
|
||||||
|
compressionQuality: 50
|
||||||
|
spriteMode: 1
|
||||||
|
spriteExtrude: 1
|
||||||
|
spriteMeshType: 0
|
||||||
|
alignment: 0
|
||||||
|
spritePivot: {x: 0.5, y: 0.5}
|
||||||
|
spritePixelsToUnits: 108
|
||||||
|
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
spriteGenerateFallbackPhysicsShape: 1
|
||||||
|
alphaUsage: 1
|
||||||
|
alphaIsTransparency: 1
|
||||||
|
spriteTessellationDetail: -1
|
||||||
|
textureType: 8
|
||||||
|
textureShape: 1
|
||||||
|
singleChannelComponent: 0
|
||||||
|
flipbookRows: 1
|
||||||
|
flipbookColumns: 1
|
||||||
|
maxTextureSizeSet: 0
|
||||||
|
compressionQualitySet: 0
|
||||||
|
textureFormatSet: 0
|
||||||
|
ignorePngGamma: 0
|
||||||
|
applyGammaDecoding: 0
|
||||||
|
swizzle: 50462976
|
||||||
|
cookieLightType: 0
|
||||||
|
platformSettings:
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: DefaultTexturePlatform
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: iOS
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Android
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: Standalone
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: WebGL
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
- serializedVersion: 4
|
||||||
|
buildTarget: WindowsStoreApps
|
||||||
|
maxTextureSize: 2048
|
||||||
|
resizeAlgorithm: 0
|
||||||
|
textureFormat: -1
|
||||||
|
textureCompression: 1
|
||||||
|
compressionQuality: 50
|
||||||
|
crunchedCompression: 0
|
||||||
|
allowsAlphaSplitting: 0
|
||||||
|
overridden: 0
|
||||||
|
ignorePlatformSupport: 0
|
||||||
|
androidETC2FallbackOverride: 0
|
||||||
|
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||||
|
spriteSheet:
|
||||||
|
serializedVersion: 2
|
||||||
|
sprites: []
|
||||||
|
outline: []
|
||||||
|
customData:
|
||||||
|
physicsShape: []
|
||||||
|
bones: []
|
||||||
|
spriteID: 5e97eb03825dee720800000000000000
|
||||||
|
internalID: 0
|
||||||
|
vertices: []
|
||||||
|
indices:
|
||||||
|
edges: []
|
||||||
|
weights: []
|
||||||
|
secondaryTextures: []
|
||||||
|
spriteCustomMetadata:
|
||||||
|
entries: []
|
||||||
|
nameFileIdTable: {}
|
||||||
|
mipmapLimitGroupName:
|
||||||
|
pSDRemoveMatte: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Assets/07_Data/Characters/PhoenixWright.asset
LFS
Normal file
BIN
Assets/07_Data/Characters/PhoenixWright.asset
LFS
Normal file
Binary file not shown.
8
Assets/07_Data/Characters/PhoenixWright.asset.meta
Normal file
8
Assets/07_Data/Characters/PhoenixWright.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e61b32e717df22545805cb47995ac773
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
BIN
Assets/07_Data/Locations/LawOffice.asset
LFS
Normal file
BIN
Assets/07_Data/Locations/LawOffice.asset
LFS
Normal file
Binary file not shown.
8
Assets/07_Data/Locations/LawOffice.asset.meta
Normal file
8
Assets/07_Data/Locations/LawOffice.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3f926a5773fa47a44b849f4623245845
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
BIN
Assets/07_Data/TypewriterStyle/TWS_Think.asset
LFS
Normal file
BIN
Assets/07_Data/TypewriterStyle/TWS_Think.asset
LFS
Normal file
Binary file not shown.
8
Assets/07_Data/TypewriterStyle/TWS_Think.asset.meta
Normal file
8
Assets/07_Data/TypewriterStyle/TWS_Think.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ed5406bc7c1b8474fb40d01bb7d79f7d
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
Reference in New Issue
Block a user