From ea75bd5225ce90df4da5cffc2fac1366a4fe1d5f Mon Sep 17 00:00:00 2001 From: SoulliesOfficial Date: Mon, 10 Nov 2025 11:18:19 -0500 Subject: [PATCH] DataEditor & StorySystem Graph --- .../Data/ObsoleteGeneral/Skill/CardData.asset | 45 +++ .../ObsoleteGeneral/Skill/CardData.asset.meta | 8 + .../Skill/CardData_Basic_Cohesion.asset | 5 +- .../CardData_Basic_FightingInspiration.asset | 5 +- .../Cards/Scripts/ObsoleteGeneral/Cohesion.cs | 2 +- .../MainGame/Card/CardData/CardData.cs | 4 +- .../MainGame/Card/Editor/CardDataEditor.cs | 43 ++- .../Character/Editor/CharacterDataEditor.cs | 2 +- .../Equipment/Editor/EquipmentDataEditor.cs | 25 +- .../Scripts/ScriptExtensions/StorySystem.meta | 8 + .../StorySystem}/Editor.meta | 0 .../StorySystem/Editor/Commom.meta | 8 + .../StorySystem/Editor/Commom/GraphBase.cs | 15 + .../Editor/Commom/GraphBase.cs.meta | 2 + .../StorySystem/Editor/Commom/Nodes.meta | 8 + .../Editor/Commom/Nodes/BaseGraphNode.cs | 36 +++ .../Editor/Commom/Nodes/BaseGraphNode.cs.meta | 2 + .../Editor/Commom/Nodes/ConditionGraphNode.cs | 47 +++ .../Commom/Nodes/ConditionGraphNode.cs.meta | 2 + .../Editor/Commom/Nodes/EndGraphNode.cs | 23 ++ .../Editor/Commom/Nodes/EndGraphNode.cs.meta | 2 + .../Editor/Commom/Nodes/EventGraphNode.cs | 43 +++ .../Commom/Nodes/EventGraphNode.cs.meta | 2 + .../Editor/Commom/Nodes/StartGraphNode.cs | 23 ++ .../Commom/Nodes/StartGraphNode.cs.meta | 2 + .../StorySystem/Editor/Commom/Window.meta | 8 + .../Editor/Commom/Window/EditorWindowBase.cs | 27 ++ .../Commom/Window/EditorWindowBase.cs.meta | 2 + .../Editor/Commom/Window/GraphViewBase.cs | 296 ++++++++++++++++++ .../Commom/Window/GraphViewBase.cs.meta | 2 + .../StorySystem/Editor/DialogEditor.meta | 8 + .../Editor/DialogEditor/Nodes.meta | 8 + .../DialogEditor/Nodes/ChoiceGraphNode.cs | 185 +++++++++++ .../Nodes/ChoiceGraphNode.cs.meta | 2 + .../DialogEditor/Nodes/CompoundDialogNode.cs | 45 +++ .../Nodes/CompoundDialogNode.cs.meta | 2 + .../DialogEditor/Nodes/DialogGraphNode.cs | 149 +++++++++ .../Nodes/DialogGraphNode.cs.meta | 2 + .../Editor/DialogEditor/Window.meta | 8 + .../Window/DialogGraphEditorWindow.cs | 125 ++++++++ .../Window/DialogGraphEditorWindow.cs.meta | 2 + .../DialogEditor/Window/DialogGraphView.cs | 129 ++++++++ .../Window/DialogGraphView.cs.meta | 2 + .../StorySystem/Editor/StorylineEditor.meta | 8 + .../Editor/StorylineEditor/Nodes.meta | 8 + .../Nodes/StorylineDialogNode.cs | 64 ++++ .../Nodes/StorylineDialogNode.cs.meta | 2 + .../Editor/StorylineEditor/Window.meta | 8 + .../Window/StorylineGraphEditorWindow.cs | 99 ++++++ .../Window/StorylineGraphEditorWindow.cs.meta | 2 + .../Window/StorylineGraphView.cs | 122 ++++++++ .../Window/StorylineGraphView.cs.meta | 2 + .../ScriptExtensions/StorySystem/Runtime.meta | 8 + .../StorySystem/Runtime/Data.meta | 8 + .../StorySystem/Runtime/Data/Dialog.meta | 8 + .../Runtime/Data/Dialog/CharacterData.cs | 26 ++ .../Runtime/Data/Dialog/CharacterData.cs.meta | 2 + .../Runtime/Data/Dialog/DialogGraph.cs | 11 + .../Runtime/Data/Dialog/DialogGraph.cs.meta | 2 + .../Runtime/Data/Dialog/DialogNodeData.cs | 41 +++ .../Data/Dialog/DialogNodeData.cs.meta | 2 + .../StorySystem/Runtime/Data/NodeData.cs | 53 ++++ .../StorySystem/Runtime/Data/NodeData.cs.meta | 2 + .../StorySystem/Runtime/Data/Storyline.meta | 8 + .../Runtime/Data/Storyline/StorylineGraph.cs | 11 + .../Data/Storyline/StorylineGraph.cs.meta | 2 + .../Data/Storyline/StorylineNodeData.cs | 13 + .../Data/Storyline/StorylineNodeData.cs.meta | 2 + .../ScriptExtensions/StorySystem/Samples.meta | 8 + .../Samples/NewCharacterData.asset | 18 ++ .../Samples/NewCharacterData.asset.meta | 8 + .../Samples/NewDialogueGraph.asset | 131 ++++++++ .../Samples/NewDialogueGraph.asset.meta | 8 + .../Samples/NewStorylineGraph.asset | 83 +++++ .../Samples/NewStorylineGraph.asset.meta | 8 + .../UModAssistance/Editor/DataEditor.cs | 288 +++++++++++++---- 76 files changed, 2340 insertions(+), 90 deletions(-) create mode 100644 Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData.asset create mode 100644 Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData.asset.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem.meta rename Assets/Scripts/{ => ScriptExtensions/StorySystem}/Editor.meta (100%) create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/GraphBase.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/GraphBase.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/BaseGraphNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/BaseGraphNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/ConditionGraphNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/ConditionGraphNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EndGraphNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EndGraphNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EventGraphNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EventGraphNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/StartGraphNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/StartGraphNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/EditorWindowBase.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/EditorWindowBase.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/GraphViewBase.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/GraphViewBase.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/ChoiceGraphNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/ChoiceGraphNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/CompoundDialogNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/CompoundDialogNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/DialogGraphNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/DialogGraphNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphEditorWindow.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphEditorWindow.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphView.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphView.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes/StorylineDialogNode.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes/StorylineDialogNode.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphEditorWindow.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphEditorWindow.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphView.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphView.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/CharacterData.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/CharacterData.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogGraph.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogGraph.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogNodeData.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogNodeData.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/NodeData.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/NodeData.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineGraph.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineGraph.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineNodeData.cs create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineNodeData.cs.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Samples.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewCharacterData.asset create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewCharacterData.asset.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewDialogueGraph.asset create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewDialogueGraph.asset.meta create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewStorylineGraph.asset create mode 100644 Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewStorylineGraph.asset.meta diff --git a/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData.asset b/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData.asset new file mode 100644 index 00000000..d475d05a --- /dev/null +++ b/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData.asset @@ -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: [] diff --git a/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData.asset.meta b/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData.asset.meta new file mode 100644 index 00000000..e56537fe --- /dev/null +++ b/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e219fcd155266f34abcddb68fb199193 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData_Basic_Cohesion.asset b/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData_Basic_Cohesion.asset index e229e869..8b7cf166 100644 --- a/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData_Basic_Cohesion.asset +++ b/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData_Basic_Cohesion.asset @@ -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 diff --git a/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData_Basic_FightingInspiration.asset b/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData_Basic_FightingInspiration.asset index 8372ed1d..1d4ed610 100644 --- a/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData_Basic_FightingInspiration.asset +++ b/Assets/Mods/Basic/Cards/Data/ObsoleteGeneral/Skill/CardData_Basic_FightingInspiration.asset @@ -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 diff --git a/Assets/Mods/Basic/Cards/Scripts/ObsoleteGeneral/Cohesion.cs b/Assets/Mods/Basic/Cards/Scripts/ObsoleteGeneral/Cohesion.cs index 54b8d846..d05bcf9a 100644 --- a/Assets/Mods/Basic/Cards/Scripts/ObsoleteGeneral/Cohesion.cs +++ b/Assets/Mods/Basic/Cards/Scripts/ObsoleteGeneral/Cohesion.cs @@ -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 { diff --git a/Assets/Scripts/MainGame/Card/CardData/CardData.cs b/Assets/Scripts/MainGame/Card/CardData/CardData.cs index a43784d1..02a0fa19 100644 --- a/Assets/Scripts/MainGame/Card/CardData/CardData.cs +++ b/Assets/Scripts/MainGame/Card/CardData/CardData.cs @@ -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; diff --git a/Assets/Scripts/MainGame/Card/Editor/CardDataEditor.cs b/Assets/Scripts/MainGame/Card/Editor/CardDataEditor.cs index ced62bf5..cb9f8342 100644 --- a/Assets/Scripts/MainGame/Card/Editor/CardDataEditor.cs +++ b/Assets/Scripts/MainGame/Card/Editor/CardDataEditor.cs @@ -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(); diff --git a/Assets/Scripts/MainGame/Character/Editor/CharacterDataEditor.cs b/Assets/Scripts/MainGame/Character/Editor/CharacterDataEditor.cs index 58b64b06..4ce6f814 100644 --- a/Assets/Scripts/MainGame/Character/Editor/CharacterDataEditor.cs +++ b/Assets/Scripts/MainGame/Character/Editor/CharacterDataEditor.cs @@ -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 { diff --git a/Assets/Scripts/MainGame/Equipment/Editor/EquipmentDataEditor.cs b/Assets/Scripts/MainGame/Equipment/Editor/EquipmentDataEditor.cs index e5d951c5..5703f44c 100644 --- a/Assets/Scripts/MainGame/Equipment/Editor/EquipmentDataEditor.cs +++ b/Assets/Scripts/MainGame/Equipment/Editor/EquipmentDataEditor.cs @@ -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); } diff --git a/Assets/Scripts/ScriptExtensions/StorySystem.meta b/Assets/Scripts/ScriptExtensions/StorySystem.meta new file mode 100644 index 00000000..727203b3 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d79e988c91a1a64449e1c0ab3ffa900e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Editor.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor.meta similarity index 100% rename from Assets/Scripts/Editor.meta rename to Assets/Scripts/ScriptExtensions/StorySystem/Editor.meta diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom.meta new file mode 100644 index 00000000..e67809fe --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9fa1138f75d85344ab4e3b8312e71959 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/GraphBase.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/GraphBase.cs new file mode 100644 index 00000000..e9c083cd --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/GraphBase.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace SLSFramework.StorySystem +{ + public abstract class GraphBase : ScriptableObject + { + // 使用 [SerializeReference] 来支持多态性,存储不同类型的节点数据 + [SerializeReference] + public List nodes = new List(); + + // 存储节点之间的连接 + public List edges = new List(); + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/GraphBase.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/GraphBase.cs.meta new file mode 100644 index 00000000..0aed97b0 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/GraphBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 88df947440cfd3841b8e5a545437b253 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes.meta new file mode 100644 index 00000000..418bedcc --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1ad08c73d10275f498f9643e948dade4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/BaseGraphNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/BaseGraphNode.cs new file mode 100644 index 00000000..b6768f76 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/BaseGraphNode.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/BaseGraphNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/BaseGraphNode.cs.meta new file mode 100644 index 00000000..0db323eb --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/BaseGraphNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 89062ba4c7f90314c86ed9cedb4053cf \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/ConditionGraphNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/ConditionGraphNode.cs new file mode 100644 index 00000000..6a3e0703 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/ConditionGraphNode.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/ConditionGraphNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/ConditionGraphNode.cs.meta new file mode 100644 index 00000000..bae633b8 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/ConditionGraphNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 95a4ba99bc298c74283b008b8b25b8bd \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EndGraphNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EndGraphNode.cs new file mode 100644 index 00000000..6bc1827e --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EndGraphNode.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EndGraphNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EndGraphNode.cs.meta new file mode 100644 index 00000000..a5bf8252 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EndGraphNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e82960b057418454eab7e7eefcc71a14 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EventGraphNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EventGraphNode.cs new file mode 100644 index 00000000..5d1d3b1f --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EventGraphNode.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EventGraphNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EventGraphNode.cs.meta new file mode 100644 index 00000000..0e248b21 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/EventGraphNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: baf926c674d6ea842b99cda704461ee2 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/StartGraphNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/StartGraphNode.cs new file mode 100644 index 00000000..95d34080 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/StartGraphNode.cs @@ -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(); + } + } +} diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/StartGraphNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/StartGraphNode.cs.meta new file mode 100644 index 00000000..65158b0d --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Nodes/StartGraphNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d3bf9fbf0029def4bbb8ce1e1ed0628c \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window.meta new file mode 100644 index 00000000..c68c4fa5 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4019e9906ff3c1f46b7db8bbb8604ef3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/EditorWindowBase.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/EditorWindowBase.cs new file mode 100644 index 00000000..080fd91c --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/EditorWindowBase.cs @@ -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() + { + + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/EditorWindowBase.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/EditorWindowBase.cs.meta new file mode 100644 index 00000000..ecd8a47b --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/EditorWindowBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2ada5daa7b9068f4c98c9af0655aef6d \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/GraphViewBase.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/GraphViewBase.cs new file mode 100644 index 00000000..28280e15 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/GraphViewBase.cs @@ -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(); + 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(name: edgeData.outputPortName); + Port inputPort = inputNode.inputContainer.Q(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()) + { + // 更新节点数据的位置 + 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 nodes = new List(); + + public List edges = new List(); + } + + protected string OnSerializeGraphElements(IEnumerable elements) + { + var nodesToCopy = new List(); + var edgesToCopy = new List(); + + var selectedNodeGuids = new HashSet( + elements.OfType().Select(n => n.NodeData.guid) + ); + + // 遍历所有选中的节点 + foreach (var nodeView in elements.OfType()) + { + // 使用 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()) + { + 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(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(); + var nodeViewMap = new Dictionary(); + + // 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(name: edgeData.outputPortName); + Port inputPort = inputNode.inputContainer.Q(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"); + } + + // --- 修复结束 --- + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/GraphViewBase.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/GraphViewBase.cs.meta new file mode 100644 index 00000000..c3a88245 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/Commom/Window/GraphViewBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 57749720ca7b80e479d90181d6499476 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor.meta new file mode 100644 index 00000000..d0c06a2e --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 50cf128ce7fdba6478161dc76a7341b8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes.meta new file mode 100644 index 00000000..68de468b --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f526df7c127191c4395a96ab46da1de6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/ChoiceGraphNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/ChoiceGraphNode.cs new file mode 100644 index 00000000..a0348b46 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/ChoiceGraphNode.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/ChoiceGraphNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/ChoiceGraphNode.cs.meta new file mode 100644 index 00000000..584f49b4 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/ChoiceGraphNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4cde0007c641b2c4a80b7642ae55ff73 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/CompoundDialogNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/CompoundDialogNode.cs new file mode 100644 index 00000000..66823b37 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/CompoundDialogNode.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/CompoundDialogNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/CompoundDialogNode.cs.meta new file mode 100644 index 00000000..ebdc6633 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/CompoundDialogNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f39fd5f93076c404ab92b10fe4c9b46f \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/DialogGraphNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/DialogGraphNode.cs new file mode 100644 index 00000000..447b7b2f --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/DialogGraphNode.cs @@ -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 evt) + { + _data.characterData = evt.newValue as CharacterData; + + // 清空旧的表情 + _data.expressionKey = null; + + // 重新填充下拉框 + PopulateExpressionDropdown(); + } + + // --- 7. 核心逻辑:当 DropdownField 变化时调用 --- + private void OnExpressionChanged(ChangeEvent 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 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); + } + } +} diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/DialogGraphNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/DialogGraphNode.cs.meta new file mode 100644 index 00000000..d6bdb6c0 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Nodes/DialogGraphNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d1de4576f57992f4abdbf42cf5d93a5d \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window.meta new file mode 100644 index 00000000..3b344ced --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c21355e0c16093d40a6e8a5871c75c5e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphEditorWindow.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphEditorWindow.cs new file mode 100644 index 00000000..aec335b6 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphEditorWindow.cs @@ -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(); + 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(); + // 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 ? "*" : "")}"); + } + } +} diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphEditorWindow.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphEditorWindow.cs.meta new file mode 100644 index 00000000..b9b6af25 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphEditorWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: edb12263f493f38458ff5b9fbdca659f \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphView.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphView.cs new file mode 100644 index 00000000..b6284419 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphView.cs @@ -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 GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) + { + var compatiblePorts = new List(); + ports.ForEach((port) => + { + if (startPort != port && startPort.node != port.node && startPort.direction != port.direction) + { + compatiblePorts.Add(port); + } + }); + return compatiblePorts; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphView.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphView.cs.meta new file mode 100644 index 00000000..06144591 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/DialogEditor/Window/DialogGraphView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f694b29c3d1202a4cba7f81a0e308b48 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor.meta new file mode 100644 index 00000000..4656728a --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8e02bc65fdf9dd84390dc0509692ba3e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes.meta new file mode 100644 index 00000000..8a3d897d --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 31a0824d0344e0340baf9ae4805f08bd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes/StorylineDialogNode.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes/StorylineDialogNode.cs new file mode 100644 index 00000000..dbc57bb9 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes/StorylineDialogNode.cs @@ -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(); + 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(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes/StorylineDialogNode.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes/StorylineDialogNode.cs.meta new file mode 100644 index 00000000..2eb0d9cc --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Nodes/StorylineDialogNode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7958263785c327641b684242ce2f8c9a \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window.meta new file mode 100644 index 00000000..42e1f430 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e3e0d4ada943b7740ba915857046c87c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphEditorWindow.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphEditorWindow.cs new file mode 100644 index 00000000..5add70d8 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphEditorWindow.cs @@ -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(); + 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(); + 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 ? "*" : "")}"); + } + } +} diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphEditorWindow.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphEditorWindow.cs.meta new file mode 100644 index 00000000..9c17835d --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphEditorWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5b3352bb0a4563d4f8778594a486a3c9 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphView.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphView.cs new file mode 100644 index 00000000..36cf4e68 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphView.cs @@ -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 GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) + { + var compatiblePorts = new List(); + ports.ForEach((port) => + { + if (startPort != port && startPort.node != port.node && startPort.direction != port.direction) + { + compatiblePorts.Add(port); + } + }); + return compatiblePorts; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphView.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphView.cs.meta new file mode 100644 index 00000000..e73d4739 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Editor/StorylineEditor/Window/StorylineGraphView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e6c66e8ba85e73f4e913053be3226ca9 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime.meta new file mode 100644 index 00000000..34c61a5c --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c706945d6aec0f44b98cfec5deb98ce4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data.meta new file mode 100644 index 00000000..413664b4 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 86843bd5cd5f21b4fb945730e1dd5cce +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog.meta new file mode 100644 index 00000000..6c763ee7 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: df71a91d0bb9e834596890d9c6dbb9a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/CharacterData.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/CharacterData.cs new file mode 100644 index 00000000..5683fa53 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/CharacterData.cs @@ -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 expressions = new List(); + } + + public partial class CharacterData + { + [Serializable] + public class Expression + { + public string key; + public Sprite sprite; + + // (将来你还可以在这里添加 audioClip, 动画, etc.) + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/CharacterData.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/CharacterData.cs.meta new file mode 100644 index 00000000..a5b41d71 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/CharacterData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a58101ba7d997824d889bd04baf1c18c \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogGraph.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogGraph.cs new file mode 100644 index 00000000..7285c82d --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogGraph.cs @@ -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 + { + + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogGraph.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogGraph.cs.meta new file mode 100644 index 00000000..2aea2e28 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogGraph.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9a36a4bb7be9da947beb608206dc240f \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogNodeData.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogNodeData.cs new file mode 100644 index 00000000..1f757e1d --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogNodeData.cs @@ -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 choices = new List(); + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogNodeData.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogNodeData.cs.meta new file mode 100644 index 00000000..ca8e4b68 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Dialog/DialogNodeData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 77b3f3b496104dc4d887f70412d2f57f \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/NodeData.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/NodeData.cs new file mode 100644 index 00000000..72142fbf --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/NodeData.cs @@ -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 = ""; + } + +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/NodeData.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/NodeData.cs.meta new file mode 100644 index 00000000..33d7ede0 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/NodeData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 95633a09783e9ad47a4980934e8af4d0 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline.meta new file mode 100644 index 00000000..7653754c --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e50c42418f41ae44c81085f2a00e2882 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineGraph.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineGraph.cs new file mode 100644 index 00000000..f9130282 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineGraph.cs @@ -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 + { + + } +} \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineGraph.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineGraph.cs.meta new file mode 100644 index 00000000..09437c36 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineGraph.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c87462c2ff921fb4e9e5f52ab691a665 \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineNodeData.cs b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineNodeData.cs new file mode 100644 index 00000000..d248e6f2 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineNodeData.cs @@ -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; + } +} diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineNodeData.cs.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineNodeData.cs.meta new file mode 100644 index 00000000..da88c179 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Runtime/Data/Storyline/StorylineNodeData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c8533bcb32514564d98e8c49f29b13ea \ No newline at end of file diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Samples.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Samples.meta new file mode 100644 index 00000000..bd7db3f0 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Samples.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 986ca523e75264d41a6778d04384c154 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewCharacterData.asset b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewCharacterData.asset new file mode 100644 index 00000000..ea22967a --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewCharacterData.asset @@ -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} diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewCharacterData.asset.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewCharacterData.asset.meta new file mode 100644 index 00000000..b2b3fcf8 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewCharacterData.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fd38f8d56d51bce4cba0715d468b6333 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewDialogueGraph.asset b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewDialogueGraph.asset new file mode 100644 index 00000000..271249b0 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewDialogueGraph.asset @@ -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} diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewDialogueGraph.asset.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewDialogueGraph.asset.meta new file mode 100644 index 00000000..97ca165b --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewDialogueGraph.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2f5c8da1567f67448a776d30bed0f4ec +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewStorylineGraph.asset b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewStorylineGraph.asset new file mode 100644 index 00000000..92fc31c2 --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewStorylineGraph.asset @@ -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: diff --git a/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewStorylineGraph.asset.meta b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewStorylineGraph.asset.meta new file mode 100644 index 00000000..27d3a4fc --- /dev/null +++ b/Assets/Scripts/ScriptExtensions/StorySystem/Samples/NewStorylineGraph.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ac57055d263c373449401520f406308d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ScriptExtensions/UModAssistance/Editor/DataEditor.cs b/Assets/Scripts/ScriptExtensions/UModAssistance/Editor/DataEditor.cs index 51e192ab..78276eef 100644 --- a/Assets/Scripts/ScriptExtensions/UModAssistance/Editor/DataEditor.cs +++ b/Assets/Scripts/ScriptExtensions/UModAssistance/Editor/DataEditor.cs @@ -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, (string[] paths, Type[] types)> _typeCache = - new Dictionary, (string[], Type[])>(); + private static Dictionary _typeCache = + new Dictionary(); /// - /// 绘制一个用于选择指定基类的所有子类的下拉菜单 + /// 绘制一个带 "Select" 按钮的字段,点击后会弹出可搜索的类型选择窗口。 /// /// 存储类名的字符串属性 /// 在Inspector中显示的标签 - /// 要查找的基类 (例如 typeof(CardLogicBase)) - /// 可选参数,用于从路径中移除特定的命名空间部分 (例如 ".Cards") - /// 如果值被用户改变,则返回true - protected bool DrawTypeSelectorGUI(SerializedProperty classNameProp, string label, Type baseType, - out Type returnedType, string namespacePrefix = null, string namespaceToRemove = null) + /// 要查找的基类 + /// 【关键】当一个类型被选中时执行的回调 + /// 要搜索的命名空间前缀 + /// 要从路径中移除的命名空间部分 + protected void DrawSearchableTypeSelector( + SerializedProperty classNameProp, + string label, + Type baseType, + Action onTypeSelected, + string namespacePrefix = null, + string namespaceToRemove = null) { - // --- 核心修改 2:使用包含 namespaceToRemove 的复合键 --- - var cacheKey = new Tuple(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 cacheKey) + + /// + /// 弹出式搜索窗口(作为私有内部类) + /// + private class TypeSelectorWindow : EditorWindow { - List<(string path, Type type)> typeList = new List<(string, Type)>(); - - IEnumerable 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 SubCategories = new Dictionary(); + public List<(string name, Type type)> Items = new List<(string, Type)>(); + } + + private static Dictionary, (string[] paths, Type[] types)> _staticTypeCache = + new Dictionary, (string[], Type[])>(); + + private CategoryNode _rootNode = new CategoryNode { Name = "Root", IsExpanded = true }; + private Action _onSelectCallback; + private Type _baseType; + private string _namespacePrefix; + private string _namespaceToRemove; + + private string _searchString = ""; + private Vector2 _scrollPos; + + private Dictionary> _groupedFilteredList; + private Dictionary _categoryFoldoutStates = new Dictionary(); + + public static void Show(Type baseType, string namespacePrefix, string namespaceToRemove, Action onSelect) + { + var window = GetWindow(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(_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 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 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(_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(); // 消耗掉这个点击事件 + } + } + } } }