DataEditor & StorySystem Graph

This commit is contained in:
SoulliesOfficial
2025-11-10 11:18:19 -05:00
parent 1bca620966
commit ea75bd5225
76 changed files with 2340 additions and 90 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9fa1138f75d85344ab4e3b8312e71959
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using UnityEngine;
namespace SLSFramework.StorySystem
{
public abstract class GraphBase : ScriptableObject
{
// 使用 [SerializeReference] 来支持多态性,存储不同类型的节点数据
[SerializeReference]
public List<BaseNodeData> nodes = new List<BaseNodeData>();
// 存储节点之间的连接
public List<EdgeData> edges = new List<EdgeData>();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 88df947440cfd3841b8e5a545437b253

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1ad08c73d10275f498f9643e948dade4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,36 @@
using SLSFramework.StorySystem.Dialog;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
namespace SLSFramework.StorySystem
{
// 自定义节点的基础类,用于存储对应的数据
public abstract class BaseGraphNode : Node
{
public BaseNodeData NodeData { get; private set; }
protected BaseGraphNode(BaseNodeData data)
{
NodeData = data;
title = data.GetType().Name.Replace("Data", ""); // 自动设置标题, e.g. "StartNodeData" -> "StartNode"
SetPosition(new Rect(data.position, Vector2.zero)); // Vector2.zero size 会被自动计算
viewDataKey = data.guid; // 确保GUID一致
}
// 辅助方法:创建端口
protected Port CreatePort(Direction direction, Port.Capacity capacity = Port.Capacity.Single, string portName = "")
{
// 如果未指定端口名,则使用方向的默认值
if (string.IsNullOrEmpty(portName))
{
portName = direction == Direction.Input ? "Input" : "Output";
}
var port = InstantiatePort(Orientation.Horizontal, direction, capacity, typeof(bool));
port.portName = portName;
port.name = portName;
return port;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 89062ba4c7f90314c86ed9cedb4053cf

View File

@@ -0,0 +1,47 @@
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem
{
public class ConditionGraphNode : BaseGraphNode
{
private ConditionNodeData _data;
public ConditionGraphNode(ConditionNodeData data) : base(data)
{
_data = data;
var titleContainer = this.Q("title");
titleContainer.style.backgroundColor = new StyleColor(new Color(0.4f, 0.2f, 0.6f)); // 紫色
// 1. 添加端口
var inputPort = CreatePort(Direction.Input, Port.Capacity.Multi, "Previous");
// Condition 节点有两个输出
var truePort = CreatePort(Direction.Output, Port.Capacity.Single, "True");
var falsePort = CreatePort(Direction.Output, Port.Capacity.Single, "False");
inputContainer.Add(inputPort);
outputContainer.Add(truePort);
outputContainer.Add(falsePort);
// 2. 添加自定义UI字段 (条件语句)
var conditionField = new TextField("Condition") { multiline = true };
conditionField.SetValueWithoutNotify(_data.conditionString);
conditionField.RegisterValueChangedCallback(evt => {
_data.conditionString = evt.newValue;
});
// 调整TextArea的样式
conditionField.Q("unity-text-input").style.minWidth = 150;
extensionContainer.Add(conditionField);
// 刷新布局
RefreshExpandedState();
RefreshPorts();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 95a4ba99bc298c74283b008b8b25b8bd

View File

@@ -0,0 +1,23 @@
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem
{
public class EndGraphNode : BaseGraphNode
{
public EndGraphNode(EndNodeData data) : base(data)
{
var titleContainer = this.Q("title");
titleContainer.style.backgroundColor = new StyleColor(new Color(0.4f, 0.4f, 0.2f)); // 红色
// End 节点只有一个输入端口
var inputPort = CreatePort(Direction.Input, Port.Capacity.Multi, "Previous");
inputContainer.Add(inputPort);
// 刷新端口和布局
RefreshExpandedState();
RefreshPorts();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e82960b057418454eab7e7eefcc71a14

View File

@@ -0,0 +1,43 @@
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Dialog
{
public class EventGraphNode : BaseGraphNode
{
private EventNodeData _data;
public EventGraphNode(EventNodeData data) : base(data)
{
_data = data;
var titleContainer = this.Q("title");
titleContainer.style.backgroundColor = new StyleColor(new Color(0.1f, 0.5f, 0.5f)); // 青色
// 1. 添加端口
var inputPort = CreatePort(Direction.Input, Port.Capacity.Multi, "Previous");
var outputPort = CreatePort(Direction.Output, Port.Capacity.Single, "Next");
inputContainer.Add(inputPort);
outputContainer.Add(outputPort);
// 2. 添加自定义UI字段 (事件语句)
var eventField = new TextField("Event") { multiline = true };
eventField.SetValueWithoutNotify(_data.eventString);
eventField.RegisterValueChangedCallback(evt => {
_data.eventString = evt.newValue;
});
// 调整TextArea的样式
eventField.Q("unity-text-input").style.minWidth = 150;
extensionContainer.Add(eventField);
// 刷新布局
RefreshExpandedState();
RefreshPorts();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: baf926c674d6ea842b99cda704461ee2

View File

@@ -0,0 +1,23 @@
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem
{
public class StartGraphNode : BaseGraphNode
{
public StartGraphNode(StartNodeData data) : base(data)
{
VisualElement titleContainer = this.Q("title");
titleContainer.style.backgroundColor = new StyleColor(new Color(0.2f, 0.5f, 0.2f)); // 绿色
// Start 节点只有一个输出端口
var outputPort = CreatePort(Direction.Output, Port.Capacity.Single, "Next");
outputContainer.Add(outputPort);
// 刷新端口和布局
RefreshExpandedState();
RefreshPorts();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d3bf9fbf0029def4bbb8ce1e1ed0628c

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4019e9906ff3c1f46b7db8bbb8604ef3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,27 @@
using UnityEditor;
using UnityEngine;
namespace SLSFramework.StorySystem
{
public class EditorWindowBase : EditorWindow
{
protected GraphViewBase graphView;
protected GraphBase currentGraph;
protected string windowTitle = "Story System Graph";
protected virtual void CreateGraphView()
{
}
public virtual void SetGraph(GraphBase graph)
{
}
public virtual void OnGraphUpdated()
{
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2ada5daa7b9068f4c98c9af0655aef6d

View File

@@ -0,0 +1,296 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem
{
public partial class GraphViewBase : GraphView
{
protected GraphBase graph;
protected EditorWindowBase editorWindow;
// 加载图表
public void LoadGraph(GraphBase graph)
{
this.graph = graph;
if (graph == null)
{
Debug.LogError("Graph is null, cannot load.");
return;
}
// 清除当前视图
DeleteElements(graphElements);
// 1. 创建所有节点
var nodeViewMap = new Dictionary<string, BaseGraphNode>();
foreach (var nodeData in this.graph.nodes)
{
BaseGraphNode nodeView = CreateNodeView(nodeData);
AddElement(nodeView);
nodeViewMap[nodeData.guid] = nodeView;
}
// 2. 创建所有连接
foreach (var edgeData in this.graph.edges)
{
if (!nodeViewMap.TryGetValue(edgeData.outputNodeGuid, out var outputNode) ||
!nodeViewMap.TryGetValue(edgeData.inputNodeGuid, out var inputNode))
{
Debug.LogWarning($"Failed to find nodes for edge: {edgeData.outputNodeGuid} -> {edgeData.inputNodeGuid}");
continue;
}
Port outputPort = outputNode.outputContainer.Q<Port>(name: edgeData.outputPortName);
Port inputPort = inputNode.inputContainer.Q<Port>(name: edgeData.inputPortName);
if (outputPort == null || inputPort == null)
{
Debug.LogWarning(
$"Failed to find ports for edge: {outputNode.title} (Port: {edgeData.outputPortName}) -> {inputNode.title} (Port: {edgeData.inputPortName})");
continue;
}
Edge edge = outputPort.ConnectTo(inputPort);
AddElement(edge);
}
}
public void SaveGraph()
{
if (graph == null) return;
graph.nodes.Clear();
graph.edges.Clear();
// 1. 保存所有节点
foreach (var nodeView in nodes.OfType<BaseGraphNode>())
{
// 更新节点数据的位置
nodeView.NodeData.position = nodeView.GetPosition().position;
graph.nodes.Add(nodeView.NodeData);
}
// 2. 保存所有连接
foreach (var edge in edges)
{
var outputNode = (BaseGraphNode)edge.output.node;
var inputNode = (BaseGraphNode)edge.input.node;
graph.edges.Add(new EdgeData
{
outputNodeGuid = outputNode.NodeData.guid,
// --- 关键修复 ---
// 我们必须保存 port.name (内部ID/GUID),
// 而不是 port.portName (可视标签, 可能是"")
outputPortName = edge.output.name, // <-- 旧代码是: edge.output.portName
inputNodeGuid = inputNode.NodeData.guid,
inputPortName = edge.input.name // <-- 旧代码是: edge.input.portName
// --- 修复结束 ---
});
}
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
editorWindow.OnGraphUpdated();
Debug.Log($"Graph '{graph.name}' saved successfully!");
}
// 泛型创建节点方法
protected void CreateNode(BaseNodeData data, Vector2 position)
{
if (graph == null)
{
EditorUtility.DisplayDialog("No Graph", "Please select a Dialogue Graph asset first.", "OK");
return;
}
// 初始化数据
data.guid = Guid.NewGuid().ToString();
data.position = position;
// 创建节点视图
var nodeView = CreateNodeView(data);
// 将节点添加到图表数据中 (保存时会再次保存,但在此处添加以便新建的节点可以立即连接)
// _graph.nodes.Add(data); // 暂时不加等SaveGraph统一处理
// 将节点视图添加到GraphView
AddElement(nodeView);
}
protected virtual BaseGraphNode CreateNodeView(BaseNodeData data)
{
return null; // 由子类实现
}
protected virtual GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
{
if (graph != null)
{
EditorUtility.SetDirty(graph);
editorWindow.OnGraphUpdated();
}
return graphViewChange;
}
}
public partial class GraphViewBase
{
[Serializable]
protected class CopyPasteData
{
[SerializeReference] // <-- 关键:确保多态性被正确序列化
public List<BaseNodeData> nodes = new List<BaseNodeData>();
public List<EdgeData> edges = new List<EdgeData>();
}
protected string OnSerializeGraphElements(IEnumerable<GraphElement> elements)
{
var nodesToCopy = new List<BaseNodeData>();
var edgesToCopy = new List<EdgeData>();
var selectedNodeGuids = new HashSet<string>(
elements.OfType<BaseGraphNode>().Select(n => n.NodeData.guid)
);
// 遍历所有选中的节点
foreach (var nodeView in elements.OfType<BaseGraphNode>())
{
// 使用 JsonUtility 创建一个深拷贝 (注意:这对于[SerializeReference]可能不完美,但对简单字段有效)
// 一个更健壮的方法是使用System.Text.Json或Newtonsoft.Json但我们先用Unity内置的
string nodeJson = JsonUtility.ToJson(nodeView.NodeData);
// 注意JsonUtility 不支持直接反序列化到基类,我们需要知道具体类型
// 这是一个简化,它可能无法正确深拷贝 [SerializeReference] 列表
// 让我们改变策略:我们只复制数据,在粘贴时创建新实例。
nodesToCopy.Add(nodeView.NodeData); // <-- 简化:直接添加引用
}
// 遍历所有选中的边
foreach (var edge in elements.OfType<Edge>())
{
var outputNode = (BaseGraphNode)edge.output.node;
var inputNode = (BaseGraphNode)edge.input.node;
if (selectedNodeGuids.Contains(outputNode.NodeData.guid) &&
selectedNodeGuids.Contains(inputNode.NodeData.guid))
{
edgesToCopy.Add(new EdgeData
{
outputNodeGuid = outputNode.NodeData.guid,
outputPortName = edge.output.name,
inputNodeGuid = inputNode.NodeData.guid,
inputPortName = edge.input.name
});
}
}
var copyData = new CopyPasteData
{
nodes = nodesToCopy,
edges = edgesToCopy
};
// 使用 JsonUtility 序列化
return JsonUtility.ToJson(copyData, true); // 'true' for pretty print
}
protected void OnUnserializeAndPaste(string operationName, string data)
{
if (string.IsNullOrEmpty(data))
{
return;
}
CopyPasteData pastedData;
try
{
pastedData = JsonUtility.FromJson<CopyPasteData>(data);
}
catch (Exception e)
{
Debug.LogError($"Failed to deserialize pasted data: {e.Message}");
return;
}
if (pastedData == null)
{
Debug.LogError("Pasted data is null.");
return;
}
var guidMap = new Dictionary<string, string>();
var nodeViewMap = new Dictionary<string, BaseGraphNode>();
// 1. 创建新节点 (并重新生成GUID)
foreach (var nodeData in pastedData.nodes)
{
var oldGuid = nodeData.guid;
var newGuid = Guid.NewGuid().ToString();
guidMap[oldGuid] = newGuid;
// --- 关键:创建数据的深拷贝 ---
// 我们必须创建一个新实例,否则粘贴的节点将引用与原始节点相同的数据
string nodeJson = JsonUtility.ToJson(nodeData);
BaseNodeData newData = (BaseNodeData)JsonUtility.FromJson(nodeJson, nodeData.GetType());
// --- 结束 ---
newData.guid = newGuid;
newData.position += new Vector2(20, 20);
var nodeView = CreateNodeView(newData);
AddElement(nodeView);
nodeViewMap[newGuid] = nodeView;
}
// 2. 创建新边
foreach (var edgeData in pastedData.edges)
{
// 检查旧GUID是否存在于映射中 (防止只复制一个节点和它的边)
if (!guidMap.ContainsKey(edgeData.outputNodeGuid) || !guidMap.ContainsKey(edgeData.inputNodeGuid))
{
continue;
}
string newOutputGuid = guidMap[edgeData.outputNodeGuid];
string newInputGuid = guidMap[edgeData.inputNodeGuid];
var outputNode = nodeViewMap[newOutputGuid];
var inputNode = nodeViewMap[newInputGuid];
Port outputPort = outputNode.outputContainer.Q<Port>(name: edgeData.outputPortName);
Port inputPort = inputNode.inputContainer.Q<Port>(name: edgeData.inputPortName);
if (outputPort == null || inputPort == null)
{
Debug.LogWarning($"Failed to find ports for pasted edge: {edgeData.outputPortName} -> {edgeData.inputPortName}");
continue;
}
Edge edge = outputPort.ConnectTo(inputPort);
AddElement(edge);
}
// OnGraphViewChanged 会自动处理 'dirty' 标记
}
protected bool OnCanPasteSerializedData(string data)
{
// (简单的检查)
return !string.IsNullOrEmpty(data) && data.Contains("nodes") && data.Contains("edges");
}
// --- 修复结束 ---
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 57749720ca7b80e479d90181d6499476

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 50cf128ce7fdba6478161dc76a7341b8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f526df7c127191c4395a96ab46da1de6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,185 @@
using System;
using System.Linq;
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Dialog
{
public class ChoiceGraphNode : BaseGraphNode
{
private ChoiceNodeData _data;
private DialogGraphView _graphView;
public ChoiceGraphNode(ChoiceNodeData data, DialogGraphView graphView) : base(data)
{
_data = data;
_graphView = graphView;
var titleContainer = this.Q("title");
titleContainer.style.backgroundColor = new StyleColor(new Color(0.7f, 0.5f, 0.1f));
var inputPort = CreatePort(Direction.Input, Port.Capacity.Multi, "Previous");
inputContainer.Add(inputPort);
var addButton = new Button(() => AddChoice(null))
{
text = "Add Choice"
};
extensionContainer.Add(addButton);
foreach (var choice in _data.choices)
{
AddChoice(choice);
}
RefreshExpandedState();
RefreshPorts();
}
// 核心方法:添加一个选项 (UI + 端口)
private void AddChoice(ChoiceData choiceData)
{
if (choiceData == null)
{
choiceData = new ChoiceData
{
guid = Guid.NewGuid().ToString(),
choiceText = "New Choice"
};
_data.choices.Add(choiceData);
}
// A. 创建端口
var outputPort = CreatePort(Direction.Output, Port.Capacity.Single, choiceData.guid);
outputPort.portName = "";
// B. 创建选项的UI控件
// 1. 总的水平根容器
var choiceUIContainer = new VisualElement();
choiceUIContainer.style.flexDirection = FlexDirection.Row;
choiceUIContainer.style.alignItems = Align.Center;
choiceUIContainer.style.paddingLeft = 10;
choiceUIContainer.style.paddingRight = 10;
// 2. 左侧的主内容容器 (垂直)
var mainContentContainer = new VisualElement();
mainContentContainer.style.flexDirection = FlexDirection.Column;
mainContentContainer.style.flexGrow = 1;
mainContentContainer.style.marginRight = 5;
// 2a. 顶部:选项文本 (单行)
var textField = new TextField();
textField.SetValueWithoutNotify(choiceData.choiceText);
textField.RegisterValueChangedCallback(evt => choiceData.choiceText = evt.newValue);
// --- 1. 修复:移除多行文本框样式 ---
// (移除了 textField.Q("unity-text-input").style.minHeight = 40;)
// --- 修复结束 ---
mainContentContainer.Add(textField);
// 2b. 底部:并排容器 (水平)
var bottomRowContainer = new VisualElement();
bottomRowContainer.style.flexDirection = FlexDirection.Row;
bottomRowContainer.style.alignItems = Align.Center;
bottomRowContainer.style.marginTop = 5;
mainContentContainer.Add(bottomRowContainer);
// --- 2. 修复:重构 "Default" 和 "Condition" 的布局 ---
// 2b-1: "Default" 标签
var defaultLabel = new Label("Default");
defaultLabel.style.alignItems = Align.Center; // 垂直居中
defaultLabel.style.marginRight = 5;
bottomRowContainer.Add(defaultLabel);
// 2b-2: "Default" 复选框 (无标签)
var defaultToggle = new Toggle(); // <-- 移除标签
defaultToggle.SetValueWithoutNotify(choiceData.isDefault);
defaultToggle.RegisterValueChangedCallback(evt => choiceData.isDefault = evt.newValue);
defaultToggle.style.marginRight = 10; // 与条件框保持距离
bottomRowContainer.Add(defaultToggle);
var conditionLabel = new Label("Condition");
conditionLabel.style.alignItems = Align.Center; // 垂直居中
conditionLabel.style.marginRight = 5;
bottomRowContainer.Add(conditionLabel);
// 2b-3: 条件语句输入框 (将占据所有剩余空间)
var conditionField = new TextField();
conditionField.SetValueWithoutNotify(choiceData.conditionString);
conditionField.RegisterValueChangedCallback(evt => choiceData.conditionString = evt.newValue);
conditionField.style.minWidth = 100;
conditionField.style.flexGrow = 1; // <-- 关键:使其填充
//conditionField.placeholder.Set("Condition (optional)");
bottomRowContainer.Add(conditionField);
// --- 修复结束 ---
// 3. 右侧的端口和按钮容器 (垂直)
var portAndButtonContainer = new VisualElement();
portAndButtonContainer.style.flexDirection = FlexDirection.Column;
portAndButtonContainer.style.alignItems = Align.Center;
portAndButtonContainer.style.width = 40;
// 3a. 端口
portAndButtonContainer.Add(outputPort);
// 3b. "X" 删除按钮
var removeButton = new Button(() => RemoveChoice(choiceData, outputPort, choiceUIContainer))
{
text = "X"
};
removeButton.style.width = 20;
removeButton.style.height = 20;
removeButton.style.marginTop = 5;
portAndButtonContainer.Add(removeButton);
// 4. 组装
choiceUIContainer.Add(mainContentContainer);
choiceUIContainer.Add(portAndButtonContainer);
// 5. 添加分割线
var separator = new VisualElement();
separator.style.height = 1;
separator.style.backgroundColor = new StyleColor(Color.grey);
separator.style.marginTop = 5;
separator.style.marginBottom = 5;
var containerWithSeparator = new VisualElement();
containerWithSeparator.Add(separator);
containerWithSeparator.Add(choiceUIContainer);
// 6. 添加到节点
outputContainer.Add(containerWithSeparator);
RefreshExpandedState();
RefreshPorts();
}
// 核心方法:移除一个选项 (此方法保持不变)
private void RemoveChoice(ChoiceData choiceData, Port port, VisualElement container)
{
_data.choices.Remove(choiceData);
if (port.connected)
{
_graphView.DeleteElements(port.connections.ToList());
}
port.DisconnectAll();
if(container.parent != null)
{
outputContainer.Remove(container.parent);
}
RefreshExpandedState();
RefreshPorts();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4cde0007c641b2c4a80b7642ae55ff73

View File

@@ -0,0 +1,45 @@
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Dialog
{
public class CompoundDialogNode : BaseGraphNode
{
private CompoundDialogNodeData _data;
public CompoundDialogNode(CompoundDialogNodeData data) : base(data)
{
_data = data;
// 1. 设置节点颜色 (选择一个灰色)
var titleContainer = this.Q("title");
titleContainer.style.backgroundColor = new StyleColor(new Color(0.6f, 0.6f, 0.6f));
// 2. 添加端口 (像DialogNode一样有输入和输出)
var inputPort = CreatePort(Direction.Input, Port.Capacity.Multi, "Previous");
var outputPort = CreatePort(Direction.Output, Port.Capacity.Single, "Next");
inputContainer.Add(inputPort);
outputContainer.Add(outputPort);
// 3. 添加自定义UI (一个 ObjectField 用于 TextAsset)
var assetField = new ObjectField("TextAsset")
{
objectType = typeof(TextAsset),
allowSceneObjects = false
};
assetField.SetValueWithoutNotify(_data.compoundDialogAsset);
assetField.RegisterValueChangedCallback(evt => {
_data.compoundDialogAsset = evt.newValue as TextAsset;
});
extensionContainer.Add(assetField);
// 刷新
RefreshExpandedState();
RefreshPorts();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f39fd5f93076c404ab92b10fe4c9b46f

View File

@@ -0,0 +1,149 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Dialog
{
public class DialogGraphNode : BaseGraphNode
{
private DialogNodeData _data;
private ObjectField _characterField;
private DropdownField _expressionField;
public DialogGraphNode(DialogNodeData data) : base(data)
{
_data = data;
var titleContainer = this.Q("title");
titleContainer.style.backgroundColor = new StyleColor(new Color(0.2f, 0.3f, 0.6f)); // 蓝色
// 1. 添加端口
var inputPort = CreatePort(Direction.Input, Port.Capacity.Multi, "Previous");
var outputPort = CreatePort(Direction.Output, Port.Capacity.Single, "Next");
inputContainer.Add(inputPort);
outputContainer.Add(outputPort);
// 2. 添加自定义UI字段
// (我们使用 extensionContainer它位于 mainContainer 下方用于放置自定义UI)
// -- 角色名 --
_characterField = new ObjectField("Character")
{
objectType = typeof(CharacterData),
allowSceneObjects = false
};
_characterField.SetValueWithoutNotify(_data.characterData);
_characterField.RegisterValueChangedCallback(OnCharacterDataChanged);
extensionContainer.Add(_characterField);
// -- 表情 (DropdownField) --
_expressionField = new DropdownField("Expression");
_expressionField.RegisterValueChangedCallback(OnExpressionChanged);
extensionContainer.Add(_expressionField);
// -- 位置 --
var positionField = new Vector2Field("Position");
positionField.SetValueWithoutNotify(_data.characterPosition);
var posFieldInput = positionField.Q(className: "unity-base-field__input");
posFieldInput.style.minWidth = 150;
positionField.RegisterValueChangedCallback(evt => {
_data.characterPosition = evt.newValue;
});
extensionContainer.Add(positionField);
// -- 对话内容 (TextArea) --
var dialogueField = new TextField("Dialogue") { multiline = true };
dialogueField.SetValueWithoutNotify(_data.dialogueText);
dialogueField.RegisterValueChangedCallback(evt => {
_data.dialogueText = evt.newValue;
});
// 调整TextArea的样式使其看起来像一个多行文本框
dialogueField.Q("unity-text-input").style.minWidth = 150;
extensionContainer.Add(dialogueField);
// -- 语音 (AudioClip) --
var audioClipField = new ObjectField("AudioClip")
{
objectType = typeof(AudioClip),
allowSceneObjects = false // 我们只接受 Project 中的资源
};
audioClipField.SetValueWithoutNotify(_data.audioClip);
audioClipField.RegisterValueChangedCallback(evt => {
_data.audioClip = evt.newValue as AudioClip;
});
extensionContainer.Add(audioClipField);
// 刷新布局
PopulateExpressionDropdown();
RefreshExpandedState();
RefreshPorts();
}
// --- 6. 核心逻辑:当 ObjectField 变化时调用 ---
private void OnCharacterDataChanged(ChangeEvent<Object> evt)
{
_data.characterData = evt.newValue as CharacterData;
// 清空旧的表情
_data.expressionKey = null;
// 重新填充下拉框
PopulateExpressionDropdown();
}
// --- 7. 核心逻辑:当 DropdownField 变化时调用 ---
private void OnExpressionChanged(ChangeEvent<string> evt)
{
// 保存新的表情key
_data.expressionKey = evt.newValue;
}
// --- 8. 核心逻辑:填充/更新表情下拉框 ---
private void PopulateExpressionDropdown()
{
// 清空旧选项
_expressionField.choices.Clear();
if (_data.characterData == null)
{
// 如果没有角色,禁用下拉框
_expressionField.SetValueWithoutNotify(null);
_expressionField.SetEnabled(false);
return;
}
// 从 CharacterData 中提取所有表情的 "name"
List<string> expressionKeys = _data.characterData.expressions
.Select(expr => expr.key)
.ToList();
if (expressionKeys.Count == 0)
{
// 如果有角色但没表情,禁用下拉框
_expressionField.SetValueWithoutNotify(null);
_expressionField.SetEnabled(false);
return;
}
// 填充下拉框
_expressionField.choices = expressionKeys;
_expressionField.SetEnabled(true);
// 检查旧的key是否还存在于新列表中
if (string.IsNullOrEmpty(_data.expressionKey) || !expressionKeys.Contains(_data.expressionKey))
{
// 如果不存在 (或为空),自动选择第一个作为默认值
_data.expressionKey = expressionKeys[0];
}
// 设置下拉框的当前值
_expressionField.SetValueWithoutNotify(_data.expressionKey);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d1de4576f57992f4abdbf42cf5d93a5d

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c21355e0c16093d40a6e8a5871c75c5e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,125 @@
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Dialog
{
public class DialogGraphEditorWindow : EditorWindowBase
{
private DialogGraphView _graphView => graphView as DialogGraphView;
private DialogGraph _currentGraph => currentGraph as DialogGraph;
[MenuItem("Dialog/Dialog Graph Editor")]
public static void OpenWindow()
{
var window = GetWindow<DialogGraphEditorWindow>();
window.titleContent = new GUIContent("Dialog Graph Editor");
}
[OnOpenAsset(1)]
public static bool OnOpenAsset(int instanceID, int line)
{
if (EditorUtility.InstanceIDToObject(instanceID) is DialogGraph graph)
{
var window = GetWindow<DialogGraphEditorWindow>();
// OnOpenAsset 可能会在 OnEnable 之前运行
// 我们只设置数据,加载操作交给 OnEnable 或 SetGraph
window.SetGraph(graph);
return true;
}
return false;
}
// --- *** 关键修复:重构生命周期 *** ---
private void OnEnable()
{
// --- 1. 新增:创建工具栏 ---
var toolbar = new Toolbar();
var saveButton = new Button() { text = "Save Dialog" };
// 将按钮添加到工具栏
toolbar.Add(saveButton);
// 将工具栏添加到窗口的根
rootVisualElement.Add(toolbar);
// --- 结束新增 ---
// 2. 创建 GraphView (保持不变)
CreateGraphView();
// --- 3. 新增:将按钮的点击事件
// 此时 _graphView 必定已被创建
saveButton.clicked += () => _graphView.SaveGraph();
// --- 结束新增 ---
// 4. 加载图表 (保持不变)
if (_currentGraph != null)
{
_graphView.schedule.Execute(() => _graphView.LoadGraph(_currentGraph));
}
UpdateWindowTitle();
}
private void OnDisable()
{
// OnDisable 时移除UI元素
if (_graphView != null)
{
rootVisualElement.Remove(_graphView);
}
}
protected override void CreateGraphView()
{
// 确保我们不会意外地创建和添加多个 GraphView
if (_graphView != null)
{
rootVisualElement.Remove(_graphView);
}
graphView = new DialogGraphView(this)
{
name = "Dialog Graph View",
style = { flexGrow = 1 }
};
rootVisualElement.Add(_graphView);
}
public override void SetGraph(GraphBase graph)
{
currentGraph = graph;
UpdateWindowTitle();
// 如果 GraphView 已经存在 (OnEnable 已经运行),
// 立即安排加载新图表
if (_graphView != null)
{
_graphView.schedule.Execute(() => _graphView.LoadGraph(_currentGraph));
}
// 如果 GraphView 尚不存在 (OnEnable 还没运行),
// OnEnable 自己会处理加载
}
// --- *** 修复结束 *** ---
public override void OnGraphUpdated()
{
UpdateWindowTitle();
}
private void UpdateWindowTitle()
{
windowTitle = "Dialog Graph Editor";
string graphName = _currentGraph != null ? _currentGraph.name : " (No Graph Loaded)";
bool isDirty = _currentGraph != null && EditorUtility.IsDirty(_currentGraph);
titleContent = new GUIContent($"{windowTitle} - {graphName}{(isDirty ? "*" : "")}");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: edb12263f493f38458ff5b9fbdca659f

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Dialog
{
public partial class DialogGraphView : GraphViewBase
{
private DialogGraph _graph => graph as DialogGraph;
private DialogGraphEditorWindow _editorWindow => editorWindow as DialogGraphEditorWindow;
public DialogGraphView(DialogGraphEditorWindow window)
{
editorWindow = window;
// 启用缩放、拖拽、选择等操作
this.AddManipulator(new ContentZoomer());
this.AddManipulator(new ContentDragger());
this.AddManipulator(new SelectionDragger());
this.AddManipulator(new RectangleSelector());
// 添加网格背景
var grid = new GridBackground();
Insert(0, grid);
grid.StretchToParentSize();
// 注册GraphView改变的回调
graphViewChanged = OnGraphViewChanged;
// 添加右键菜单
AddContextMenuManipulator();
serializeGraphElements = OnSerializeGraphElements;
unserializeAndPaste = OnUnserializeAndPaste;
canPasteSerializedData = OnCanPasteSerializedData;
}
// 构建右键菜单
private void AddContextMenuManipulator()
{
var manipulator = new ContextualMenuManipulator(evt =>
{
Vector2 pos = evt.localMousePosition;
evt.menu.AppendAction("Create Dialog Node", (a) => CreateNode(new DialogNodeData(), pos));
evt.menu.AppendAction("Create Compound Dialog Node", (a) => CreateNode(new CompoundDialogNodeData(), pos));
evt.menu.AppendAction("Create Choice Node", (a) => CreateNode(new ChoiceNodeData(), pos));
evt.menu.AppendAction("Create Condition Node", (a) => CreateNode(new ConditionNodeData(), pos));
evt.menu.AppendAction("Create Event Node", (a) => CreateNode(new EventNodeData(), pos));
evt.menu.AppendSeparator();
evt.menu.AppendAction("Create Start Node", (a) => CreateNode(new StartNodeData(), pos));
evt.menu.AppendAction("Create End Node", (a) => CreateNode(new EndNodeData(), pos));
evt.menu.AppendSeparator();
evt.menu.AppendAction("Save Asset", (a) => SaveGraph(),
(a) => _graph != null ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
});
this.AddManipulator(manipulator);
}
// GraphView发生变化时的回调
protected override GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
{
// 处理连接
if (graphViewChange.edgesToCreate != null)
{
foreach (var edge in graphViewChange.edgesToCreate)
{
// (可选) 在此处添加额外的连接验证逻辑
// Debug.Log($"Edge created from {edge.output.node.title} to {edge.input.node.title}");
}
}
// (可选) 处理删除
if (graphViewChange.elementsToRemove != null)
{
// ...
}
base.OnGraphViewChanged(graphViewChange);
return graphViewChange;
}
// 节点视图的工厂方法
protected override BaseGraphNode CreateNodeView(BaseNodeData data)
{
switch (data)
{
case StartNodeData startData:
return new StartGraphNode(startData);
case EndNodeData endData:
return new EndGraphNode(endData);
case ChoiceNodeData choiceData:
return new ChoiceGraphNode(choiceData, this);
case DialogNodeData dialogData:
return new DialogGraphNode(dialogData);
case CompoundDialogNodeData compoundData:
return new CompoundDialogNode(compoundData);
case ConditionNodeData conditionData:
return new ConditionGraphNode(conditionData);
case EventNodeData eventData:
return new EventGraphNode(eventData);
default:
Debug.LogError($"Unknown node data type: {data.GetType()}");
throw new ArgumentException($"Unknown node data type: {data.GetType()}");
}
}
// (必需) 重写 GetCompatiblePorts 以允许连接
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
ports.ForEach((port) =>
{
if (startPort != port && startPort.node != port.node && startPort.direction != port.direction)
{
compatiblePorts.Add(port);
}
});
return compatiblePorts;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f694b29c3d1202a4cba7f81a0e308b48

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e02bc65fdf9dd84390dc0509692ba3e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 31a0824d0344e0340baf9ae4805f08bd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
using SLSFramework.StorySystem.Dialog;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Storyline
{
public class StorylineDialogNode : BaseGraphNode
{
private StorylineDialogNodeData _data;
public StorylineDialogNode(StorylineDialogNodeData data) : base(data)
{
_data = data;
// 1. 设置节点颜色
var titleContainer = this.Q("title");
titleContainer.style.backgroundColor = new StyleColor(new Color(0.3f, 0.7f, 0.4f)); // 蓝绿色
// 2. 添加端口
var inputPort = CreatePort(Direction.Input, Port.Capacity.Multi, "Previous");
var outputPort = CreatePort(Direction.Output, Port.Capacity.Single, "Next");
inputContainer.Add(inputPort);
outputContainer.Add(outputPort);
// 3. 添加 ObjectField
var assetField = new ObjectField("Dialog Graph")
{
objectType = typeof(DialogGraph),
allowSceneObjects = false
};
assetField.SetValueWithoutNotify(_data.dialogGraph);
assetField.RegisterValueChangedCallback(evt => { _data.dialogGraph = evt.newValue as DialogGraph; });
extensionContainer.Add(assetField);
// 4. --- 关键:添加 "Open" 按钮 ---
var openButton = new Button(() =>
{
if (_data.dialogGraph != null)
{
// 找到 DialogGraph 窗口并打开它
var window = EditorWindow.GetWindow<DialogGraphEditorWindow>();
window.SetGraph(_data.dialogGraph);
}
else
{
EditorUtility.DisplayDialog("Graph Not Set", "Please assign a Dialog Graph asset first.", "OK");
}
})
{
text = "Open Graph"
};
extensionContainer.Add(openButton);
RefreshExpandedState();
RefreshPorts();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7958263785c327641b684242ce2f8c9a

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e3e0d4ada943b7740ba915857046c87c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,99 @@
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Storyline
{
public class StorylineGraphEditorWindow : EditorWindowBase
{
private StorylineGraphView _graphView => graphView as StorylineGraphView;
private StorylineGraph _currentGraph => currentGraph as StorylineGraph;
[MenuItem("Storyline/Storyline Graph Editor")]
public static void OpenWindow()
{
var window = GetWindow<StorylineGraphEditorWindow>();
window.titleContent = new GUIContent("Storyline Graph Editor");
}
[OnOpenAsset(1)]
public static bool OnOpenAsset(int instanceID, int line)
{
if (EditorUtility.InstanceIDToObject(instanceID) is StorylineGraph graph)
{
var window = GetWindow<StorylineGraphEditorWindow>();
window.SetGraph(graph);
return true;
}
return false;
}
private void OnEnable()
{
var toolbar = new Toolbar();
var saveButton = new Button() { text = "Save Asset" };
toolbar.Add(saveButton);
rootVisualElement.Add(toolbar);
CreateGraphView();
// 确保 _graphView 已创建
saveButton.clicked += () => _graphView.SaveGraph();
if (_currentGraph != null)
{
_graphView.schedule.Execute(() => _graphView.LoadGraph(_currentGraph));
}
UpdateWindowTitle();
}
private void OnDisable()
{
if (_graphView != null)
{
rootVisualElement.Remove(_graphView);
}
}
protected override void CreateGraphView()
{
if (_graphView != null)
{
rootVisualElement.Remove(_graphView);
}
graphView = new StorylineGraphView(this)
{
name = "Storyline Graph View",
style = { flexGrow = 1 }
};
rootVisualElement.Add(_graphView);
}
public override void SetGraph(GraphBase graph)
{
currentGraph = graph;
UpdateWindowTitle();
if (_graphView != null)
{
_graphView.schedule.Execute(() => _graphView.LoadGraph(_currentGraph));
}
}
public override void OnGraphUpdated()
{
UpdateWindowTitle();
}
private void UpdateWindowTitle()
{
windowTitle = "Storyline Graph";
string graphName = _currentGraph != null ? _currentGraph.name : " (No Graph Loaded)";
bool isDirty = _currentGraph != null && EditorUtility.IsDirty(_currentGraph);
titleContent = new GUIContent($"{windowTitle} - {graphName}{(isDirty ? "*" : "")}");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5b3352bb0a4563d4f8778594a486a3c9

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SLSFramework.StorySystem.Dialog;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
namespace SLSFramework.StorySystem.Storyline
{
public class StorylineGraphView : GraphViewBase
{
private StorylineGraph _graph => graph as StorylineGraph;
private StorylineGraphEditorWindow _editorWindow => editorWindow as StorylineGraphEditorWindow;
// (复制/粘贴的数据容器)
[Serializable]
private class CopyPasteData { /* ... (和 DialogGraphView 中完全一样) ... */ }
public StorylineGraphView(StorylineGraphEditorWindow window)
{
editorWindow = window;
// (所有 Manipulator 和 GridBackground 代码... 和 DialogGraphView 一样)
this.AddManipulator(new ContentZoomer());
this.AddManipulator(new ContentDragger());
this.AddManipulator(new SelectionDragger());
this.AddManipulator(new RectangleSelector());
var grid = new GridBackground();
Insert(0, grid);
grid.StretchToParentSize();
graphViewChanged = OnGraphViewChanged;
AddContextMenuManipulator();
// 启用复制/粘贴 (和 DialogGraphView 一样)
serializeGraphElements = OnSerializeGraphElements;
unserializeAndPaste = OnUnserializeAndPaste;
canPasteSerializedData = OnCanPasteSerializedData;
}
// --- 简化的右键菜单 ---
private void AddContextMenuManipulator()
{
var manipulator = new ContextualMenuManipulator(evt =>
{
Vector2 pos = evt.localMousePosition;
// --- 只添加我们需要
evt.menu.AppendAction("Create Start Node", (a) => CreateNode(new StartNodeData(), pos));
evt.menu.AppendAction("Create End Node", (a) => CreateNode(new EndNodeData(), pos));
evt.menu.AppendAction("Create Condition Node", (a) => CreateNode(new ConditionNodeData(), pos));
evt.menu.AppendAction("Create Storyline Dialog Node", (a) => CreateNode(new StorylineDialogNodeData(), pos));
evt.menu.AppendSeparator();
evt.menu.AppendAction("Save Asset", (a) => SaveGraph(), (a) => _graph != null ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
});
this.AddManipulator(manipulator);
}
// GraphView发生变化时的回调
protected override GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
{
// 处理连接
if (graphViewChange.edgesToCreate != null)
{
foreach (var edge in graphViewChange.edgesToCreate)
{
// (可选) 在此处添加额外的连接验证逻辑
// Debug.Log($"Edge created from {edge.output.node.title} to {edge.input.node.title}");
}
}
// (可选) 处理删除
if (graphViewChange.elementsToRemove != null)
{
// ...
}
base.OnGraphViewChanged(graphViewChange);
return graphViewChange;
}
// --- 简化的节点工厂 ---
protected override BaseGraphNode CreateNodeView(BaseNodeData data)
{
switch (data)
{
// --- 重用 Dialog 系统的节点UI ---
case StartNodeData startData:
return new StartGraphNode(startData);
case EndNodeData endData:
return new EndGraphNode(endData);
case ConditionNodeData conditionData:
return new ConditionGraphNode(conditionData);
case StorylineDialogNodeData storyDialogData:
return new StorylineDialogNode(storyDialogData);
default:
Debug.LogWarning($"Unknown node data type: {data.GetType()}");
return null;
}
}
// (必需) 重写 GetCompatiblePorts 以允许连接
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
ports.ForEach((port) =>
{
if (startPort != port && startPort.node != port.node && startPort.direction != port.direction)
{
compatiblePorts.Add(port);
}
});
return compatiblePorts;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e6c66e8ba85e73f4e913053be3226ca9