471 lines
19 KiB
C#
471 lines
19 KiB
C#
#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<IngameGalgameGraphWindow>("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<KeyDownEvent>(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<GraphElement>();
|
|
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<DialogueTextNode>();
|
|
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<DialogueCopyPayload>(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<DialogueCopyPayload>(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<DialogueTextCopyData> items = new List<DialogueTextCopyData>();
|
|
}
|
|
}
|
|
|
|
class DialogueTextNode : Node
|
|
{
|
|
public Port input;
|
|
public Port output;
|
|
public int elementIndex;
|
|
public int textIndex;
|
|
}
|
|
#endif
|