Files
bansonic_beta_main/Assets/_galgame_inGame/Editor/IngameGalgameGraphWindow.cs
T

600 lines
20 KiB
C#

#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
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();
toolbar.Add(new ToolbarButton(() => graphView?.CreateNodeAt(graphView.GetDefaultNodePosition())) { text = "+" });
toolbar.Add(new ToolbarButton(() => graphView?.DeleteSelected()) { text = "-" });
soField = new ObjectField("数据")
{
objectType = typeof(ingame_galgame_so),
allowSceneObjects = false
};
soField.RegisterValueChangedCallback(evt => SetSo(evt.newValue as ingame_galgame_so));
toolbar.Add(soField);
toolbar.Add(new ToolbarButton(RefreshGraph) { text = "刷新" });
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);
}
}
internal 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;
}
string[] 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)
{
AddElement(prev.output.ConnectTo(node.input));
}
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
{
title = $"段落 {elemIndex + 1} / 文本 {textIndex + 1}",
elementIndex = elemIndex,
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);
}
AddField(node, so, textProp.FindPropertyRelative("content"), "文本内容");
AddSpeakerPopup(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 static 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 static void AddSpeakerPopup(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 count = speakersList.arraySize;
string[] names = new string[count + 1];
names[0] = "(无)";
for (int i = 0; i < count; i++)
{
var element = speakersList.GetArrayElementAtIndex(i);
var nameProp = element.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 element in selection)
{
if (element is DialogueTextNode node)
{
selectedNodes.Add(node);
}
}
if (selectedNodes.Count == 0)
{
return;
}
selectedNodes.Sort((a, b) =>
{
int compare = b.elementIndex.CompareTo(a.elementIndex);
return compare != 0 ? compare : 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 element in selection)
{
if (element 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 element in selection)
{
if (element is not DialogueTextNode node || dialogueList == null)
{
continue;
}
if (node.elementIndex < 0 || node.elementIndex >= dialogueList.arraySize)
{
continue;
}
var elemProp = dialogueList.GetArrayElementAtIndex(node.elementIndex);
var positionTagProp = elemProp?.FindPropertyRelative("positionTag");
var textListProp = elemProp?.FindPropertyRelative("textList");
if (textListProp == null || node.textIndex < 0 || node.textIndex >= textListProp.arraySize)
{
continue;
}
var textProp = textListProp.GetArrayElementAtIndex(node.textIndex);
if (textProp == null)
{
continue;
}
payload.items.Add(new DialogueTextCopyData
{
positionTag = positionTagProp != null ? positionTagProp.enumValueIndex : 0,
content = textProp.FindPropertyRelative("content")?.stringValue,
textNumber = textProp.FindPropertyRelative("textNumber")?.intValue ?? 0,
senderIndex = textProp.FindPropertyRelative("senderIndex")?.intValue ?? -1,
imagePos = textProp.FindPropertyRelative("imagePos")?.enumValueIndex ?? 0,
flipX = textProp.FindPropertyRelative("flipX")?.boolValue ?? false,
appearTime = textProp.FindPropertyRelative("appearTime")?.floatValue ?? 0f,
duration = textProp.FindPropertyRelative("duration")?.floatValue ?? 0f,
stepMethod = textProp.FindPropertyRelative("stepMethod")?.enumValueIndex ?? 0,
timeAction = textProp.FindPropertyRelative("timeAction")?.enumValueIndex ?? 0,
slowMotionScale = textProp.FindPropertyRelative("slowMotionScale")?.floatValue ?? 0f,
endAction = textProp.FindPropertyRelative("endAction")?.enumValueIndex ?? 0
});
}
return payload.items.Count == 0 ? string.Empty : JsonUtility.ToJson(payload);
}
private static 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 static 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 = 1f;
textProp.FindPropertyRelative("stepMethod").enumValueIndex = 0;
textProp.FindPropertyRelative("timeAction").enumValueIndex = 0;
textProp.FindPropertyRelative("slowMotionScale").floatValue = 0.5f;
textProp.FindPropertyRelative("endAction").enumValueIndex = 0;
}
private static 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>();
}
}
internal class DialogueTextNode : Node
{
public Port input;
public Port output;
public int elementIndex;
public int textIndex;
}
#endif