DataEditor & StorySystem Graph
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
%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: 9a4129cdd7011ca46b83d8c17d9f3623, type: 3}
|
||||
m_Name: CardData
|
||||
m_EditorClassIdentifier: GameAPI::Continentis.MainGame.Card.CardData
|
||||
modName: Basic
|
||||
categoryName:
|
||||
className: ArcaneMissiles
|
||||
displayName: Card_Basic_ArcaneMissiles_DisplayName
|
||||
cardRarity: 0
|
||||
cardType: 0
|
||||
keywords: []
|
||||
cardSprite: {fileID: 0}
|
||||
cardLayoutTags: []
|
||||
functionText: Card_Basic_ArcaneMissiles_FunctionText
|
||||
cardDescription:
|
||||
baseWeight: 1
|
||||
variableAttributes:
|
||||
dictionaryList: []
|
||||
dividerPosProp: 0.5
|
||||
originalAttributes:
|
||||
dictionaryList: []
|
||||
dividerPosProp: 0.5
|
||||
runtimeCurrentAttributes:
|
||||
dictionaryList: []
|
||||
dividerPosProp: 0.5
|
||||
upgradeNode:
|
||||
sourceCard: {fileID: 0}
|
||||
isTerminalNode: 0
|
||||
isInfiniteUpgrade: 0
|
||||
maxUpgradeLevel: 0
|
||||
upgradeCards: []
|
||||
customDescriptions: []
|
||||
prefabRefs: []
|
||||
derivativeCardDataRefs: []
|
||||
derivativeCharacterDataRefs: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e219fcd155266f34abcddb68fb199193
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -13,13 +13,14 @@ MonoBehaviour:
|
||||
m_Name: CardData_Basic_Cohesion
|
||||
m_EditorClassIdentifier:
|
||||
modName: Basic
|
||||
categoryName: General.Skills
|
||||
className: Cohesion
|
||||
displayName: Card_Basic_Cohesion_DisplayName
|
||||
cardRarity: 20
|
||||
cardType: 10
|
||||
tags:
|
||||
- TargetSelf
|
||||
keywords: []
|
||||
cardSprite: {fileID: 21300000, guid: b07c10d1954a22246bac8ce4e1435846, type: 3}
|
||||
cardLayoutTags: []
|
||||
functionText: Card_Basic_Cohesion_FunctionText
|
||||
cardDescription:
|
||||
baseWeight: 1
|
||||
|
||||
@@ -13,13 +13,14 @@ MonoBehaviour:
|
||||
m_Name: CardData_Basic_FightingInspiration
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Continentis.MainGame.Card.CardData
|
||||
modName: Basic
|
||||
categoryName: General.Skills
|
||||
className: FightingInspiration
|
||||
displayName: Card_Basic_FightingInspiration_DisplayName
|
||||
cardRarity: 20
|
||||
cardType: 10
|
||||
tags:
|
||||
- TargetSelf
|
||||
keywords: []
|
||||
cardSprite: {fileID: 21300000, guid: 4ba23069b5c59e448a6aa73cfb3bcabc, type: 3}
|
||||
cardLayoutTags: []
|
||||
functionText: Card_Basic_FightingInspiration_FunctionText
|
||||
cardDescription:
|
||||
baseWeight: 1
|
||||
|
||||
@@ -7,7 +7,7 @@ using Continentis.MainGame.Commands;
|
||||
using SLSFramework.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.Mods.Basic.Cards
|
||||
namespace Continentis.Mods.Basic.Cards.General.Skills
|
||||
{
|
||||
public class Cohesion : CardLogicBase
|
||||
{
|
||||
|
||||
@@ -28,7 +28,9 @@ namespace Continentis.MainGame.Card
|
||||
[CreateAssetMenu(menuName = "Continentis/MainGame/Card/CardData", fileName = "CardData")]
|
||||
public partial class CardData : ScriptableObject
|
||||
{
|
||||
[Header("Fundamental")] public string modName;
|
||||
[Header("Fundamental")]
|
||||
public string modName;
|
||||
public string categoryName;
|
||||
public string className;
|
||||
public string displayName;
|
||||
public Rarity cardRarity;
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Continentis.MainGame.Card
|
||||
{
|
||||
// 存储我们需要自定义绘制的属性的引用
|
||||
private SerializedProperty modNameProp;
|
||||
private SerializedProperty categoryNameProp;
|
||||
private SerializedProperty classNameProp;
|
||||
private SerializedProperty displayNameProp;
|
||||
private SerializedProperty cardRarityProp;
|
||||
@@ -47,6 +48,7 @@ namespace Continentis.MainGame.Card
|
||||
// 在启用时,根据我们修改后的字段名找到对应的SerializedProperty
|
||||
modNameProp = serializedObject.FindProperty("modName");
|
||||
classNameProp = serializedObject.FindProperty("className");
|
||||
categoryNameProp = serializedObject.FindProperty("categoryName");
|
||||
displayNameProp = serializedObject.FindProperty("displayName");
|
||||
cardRarityProp = serializedObject.FindProperty("cardRarity");
|
||||
cardTypeProp = serializedObject.FindProperty("cardType");
|
||||
@@ -73,20 +75,41 @@ namespace Continentis.MainGame.Card
|
||||
// 我们把它从所有自动绘制的属性中分离出来,放在最前面或最后面,让布局更清晰
|
||||
EditorGUILayout.Space(); // 增加一点间距
|
||||
EditorGUILayout.LabelField("Fundamental", EditorStyles.boldLabel);
|
||||
if (DrawTypeSelectorGUI(classNameProp, "Card Logic Class", typeof(CardLogicBase), out Type outType, "Continentis.Mods", "Cards"))
|
||||
{
|
||||
string className = classNameProp.stringValue;
|
||||
string modName = outType.Namespace!.Replace("Continentis.Mods.", "").Split('.')[0];
|
||||
string displayName = "Card_" + modName + "_" + className + "_DisplayName";
|
||||
string functionTextName = "Card_" + modName + "_" + className + "_FunctionText";
|
||||
|
||||
DrawSearchableTypeSelector(
|
||||
classNameProp, // 1. 存储类名的字符串属性
|
||||
"Card Logic Class", // 2. 显示的标签
|
||||
typeof(CardLogicBase), // 3. 要搜索的基类
|
||||
(outType) => // 4. 【关键】当用户选择后执行的回调
|
||||
{
|
||||
string className = outType.Name;
|
||||
string modName = outType.Namespace!.Replace("Continentis.Mods.", "").Split('.')[0];
|
||||
string categoryName = outType.Namespace!.Replace("Continentis.Mods." + modName + ".Cards", "");
|
||||
if (!string.IsNullOrEmpty(categoryName))
|
||||
{
|
||||
categoryName = outType.Namespace!.Replace("Continentis.Mods." + modName + ".Cards", "").Substring(1); // 去掉开头的点
|
||||
}
|
||||
string displayName = "Card_" + modName + "_" + className + "_DisplayName";
|
||||
string functionTextName = "Card_" + modName + "_" + className + "_FunctionText";
|
||||
|
||||
modNameProp.stringValue = modName;
|
||||
displayNameProp.stringValue = displayName;
|
||||
functionTextProp.stringValue = functionTextName;
|
||||
}
|
||||
classNameProp.stringValue = className;
|
||||
modNameProp.stringValue = modName;
|
||||
categoryNameProp.stringValue = categoryName;
|
||||
displayNameProp.stringValue = displayName;
|
||||
functionTextProp.stringValue = functionTextName;
|
||||
|
||||
Debug.Log(outType.FullName);
|
||||
Debug.Log($"modName: {modName}, categoryName: {categoryName}, className: {className}, displayName: {displayName}, functionText: {functionTextName}");
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
},
|
||||
"Continentis.Mods", // 5. 你的 namespacePrefix
|
||||
"Cards" // 6. 你的 namespaceToRemove (注意:根据你的代码,这里不应包含".")
|
||||
);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
EditorGUILayout.PropertyField(modNameProp);
|
||||
EditorGUILayout.PropertyField(categoryNameProp);
|
||||
EditorGUILayout.PropertyField(classNameProp);
|
||||
EditorGUILayout.PropertyField(displayNameProp);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Continentis.MainGame.Character
|
||||
if (_haveCustomClassProp.boolValue)
|
||||
{
|
||||
// 如果勾选,则显示class选择器
|
||||
DrawTypeSelectorGUI(_classFullNameProp, "Character Class", typeof(CharacterBase), out _, "Continentis.Mods", ".Characters");
|
||||
DrawSearchableTypeSelector(_classFullNameProp, "Character Class", typeof(CharacterBase), null, "Continentis.Mods", "Characters");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -74,15 +74,22 @@ namespace Continentis.MainGame.Equipment
|
||||
if (_haveCustomClassProp.boolValue)
|
||||
{
|
||||
// 如果勾选,则显示class选择器 (假设基类为EquipmentBase, 命名空间为.Equipments)
|
||||
if (DrawTypeSelectorGUI(_classNameProp, "Equipment Class", typeof(EquipmentBase), out Type outType, "Continentis.Mods", ".Equipments"))
|
||||
{
|
||||
string className = _classNameProp.stringValue;
|
||||
string modName = outType.Namespace!.Replace("Continentis.Mods.", "").Split('.')[0];
|
||||
string displayName = "Card_" + modName + "_" + className + "_DisplayName";
|
||||
|
||||
_modNameProp.stringValue = modName;
|
||||
_displayNameProp.stringValue = displayName;
|
||||
}
|
||||
DrawSearchableTypeSelector(
|
||||
_classNameProp,
|
||||
"Equipment Class",
|
||||
typeof(EquipmentBase),
|
||||
(outType) =>
|
||||
{
|
||||
string className = outType.Name;
|
||||
string modName = outType.Namespace!.Replace("Continentis.Mods.", "").Split('.')[0];
|
||||
string displayName = "Card_" + modName + "_" + className + "_DisplayName";
|
||||
|
||||
_classNameProp.stringValue = className;
|
||||
_modNameProp.stringValue = modName;
|
||||
_displayNameProp.stringValue = displayName;
|
||||
},
|
||||
"Continentis.Mods",
|
||||
"Equipments");
|
||||
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
}
|
||||
|
||||
8
Assets/Scripts/ScriptExtensions/StorySystem.meta
Normal file
8
Assets/Scripts/ScriptExtensions/StorySystem.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d79e988c91a1a64449e1c0ab3ffa900e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fa1138f75d85344ab4e3b8312e71959
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88df947440cfd3841b8e5a545437b253
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ad08c73d10275f498f9643e948dade4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89062ba4c7f90314c86ed9cedb4053cf
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95a4ba99bc298c74283b008b8b25b8bd
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e82960b057418454eab7e7eefcc71a14
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: baf926c674d6ea842b99cda704461ee2
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3bf9fbf0029def4bbb8ce1e1ed0628c
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4019e9906ff3c1f46b7db8bbb8604ef3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ada5daa7b9068f4c98c9af0655aef6d
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
// --- 修复结束 ---
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57749720ca7b80e479d90181d6499476
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50cf128ce7fdba6478161dc76a7341b8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f526df7c127191c4395a96ab46da1de6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cde0007c641b2c4a80b7642ae55ff73
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f39fd5f93076c404ab92b10fe4c9b46f
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1de4576f57992f4abdbf42cf5d93a5d
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c21355e0c16093d40a6e8a5871c75c5e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 ? "*" : "")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edb12263f493f38458ff5b9fbdca659f
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f694b29c3d1202a4cba7f81a0e308b48
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e02bc65fdf9dd84390dc0509692ba3e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31a0824d0344e0340baf9ae4805f08bd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7958263785c327641b684242ce2f8c9a
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3e0d4ada943b7740ba915857046c87c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 ? "*" : "")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b3352bb0a4563d4f8778594a486a3c9
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6c66e8ba85e73f4e913053be3226ca9
|
||||
8
Assets/Scripts/ScriptExtensions/StorySystem/Runtime.meta
Normal file
8
Assets/Scripts/ScriptExtensions/StorySystem/Runtime.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c706945d6aec0f44b98cfec5deb98ce4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86843bd5cd5f21b4fb945730e1dd5cce
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df71a91d0bb9e834596890d9c6dbb9a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewCharacterData", menuName = "StorySystem/Dialog/Character Data")]
|
||||
public partial class CharacterData : ScriptableObject
|
||||
{
|
||||
public string characterName;
|
||||
|
||||
public List<Expression> expressions = new List<Expression>();
|
||||
}
|
||||
|
||||
public partial class CharacterData
|
||||
{
|
||||
[Serializable]
|
||||
public class Expression
|
||||
{
|
||||
public string key;
|
||||
public Sprite sprite;
|
||||
|
||||
// (将来你还可以在这里添加 audioClip, 动画, etc.)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a58101ba7d997824d889bd04baf1c18c
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewDialogueGraph", menuName = "StorySystem/Dialog/Dialog Graph")]
|
||||
public class DialogGraph : GraphBase
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a36a4bb7be9da947beb608206dc240f
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem.Dialog
|
||||
{
|
||||
// ----------------------------------------------------------------------
|
||||
// 各种具体节点的数据
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
[Serializable]
|
||||
public class DialogNodeData : BaseNodeData
|
||||
{
|
||||
public CharacterData characterData;
|
||||
public string expressionKey; // 用于存储所选表情的 name
|
||||
public Vector2 characterPosition;
|
||||
[TextArea(3, 10)] public string dialogueText;
|
||||
public AudioClip audioClip;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CompoundDialogNodeData : BaseNodeData
|
||||
{
|
||||
public TextAsset compoundDialogAsset;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ChoiceData
|
||||
{
|
||||
public string guid;
|
||||
public string choiceText;
|
||||
public bool isDefault;
|
||||
public string conditionString;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ChoiceNodeData : BaseNodeData
|
||||
{
|
||||
public List<ChoiceData> choices = new List<ChoiceData>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77b3f3b496104dc4d887f70412d2f57f
|
||||
@@ -0,0 +1,53 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor.Experimental.GraphView;
|
||||
|
||||
// 节点连接的数据结构
|
||||
[Serializable]
|
||||
public class EdgeData
|
||||
{
|
||||
public string outputNodeGuid;
|
||||
public string outputPortName;
|
||||
public string inputNodeGuid;
|
||||
public string inputPortName;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 基础节点数据
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
[Serializable]
|
||||
public abstract class BaseNodeData
|
||||
{
|
||||
public string guid;
|
||||
public Vector2 position;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class StartNodeData : BaseNodeData
|
||||
{
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EndNodeData : BaseNodeData
|
||||
{
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ConditionNodeData : BaseNodeData
|
||||
{
|
||||
public string conditionString = "";
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EventNodeData : BaseNodeData
|
||||
{
|
||||
public string eventString = "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95633a09783e9ad47a4980934e8af4d0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e50c42418f41ae44c81085f2a00e2882
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem.Storyline
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewStorylineGraph", menuName = "StorySystem/Storyline/Storyline Graph")]
|
||||
public class StorylineGraph : GraphBase
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c87462c2ff921fb4e9e5f52ab691a665
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using SLSFramework.StorySystem.Dialog;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSFramework.StorySystem.Storyline
|
||||
{
|
||||
[Serializable]
|
||||
public class StorylineDialogNodeData : BaseNodeData
|
||||
{
|
||||
// 对 DialogGraph 资源的引用
|
||||
public DialogGraph dialogGraph;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8533bcb32514564d98e8c49f29b13ea
|
||||
8
Assets/Scripts/ScriptExtensions/StorySystem/Samples.meta
Normal file
8
Assets/Scripts/ScriptExtensions/StorySystem/Samples.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 986ca523e75264d41a6778d04384c154
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
%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: a58101ba7d997824d889bd04baf1c18c, type: 3}
|
||||
m_Name: NewCharacterData
|
||||
m_EditorClassIdentifier: GameAPI::SLSFramework.StorySystem.Dialogue.CharacterData
|
||||
characterName: SLS
|
||||
expressions:
|
||||
- key: Default
|
||||
sprite: {fileID: 21300000, guid: f36de84dc677bf244842bffd925f3d1f, type: 3}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd38f8d56d51bce4cba0715d468b6333
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,131 @@
|
||||
%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: 9a36a4bb7be9da947beb608206dc240f, type: 3}
|
||||
m_Name: NewDialogueGraph
|
||||
m_EditorClassIdentifier: GameAPI::SLSFramework.StorySystem.Dialog.DialogGraph
|
||||
nodes:
|
||||
- rid: 5266334379010163019
|
||||
- rid: 5266334379010163046
|
||||
- rid: 5266334379010163017
|
||||
- rid: 5266334379010163047
|
||||
- rid: 5266334379010163048
|
||||
- rid: 5266334379010163049
|
||||
- rid: 5266334379010163050
|
||||
- rid: 5266334379010163018
|
||||
- rid: 5266334379010163051
|
||||
edges:
|
||||
- outputNodeGuid: 72f2b2bc-aca6-4892-8610-7e509ac9c074
|
||||
outputPortName: Next
|
||||
inputNodeGuid: 738fe6c7-df1b-4925-8e81-72e81ed982a7
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: f6633c3e-9e6c-41dd-8828-09e7562ff730
|
||||
outputPortName: Next
|
||||
inputNodeGuid: 090a01a7-3a45-4edc-9928-a6beb5dfa031
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 738fe6c7-df1b-4925-8e81-72e81ed982a7
|
||||
outputPortName: Next
|
||||
inputNodeGuid: 485ebe8c-5f63-4926-b1fd-273f4f71ab66
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 485ebe8c-5f63-4926-b1fd-273f4f71ab66
|
||||
outputPortName: True
|
||||
inputNodeGuid: f6633c3e-9e6c-41dd-8828-09e7562ff730
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 485ebe8c-5f63-4926-b1fd-273f4f71ab66
|
||||
outputPortName: False
|
||||
inputNodeGuid: a928a097-4270-48d7-b13c-7dc4869ba56b
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 090a01a7-3a45-4edc-9928-a6beb5dfa031
|
||||
outputPortName: 62d7a61f-2e7d-4b93-81cd-6593c8a86763
|
||||
inputNodeGuid: 30955ff8-c3a7-4f3c-baf0-08cb51897afc
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 090a01a7-3a45-4edc-9928-a6beb5dfa031
|
||||
outputPortName: 2d93f014-fa75-4fdf-9950-e2b13dd198dc
|
||||
inputNodeGuid: 97bc6a1e-55aa-4a81-9bf1-99cd70ea8247
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 97bc6a1e-55aa-4a81-9bf1-99cd70ea8247
|
||||
outputPortName: Next
|
||||
inputNodeGuid: 470b7850-010c-4134-915b-684f2e5a56ea
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 30955ff8-c3a7-4f3c-baf0-08cb51897afc
|
||||
outputPortName: Next
|
||||
inputNodeGuid: 470b7850-010c-4134-915b-684f2e5a56ea
|
||||
inputPortName: Previous
|
||||
references:
|
||||
version: 2
|
||||
RefIds:
|
||||
- rid: 5266334379010163017
|
||||
type: {class: StartNodeData, ns: SLSFramework.StorySystem, asm: GameAPI}
|
||||
data:
|
||||
guid: 72f2b2bc-aca6-4892-8610-7e509ac9c074
|
||||
position: {x: -1.3333334, y: 408}
|
||||
- rid: 5266334379010163018
|
||||
type: {class: EndNodeData, ns: SLSFramework.StorySystem, asm: GameAPI}
|
||||
data:
|
||||
guid: 470b7850-010c-4134-915b-684f2e5a56ea
|
||||
position: {x: 1890.6666, y: 368.66666}
|
||||
- rid: 5266334379010163019
|
||||
type: {class: DialogNodeData, ns: SLSFramework.StorySystem.Dialog, asm: GameAPI}
|
||||
data:
|
||||
guid: f6633c3e-9e6c-41dd-8828-09e7562ff730
|
||||
position: {x: 746, y: 302}
|
||||
characterData: {fileID: 11400000, guid: fd38f8d56d51bce4cba0715d468b6333, type: 2}
|
||||
expressionKey: Default
|
||||
characterPosition: {x: 0, y: 0}
|
||||
dialogueText:
|
||||
audioClip: {fileID: 0}
|
||||
- rid: 5266334379010163046
|
||||
type: {class: ConditionNodeData, ns: SLSFramework.StorySystem, asm: GameAPI}
|
||||
data:
|
||||
guid: 485ebe8c-5f63-4926-b1fd-273f4f71ab66
|
||||
position: {x: 414.66666, y: 345.3333}
|
||||
conditionString:
|
||||
- rid: 5266334379010163047
|
||||
type: {class: ChoiceNodeData, ns: SLSFramework.StorySystem.Dialog, asm: GameAPI}
|
||||
data:
|
||||
guid: 090a01a7-3a45-4edc-9928-a6beb5dfa031
|
||||
position: {x: 1143.3334, y: 240.00002}
|
||||
choices:
|
||||
- guid: 2d93f014-fa75-4fdf-9950-e2b13dd198dc
|
||||
choiceText: New Choice
|
||||
isDefault: 0
|
||||
conditionString:
|
||||
- guid: 62d7a61f-2e7d-4b93-81cd-6593c8a86763
|
||||
choiceText: New Choice
|
||||
isDefault: 0
|
||||
conditionString:
|
||||
- rid: 5266334379010163048
|
||||
type: {class: EventNodeData, ns: SLSFramework.StorySystem, asm: GameAPI}
|
||||
data:
|
||||
guid: 30955ff8-c3a7-4f3c-baf0-08cb51897afc
|
||||
position: {x: 1563.3334, y: 486.66666}
|
||||
eventString:
|
||||
- rid: 5266334379010163049
|
||||
type: {class: EndNodeData, ns: SLSFramework.StorySystem, asm: GameAPI}
|
||||
data:
|
||||
guid: a928a097-4270-48d7-b13c-7dc4869ba56b
|
||||
position: {x: 791.3333, y: 548}
|
||||
- rid: 5266334379010163050
|
||||
type: {class: DialogNodeData, ns: SLSFramework.StorySystem.Dialog, asm: GameAPI}
|
||||
data:
|
||||
guid: 97bc6a1e-55aa-4a81-9bf1-99cd70ea8247
|
||||
position: {x: 1563.3334, y: 186}
|
||||
characterData: {fileID: 0}
|
||||
expressionKey:
|
||||
characterPosition: {x: 0, y: 0}
|
||||
dialogueText:
|
||||
audioClip: {fileID: 0}
|
||||
- rid: 5266334379010163051
|
||||
type: {class: CompoundDialogNodeData, ns: SLSFramework.StorySystem.Dialog, asm: GameAPI}
|
||||
data:
|
||||
guid: 738fe6c7-df1b-4925-8e81-72e81ed982a7
|
||||
position: {x: 134.66667, y: 357.3333}
|
||||
compoundDialogAsset: {fileID: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f5c8da1567f67448a776d30bed0f4ec
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
%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: c87462c2ff921fb4e9e5f52ab691a665, type: 3}
|
||||
m_Name: NewStorylineGraph
|
||||
m_EditorClassIdentifier: GameAPI::SLSFramework.StorySystem.Storyline.StorylineGraph
|
||||
nodes:
|
||||
- rid: 5266334379010163042
|
||||
- rid: 5266334379010163043
|
||||
- rid: 5266334379010163044
|
||||
- rid: 5266334379010163040
|
||||
- rid: 5266334379010163041
|
||||
- rid: 5266334379010163045
|
||||
edges:
|
||||
- outputNodeGuid: 2a5afb51-7344-4c88-ba12-158f08e9b48e
|
||||
outputPortName: Next
|
||||
inputNodeGuid: 57ab9788-bc80-4caa-ad8b-784ff6bc1549
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 57ab9788-bc80-4caa-ad8b-784ff6bc1549
|
||||
outputPortName: Next
|
||||
inputNodeGuid: a9042ccb-2243-493e-a990-45543b531548
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: a9042ccb-2243-493e-a990-45543b531548
|
||||
outputPortName: True
|
||||
inputNodeGuid: dd112a20-ca1b-4da9-b781-80bc55abf37a
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: a9042ccb-2243-493e-a990-45543b531548
|
||||
outputPortName: False
|
||||
inputNodeGuid: 6b4ceb0e-bf71-4254-9da0-4cb83b65ff98
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: dd112a20-ca1b-4da9-b781-80bc55abf37a
|
||||
outputPortName: Next
|
||||
inputNodeGuid: 970be608-5798-4896-95c6-3bce9dba5f4a
|
||||
inputPortName: Previous
|
||||
- outputNodeGuid: 6b4ceb0e-bf71-4254-9da0-4cb83b65ff98
|
||||
outputPortName: Next
|
||||
inputNodeGuid: 970be608-5798-4896-95c6-3bce9dba5f4a
|
||||
inputPortName: Previous
|
||||
references:
|
||||
version: 2
|
||||
RefIds:
|
||||
- rid: 5266334379010163040
|
||||
type: {class: EndNodeData, ns: SLSFramework.StorySystem, asm: GameAPI}
|
||||
data:
|
||||
guid: 970be608-5798-4896-95c6-3bce9dba5f4a
|
||||
position: {x: 970, y: 190.66667}
|
||||
- rid: 5266334379010163041
|
||||
type: {class: StartNodeData, ns: SLSFramework.StorySystem, asm: GameAPI}
|
||||
data:
|
||||
guid: 2a5afb51-7344-4c88-ba12-158f08e9b48e
|
||||
position: {x: -217.33333, y: 190.66667}
|
||||
- rid: 5266334379010163042
|
||||
type: {class: StorylineDialogNodeData, ns: SLSFramework.StorySystem.Storyline, asm: GameAPI}
|
||||
data:
|
||||
guid: 57ab9788-bc80-4caa-ad8b-784ff6bc1549
|
||||
position: {x: -67.333336, y: 142}
|
||||
dialogGraph: {fileID: 11400000, guid: 2f5c8da1567f67448a776d30bed0f4ec, type: 2}
|
||||
- rid: 5266334379010163043
|
||||
type: {class: StorylineDialogNodeData, ns: SLSFramework.StorySystem.Storyline, asm: GameAPI}
|
||||
data:
|
||||
guid: dd112a20-ca1b-4da9-b781-80bc55abf37a
|
||||
position: {x: 638, y: 17.333332}
|
||||
dialogGraph: {fileID: 0}
|
||||
- rid: 5266334379010163044
|
||||
type: {class: StorylineDialogNodeData, ns: SLSFramework.StorySystem.Storyline, asm: GameAPI}
|
||||
data:
|
||||
guid: 6b4ceb0e-bf71-4254-9da0-4cb83b65ff98
|
||||
position: {x: 638, y: 302}
|
||||
dialogGraph: {fileID: 0}
|
||||
- rid: 5266334379010163045
|
||||
type: {class: ConditionNodeData, ns: SLSFramework.StorySystem, asm: GameAPI}
|
||||
data:
|
||||
guid: a9042ccb-2243-493e-a990-45543b531548
|
||||
position: {x: 311.33334, y: 128}
|
||||
conditionString:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac57055d263c373449401520f406308d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,6 +1,7 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
@@ -135,92 +136,251 @@ namespace SLSFramework.UModAssistance
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type选择器,将获取的类型名存入一个string中
|
||||
#region Searchable Type选择器,将获取的类型名存入一个string中
|
||||
|
||||
public partial class DataEditor
|
||||
{
|
||||
private static Dictionary<Tuple<Type, string>, (string[] paths, Type[] types)> _typeCache =
|
||||
new Dictionary<Tuple<Type, string>, (string[], Type[])>();
|
||||
private static Dictionary<Type, (string[] paths, Type[] types)> _typeCache =
|
||||
new Dictionary<Type, (string[], Type[])>();
|
||||
|
||||
/// <summary>
|
||||
/// 绘制一个用于选择指定基类的所有子类的下拉菜单
|
||||
/// 绘制一个带 "Select" 按钮的字段,点击后会弹出可搜索的类型选择窗口。
|
||||
/// </summary>
|
||||
/// <param name="classNameProp">存储类名的字符串属性</param>
|
||||
/// <param name="label">在Inspector中显示的标签</param>
|
||||
/// <param name="baseType">要查找的基类 (例如 typeof(CardLogicBase))</param>
|
||||
/// <param name="namespaceToRemove">可选参数,用于从路径中移除特定的命名空间部分 (例如 ".Cards")</param>
|
||||
/// <returns>如果值被用户改变,则返回true</returns>
|
||||
protected bool DrawTypeSelectorGUI(SerializedProperty classNameProp, string label, Type baseType,
|
||||
out Type returnedType, string namespacePrefix = null, string namespaceToRemove = null)
|
||||
/// <param name="baseType">要查找的基类</param>
|
||||
/// <param name="onTypeSelected">【关键】当一个类型被选中时执行的回调</param>
|
||||
/// <param name="namespacePrefix">要搜索的命名空间前缀</param>
|
||||
/// <param name="namespaceToRemove">要从路径中移除的命名空间部分</param>
|
||||
protected void DrawSearchableTypeSelector(
|
||||
SerializedProperty classNameProp,
|
||||
string label,
|
||||
Type baseType,
|
||||
Action<Type> onTypeSelected,
|
||||
string namespacePrefix = null,
|
||||
string namespaceToRemove = null)
|
||||
{
|
||||
// --- 核心修改 2:使用包含 namespaceToRemove 的复合键 ---
|
||||
var cacheKey = new Tuple<Type, string>(baseType, namespaceToRemove ?? string.Empty);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.TextField(label, classNameProp.stringValue);
|
||||
|
||||
if (!_typeCache.ContainsKey(cacheKey))
|
||||
if (GUILayout.Button("Select", GUILayout.Width(60)))
|
||||
{
|
||||
CacheDerivedTypes(baseType, namespacePrefix, namespaceToRemove, cacheKey);
|
||||
}
|
||||
|
||||
var (paths, types) = _typeCache[cacheKey];
|
||||
|
||||
int currentIndex = -1;
|
||||
for (int i = 0; i < types.Length; i++)
|
||||
{
|
||||
if (types[i].Name == classNameProp.stringValue)
|
||||
TypeSelectorWindow.Show(baseType, namespacePrefix, namespaceToRemove, (selectedType) =>
|
||||
{
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
classNameProp.stringValue = selectedType.Name;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
onTypeSelected?.Invoke(selectedType);
|
||||
});
|
||||
}
|
||||
|
||||
int newIndex = EditorGUILayout.Popup(label, currentIndex, paths);
|
||||
|
||||
if (newIndex != currentIndex)
|
||||
{
|
||||
classNameProp.stringValue = (newIndex >= 0 && newIndex < types.Length)
|
||||
? types[newIndex].Name
|
||||
: string.Empty;
|
||||
|
||||
returnedType = types[newIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
returnedType = null;
|
||||
return false;
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void CacheDerivedTypes(Type baseType, string namespacePrefix, string namespaceToRemove, Tuple<Type, string> cacheKey)
|
||||
|
||||
/// <summary>
|
||||
/// 弹出式搜索窗口(作为私有内部类)
|
||||
/// </summary>
|
||||
private class TypeSelectorWindow : EditorWindow
|
||||
{
|
||||
List<(string path, Type type)> typeList = new List<(string, Type)>();
|
||||
|
||||
IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(assembly => assembly.GetTypes())
|
||||
.Where(t => baseType.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface && t != baseType);
|
||||
|
||||
foreach (var type in types)
|
||||
// --- 核心修改 1:新的树状数据结构 ---
|
||||
private class CategoryNode
|
||||
{
|
||||
string path = "Uncategorized/" + type.Name;
|
||||
if (type.Namespace != null && type.Namespace.StartsWith(namespacePrefix))
|
||||
public string Name;
|
||||
public bool IsExpanded = false;
|
||||
public Dictionary<string, CategoryNode> SubCategories = new Dictionary<string, CategoryNode>();
|
||||
public List<(string name, Type type)> Items = new List<(string, Type)>();
|
||||
}
|
||||
|
||||
private static Dictionary<Tuple<Type, string, string>, (string[] paths, Type[] types)> _staticTypeCache =
|
||||
new Dictionary<Tuple<Type, string, string>, (string[], Type[])>();
|
||||
|
||||
private CategoryNode _rootNode = new CategoryNode { Name = "Root", IsExpanded = true };
|
||||
private Action<Type> _onSelectCallback;
|
||||
private Type _baseType;
|
||||
private string _namespacePrefix;
|
||||
private string _namespaceToRemove;
|
||||
|
||||
private string _searchString = "";
|
||||
private Vector2 _scrollPos;
|
||||
|
||||
private Dictionary<string, List<(string path, Type type)>> _groupedFilteredList;
|
||||
private Dictionary<string, bool> _categoryFoldoutStates = new Dictionary<string, bool>();
|
||||
|
||||
public static void Show(Type baseType, string namespacePrefix, string namespaceToRemove, Action<Type> onSelect)
|
||||
{
|
||||
var window = GetWindow<TypeSelectorWindow>(true, "Select Type", true);
|
||||
window.minSize = new Vector2(300, 400);
|
||||
window._baseType = baseType;
|
||||
window._namespacePrefix = namespacePrefix;
|
||||
window._namespaceToRemove = namespaceToRemove;
|
||||
window._onSelectCallback = onSelect;
|
||||
window.FilterList();
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
EditorGUI.BeginChangeCheck();
|
||||
_searchString = GUILayout.TextField(_searchString, GUI.skin.FindStyle("ToolbarSearchTextField"));
|
||||
if (GUILayout.Button("", GUI.skin.FindStyle("ToolbarSearchCancelButton"))) _searchString = "";
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
string formattedNamespace = type.Namespace.Substring(namespacePrefix.Length);
|
||||
|
||||
// --- 核心修改 3:使用传入的参数来替换硬编码 ---
|
||||
if (!string.IsNullOrEmpty(namespaceToRemove))
|
||||
{
|
||||
formattedNamespace = formattedNamespace.Replace("." + namespaceToRemove, "");
|
||||
}
|
||||
|
||||
formattedNamespace = formattedNamespace.Replace('.', '/');
|
||||
if (formattedNamespace.StartsWith("/")) formattedNamespace = formattedNamespace.Substring(1);
|
||||
path = formattedNamespace + "/" + type.Name;
|
||||
FilterList();
|
||||
}
|
||||
|
||||
typeList.Add((path, type));
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
||||
if (_rootNode.SubCategories.Count == 0 && _rootNode.Items.Count == 0)
|
||||
{
|
||||
EditorGUILayout.LabelField("No matching types found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawNodeGUI(_rootNode, 0);
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
typeList.Sort((a, b) => a.path.CompareTo(b.path));
|
||||
// --- 核心修改 1:重写 CacheDerivedTypes,使其更健壮 ---
|
||||
private void CacheDerivedTypes()
|
||||
{
|
||||
var cacheKey = new Tuple<Type, string, string>(_baseType, _namespacePrefix ?? string.Empty, _namespaceToRemove ?? string.Empty);
|
||||
if (_staticTypeCache.ContainsKey(cacheKey)) return;
|
||||
|
||||
List<(string path, Type type)> typeList = new List<(string, Type)>();
|
||||
|
||||
_typeCache[cacheKey] = (typeList.Select(t => t.path).ToArray(), typeList.Select(t => t.type).ToArray());
|
||||
IEnumerable<Type> types = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(assembly => assembly.GetTypes())
|
||||
.Where(t => _baseType.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface && t != _baseType);
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
string path;
|
||||
if (!string.IsNullOrEmpty(_namespacePrefix) && type.Namespace != null && type.Namespace.StartsWith(_namespacePrefix))
|
||||
{
|
||||
// 1. 获取前缀后的命名空间
|
||||
// e.g., ".Basic.Cards.General.Skills"
|
||||
string formattedNamespace = type.Namespace.Substring(_namespacePrefix.Length);
|
||||
|
||||
// 2. 按 '.' 拆分为段落,并移除空条目 (比如开头的那个)
|
||||
List<string> segments = formattedNamespace.Split('.').Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||
|
||||
// 3. 安全地移除指定的 'namespaceToRemove' 段落
|
||||
if (!string.IsNullOrEmpty(_namespaceToRemove))
|
||||
{
|
||||
segments.Remove(_namespaceToRemove);
|
||||
}
|
||||
|
||||
// 4. 用 '/' 重新组合路径
|
||||
if (segments.Count > 0)
|
||||
{
|
||||
path = string.Join("/", segments) + "/" + type.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = type.Name; // 如果没有剩余段落,则直接使用类名
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
path = "Uncategorized/" + type.Name;
|
||||
}
|
||||
typeList.Add((path, type));
|
||||
}
|
||||
typeList.Sort((a, b) => a.path.CompareTo(b.path));
|
||||
_staticTypeCache[cacheKey] = (typeList.Select(t => t.path).ToArray(), typeList.Select(t => t.type).ToArray());
|
||||
}
|
||||
|
||||
private void FilterList()
|
||||
{
|
||||
var cacheKey = new Tuple<Type, string, string>(_baseType, _namespacePrefix ?? string.Empty, _namespaceToRemove ?? string.Empty);
|
||||
if (!_staticTypeCache.ContainsKey(cacheKey))
|
||||
{
|
||||
CacheDerivedTypes();
|
||||
}
|
||||
|
||||
var (allPaths, allTypes) = _staticTypeCache[cacheKey];
|
||||
|
||||
// 1. 重置根节点
|
||||
_rootNode = new CategoryNode { Name = "Root", IsExpanded = true };
|
||||
|
||||
for (int i = 0; i < allPaths.Length; i++)
|
||||
{
|
||||
string fullPath = allPaths[i];
|
||||
Type type = allTypes[i];
|
||||
|
||||
// 2. 检查是否匹配搜索词
|
||||
if (string.IsNullOrEmpty(_searchString) ||
|
||||
fullPath.IndexOf(_searchString, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
var segments = fullPath.Split('/');
|
||||
CategoryNode currentNode = _rootNode;
|
||||
|
||||
// 3. 遍历所有“目录”部分,构建树
|
||||
for (int j = 0; j < segments.Length - 1; j++)
|
||||
{
|
||||
string categoryName = segments[j];
|
||||
if (!currentNode.SubCategories.ContainsKey(categoryName))
|
||||
{
|
||||
var newNode = new CategoryNode { Name = categoryName };
|
||||
// 如果在搜索,自动展开所有匹配路径上的节点
|
||||
if (!string.IsNullOrEmpty(_searchString))
|
||||
{
|
||||
newNode.IsExpanded = true;
|
||||
}
|
||||
currentNode.SubCategories[categoryName] = newNode;
|
||||
}
|
||||
currentNode = currentNode.SubCategories[categoryName];
|
||||
}
|
||||
|
||||
// 4. 将“文件”部分(即类名)添加到它所属的节点
|
||||
string itemName = segments.Last();
|
||||
currentNode.Items.Add((itemName, type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 核心修改 4:全新的递归绘制方法 ---
|
||||
private void DrawNodeGUI(CategoryNode node, int indentLevel)
|
||||
{
|
||||
// 1. 绘制所有子类别(作为可折叠的Foldouts)
|
||||
foreach (var subCategory in node.SubCategories.Values.OrderBy(c => c.Name))
|
||||
{
|
||||
// 设置当前层级的缩进
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
|
||||
// 使用我们自定义的、带三角箭头的粗体样式
|
||||
subCategory.IsExpanded = EditorGUILayout.Foldout(subCategory.IsExpanded, subCategory.Name, true);
|
||||
|
||||
if (subCategory.IsExpanded)
|
||||
{
|
||||
// 递归调用,绘制子节点的内容,缩进+1
|
||||
DrawNodeGUI(subCategory, indentLevel + 1);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
foreach (var (name, type) in node.Items.OrderBy(i => i.name))
|
||||
{
|
||||
// B. 从布局系统中获取一个**已经正确缩进**的矩形区域
|
||||
Rect controlRect = EditorGUILayout.GetControlRect();
|
||||
|
||||
// C. 在这个矩形区域内绘制标签,它会自动使用我们设置的缩进
|
||||
// 我们使用 _clickableLabelStyle 来实现悬停变色效果
|
||||
EditorGUI.LabelField(controlRect, name);
|
||||
|
||||
// D. 为这个矩形区域添加一个可点击的光标提示
|
||||
EditorGUIUtility.AddCursorRect(controlRect, MouseCursor.Link);
|
||||
|
||||
// E. 手动检查在这个矩形区域内的点击事件
|
||||
if (Event.current.type == EventType.MouseDown && Event.current.button == 0 &&
|
||||
controlRect.Contains(Event.current.mousePosition))
|
||||
{
|
||||
_onSelectCallback?.Invoke(type);
|
||||
this.Close();
|
||||
Event.current.Use(); // 消耗掉这个点击事件
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user