#if UNITY_EDITOR using UnityEditor; using UnityEditor.Experimental.GraphView; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using System.Collections.Generic; public class IngameGalgameGraphWindow : EditorWindow { private ingame_galgame_so currentSo; private IngameGalgameGraphView graphView; private ObjectField soField; public static void Open(ingame_galgame_so so) { var window = GetWindow("Galgame 蓝图"); window.Show(); window.SetSo(so); } private void OnEnable() { CreateGraphView(); CreateToolbar(); if (currentSo == null) { currentSo = Selection.activeObject as ingame_galgame_so; } SetSo(currentSo); } private void OnDisable() { rootVisualElement.Clear(); graphView = null; } private void CreateGraphView() { graphView = new IngameGalgameGraphView(); graphView.StretchToParentSize(); rootVisualElement.Add(graphView); } private void CreateToolbar() { var toolbar = new Toolbar(); var addBtn = new ToolbarButton(() => graphView?.CreateNodeAt(graphView.GetDefaultNodePosition())) { text = "+" }; var delBtn = new ToolbarButton(() => graphView?.DeleteSelected()) { text = "-" }; toolbar.Add(addBtn); toolbar.Add(delBtn); soField = new ObjectField("数据") { objectType = typeof(ingame_galgame_so), allowSceneObjects = false }; soField.RegisterValueChangedCallback(evt => SetSo(evt.newValue as ingame_galgame_so)); toolbar.Add(soField); var refreshBtn = new ToolbarButton(RefreshGraph) { text = "刷新" }; toolbar.Add(refreshBtn); rootVisualElement.Add(toolbar); } private void SetSo(ingame_galgame_so so) { currentSo = so; if (soField != null && soField.value != so) { soField.SetValueWithoutNotify(so); } RefreshGraph(); } private void RefreshGraph() { if (graphView == null) return; graphView.Build(currentSo); } } class IngameGalgameGraphView : GraphView { private ingame_galgame_so currentSo; public IngameGalgameGraphView() { 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(); SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); RegisterCallback(OnKeyDown); } public void Build(ingame_galgame_so so) { ClearGraph(); currentSo = so; if (so == null) return; var serialized = new SerializedObject(so); var dialogueList = serialized.FindProperty("dialogueList"); if (dialogueList == null || !dialogueList.isArray) return; var speakerNames = BuildSpeakerNames(serialized); DialogueTextNode prev = null; for (int i = 0; i < dialogueList.arraySize; i++) { var elemProp = dialogueList.GetArrayElementAtIndex(i); var positionTagProp = elemProp.FindPropertyRelative("positionTag"); var textListProp = elemProp.FindPropertyRelative("textList"); if (textListProp == null || !textListProp.isArray) continue; for (int j = 0; j < textListProp.arraySize; j++) { var textProp = textListProp.GetArrayElementAtIndex(j); var node = CreateTextNode(serialized, positionTagProp, textProp, speakerNames, i, j); node.SetPosition(new Rect(80 + i * 420, 80 + j * 260, 360, 220)); AddElement(node); if (prev != null) { var edge = prev.output.ConnectTo(node.input); AddElement(edge); } prev = node; } } } private void ClearGraph() { var toRemove = new List(); foreach (var element in graphElements) { toRemove.Add(element); } DeleteElements(toRemove); } private DialogueTextNode CreateTextNode(SerializedObject so, SerializedProperty positionTagProp, SerializedProperty textProp, string[] speakerNames, int elemIndex, int textIndex) { var node = new DialogueTextNode(); node.title = $"段落 {elemIndex + 1} / 文本 {textIndex + 1}"; node.elementIndex = elemIndex; node.textIndex = textIndex; node.input = node.InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(float)); node.input.portName = "输入"; node.output = node.InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(float)); node.output.portName = "输出"; node.inputContainer.Add(node.input); node.outputContainer.Add(node.output); if (positionTagProp != null) { var field = new PropertyField(positionTagProp, "位置标签"); field.Bind(so); node.extensionContainer.Add(field); } AddTextField(node, so, textProp.FindPropertyRelative("content"), "内容"); AddIntPopup(node, so, textProp.FindPropertyRelative("senderIndex"), speakerNames); AddField(node, so, textProp.FindPropertyRelative("imagePos"), "图片位置"); AddField(node, so, textProp.FindPropertyRelative("flipX"), "水平翻转"); AddField(node, so, textProp.FindPropertyRelative("appearTime"), "出现时间"); AddField(node, so, textProp.FindPropertyRelative("duration"), "持续时间"); AddField(node, so, textProp.FindPropertyRelative("stepMethod"), "步进方式"); AddField(node, so, textProp.FindPropertyRelative("timeAction"), "时间动作"); AddField(node, so, textProp.FindPropertyRelative("slowMotionScale"), "慢动作倍率"); AddField(node, so, textProp.FindPropertyRelative("endAction"), "结束动作"); node.RefreshExpandedState(); node.RefreshPorts(); return node; } private void AddTextField(Node node, SerializedObject so, SerializedProperty prop, string label) { if (prop == null) return; var field = new PropertyField(prop, label); field.Bind(so); node.extensionContainer.Add(field); } private void AddField(Node node, SerializedObject so, SerializedProperty prop, string label) { if (prop == null) return; var field = new PropertyField(prop, label); field.Bind(so); node.extensionContainer.Add(field); } private void AddIntPopup(Node node, SerializedObject so, SerializedProperty senderIndexProp, string[] speakerNames) { if (senderIndexProp == null) return; if (speakerNames == null || speakerNames.Length == 0) { AddField(node, so, senderIndexProp, "发言者索引"); return; } var imgui = new IMGUIContainer(() => { so.Update(); int current = Mathf.Clamp(senderIndexProp.intValue + 1, 0, speakerNames.Length - 1); int chosen = EditorGUILayout.Popup("发言者", current, speakerNames); senderIndexProp.intValue = Mathf.Max(-1, chosen - 1); so.ApplyModifiedProperties(); }); node.extensionContainer.Add(imgui); } private string[] BuildSpeakerNames(SerializedObject so) { var speakersDataSOProp = so.FindProperty("speakersDataSO"); if (speakersDataSOProp == null || speakersDataSOProp.objectReferenceValue == null) return null; var speakersSO = new SerializedObject(speakersDataSOProp.objectReferenceValue); var speakersList = speakersSO.FindProperty("speakers"); if (speakersList == null || !speakersList.isArray) return null; int c = speakersList.arraySize; var names = new string[c + 1]; names[0] = "(无)"; for (int i = 0; i < c; i++) { var e = speakersList.GetArrayElementAtIndex(i); var nameProp = e.FindPropertyRelative("spkrName"); names[i + 1] = nameProp != null ? nameProp.stringValue : $"Speaker {i}"; } return names; } public Vector2 GetDefaultNodePosition() { var worldCenter = new Vector2(layout.xMin + layout.width * 0.5f, layout.yMin + layout.height * 0.5f); return contentViewContainer.WorldToLocal(worldCenter); } public void CreateNodeAt(Vector2 position) { if (currentSo == null) return; Undo.RecordObject(currentSo, "Create Dialogue Node"); var so = new SerializedObject(currentSo); var dialogueList = so.FindProperty("dialogueList"); if (dialogueList == null) return; int elemIndex = dialogueList.arraySize; dialogueList.arraySize++; var elemProp = dialogueList.GetArrayElementAtIndex(elemIndex); if (elemProp != null) { var positionTagProp = elemProp.FindPropertyRelative("positionTag"); if (positionTagProp != null) positionTagProp.enumValueIndex = 0; var textListProp = elemProp.FindPropertyRelative("textList"); if (textListProp != null) { textListProp.arraySize = 1; var textProp = textListProp.GetArrayElementAtIndex(0); ApplyTextDefaults(textProp); } } so.ApplyModifiedProperties(); EditorUtility.SetDirty(currentSo); Build(currentSo); } public void DeleteSelected() { if (currentSo == null) return; var selectedNodes = new List(); foreach (var e in selection) { if (e is DialogueTextNode n) selectedNodes.Add(n); } if (selectedNodes.Count == 0) return; selectedNodes.Sort((a, b) => { int c = b.elementIndex.CompareTo(a.elementIndex); return c != 0 ? c : b.textIndex.CompareTo(a.textIndex); }); Undo.RecordObject(currentSo, "Delete Dialogue Node"); var so = new SerializedObject(currentSo); var dialogueList = so.FindProperty("dialogueList"); if (dialogueList == null) return; foreach (var node in selectedNodes) { if (node.elementIndex < 0 || node.elementIndex >= dialogueList.arraySize) continue; var elemProp = dialogueList.GetArrayElementAtIndex(node.elementIndex); if (elemProp == null) continue; var textListProp = elemProp.FindPropertyRelative("textList"); if (textListProp == null) continue; if (node.textIndex >= 0 && node.textIndex < textListProp.arraySize) { textListProp.DeleteArrayElementAtIndex(node.textIndex); } if (textListProp.arraySize <= 0) { dialogueList.DeleteArrayElementAtIndex(node.elementIndex); } } so.ApplyModifiedProperties(); EditorUtility.SetDirty(currentSo); Build(currentSo); } public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) { evt.menu.AppendAction("新建节点", _ => CreateNodeAt(evt.localMousePosition)); bool hasSelection = false; foreach (var e in selection) { if (e is DialogueTextNode) { hasSelection = true; break; } } evt.menu.AppendAction("删除所选", _ => DeleteSelected(), hasSelection ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled); } private void OnKeyDown(KeyDownEvent evt) { if (evt.ctrlKey && evt.keyCode == KeyCode.C) { EditorGUIUtility.systemCopyBuffer = SerializeSelection(); evt.StopPropagation(); } else if (evt.ctrlKey && evt.keyCode == KeyCode.V) { PasteFromClipboard(); evt.StopPropagation(); } else if (evt.keyCode == KeyCode.Delete || evt.keyCode == KeyCode.Backspace) { DeleteSelected(); evt.StopPropagation(); } } private string SerializeSelection() { if (currentSo == null) return string.Empty; var payload = new DialogueCopyPayload(); var so = new SerializedObject(currentSo); var dialogueList = so.FindProperty("dialogueList"); foreach (var e in selection) { if (e is not DialogueTextNode node) continue; if (dialogueList == null) continue; if (node.elementIndex < 0 || node.elementIndex >= dialogueList.arraySize) continue; var elemProp = dialogueList.GetArrayElementAtIndex(node.elementIndex); if (elemProp == null) continue; var positionTagProp = elemProp.FindPropertyRelative("positionTag"); var textListProp = elemProp.FindPropertyRelative("textList"); if (textListProp == null) continue; if (node.textIndex < 0 || node.textIndex >= textListProp.arraySize) continue; var textProp = textListProp.GetArrayElementAtIndex(node.textIndex); if (textProp == null) continue; var data = new DialogueTextCopyData(); data.positionTag = positionTagProp != null ? positionTagProp.enumValueIndex : 0; data.content = textProp.FindPropertyRelative("content")?.stringValue; data.textNumber = textProp.FindPropertyRelative("textNumber")?.intValue ?? 0; data.senderIndex = textProp.FindPropertyRelative("senderIndex")?.intValue ?? -1; data.imagePos = textProp.FindPropertyRelative("imagePos")?.enumValueIndex ?? 0; data.flipX = textProp.FindPropertyRelative("flipX")?.boolValue ?? false; data.appearTime = textProp.FindPropertyRelative("appearTime")?.floatValue ?? 0f; data.duration = textProp.FindPropertyRelative("duration")?.floatValue ?? 0f; data.stepMethod = textProp.FindPropertyRelative("stepMethod")?.enumValueIndex ?? 0; data.timeAction = textProp.FindPropertyRelative("timeAction")?.enumValueIndex ?? 0; data.slowMotionScale = textProp.FindPropertyRelative("slowMotionScale")?.floatValue ?? 0f; data.endAction = textProp.FindPropertyRelative("endAction")?.enumValueIndex ?? 0; payload.items.Add(data); } return payload.items.Count == 0 ? string.Empty : JsonUtility.ToJson(payload); } private bool CanPasteDataInternal(string serializedData) { if (string.IsNullOrEmpty(serializedData)) return false; var payload = JsonUtility.FromJson(serializedData); return payload != null && payload.items != null && payload.items.Count > 0; } private void PasteFromClipboard() { if (currentSo == null) return; var serializedData = EditorGUIUtility.systemCopyBuffer; if (!CanPasteDataInternal(serializedData)) return; var payload = JsonUtility.FromJson(serializedData); if (payload == null || payload.items == null || payload.items.Count == 0) return; Undo.RecordObject(currentSo, "Paste Dialogue Node"); var so = new SerializedObject(currentSo); var dialogueList = so.FindProperty("dialogueList"); if (dialogueList == null) return; foreach (var item in payload.items) { int elemIndex = dialogueList.arraySize; dialogueList.arraySize++; var elemProp = dialogueList.GetArrayElementAtIndex(elemIndex); if (elemProp == null) continue; var positionTagProp = elemProp.FindPropertyRelative("positionTag"); if (positionTagProp != null) positionTagProp.enumValueIndex = item.positionTag; var textListProp = elemProp.FindPropertyRelative("textList"); if (textListProp != null) { textListProp.arraySize = 1; var textProp = textListProp.GetArrayElementAtIndex(0); ApplyTextData(textProp, item); } } so.ApplyModifiedProperties(); EditorUtility.SetDirty(currentSo); Build(currentSo); } private void ApplyTextDefaults(SerializedProperty textProp) { if (textProp == null) return; textProp.FindPropertyRelative("content").stringValue = string.Empty; textProp.FindPropertyRelative("textNumber").intValue = 1; textProp.FindPropertyRelative("senderIndex").intValue = -1; textProp.FindPropertyRelative("imagePos").enumValueIndex = 0; var flipXProp = textProp.FindPropertyRelative("flipX"); if (flipXProp != null) flipXProp.boolValue = false; textProp.FindPropertyRelative("appearTime").floatValue = 0f; textProp.FindPropertyRelative("duration").floatValue = 0f; textProp.FindPropertyRelative("stepMethod").enumValueIndex = 0; textProp.FindPropertyRelative("timeAction").enumValueIndex = 0; textProp.FindPropertyRelative("slowMotionScale").floatValue = 0f; textProp.FindPropertyRelative("endAction").enumValueIndex = 0; } private void ApplyTextData(SerializedProperty textProp, DialogueTextCopyData data) { if (textProp == null || data == null) return; textProp.FindPropertyRelative("content").stringValue = data.content ?? string.Empty; textProp.FindPropertyRelative("textNumber").intValue = data.textNumber; textProp.FindPropertyRelative("senderIndex").intValue = data.senderIndex; textProp.FindPropertyRelative("imagePos").enumValueIndex = data.imagePos; var flipXProp = textProp.FindPropertyRelative("flipX"); if (flipXProp != null) flipXProp.boolValue = data.flipX; textProp.FindPropertyRelative("appearTime").floatValue = data.appearTime; textProp.FindPropertyRelative("duration").floatValue = data.duration; textProp.FindPropertyRelative("stepMethod").enumValueIndex = data.stepMethod; textProp.FindPropertyRelative("timeAction").enumValueIndex = data.timeAction; textProp.FindPropertyRelative("slowMotionScale").floatValue = data.slowMotionScale; textProp.FindPropertyRelative("endAction").enumValueIndex = data.endAction; } [System.Serializable] private class DialogueTextCopyData { public int positionTag; public string content; public int textNumber; public int senderIndex; public int imagePos; public bool flipX; public float appearTime; public float duration; public int stepMethod; public int timeAction; public float slowMotionScale; public int endAction; } [System.Serializable] private class DialogueCopyPayload { public List items = new List(); } } class DialogueTextNode : Node { public Port input; public Port output; public int elementIndex; public int textIndex; } #endif