Merge branch 'feature/章节管理' into feature/章节管理合入

This commit is contained in:
2026-04-28 15:53:30 +08:00
75 changed files with 1510 additions and 103 deletions
@@ -1,8 +1,8 @@
fileFormatVersion: 2
guid: f9314e7bc5b932c41b8cd00507d7ac19
NativeFormatImporter:
guid: 09d2581fcb827614bba6bf5c3279358e
folderAsset: yes
DefaultImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,267 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterGraphEditorWindow : EditorWindow
{
private ChapterGraphView _graphView;
private ChapterGraphInspector _inspector;
private IMGUIContainer _inspectorContainer;
private const string RootFolderPrefKey = "AibisDream.ChapterGraph.RootFolder";
private const string CurrentFolderPrefKey = "AibisDream.ChapterGraph.CurrentFolder";
private string _rootFolder;
private string _currentFolder;
private DropdownField _folderDropdown;
private readonly Dictionary<string, string> _displayToPath = new();
[MenuItem("Window/Chapter Graph Editor")]
public static void ShowWindow()
{
var window = GetWindow<ChapterGraphEditorWindow>("Chapter Graph Editor");
window.minSize = new Vector2(800, 600);
}
private void OnEnable()
{
if (_graphView == null) return;
_graphView.LoadGraph(_currentFolder);
}
private void OnDisable()
{
if (_graphView != null)
{
_graphView.OnNodeDeleted -= OnNodeDeleted;
}
if (_inspector != null)
{
_inspector.OnColorChanged -= OnNodeColorChanged;
_inspector.Destroy();
}
}
private void CreateGUI()
{
// 根容器:纵向排列
var root = rootVisualElement;
root.style.flexDirection = FlexDirection.Column;
// 顶部工具栏
var toolbar = new Toolbar();
toolbar.Add(CreateToolbarButton("Save", OnSaveClicked));
toolbar.Add(CreateToolbarButton("Auto Layout", OnAutoLayoutClicked));
toolbar.Add(CreateToolbarButton("Validate", OnValidateClicked));
_rootFolder = EditorPrefs.GetString(RootFolderPrefKey, "Assets/ScriptableObjects/SceneSO");
_folderDropdown = new DropdownField("路径");
_folderDropdown.style.width = 160;
_folderDropdown.style.marginLeft = 8;
_folderDropdown.style.marginRight = 4;
RefreshFolderChoices();
_folderDropdown.RegisterValueChangedCallback(OnFolderChanged);
toolbar.Add(_folderDropdown);
var setRootBtn = new Button(OnSetRootFolderClicked) { text = "..." };
setRootBtn.style.width = 28;
toolbar.Add(setRootBtn);
toolbar.Add(new ToolbarSpacer() { style = { flexGrow = 1 } });
toolbar.Add(CreateToolbarButton("Create Chapter", OnCreateChapterClicked));
root.Add(toolbar);
// 主内容区:横向分栏
var mainContainer = new VisualElement();
mainContainer.style.flexGrow = 1;
mainContainer.style.flexDirection = FlexDirection.Row;
mainContainer.style.overflow = Overflow.Hidden;
// 左侧 Inspector 面板
var leftPanel = new VisualElement();
leftPanel.style.width = 320;
leftPanel.style.minWidth = 200;
leftPanel.style.maxWidth = 500;
leftPanel.style.flexShrink = 0;
leftPanel.style.flexDirection = FlexDirection.Column;
leftPanel.style.borderRightWidth = 1;
leftPanel.style.borderRightColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
// Inspector 标题
var header = new Label("章节属性")
{
style =
{
unityFontStyleAndWeight = FontStyle.Bold,
fontSize = 14,
paddingTop = 8,
paddingBottom = 8,
paddingLeft = 12,
paddingRight = 12,
borderBottomWidth = 1,
borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f))
}
};
leftPanel.Add(header);
// IMGUI Inspector
_inspector = new ChapterGraphInspector();
_inspectorContainer = new IMGUIContainer(() => _inspector.OnGUI());
_inspectorContainer.style.flexGrow = 1;
leftPanel.Add(_inspectorContainer);
mainContainer.Add(leftPanel);
// 拖拽条
var resizer = new VisualElement();
resizer.style.width = 5;
resizer.style.backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f));
mainContainer.Add(resizer);
float startWidth = 0;
float startMouseX = 0;
resizer.RegisterCallback<MouseDownEvent>(evt =>
{
startWidth = leftPanel.resolvedStyle.width;
startMouseX = evt.mousePosition.x;
resizer.CaptureMouse();
evt.StopPropagation();
});
resizer.RegisterCallback<MouseMoveEvent>(evt =>
{
if (!resizer.HasMouseCapture()) return;
var delta = evt.mousePosition.x - startMouseX;
var newWidth = Mathf.Clamp(startWidth + delta, 200, 600);
leftPanel.style.width = newWidth;
});
resizer.RegisterCallback<MouseUpEvent>(evt =>
{
if (resizer.HasMouseCapture())
resizer.ReleaseMouse();
});
// 右侧 GraphView
_graphView = new ChapterGraphView();
_graphView.style.flexGrow = 1;
mainContainer.Add(_graphView);
root.Add(mainContainer);
// 加载图数据
_graphView.LoadGraph(_currentFolder);
// 选中节点同步到 Inspector
_graphView.OnNodeSelected += OnNodeSelected;
_graphView.OnNodeDeleted += OnNodeDeleted;
_inspector.OnColorChanged += OnNodeColorChanged;
}
private void OnNodeDeleted(TalkSceneSO so)
{
_inspector?.SetTarget(null);
_inspectorContainer?.MarkDirtyRepaint();
}
private void OnNodeColorChanged(TalkSceneSO so)
{
_graphView?.UpdateNodeColor(so);
}
private Button CreateToolbarButton(string text, System.Action onClick)
{
var btn = new Button(onClick) { text = text };
return btn;
}
private void OnSaveClicked()
{
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("保存", "所有资源已保存。", "确定");
}
private void OnAutoLayoutClicked()
{
_graphView?.AutoLayout();
}
private void OnValidateClicked()
{
_graphView?.ValidateGraph();
}
private void OnCreateChapterClicked()
{
var center = new Vector2(position.width / 2f, position.height / 2f);
var localPos = _graphView.contentViewContainer.WorldToLocal(center);
var defaultFolder = _currentFolder ?? EditorPrefs.GetString(ChapterGraphView.DefaultFolderPrefKey, _rootFolder);
_graphView?.CreateChapterNodeAt(localPos, defaultFolder);
}
private void OnNodeSelected(TalkSceneSO so)
{
_inspector?.SetTarget(so);
_inspectorContainer?.MarkDirtyRepaint();
}
private void RefreshFolderChoices()
{
_displayToPath.Clear();
_displayToPath["All"] = null;
var subFolders = AssetDatabase.GetSubFolders(_rootFolder);
foreach (var folder in subFolders)
{
var name = Path.GetFileName(folder);
_displayToPath[name] = folder;
}
var rootHasSO = AssetDatabase.FindAssets("t:TalkSceneSO", new[] { _rootFolder })
.Select(g => AssetDatabase.GUIDToAssetPath(g))
.Any(path => Path.GetDirectoryName(path)?.Replace('\\', '/') == _rootFolder);
if (rootHasSO)
{
_displayToPath["(Root)"] = _rootFolder;
}
_folderDropdown.choices = _displayToPath.Keys.ToList();
var saved = EditorPrefs.GetString(CurrentFolderPrefKey, "All");
_folderDropdown.value = _displayToPath.ContainsKey(saved) ? saved : "All";
_currentFolder = _displayToPath.TryGetValue(_folderDropdown.value, out var path) ? path : null;
}
private void OnFolderChanged(ChangeEvent<string> evt)
{
_currentFolder = _displayToPath.TryGetValue(evt.newValue, out var path) ? path : null;
EditorPrefs.SetString(CurrentFolderPrefKey, evt.newValue);
_graphView?.LoadGraph(_currentFolder);
}
private void OnSetRootFolderClicked()
{
var path = EditorUtility.OpenFolderPanel("选择章节根目录", _rootFolder, "");
if (string.IsNullOrEmpty(path)) return;
var absoluteRoot = Application.dataPath.Replace('\\', '/');
var selected = path.Replace('\\', '/');
if (!selected.StartsWith(absoluteRoot))
{
EditorUtility.DisplayDialog("错误", "请选择项目 Assets 目录内的文件夹。", "确定");
return;
}
_rootFolder = "Assets" + selected.Substring(absoluteRoot.Length);
EditorPrefs.SetString(RootFolderPrefKey, _rootFolder);
RefreshFolderChoices();
_graphView?.LoadGraph(_currentFolder);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c30bb2152169f544cb672c29ac8cb5d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,70 @@
using AibisDream;
using UnityEditor;
using UnityEngine;
namespace AibisDream.SystemEditor
{
public class ChapterGraphInspector
{
private Editor _editor;
private TalkSceneSO _target;
private Vector2 _scrollPosition;
public event System.Action<TalkSceneSO> OnColorChanged;
public void SetTarget(TalkSceneSO so)
{
// 不能用 ==,Unity 重载了 ==,已销毁的对象 == null 为 true
if (ReferenceEquals(_target, so)) return;
_target = so;
if (_editor != null)
{
Object.DestroyImmediate(_editor);
_editor = null;
}
if (so != null)
{
_editor = Editor.CreateEditor(so);
}
}
public void OnGUI()
{
if (_editor == null)
{
EditorGUILayout.HelpBox("请在图中选中一个章节节点以编辑属性", MessageType.Info);
return;
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
EditorGUI.BeginChangeCheck();
var newColor = EditorGUILayout.ColorField("节点颜色", _target.nodeColor);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(_target, "Change Node Color");
_target.nodeColor = newColor;
EditorUtility.SetDirty(_target);
OnColorChanged?.Invoke(_target);
}
EditorGUILayout.Space(6);
_editor.OnInspectorGUI();
EditorGUILayout.EndScrollView();
}
public void Destroy()
{
if (_editor != null)
{
Object.DestroyImmediate(_editor);
_editor = null;
}
_target = null;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9b122b6f926732f46b067d9b26eb191b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,503 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterGraphView : GraphView
{
public event Action<TalkSceneSO> OnNodeSelected;
public event Action<TalkSceneSO> OnNodeDeleted;
private const float HorizontalSpacing = 420f;
private const float VerticalSpacing = 220f;
public const string DefaultFolderPrefKey = "AibisDream.ChapterGraph.DefaultFolder";
public const string DefaultFolder = "Assets/ScriptableObjects/SceneSO";
public ChapterGraphView()
{
style.flexGrow = 1;
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentDragger());
this.AddManipulator(new SelectionDragger());
this.AddManipulator(new RectangleSelector());
var grid = new GridBackground();
Insert(0, grid);
grid.StretchToParentSize();
graphViewChanged += OnGraphViewChanged;
}
public void UpdateNodeColor(TalkSceneSO so)
{
foreach (var node in graphElements.OfType<ChapterNode>())
{
if (node.SceneSo == so)
{
node.UpdateColor();
break;
}
}
}
public void LoadGraph(string folderPath = null)
{
ClearGraph();
var guids = AssetDatabase.FindAssets("t:TalkSceneSO", folderPath != null ? new[] { folderPath } : null);
var allSos = guids.Select(g => AssetDatabase.LoadAssetAtPath<TalkSceneSO>(AssetDatabase.GUIDToAssetPath(g))).Where(so => so != null).ToList();
var nodeMap = new Dictionary<TalkSceneSO, ChapterNode>();
// 创建节点
foreach (var so in allSos)
{
var node = new ChapterNode(so);
AddElement(node);
nodeMap[so] = node;
}
// 创建连线
foreach (var so in allSos)
{
if (so.exits == null) continue;
if (!nodeMap.TryGetValue(so, out var sourceNode)) continue;
for (int i = 0; i < so.exits.Count; i++)
{
var exit = so.exits[i];
if (exit == null || exit.targetScene == null) continue;
if (!nodeMap.TryGetValue(exit.targetScene, out var targetNode)) continue;
var outputPort = sourceNode.GetOutputPort(i);
var inputPort = targetNode.InputPort;
if (outputPort == null || inputPort == null) continue;
var edge = new Edge
{
output = outputPort,
input = inputPort
};
edge.output.Connect(edge);
edge.input.Connect(edge);
AddElement(edge);
}
}
}
public void ClearGraph()
{
// 清除所有元素
var elementsToRemove = graphElements.ToList();
foreach (var element in elementsToRemove)
{
RemoveElement(element);
}
}
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
base.BuildContextualMenu(evt);
if (evt.target is GraphView)
{
var mousePosition = contentViewContainer.WorldToLocal(evt.mousePosition);
evt.menu.AppendAction("添加章节节点", (_) => CreateChapterNodeAt(mousePosition));
}
}
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
var startNode = startPort.node as ChapterNode;
if (startNode == null) return compatiblePorts;
ports.ForEach(port =>
{
var targetNode = port.node as ChapterNode;
if (targetNode == null) return;
if (targetNode == startNode) return;
if (startPort.direction == port.direction) return;
if (startPort.node == port.node) return;
compatiblePorts.Add(port);
});
return compatiblePorts;
}
private GraphViewChange OnGraphViewChanged(GraphViewChange change)
{
// 先处理节点删除确认:用户取消则从移除列表中剔除
if (change.elementsToRemove != null)
{
var nodesToRemove = change.elementsToRemove.OfType<ChapterNode>().ToList();
foreach (var node in nodesToRemove)
{
if (!ConfirmRemoveNode(node))
{
change.elementsToRemove.Remove(node);
// 同时保留与该节点相连的边,避免边被单独删掉
var edgesToKeep = change.elementsToRemove.OfType<Edge>()
.Where(e => e.output?.node == node || e.input?.node == node)
.ToList();
foreach (var edge in edgesToKeep)
{
change.elementsToRemove.Remove(edge);
}
}
}
}
// 处理 Edge 删除
if (change.elementsToRemove != null)
{
foreach (var edge in change.elementsToRemove.OfType<Edge>())
{
HandleEdgeRemoved(edge);
}
foreach (var node in change.elementsToRemove.OfType<ChapterNode>())
{
HandleNodeRemoved(node);
}
}
// 处理 Edge 创建
if (change.edgesToCreate != null)
{
foreach (var edge in change.edgesToCreate)
{
HandleEdgeCreated(edge);
}
}
// 处理节点移动
if (change.movedElements != null)
{
foreach (var element in change.movedElements)
{
if (element is ChapterNode node)
{
node.SyncPositionToSo();
}
}
}
return change;
}
private void HandleEdgeCreated(Edge edge)
{
if (!(edge.output?.node is ChapterNode sourceNode)) return;
if (!(edge.input?.node is ChapterNode targetNode)) return;
var sourceSo = sourceNode.SceneSo;
var targetSo = targetNode.SceneSo;
if (sourceSo == null || targetSo == null) return;
int exitIndex = edge.output.userData is int idx ? idx : -1;
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
Undo.RecordObject(sourceSo, "Connect Chapter");
sourceSo.exits[exitIndex].targetScene = targetSo;
EditorUtility.SetDirty(sourceSo);
}
private void HandleEdgeRemoved(Edge edge)
{
if (!(edge.output?.node is ChapterNode sourceNode)) return;
var sourceSo = sourceNode.SceneSo;
if (sourceSo == null) return;
int exitIndex = edge.output.userData is int idx ? idx : -1;
if (exitIndex < 0 || exitIndex >= sourceSo.exits.Count) return;
Undo.RecordObject(sourceSo, "Disconnect Chapter");
sourceSo.exits[exitIndex].targetScene = null;
EditorUtility.SetDirty(sourceSo);
}
private bool ConfirmRemoveNode(ChapterNode node)
{
var so = node.SceneSo;
if (so == null) return true;
var referrers = new List<string>();
foreach (var other in graphElements.OfType<ChapterNode>())
{
if (other == node) continue;
if (other.SceneSo == null || other.SceneSo.exits == null) continue;
foreach (var exit in other.SceneSo.exits)
{
if (exit.targetScene == so)
{
referrers.Add(string.IsNullOrEmpty(other.SceneSo.title) ? other.SceneSo.name : other.SceneSo.title);
break;
}
}
}
var name = string.IsNullOrEmpty(so.title) ? so.name : so.title;
var msg = $"确定要删除章节节点 \"{name}\" 吗?";
if (referrers.Count > 0)
{
msg += $"\n\n以下节点仍引用该节点:\n• {string.Join("\n ", referrers)}";
}
return EditorUtility.DisplayDialog("删除确认", msg, "删除", "取消");
}
private void HandleNodeRemoved(ChapterNode node)
{
var so = node.SceneSo;
if (so == null) return;
var path = AssetDatabase.GetAssetPath(so);
if (!string.IsNullOrEmpty(path))
{
AssetDatabase.DeleteAsset(path);
}
OnNodeDeleted?.Invoke(so);
}
public override void AddToSelection(ISelectable selectable)
{
base.AddToSelection(selectable);
NotifySelectionChanged();
}
public override void RemoveFromSelection(ISelectable selectable)
{
base.RemoveFromSelection(selectable);
NotifySelectionChanged();
}
public override void ClearSelection()
{
base.ClearSelection();
NotifySelectionChanged();
}
private void NotifySelectionChanged()
{
var selectedNode = selection.OfType<ChapterNode>().FirstOrDefault();
OnNodeSelected?.Invoke(selectedNode?.SceneSo);
}
public void CreateChapterNodeAt(Vector2 position, string defaultFolder = null)
{
var folder = defaultFolder ?? EditorPrefs.GetString(DefaultFolderPrefKey, DefaultFolder);
var path = EditorUtility.SaveFilePanelInProject("新建章节", "NewChapter", "asset", "选择新章节的保存位置", folder);
if (string.IsNullOrEmpty(path)) return;
var so = ScriptableObject.CreateInstance<TalkSceneSO>();
so.name = System.IO.Path.GetFileNameWithoutExtension(path);
so.title = "新章节";
so.nodeColor = new Color(0.85f, 0.45f, 0.45f);
so.graphPosition = position;
so.exits = new System.Collections.Generic.List<SceneExit>();
AssetDatabase.CreateAsset(so, path);
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
// 记住文件夹
var savedFolder = System.IO.Path.GetDirectoryName(path)?.Replace('\\', '/');
if (!string.IsNullOrEmpty(savedFolder))
{
EditorPrefs.SetString(DefaultFolderPrefKey, savedFolder);
}
var node = new ChapterNode(so);
AddElement(node);
}
public void AutoLayout()
{
var nodes = graphElements.OfType<ChapterNode>().ToList();
if (nodes.Count == 0) return;
// 计算入度
var inDegree = nodes.ToDictionary(n => n, _ => 0);
var edges = graphElements.OfType<Edge>().ToList();
foreach (var edge in edges)
{
if (edge.input?.node is ChapterNode target && inDegree.ContainsKey(target))
{
inDegree[target]++;
}
}
// 分层 BFS
var layer = new Dictionary<ChapterNode, int>();
var queue = new Queue<ChapterNode>();
// 入度为 0 的节点作为起点
foreach (var node in nodes)
{
if (inDegree[node] == 0)
{
layer[node] = 0;
queue.Enqueue(node);
}
}
// 如果没有入度为 0 的节点(纯循环),任选一个作为起点
if (queue.Count == 0)
{
layer[nodes[0]] = 0;
queue.Enqueue(nodes[0]);
}
while (queue.Count > 0)
{
var current = queue.Dequeue();
var currentLayer = layer[current];
foreach (var edge in edges)
{
if (edge.output?.node == current && edge.input?.node is ChapterNode next)
{
if (!layer.ContainsKey(next))
{
layer[next] = currentLayer + 1;
queue.Enqueue(next);
}
else
{
layer[next] = Math.Max(layer[next], currentLayer + 1);
}
}
}
}
// 为未分配层级的节点(孤立或不连通部分)分配层级
foreach (var node in nodes)
{
if (!layer.ContainsKey(node))
{
layer[node] = 0;
}
}
// 按层级分组并计算位置
var groups = layer.GroupBy(kvp => kvp.Value).OrderBy(g => g.Key).ToList();
Undo.RecordObjects(nodes.Select(n => n.SceneSo).ToArray(), "Auto Layout");
for (int i = 0; i < groups.Count; i++)
{
var group = groups[i];
var groupNodes = group.ToList();
float x = group.Key * HorizontalSpacing;
float startY = -(groupNodes.Count - 1) * VerticalSpacing / 2f;
for (int j = 0; j < groupNodes.Count; j++)
{
var node = groupNodes[j].Key;
var y = startY + j * VerticalSpacing;
var newPos = new Vector2(x, y);
node.SetPosition(new Rect(newPos, Vector2.zero));
node.SceneSo.graphPosition = newPos;
EditorUtility.SetDirty(node.SceneSo);
}
}
}
public void ValidateGraph()
{
var nodes = graphElements.OfType<ChapterNode>().ToList();
var issues = new List<string>();
var soSet = new HashSet<TalkSceneSO>(nodes.Select(n => n.SceneSo));
// 孤立节点
foreach (var node in nodes)
{
var so = node.SceneSo;
bool hasIncoming = graphElements.OfType<Edge>().Any(e => e.input?.node == node);
bool hasOutgoing = so.exits != null && so.exits.Any(e => e.targetScene != null);
if (!hasIncoming && !hasOutgoing)
{
issues.Add($"孤立节点: {so.name} (没有入边也没有出边)");
}
}
// 循环引用 (DFS)
var visited = new HashSet<TalkSceneSO>();
var visiting = new HashSet<TalkSceneSO>();
foreach (var so in soSet)
{
if (!visited.Contains(so))
{
CheckCycle(so, visited, visiting, soSet, issues);
}
}
// 空 exitName
foreach (var node in nodes)
{
var so = node.SceneSo;
if (so.exits == null) continue;
for (int i = 0; i < so.exits.Count; i++)
{
if (string.IsNullOrEmpty(so.exits[i].exitName))
{
issues.Add($"空 exitName: {so.name} 的第 {i + 1} 个出口");
}
}
}
if (issues.Count == 0)
{
EditorUtility.DisplayDialog("验证结果", "图表验证通过,未发现问题。", "确定");
}
else
{
var msg = string.Join("\n", issues.Take(20));
if (issues.Count > 20)
{
msg += $"\n... 还有 {issues.Count - 20} 个问题";
}
EditorUtility.DisplayDialog($"验证发现 {issues.Count} 个问题", msg, "确定");
}
}
private void CheckCycle(TalkSceneSO current, HashSet<TalkSceneSO> visited, HashSet<TalkSceneSO> visiting, HashSet<TalkSceneSO> soSet, List<string> issues)
{
visited.Add(current);
visiting.Add(current);
if (current.exits != null)
{
foreach (var exit in current.exits)
{
var next = exit.targetScene;
if (next == null || !soSet.Contains(next)) continue;
if (visiting.Contains(next))
{
issues.Add($"循环引用: {current.name} -> {next.name}");
continue;
}
if (!visited.Contains(next))
{
CheckCycle(next, visited, visiting, soSet, issues);
}
}
}
visiting.Remove(current);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 913b090485a3d844fab3e436e7bf4f57
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+282
View File
@@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace AibisDream.SystemEditor
{
public class ChapterNode : Node
{
public TalkSceneSO SceneSo { get; }
public Port InputPort { get; private set; }
private const float PortLabelMaxWidth = 120f;
public ChapterNode(TalkSceneSO so)
{
SceneSo = so ?? throw new ArgumentNullException(nameof(so));
// 节点整体放大
style.minWidth = 240;
style.paddingTop = 8;
style.paddingBottom = 8;
style.paddingLeft = 10;
style.paddingRight = 10;
UpdateTitle();
UpdateColor();
BuildInputPort();
BuildOutputPorts();
BuildSubtitle();
BuildAddExitButton();
SetPosition(new Rect(so.graphPosition, Vector2.zero));
// 放大标题
var titleLabel = this.Q<Label>("title-label");
if (titleLabel != null)
{
titleLabel.style.fontSize = 16;
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
}
}
public void UpdateTitle()
{
title = string.IsNullOrEmpty(SceneSo.title) ? SceneSo.name : SceneSo.title;
}
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
base.BuildContextualMenu(evt);
if (SceneSo.exits != null && SceneSo.exits.Count > 0)
{
for (int i = 0; i < SceneSo.exits.Count; i++)
{
var exit = SceneSo.exits[i];
var name = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
var idx = i;
evt.menu.AppendAction($"删除出口/{name}", _ => RemoveExitAt(idx));
}
}
}
public void UpdateColor()
{
style.backgroundColor = new StyleColor(SceneSo.nodeColor);
}
private void BuildInputPort()
{
InputPort = Port.Create<Edge>(
Orientation.Horizontal,
Direction.Input,
Port.Capacity.Single,
typeof(TalkSceneSO));
InputPort.portName = "▶";
InputPort.style.fontSize = 14;
InputPort.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
inputContainer.Add(InputPort);
}
private void BuildOutputPorts()
{
outputContainer.Clear();
if (SceneSo.exits == null) return;
for (int i = 0; i < SceneSo.exits.Count; i++)
{
var exit = SceneSo.exits[i];
var port = CreateOutputPort(exit, i);
outputContainer.Add(port);
}
}
private Port CreateOutputPort(SceneExit exit, int index)
{
var port = Port.Create<Edge>(
Orientation.Horizontal,
Direction.Output,
Port.Capacity.Single,
typeof(TalkSceneSO));
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{index}" : exit.exitName;
port.userData = index;
// 限制端口标签宽度并放大字体
var label = port.Q<Label>("type");
if (label != null)
{
label.style.maxWidth = PortLabelMaxWidth;
label.style.fontSize = 12;
}
port.AddManipulator(new EdgeConnector<Edge>(new EmptyEdgeListener()));
port.RegisterCallback<ContextualMenuPopulateEvent>((evt) =>
{
var currentIndex = port.userData is int idx ? idx : -1;
evt.menu.AppendAction("删除出口", (_) => RemoveExitAt(currentIndex));
}, TrickleDown.TrickleDown);
return port;
}
private void BuildSubtitle()
{
var subtitleLabel = new Label($"{SceneSo.name}")
{
style =
{
fontSize = 12,
unityTextAlign = TextAnchor.MiddleCenter,
color = new Color(0.8f, 0.8f, 0.8f),
paddingTop = 4,
paddingBottom = 2
}
};
extensionContainer.Add(subtitleLabel);
RefreshExpandedState();
}
private void BuildAddExitButton()
{
var removeButton = new Button(() => RemoveLastExit())
{
text = "-",
style =
{
width = 26,
height = 22,
fontSize = 16,
unityFontStyleAndWeight = FontStyle.Bold,
marginRight = 4
}
};
titleButtonContainer.Add(removeButton);
var addButton = new Button(() => AddExit())
{
text = "+",
style = { width = 26, height = 22, fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold }
};
titleButtonContainer.Add(addButton);
}
private void RemoveLastExit()
{
if (SceneSo.exits == null || SceneSo.exits.Count == 0) return;
RemoveExitAt(SceneSo.exits.Count - 1);
}
public void AddExit()
{
Undo.RecordObject(SceneSo, "Add Exit");
if (SceneSo.exits == null)
SceneSo.exits = new System.Collections.Generic.List<SceneExit>();
var newExit = new SceneExit
{
exitName = $"exit_{SceneSo.exits.Count}"
};
SceneSo.exits.Add(newExit);
EditorUtility.SetDirty(SceneSo);
var newPort = CreateOutputPort(newExit, SceneSo.exits.Count - 1);
outputContainer.Add(newPort);
RefreshPorts();
}
public void RemoveExitAt(int index)
{
if (SceneSo.exits == null || index < 0 || index >= SceneSo.exits.Count) return;
// 断开并移除该端口的所有边
var portToRemove = GetOutputPort(index);
if (portToRemove != null)
{
var edges = portToRemove.connections.ToList();
foreach (var edge in edges)
{
edge.input?.Disconnect(edge);
edge.output?.Disconnect(edge);
edge.RemoveFromHierarchy();
}
portToRemove.RemoveFromHierarchy();
}
Undo.RecordObject(SceneSo, "Remove Exit");
SceneSo.exits.RemoveAt(index);
EditorUtility.SetDirty(SceneSo);
// 更新后续端口的 userData 和 portName
for (int i = index; i < SceneSo.exits.Count; i++)
{
var port = GetOutputPort(i);
if (port != null)
{
port.userData = i;
var exit = SceneSo.exits[i];
port.portName = string.IsNullOrEmpty(exit.exitName) ? $"出口{i}" : exit.exitName;
}
}
RefreshPorts();
}
public void RefreshOutputPorts()
{
// 先断开所有输出端口的边
var edgesToRemove = outputContainer.Children()
.OfType<Port>()
.SelectMany(p => p.connections.ToList())
.ToList();
foreach (var edge in edgesToRemove)
{
edge.input?.Disconnect(edge);
edge.output?.Disconnect(edge);
edge.RemoveFromHierarchy();
}
BuildOutputPorts();
RefreshPorts();
}
public Port GetOutputPort(int index)
{
return outputContainer.Children().OfType<Port>().ElementAtOrDefault(index);
}
public override void SetPosition(Rect newPos)
{
base.SetPosition(newPos);
if (SceneSo != null)
{
SceneSo.graphPosition = newPos.position;
EditorUtility.SetDirty(SceneSo);
}
}
public void SyncPositionToSo()
{
if (SceneSo == null) return;
var pos = GetPosition();
if (SceneSo.graphPosition != pos.position)
{
SceneSo.graphPosition = pos.position;
EditorUtility.SetDirty(SceneSo);
}
}
private class EmptyEdgeListener : IEdgeConnectorListener
{
public void OnDropOutsidePort(Edge edge, Vector2 position) { }
public void OnDrop(GraphView graphView, Edge edge) { }
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3a4f1beaaa3a5740b1904f8b9b926b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using UnityEditor;
using UnityEngine;
namespace AibisDream.SystemEditor
{
[CustomEditor(typeof(TalkSceneSO))]
public class TalkSceneSOEditor : Editor
{
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(serializedObject.FindProperty("exits"), true);
EditorGUILayout.PropertyField(serializedObject.FindProperty("displayOrder"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("yarnProject"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("firstSceneName"));
EditorGUILayout.Space();
EditorGUILayout.LabelField("显示信息", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedObject.FindProperty("title"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("description"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("coverPic"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("chapter"));
EditorGUILayout.Space();
EditorGUILayout.LabelField("编辑器设置", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedObject.FindProperty("nodeColor"), new GUIContent("节点颜色"));
serializedObject.ApplyModifiedProperties();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4988e2ee30c565e40989210408a234e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,8 +1,8 @@
fileFormatVersion: 2
guid: 6d49b124127007b48b6881ce33350263
NativeFormatImporter:
guid: b69b73da769b9f74e8d1da23717b76bf
folderAsset: yes
DefaultImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -12,10 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day3_mid
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: aaef579ad8bdda542b503b40ce6773a8, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: aaef579ad8bdda542b503b40ce6773a8, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 640f90d7758f6524f8735352da531604, type: 3}
firstSceneName: Scene/ClinicOut
title:
description:
coverPic: {fileID: 0}
chapter: "\u96EAxue"
graphPosition: {x: 420, y: 110}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,8 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day3_night
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: ff82a99e791be3e41acf40232f908eb3, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: ff82a99e791be3e41acf40232f908eb3, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 640f90d7758f6524f8735352da531604, type: 3}
firstSceneName: Scene/ClinicOut
isEndOfDay: 1
nameToPlayer: "\u7B2C\u4E09\u5929\u4E2D\u5348\u4F11\u606F"
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: -990}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,8 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day4_mid
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 362a7dfb573114247a33c0e290b698eb, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 362a7dfb573114247a33c0e290b698eb, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: fceb65c69f946c740992b742bc01a6a2, type: 3}
firstSceneName: Scene/ClinicOut
isEndOfDay: 0
nameToPlayer: "\u7B2C\u56DB\u5929\u4E2D\u5348\u4F11\u606F"
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: -770}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: FP_HuoShan
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 8d8b11f0c2fbc4d4cb8c2857d98383c9, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 8d8b11f0c2fbc4d4cb8c2857d98383c9, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: dc075ea1c729cec4484634a00ebb03a4, type: 3}
firstSceneName: Scene/HuoShanFixScene
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: -550}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Gts1
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 172e1298764e87a4b9ed51264b908ba9, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 172e1298764e87a4b9ed51264b908ba9, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 31109973cccc10642a3bc09bc70f47d3, type: 3}
firstSceneName: Scene/PeipeiFixScene
title: "10\u5E74\u524D"
description: "\u201C\u4E8B\u6545\u201D"
coverPic: {fileID: -3514371587045790464, guid: fd04c28739017964789d58d573f9fabb, type: 3}
chapter: "\u201C\u4E8B\u6545\u201D"
graphPosition: {x: 0, y: -330}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: PeipeiDay1
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 68a9c1b8c5230454bb9498fd2ab5714b, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 68a9c1b8c5230454bb9498fd2ab5714b, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 26fc6619bbcda734a81361b8bb615f04, type: 3}
firstSceneName: Scene/PeipeiFixScene
title: "\u4F69\u4F69"
description: "\u7B2C\u4E00\u4F4D\u75C5\u4EBA"
coverPic: {fileID: 21300000, guid: ef7ba7c21d2aea048abe5729d7da2eee, type: 3}
chapter: "\u7B2C\u4E00\u4F4D\u75C5\u4EBA"
graphPosition: {x: 0, y: 110}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: PeipeiDay2
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: afc5b01ef301cc74db7cae741acad427, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: afc5b01ef301cc74db7cae741acad427, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 247e2ccd847942843bb87841bb9888c5, type: 3}
firstSceneName: Scene/PeipeiFixScene
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 420, y: -110}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,8 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: PeipeiDay3
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: e749fe0f9aa32f44097e037f52a5bfe0, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: e749fe0f9aa32f44097e037f52a5bfe0, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 05ccf66391562af43835d718ca36f508, type: 3}
firstSceneName: Scene/PeipeiFixScene
isEndOfDay: 1
nameToPlayer: "\u7B2C\u56DB\u5929\u4E0A\u5348\u4F69\u4F69"
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: 330}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,8 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Prologue_begin
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 2f763a2c04ecafb4981379b7363f9791, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 2f763a2c04ecafb4981379b7363f9791, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: ab71ff7b298372f48a262ad5b949c395, type: 3}
firstSceneName: Scene/OpenFixScene
isEndOfDay: 0
nameToPlayer: "\u5E8F\u7AE0"
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: 550}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,8 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: StoneDay2
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: afc5b01ef301cc74db7cae741acad427, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: afc5b01ef301cc74db7cae741acad427, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: fa8082420f7bb5b4a8ab33a59c70bcc9, type: 3}
firstSceneName: Scene/ExpressFixScene
isEndOfDay: 0
nameToPlayer: "\u77F3\u5934\u590D\u8BCA"
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: 770}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,8 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: YingliDay1
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: e749fe0f9aa32f44097e037f52a5bfe0, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: e749fe0f9aa32f44097e037f52a5bfe0, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 3bb399faf6caadd4c89f85b7aa83c388, type: 3}
firstSceneName: Scene/YingliFixScene
isEndOfDay: 0
nameToPlayer: "\u82F1\u7406\u521D\u8BCA"
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: 990}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d9ce4de6ffe606743a73b423d93578c5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day0_Prologue
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: ac5ca9a66ff22ab4b9cd5adbdade6b8c, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: ac5ca9a66ff22ab4b9cd5adbdade6b8c, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 2fa76ffaa57f62244b77be9b25ca14c2, type: 3}
firstSceneName: Scene/OpenFixScene
title: "\u68A6"
description: "\u6D77\u6D6A\u4E0E\u75C5\u75DB"
coverPic: {fileID: 21300000, guid: c66b2b5ca74393c45a0d281a6662aa54, type: 3}
chapter: "\u6D77\u6D6A\u4E0E\u75C5\u75DB"
graphPosition: {x: -8.666687, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day1_FIRST_PP1
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 68a9c1b8c5230454bb9498fd2ab5714b, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 68a9c1b8c5230454bb9498fd2ab5714b, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 26fc6619bbcda734a81361b8bb615f04, type: 3}
firstSceneName: Scene/PeipeiFixScene
title: "\u4F69\u4F69"
description: "\u7B2C\u4E00\u4F4D\u75C5\u4EBA"
coverPic: {fileID: 21300000, guid: ef7ba7c21d2aea048abe5729d7da2eee, type: 3}
chapter: "\u7B2C\u4E00\u4F4D\u75C5\u4EBA"
graphPosition: {x: 640, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day1_begin
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: dadf07a02eb4b8d4c8bdf1739a10c9c5, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: dadf07a02eb4b8d4c8bdf1739a10c9c5, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 533dc422c8871c143a551bd5d446b439, type: 3}
firstSceneName: Scene/SubWay
title: "\u5730\u94C1\u4E0A"
description: "\u8BB0\u5F97\u6253\u5361"
coverPic: {fileID: 21300000, guid: f78e34492dd89b341928553dd1dfcbdb, type: 3}
chapter: "\u8BB0\u5F97\u6253\u5361"
graphPosition: {x: 320, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day1_mid
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: ada4bdc2db5594e4f860c90b3a4b9da8, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: ada4bdc2db5594e4f860c90b3a4b9da8, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 293561ce52efc594399242651d44b366, type: 3}
firstSceneName: Scene/ClinicOut
title: "\u5348\u4F11"
description: "\u51AC\u5929\u5230\u4E86"
coverPic: {fileID: 21300000, guid: 7715c89797cc6ca4e914b85f5524e9fd, type: 3}
chapter: "\u51AC\u5929\u5230\u4E86"
graphPosition: {x: 960, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day1_night
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 653fbfbb55fef7740a2360590e29d552, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 653fbfbb55fef7740a2360590e29d552, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 29ebe788cfe2c084e88ffdd8e2f93378, type: 3}
firstSceneName: Scene/ClinicOut
title: "\u9152\u676F\u548C\u724C"
description: "\u9152\u676F\u548C\u724C"
coverPic: {fileID: 21300000, guid: df86302e77350e34ea422cd24043dd93, type: 3}
chapter: "\u9152\u676F\u548C\u724C"
graphPosition: {x: 1280, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day1_sleep
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 172e1298764e87a4b9ed51264b908ba9, type: 2}
yarnProject: {fileID: 7545569452688597077, guid: 759b6e8a16f70084e8fe77eded076218, type: 3}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 172e1298764e87a4b9ed51264b908ba9, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: beb6e77de3427b04dae210b2c9ca29a1, type: 3}
firstSceneName: "Scene/\u68A6\u5883"
title: "\u542F\u793A"
description: "\u627E\u5230\u5929\u6587\u53F0\u7684\u5165\u53E3"
coverPic: {fileID: 21300000, guid: 20296b9c29cb10f4a97913165e98fb62, type: 3}
chapter: "\u627E\u5230\u5929\u6587\u53F0\u7684\u5165\u53E3"
graphPosition: {x: 1600, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day2_FIRST_HS1
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 8d8b11f0c2fbc4d4cb8c2857d98383c9, type: 2}
yarnProject: {fileID: 7545569452688597077, guid: f7d7db96cdd264f4dbb3341df5deeccc, type: 3}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 8d8b11f0c2fbc4d4cb8c2857d98383c9, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: dc075ea1c729cec4484634a00ebb03a4, type: 3}
firstSceneName: Scene/HuoShanFixScene
title: "\u706B\u5C71"
description: "\u6211\u7684\u7535\u89C6\u5934\u670B\u53CB"
coverPic: {fileID: 21300000, guid: 77af8281471ddab42993b4b7795f2f57, type: 3}
chapter: "\u6211\u7684\u7535\u89C6\u5934\u670B\u53CB"
graphPosition: {x: 2240, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day2_NEXT_PP2
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: afc5b01ef301cc74db7cae741acad427, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: afc5b01ef301cc74db7cae741acad427, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 247e2ccd847942843bb87841bb9888c5, type: 3}
firstSceneName: Scene/PeipeiFixScene
title: "\u505C\u6B62\u60F3\u8C61"
description: "\u505C\u6B62\u60F3\u8C61"
coverPic: {fileID: 21300000, guid: ef7ba7c21d2aea048abe5729d7da2eee, type: 3}
chapter: "\u505C\u6B62\u60F3\u8C61"
graphPosition: {x: 2880, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day2_begin
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 0bdbd8e46fa688e49a5a670b78b6f956, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 0bdbd8e46fa688e49a5a670b78b6f956, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 6ecfe80fd0370554db9a7ec3a173f7e3, type: 3}
firstSceneName: Scene/SubWay
title: "\u5730\u94C1\u4E0A"
description: "\u5BBF\u9189"
coverPic: {fileID: 21300000, guid: 922da7b13a1cf2747a43933c2afea754, type: 3}
chapter: "\u5BBF\u9189"
graphPosition: {x: 1920, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day2_mid
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: a4dba2e2976607d45a28d4c8cade6b0a, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: a4dba2e2976607d45a28d4c8cade6b0a, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 25f49957c68bca34bb1aeafec7feca8b, type: 3}
firstSceneName: Scene/ClinicOut
title: "\u7B2C\u4E8C\u5929\u4E2D\u5348"
description: "\u7B2C\u4E8C\u5929\u4E2D\u5348"
coverPic: {fileID: 21300000, guid: 7715c89797cc6ca4e914b85f5524e9fd, type: 3}
chapter: "\u7B2C\u4E8C\u5929\u4E2D\u5348"
graphPosition: {x: 2560, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day2_night
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: aaef579ad8bdda542b503b40ce6773a8, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: aaef579ad8bdda542b503b40ce6773a8, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: ec06c945cb99fb544b1dd9a2a6ea9a7b, type: 3}
firstSceneName: Scene/ClinicOut
title: "\u5915\u9633"
description: "\u5915\u9633"
coverPic: {fileID: 21300000, guid: dc09bd90b2ce79249ac504f07b98944f, type: 3}
chapter: "\u5915\u9633"
graphPosition: {x: 3200, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day2_sleep
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 362a7dfb573114247a33c0e290b698eb, type: 2}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 362a7dfb573114247a33c0e290b698eb, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 62f2a99a47fc652449b846035a739529, type: 3}
firstSceneName: "Scene/\u68A6\u5883"
title: "\u542F\u793A"
description: "\u6D77\u662F\u5B58\u5728\u7684\u5417\uFF1F"
coverPic: {fileID: 21300000, guid: 2796d4ad771bcde488978d1c1e67ed8f, type: 3}
chapter: "\u6D77\u662F\u5B58\u5728\u7684\u5417\uFF1F"
graphPosition: {x: 3520, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,10 +12,13 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: GeneralEnd
m_EditorClassIdentifier:
nextScene: {fileID: 0}
exits: []
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: e6bac2644a8a04b4899ce2fe8ae1f4fc, type: 3}
firstSceneName: Scene/Bridge
title: "\u7ED3\u675F"
description: "\u7ED3\u675F"
coverPic: {fileID: 21300000, guid: 8344881c78a9845499555d573e34e362, type: 3}
chapter: "\u7ED3\u675F"
graphPosition: {x: 3840, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: df234a5ef69dc6d468e8accd8fe47fbe
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -12,8 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: Day1_NEXT_ST1
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: ada4bdc2db5594e4f860c90b3a4b9da8, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: ada4bdc2db5594e4f860c90b3a4b9da8, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: 9c6e0818864e6964d83f946e81e32916, type: 3}
firstSceneName: Scene/ExpressFixScene
isEndOfDay: 0
nameToPlayer: "\u77F3\u5934\u521D\u8BCA"
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: 0}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -1,19 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: HuoshanDay1
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 8d8b11f0c2fbc4d4cb8c2857d98383c9, type: 2}
yarnProject: {fileID: 7545569452688597077, guid: 5b30d815fe3ef1d4aa1af835b6801fd4, type: 3}
firstSceneName: Scene/HuoShanFixScene
isEndOfDay: 0
nameToPlayer: "\u706B\u5C71\u521D\u8BCA"
@@ -1,19 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: StoneDay1
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: ada4bdc2db5594e4f860c90b3a4b9da8, type: 2}
yarnProject: {fileID: 7545569452688597077, guid: 9c6e0818864e6964d83f946e81e32916, type: 3}
firstSceneName: Scene/ExpressFixScene
isEndOfDay: 0
nameToPlayer: "\u77F3\u5934\u521D\u8BCA"
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f47b16116007f004580e98b8f50d35ac
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -12,10 +12,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 157431981b8a721458e6706c374a3867, type: 3}
m_Name: TestSO
m_EditorClassIdentifier:
nextScene: {fileID: 11400000, guid: 10601ce26230723479d875a630f15732, type: 2}
nextScene: {fileID: 0}
exits:
- exitName: default
targetScene: {fileID: 11400000, guid: 10601ce26230723479d875a630f15732, type: 2}
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: b81ce7d02242fc74daf531067098286f, type: 3}
firstSceneName: Scene/ExpressFixScene
title:
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: 640}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -13,9 +13,13 @@ MonoBehaviour:
m_Name: Test_Shitou
m_EditorClassIdentifier:
nextScene: {fileID: 0}
exits: []
displayOrder: 0
yarnProject: {fileID: 7545569452688597077, guid: a2b0fa96399ebca4c9c61e346f95b037, type: 3}
firstSceneName: Scene/Scene/ExpressFixScene
title: "\u77F3\u5934\u7684\u5FC3"
description:
coverPic: {fileID: 0}
chapter:
graphPosition: {x: 0, y: 480}
nodeColor: {r: 0.85, g: 0.45, b: 0.45, a: 1}
@@ -12,11 +12,11 @@ namespace AibisDream
#region
[YarnCommand("NextYarn")]
public static void NextYarn()
public static void NextYarn(string exitName = null)
{
EnumEventSystem.Global.Send(EventEnum.NextYarn);
StorageSystem.Instance.ClearLocal();
DialogController.Instance.StartCoroutine(GameManager.Instance.NextSceneSo());
DialogController.Instance.StartCoroutine(GameManager.Instance.NextSceneSo(exitName));
}
[YarnCommand("OpenEndMenu")]
+21 -8
View File
@@ -24,9 +24,14 @@ namespace AibisDream
cacheChapter = ParseChapterTitle(saveFilePath);
}
// 优先从 GameManager 获取全量章节并按 displayOrder 排序
var allChapters = GameManager.Instance?.GetAllSceneSos();
var sourceList = allChapters != null
? allChapters.OrderBy(so => so.displayOrder).ToList()
: ChapterData.chapterList;
// 拼接章节列表
var chapterList = ChapterData.chapterList
var chapterList = sourceList
.Where(item => ChapterData.unlockedChapterList.Contains(item.name))
.Select(chapterSo => new ChapterVo
{
@@ -98,14 +103,22 @@ namespace AibisDream
private void LoadChapterList()
{
chapterList = new List<TalkSceneSO>();
var curChapter = firstChapter;
do
var all = GameManager.Instance?.GetAllSceneSos();
if (all != null)
{
chapterList.Add(curChapter);
curChapter = curChapter.nextScene;
} while (curChapter != null && !chapterList.Contains(curChapter));
chapterList = all.OrderBy(so => so.displayOrder).ToList();
}
else
{
chapterList = new List<TalkSceneSO>();
var curChapter = firstChapter;
do
{
chapterList.Add(curChapter);
curChapter = curChapter.GetNextScene();
} while (curChapter != null && !chapterList.Contains(curChapter));
}
}
private void LoadUnlockChapters()
+7 -4
View File
@@ -237,11 +237,12 @@ namespace AibisDream
#endregion
public IEnumerator NextSceneSo()
public IEnumerator NextSceneSo(string exitName = null)
{
if (_currentTalkSceneSo.Value && _currentTalkSceneSo.Value.nextScene)
var next = _currentTalkSceneSo.Value?.GetNextScene(exitName);
if (next != null)
{
_currentTalkSceneSo.Value = _currentTalkSceneSo.Value.nextScene;
_currentTalkSceneSo.Value = next;
ChapterController.Instance.UnlockChapter(_currentTalkSceneSo.Value);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync();
yield return SceneLoader.Instance.LoadSceneAsync(_currentTalkSceneSo.Value.firstSceneName);
@@ -249,11 +250,13 @@ namespace AibisDream
}
else
{
Debug.Log($"没有下个场景了 (exitName: {exitName})");
yield return null;
Debug.Log("没有下个场景了");
}
}
public TalkSceneSO[] GetAllSceneSos() => _totalSceneSos;
public void SetSceneSoByName(string soName)
{
_currentTalkSceneSo.Value = Array.Find(_totalSceneSos, so => so.name == soName);
+40 -3
View File
@@ -1,15 +1,29 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Yarn.Unity;
namespace AibisDream
{
[Serializable]
public class SceneExit
{
[Tooltip("出口名称,用于Yarn命令指定跳转目标,如 good_end / bad_end / default")]
public string exitName;
public TalkSceneSO targetScene;
}
/// <summary>
/// 章节类型数据
/// </summary>
[CreateAssetMenu(menuName = "Game Scene/TalkSceneSO")]
public class TalkSceneSO : ScriptableObject
{
public TalkSceneSO nextScene;
{
private const string DEFAULT_EXIT_NAME = "default";
public List<SceneExit> exits = new();
public int displayOrder;
public YarnProject yarnProject;
@@ -23,6 +37,29 @@ namespace AibisDream
public string chapter;
#endregion
#region 使
[HideInInspector] public Vector2 graphPosition;
public Color nodeColor = new(0.85f, 0.45f, 0.45f);
#endregion
public TalkSceneSO GetNextScene(string exitName = null)
{
if (exits == null || exits.Count == 0)
return null;
if (string.IsNullOrEmpty(exitName))
exitName = DEFAULT_EXIT_NAME;
foreach (var exit in exits)
{
if (exit.exitName == exitName)
return exit.targetScene;
}
return null;
}
}
}