2026-01-06 14:24:23 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using Cysharp.Threading.Tasks;
|
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
using Sirenix.OdinInspector;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using XNode;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Stary.Evo.StoryEditor
|
|
|
|
|
|
{
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
|
public class ScriptGraph : NodeGraph
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 起始节点
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[SerializeField, ReadOnly]
|
|
|
|
|
|
private BeginNode begin;
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 结束节点
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[SerializeField, ReadOnly]
|
|
|
|
|
|
private EndNode end;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化标识
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[SerializeField, ReadOnly]
|
|
|
|
|
|
private bool initialized;
|
|
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (nodes == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError("Graph异常,无有效节点列表存在");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
if (!initialized)
|
|
|
|
|
|
{
|
|
|
|
|
|
_ = DelayInitialize();
|
|
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
UnityEditor.EditorUtility.SetDirty(this);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
protected override void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 清理所有节点
|
|
|
|
|
|
nodes.ForEach(DestroyImmediate);
|
|
|
|
|
|
nodes.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#region 初始化
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 延迟初始化
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async UniTask DelayInitialize()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 等待Graph资产落盘
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
while (string.IsNullOrEmpty(UnityEditor.AssetDatabase.GetAssetPath(this)))
|
|
|
|
|
|
{
|
|
|
|
|
|
await UniTask.Delay(TimeSpan.FromSeconds(0.01));
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
await UniTask.Delay(TimeSpan.FromSeconds(0.01));
|
|
|
|
|
|
|
|
|
|
|
|
// 创建初始节点
|
|
|
|
|
|
InitBeginNode();
|
|
|
|
|
|
InitEndNode();
|
|
|
|
|
|
CreateSampleScriptParagraph();
|
|
|
|
|
|
|
|
|
|
|
|
initialized = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化起始节点
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void InitBeginNode()
|
|
|
|
|
|
{
|
|
|
|
|
|
var existing = nodes.FirstOrDefault(n => n is BeginNode) as BeginNode;
|
|
|
|
|
|
if (existing != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
begin = existing;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
begin = NodeBase.Create<BeginNode>(this, "Begin", Vector2.zero);
|
|
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化结束节点
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void InitEndNode()
|
|
|
|
|
|
{
|
|
|
|
|
|
var existing = nodes.FirstOrDefault(n => n is EndNode) as EndNode;
|
|
|
|
|
|
if (existing != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
end = existing;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
end = NodeBase.Create<EndNode>(this, "End", new Vector2(NodeBase.DefaultNodeXInterval, NodeBase.DefaultNodeYInterval)* 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建剧本段落样例
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void CreateSampleScriptParagraph()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 创建新的剧本段落
|
|
|
|
|
|
var paragraph = ScriptParagraphNode.Create(this, $"{name}_para_sample", new Vector2(NodeBase.DefaultNodeXInterval, NodeBase.DefaultNodeYInterval));
|
|
|
|
|
|
|
|
|
|
|
|
// 将剧本与起始节点和结束节点相连
|
|
|
|
|
|
begin.GetOutputPort("exit").Connect(paragraph.GetInputPort("enter"));
|
|
|
|
|
|
paragraph.GetOutputPort("exit").Connect(end.GetInputPort("enter"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 导出设置
|
|
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
2026-01-08 11:07:40 +08:00
|
|
|
|
|
|
|
|
|
|
[BoxGroup("Export")]
|
|
|
|
|
|
[SerializeField, ValueDropdown(nameof(GetAllAssemblyNames))]
|
|
|
|
|
|
private string assembly;
|
|
|
|
|
|
private IEnumerable<string> GetAllAssemblyNames() =>
|
|
|
|
|
|
AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).OrderBy(n => n);
|
2026-01-08 15:23:32 +08:00
|
|
|
|
|
2026-01-06 14:24:23 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取继承 IResource 的所有类
|
|
|
|
|
|
/// </summary>
|
2026-01-08 15:23:32 +08:00
|
|
|
|
private HashSet<string> IResourceTypes
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(assembly))
|
|
|
|
|
|
return new();
|
|
|
|
|
|
var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == assembly);
|
|
|
|
|
|
return asm == null
|
|
|
|
|
|
? new()
|
|
|
|
|
|
: asm.GetTypes().Where(t => t.IsClass && !t.IsAbstract && typeof(IResource).IsAssignableFrom(t))
|
|
|
|
|
|
.Select(t => t.Name).ToHashSet();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-06 14:24:23 +08:00
|
|
|
|
#else
|
|
|
|
|
|
private HashSet<string> _iResourceTypes = new();
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2026-01-08 11:07:40 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 资源加载方式
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[BoxGroup("Export", centerLabel:true)]
|
2026-01-08 15:23:32 +08:00
|
|
|
|
[LabelText("资源加载方式"), ValueDropdown(nameof(IResourceTypes)),SerializeField]
|
2026-01-08 11:07:40 +08:00
|
|
|
|
private string loaderType;
|
|
|
|
|
|
|
2026-01-06 14:24:23 +08:00
|
|
|
|
private IResource _loader;
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 资源加载器
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public IResource Loader
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_loader == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var type = Type.GetType(loaderType);
|
|
|
|
|
|
_loader = type == null ? null : (IResource)Activator.CreateInstance(type);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return _loader;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[BoxGroup("Export")]
|
|
|
|
|
|
[LabelText("包体ID")]
|
|
|
|
|
|
public string packageID;
|
|
|
|
|
|
|
|
|
|
|
|
[BoxGroup("Export")]
|
|
|
|
|
|
[LabelText("Graph目录地址")]
|
|
|
|
|
|
public string graphPath;
|
|
|
|
|
|
|
|
|
|
|
|
[BoxGroup("Export")]
|
|
|
|
|
|
[Button("导出")]
|
|
|
|
|
|
public async void Export()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 资源加载方式排空
|
|
|
|
|
|
if (Loader == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError("导出失败,没有选择资源加载方式");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Dictionary<Node, NodeData> dataMatch = new();
|
|
|
|
|
|
Dictionary<NodeData, NodeBase> nodeMatch = new();
|
|
|
|
|
|
|
|
|
|
|
|
// 创建图表数据
|
|
|
|
|
|
GraphData graph = new();
|
|
|
|
|
|
graph.name = name;
|
|
|
|
|
|
graph.resourceType = loaderType;
|
|
|
|
|
|
|
|
|
|
|
|
// 转录节点数据
|
|
|
|
|
|
foreach (var node in nodes)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(node is not NodeBase bNode)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
NodeData dNode = new();
|
|
|
|
|
|
switch (bNode)
|
|
|
|
|
|
{
|
|
|
|
|
|
case BeginNode beginNode:
|
|
|
|
|
|
dNode = await beginNode.Export();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case EndNode endNode:
|
|
|
|
|
|
dNode = await endNode.Export();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case ScriptParagraphNode paraNode:
|
|
|
|
|
|
dNode = await paraNode.Export();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case FlowNode flowNode:
|
|
|
|
|
|
dNode = await flowNode.Export();
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
Debug.LogError($"节点{node.name}[type:{node.GetType()}]没有定义对应的数据转换逻辑,该节点将被跳过,此举将可能导致graph出现流程断裂或其他未知异常");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dataMatch.Add(bNode, dNode);
|
|
|
|
|
|
nodeMatch.Add(dNode, bNode);
|
|
|
|
|
|
graph.nodes.Add(dNode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转录节点连接
|
|
|
|
|
|
foreach (var data in nodeMatch.Keys)
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (data)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 起始节点
|
|
|
|
|
|
case BeginNodeData beginData:
|
|
|
|
|
|
// 记录图表首个节点的索引
|
|
|
|
|
|
graph.startNodeIndex = graph.nodes.IndexOf(beginData);
|
|
|
|
|
|
// 连接后部节点
|
|
|
|
|
|
nodeMatch[beginData].GetOutputPort("exit").GetConnections().ForEach(oNode => beginData.next.Add(graph.nodes.IndexOf(dataMatch[oNode.node])));
|
|
|
|
|
|
break;
|
|
|
|
|
|
// 结束节点
|
|
|
|
|
|
case EndNodeData endData:
|
|
|
|
|
|
// 连接前部节点
|
|
|
|
|
|
nodeMatch[endData].GetInputPort("enter").GetConnections().ForEach(oNode => endData.pre.Add(graph.nodes.IndexOf(dataMatch[oNode.node])));
|
|
|
|
|
|
break;
|
|
|
|
|
|
// 空节点(流程节点)
|
|
|
|
|
|
case FlowNodeData flowData:
|
|
|
|
|
|
// 连接前部节点
|
|
|
|
|
|
nodeMatch[flowData].GetInputPort("enter").GetConnections().ForEach(oNode => flowData.pre.Add(graph.nodes.IndexOf(dataMatch[oNode.node])));
|
|
|
|
|
|
// 连接后部节点
|
|
|
|
|
|
nodeMatch[flowData].GetOutputPort("exit").GetConnections().ForEach(oNode => flowData.next.Add(graph.nodes.IndexOf(dataMatch[oNode.node])));
|
|
|
|
|
|
break;
|
|
|
|
|
|
// 处理异常情况
|
|
|
|
|
|
default:
|
|
|
|
|
|
Debug.LogError($"节点{data.name}[type:{data.GetType()}]没有定义对应的数据转换逻辑,该节点将被跳过,此举将可能导致graph出现流程断裂或其他未知异常");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Debug.Log("转录完成");
|
|
|
|
|
|
|
|
|
|
|
|
// 将转录完成的图表数据序列化为json
|
|
|
|
|
|
var json = JsonConvert.SerializeObject(graph, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All });
|
|
|
|
|
|
// 文件目录排空
|
|
|
|
|
|
var graphFilePath = Path.Combine(Application.dataPath, graphPath.Replace(".asset", ".sg.json"));
|
|
|
|
|
|
if (!Directory.Exists(Path.GetDirectoryName(graphFilePath)))
|
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(graphFilePath) ?? string.Empty);
|
|
|
|
|
|
// 写入json
|
|
|
|
|
|
await File.WriteAllTextAsync(graphFilePath, json);
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
|
UnityEditor.AssetDatabase.ImportAsset(graphFilePath.Replace(Application.dataPath, "Assets"));
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
Debug.Log("剧本导出完成");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|