Storyline+Dialog初步

This commit is contained in:
SoulliesOfficial
2026-07-05 16:08:23 -04:00
parent afa8a56e1d
commit d031afd075
464 changed files with 25716 additions and 4209 deletions

View File

@@ -0,0 +1,238 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using Yarn.Unity;
#if UNITY_EDITOR
using UnityEditor;
using Sirenix.Utilities.Editor;
#endif
namespace Ichni.Story
{
/// <summary>
/// 单个章节的故事树数据,以 ScriptableObject 形式存储所有 block 定义与关联资产引用。
/// 布局完全由每个 block 的 (gridColumn, gridRow) 静态决定,运行时全量展示。
/// </summary>
[CreateAssetMenu(fileName = "StoryData", menuName = "Ichni/Story/New/StoryData")]
public class StoryData : SerializedScriptableObject
{
[LabelText("Chapter Index")]
public string chapterIndex;
[LabelText("Yarn Project")]
[InfoBox("该章节对应的 YarnProject 编译资产,阶段 2 接线。")]
public YarnProject yarnProject;
[LabelText("Blocks")]
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
public List<StoryBlockDefinition> blocks = new List<StoryBlockDefinition>();
// ── Queries ─────────────────────────────────────────────────────────────
/// <summary>
/// 根据 blockId 获取 block 定义,未找到时返回 null。
/// </summary>
public StoryBlockDefinition GetBlock(string blockId)
{
foreach (StoryBlockDefinition block in blocks)
{
if (block.blockId == blockId)
return block;
}
return null;
}
/// <summary>
/// 获取指定 blockId 的所有直接后继 block 定义。
/// </summary>
public IEnumerable<StoryBlockDefinition> GetNextBlocks(string blockId)
{
StoryBlockDefinition source = GetBlock(blockId);
if (source == null) yield break;
foreach (string nextId in source.nextBlockIds)
{
StoryBlockDefinition next = GetBlock(nextId);
if (next != null)
yield return next;
}
}
// ── Editor Preview ────────────────────────────────────────────────────────
#if UNITY_EDITOR
private enum StoryPreviewMode { Detailed, Simple }
// 编辑器 UI 临时状态(不参与序列化)
[System.NonSerialized] private StoryPreviewMode _previewMode = StoryPreviewMode.Detailed;
[System.NonSerialized] private Vector2 _previewPan;
[System.NonSerialized] private bool _isPanningPreview;
private const float PreviewViewportHeight = 380f;
private const float PreviewPaddingLeft = 24f;
private const float PreviewTangent = 44f;
// 详细版:较大方块,标注 id / 类型 / 标题
private const float DetailCellWidth = 150f;
private const float DetailCellHeight = 72f;
private const float DetailStepX = 210f;
private const float DetailStepY = 120f;
// 简略版:很小的方块,仅标注 Block ID
private const float SimpleCellWidth = 62f;
private const float SimpleCellHeight = 26f;
private const float SimpleStepX = 92f;
private const float SimpleStepY = 42f;
/// <summary>
/// 在 Inspector 顶部绘制只读的故事树结构预览(流程图样式,连线左右相连)。
/// 提供"详细 / 简略"两种可拖拽平移的视图;按 (gridColumn, gridRow) 摆放 block
/// 支持负数与小数坐标,(0,0) 对应左侧中部。
/// </summary>
[PropertyOrder(-10)]
[OnInspectorGUI]
private void DrawStoryPreview()
{
SirenixEditorGUI.Title("Story Tree Preview", null, TextAlignment.Left, true);
if (blocks == null || blocks.Count == 0)
{
EditorGUILayout.HelpBox("尚未定义任何 block。", MessageType.Info);
return;
}
// 顶部工具条:模式切换 + 复位视图
EditorGUILayout.BeginHorizontal();
_previewMode = (StoryPreviewMode)GUILayout.Toolbar(
(int)_previewMode, new[] { "Detailed", "Simple" }, GUILayout.Height(20f));
if (GUILayout.Button("Reset View", GUILayout.Width(90f), GUILayout.Height(20f)))
_previewPan = Vector2.zero;
EditorGUILayout.EndHorizontal();
bool simple = _previewMode == StoryPreviewMode.Simple;
float cellW = simple ? SimpleCellWidth : DetailCellWidth;
float cellH = simple ? SimpleCellHeight : DetailCellHeight;
float stepX = simple ? SimpleStepX : DetailStepX;
float stepY = simple ? SimpleStepY : DetailStepY;
Rect area = GUILayoutUtility.GetRect(0f, PreviewViewportHeight, GUILayout.ExpandWidth(true));
EditorGUI.DrawRect(area, new Color(0.12f, 0.12f, 0.14f, 1f));
HandlePreviewPanning(area);
// 裁剪到预览区,超出部分(平移后)不外溢到其它 Inspector 元素
GUI.BeginClip(area);
{
// (0,0) 网格点 = 视口左侧中线(叠加平移量)
float originX = PreviewPaddingLeft + _previewPan.x;
float originY = area.height * 0.5f + _previewPan.y;
// 计算各 block 矩形(左中对齐网格点,与运行时 pivot(0,0.5) 一致),按 blockId 索引
Dictionary<string, Rect> cellRects = new Dictionary<string, Rect>();
foreach (StoryBlockDefinition block in blocks)
{
if (block == null || string.IsNullOrEmpty(block.blockId)) continue;
float px = originX + block.gridColumn * stepX; // 列向右为正(可负、可小数)
float py = originY + block.gridRow * stepY; // 行向下为正、向上为负
cellRects[block.blockId] = new Rect(px, py - cellH * 0.5f, cellW, cellH);
}
// 先画连线:左右相连的流程图(起点右中 → 终点左中,水平切线)
Handles.BeginGUI();
foreach (StoryBlockDefinition block in blocks)
{
if (block == null || !cellRects.TryGetValue(block.blockId, out Rect fromRect)) continue;
foreach (string nextId in block.nextBlockIds)
{
if (string.IsNullOrEmpty(nextId) || !cellRects.TryGetValue(nextId, out Rect toRect)) continue;
Vector3 start = new Vector3(fromRect.xMax, fromRect.center.y, 0f);
Vector3 end = new Vector3(toRect.xMin, toRect.center.y, 0f);
Vector3 startTan = start + Vector3.right * PreviewTangent;
Vector3 endTan = end + Vector3.left * PreviewTangent;
Handles.DrawBezier(start, end, startTan, endTan,
new Color(0.82f, 0.82f, 0.88f, 1f), null, simple ? 2f : 3f);
}
}
Handles.EndGUI();
// 再画 block 方块
GUIStyle labelStyle = new GUIStyle(EditorStyles.boldLabel)
{
alignment = TextAnchor.MiddleCenter,
wordWrap = true,
richText = false,
fontSize = simple ? 9 : 11,
normal = { textColor = Color.white }
};
foreach (StoryBlockDefinition block in blocks)
{
if (block == null || !cellRects.TryGetValue(block.blockId, out Rect rect)) continue;
EditorGUI.DrawRect(rect, GetTypeColor(block.blockType));
string label = simple
? block.blockId
: $"{block.blockId}\n[{block.blockType}]\n{block.GetDisplayTitle()}";
GUI.Label(rect, label, labelStyle);
}
}
GUI.EndClip();
EditorGUILayout.HelpBox(
"在预览区内拖拽可平移视图。列 = 水平(右为正),行 = 垂直(下为正、上为负),(0,0) 位于左侧中部,支持负数与小数。",
MessageType.None);
}
/// <summary>
/// 处理预览区内的鼠标拖拽平移。
/// </summary>
private void HandlePreviewPanning(Rect area)
{
Event e = Event.current;
switch (e.type)
{
case EventType.MouseDown:
if (area.Contains(e.mousePosition))
{
_isPanningPreview = true;
e.Use();
}
break;
case EventType.MouseDrag:
if (_isPanningPreview)
{
_previewPan += e.delta;
GUI.changed = true;
e.Use();
}
break;
case EventType.MouseUp:
if (_isPanningPreview)
{
_isPanningPreview = false;
e.Use();
}
break;
}
}
/// <summary>
/// 预览中不同 block 类型的填充色。
/// </summary>
private static Color GetTypeColor(StoryBlockType type)
{
return type switch
{
StoryBlockType.Text => new Color(0.20f, 0.42f, 0.72f, 1f), // 蓝
StoryBlockType.Song => new Color(0.62f, 0.30f, 0.66f, 1f), // 紫
StoryBlockType.Tutorial => new Color(0.28f, 0.56f, 0.36f, 1f), // 绿
_ => new Color(0.4f, 0.4f, 0.4f, 1f)
};
}
#endif
}
}