【a】可视化剧本编辑器 10.StoryEditor

This commit is contained in:
mzh
2026-01-06 14:24:23 +08:00
parent f055116d4d
commit 2e8accfed8
80 changed files with 3145 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
using System;
using Cysharp.Threading.Tasks;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 起始节点
/// </summary>
[Serializable, CreateNodeMenu("")]
public class BeginNode : NodeBase
{
[Output]
public Exit exit;
/// <summary>
/// 导出
/// </summary>
public new async UniTask<BeginNodeData> Export() => new(await base.Export());
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7bdd4ad134525043830b54f6a2391f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using System;
using Cysharp.Threading.Tasks;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 结束节点
/// </summary>
[Serializable, CreateNodeMenu("")]
public class EndNode : NodeBase
{
[Input]
public Enter enter;
/// <summary>
/// 导出
/// </summary>
public new async UniTask<EndNodeData> Export() => new(await base.Export());
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e8d1b2bbe7b8154c8822cf7c0b6e064
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Sirenix.OdinInspector;
using UnityEngine;
using XNode;
namespace Stary.Evo.StoryEditor
{
[Serializable,CreateNodeMenu("")]
public class FlowNode : NodeBase
{
[HorizontalGroup("Port", order:0)]
[Input]
public Enter enter;
[HorizontalGroup("Port")]
[Output]
public Exit exit;
/// <summary>
/// Exit端口连接的节点的执行类型
/// </summary>
[BoxGroup("Config", order:5, showLabel:false)]
[LabelText("后续执行")]
public NodeExecuteType exitNodeExecuteType = NodeExecuteType.Async;
/// <summary>
/// 创建新的流程节点
/// </summary>
/// <param name="graph">所在的块</param>
/// <param name="name">节点名称</param>
/// <param name="position">节点位置</param>
/// <param name="prePorts">节点前部连接</param>
/// <param name="nextPorts">节点后部连接</param>
public static T Create<T>(NodeGraph graph, string name = null, Vector2 position = default,
List<NodePort> prePorts = null, List<NodePort> nextPorts = null) where T:FlowNode
{
// 创建节点
var node = NodeBase.Create<T>(graph, name, position);
if (node == null)
return null;
// 将剧本与前部节点和后部节点相连
var enterPort = node.GetInputPort("enter");
if (enterPort != null && prePorts != null)
prePorts.ForEach(port => port.Connect(enterPort));
var exitPort = node.GetInputPort("exit");
if (exitPort != null && nextPorts != null)
nextPorts.ForEach(port => port.Connect(exitPort));
return node;
}
#region
/// <summary>
/// 将自身的入口连接传递给指定端口
/// </summary>
/// <param name="otherPort">指定端口</param>
/// <param name="deleteSelf">传递后删除自身连接</param>
public void GiveEnterPortToOtherPort(NodePort otherPort, bool deleteSelf)
=> GiveConnectionToOtherPort(GetInputPort("enter"), otherPort, deleteSelf);
/// <summary>
/// 将自身的出口连接传递给指定端口
/// </summary>
/// <param name="otherPort">指定端口</param>
/// <param name="deleteSelf">传递后删除自身连接</param>
public void GiveExitPortToOtherPort(NodePort otherPort, bool deleteSelf = false)
=> GiveConnectionToOtherPort(GetOutputPort("exit"), otherPort, deleteSelf);
#endregion
#region
/// <summary>
/// 向前插入节点
/// </summary>
[HorizontalGroup("Insert", order:1)]
[Button("(←)插入节点")]
public void InsertForward()
{
// 生成新的空节点
var newNode = SelectionNode.Create(graph);
// 将本节点的Enter连接给到新的节点的Enter
GiveEnterPortToOtherPort(newNode.GetInputPort("enter"), true);
// 将新的节点的Exit连接到本节点的Enter
GetInputPort("enter").Connect(newNode.GetOutputPort("exit"));
// 将新节点设定为本节点位置
newNode.position = position;
// 将包括自身在内的所有后续节点向后移动一个默认间隔
newNode.DoActionToAllNodesAfter((thisNode, otherNode) => otherNode.position = thisNode.position + new Vector2(DefaultNodeXInterval, DefaultNodeYInterval));
}
/// <summary>
/// 向后插入节点
/// </summary>
[HorizontalGroup("Insert")]
[Button("插入节点(→)")]
public void InsertBackward()
{
// 生成新的空节点
var newNode = SelectionNode.Create(graph);
// 将本节点的Exit连接给到新的节点的Exit
GiveExitPortToOtherPort(newNode.GetOutputPort("exit"), true);
// 将新的节点的Enter连接到本节点的Exit
GetOutputPort("exit").Connect(newNode.GetInputPort("enter"));
// 将新节点设置为本节点位置后移一格
newNode.position = position + new Vector2(DefaultNodeXInterval, DefaultNodeYInterval);
// 将所有后续节点向后移动一个默认间隔
newNode.DoActionToAllNodesAfter((thisNode, otherNode) => otherNode.position = thisNode.position + new Vector2(DefaultNodeXInterval, DefaultNodeYInterval));
}
#endregion
/// <summary>
/// 导出
/// </summary>
public new async UniTask<FlowNodeData> Export()
{
FlowNodeData node = new(await base.Export());
node.executeType = exitNodeExecuteType;
return node;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e85b8c018e8b68b418fff4c389089665
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,134 @@
using System;
using System.Linq;
using Cysharp.Threading.Tasks;
using UnityEngine;
using XNode;
namespace Stary.Evo.StoryEditor
{
[Serializable,CreateNodeMenu("")]
public class NodeBase : Node
{
[Serializable] public class Enter { }
[Serializable] public class Exit { }
/// <summary>
/// 默认节点位置间隔x轴
/// </summary>
public const float DefaultNodeXInterval = 250;
/// <summary>
/// 默认节点位置间隔y轴
/// </summary>
public const float DefaultNodeYInterval = 0;
/// <summary>
/// 创建新的节点
/// </summary>
/// <param name="graph">所在的块</param>
/// <param name="name">节点名称</param>
/// <param name="position">节点位置</param>
public static T Create<T>(NodeGraph graph, string name = null, Vector2 position = default) where T : NodeBase
{
// 创建节点
var node = graph.AddNode<T>();
if (node == null)
{
Debug.LogError("节点创建失败");
return null;
}
#if UNITY_EDITOR
// 将节点落盘
UnityEditor.AssetDatabase.AddObjectToAsset(node, graph);
UnityEditor.EditorUtility.SetDirty(graph);
#endif
// 设置节点变量
node.position = position;
node.name = name ?? $"{name}_para_sample";
// 初始化节点
node.Init();
return node;
}
/// <summary>
/// 移除自身
/// </summary>
public void DestroySelf()
{
// 清理连接
ClearConnections();
// 移除与Graph之间的联系
if (graph)
{
graph.nodes.Remove(this);
#if UNITY_EDITOR
UnityEditor.AssetDatabase.RemoveObjectFromAsset(this);
#endif
}
// 销毁自身
DestroyImmediate(this);
}
public UniTask<NodeData> Export()
{
NodeData nodeData = new();
nodeData.name = name;
return UniTask.FromResult(nodeData);
}
#region
/// <summary>
/// 将端口A的连接传递给端口B
/// </summary>
/// <param name="portA">端口A</param>
/// <param name="portB">端口B</param>
/// <param name="deleteA">移除端口A的连接</param>
public static void GiveConnectionToOtherPort(NodePort portA, NodePort portB, bool deleteA = false)
{
// 排除端口A与端口B重合的情况
if (portA == portB)
return;
// 获取当前Enter端口的所有连接
var enterPorts = portA.GetConnections();
enterPorts.ForEach(port =>
{
// 将Enter端口与该端口断开
if (deleteA)
port.Disconnect(portA);
// 将指定的端口与该端口连接
port.Connect(portB);
});
}
/// <summary>
/// 对所有之后的节点执行动作
/// </summary>
/// <param name="action">指定动作</param>
public void DoActionToAllNodesAfter(Action<NodeBase, NodeBase> action)
{
// 所有出口端口
Ports.Where(p => p.IsOutput).ToList().ForEach(port =>
{
// 所有出口端口连接的所有端口
port.GetConnections().ForEach(otherSide =>
{
// 所有出口端口连接的所有端口的是NodeBase的节点
if (otherSide.node is NodeBase baseNode)
{
action?.Invoke(this, baseNode);
baseNode.DoActionToAllNodesAfter(action);
}
});
});
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 128757ef2e3a23c48b67ade15852c36c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Sirenix.OdinInspector;
using UnityEngine;
using XNode;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 剧本自然段
/// </summary>
[Serializable, CreateNodeMenu("创建剧本自然段", order = 5)]
public class ScriptParagraphNode : FlowNode
{
/// <summary>
/// 名称
/// (仅供辨识,不起实际作用)
/// </summary>
[BoxGroup("Info", order:10, showLabel:false)]
[LabelText("名称"),LabelWidth(30)]
public string displayName;
/// <summary>
/// 字幕
/// </summary>
[BoxGroup("Info")]
[LabelText("字幕"),LabelWidth(30)]
public Sprite caption;
/// <summary>
/// 语音
/// </summary>
[BoxGroup("Info")]
[LabelText("音频"),LabelWidth(30)]
public AudioClip audio;
/// <summary>
/// 创建新的剧本段落节点
/// </summary>
/// <param name="graph">所在的块</param>
/// <param name="name">节点名称</param>
/// <param name="position">节点位置</param>
/// <param name="prePorts">节点前部连接</param>
/// <param name="nextPorts">节点后部连接</param>
/// <param name="caption">字幕</param>
/// <param name="audio">音频</param>
public static ScriptParagraphNode Create(NodeGraph graph, string name = null, Vector2 position = default, List<NodePort> prePorts = null, List<NodePort> nextPorts = null, Sprite caption = null, AudioClip audio = null)
{
// 创建节点
var node = Create<ScriptParagraphNode>(graph,name,position, prePorts, nextPorts);
if (node == null)
return null;
// 设置变量
node.caption = caption;
node.audio = audio;
return node;
}
protected override void Init()
{
base.Init();
UpdateName();
}
private void OnValidate()
{
if (graph == null)
{
Debug.LogWarning($"项目中存在幽灵节点:{name}");
return;
}
UpdateName();
}
/// <summary>
/// 更新名称
/// </summary>
private void UpdateName()
{
if (string.IsNullOrEmpty(displayName))
{
displayName = graph.name + "_para";
}
name = displayName;
}
public new async UniTask<ScriptParagraphNodeData> Export()
{
var node = new ScriptParagraphNodeData(await base.Export());
var sGraph = (ScriptGraph)graph;
// 记录音频资源地址
if (audio)
node.audioPath = await sGraph.Loader.Save(audio,sGraph.packageID);
// 记录字幕资源地址
if (caption)
node.captionPath = await sGraph.Loader.Save(caption,sGraph.packageID);
return node;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e0dc65de1614514883efb2a6848e5a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using XNode;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 空节点
/// (可选择应用哪种节点)
/// </summary>
[Serializable, CreateNodeMenu("创建空节点", order = 0)]
public class SelectionNode : FlowNode
{
/// <summary>
/// 创建新的空节点
/// </summary>
/// <param name="graph">所在的块</param>
/// <param name="name">节点名称</param>
/// <param name="position">节点位置</param>
/// <param name="prePorts">节点前部连接</param>
/// <param name="nextPorts">节点后部连接</param>
public static SelectionNode Create(NodeGraph graph, string name = null, Vector2 position = default,
List<NodePort> prePorts = null, List<NodePort> nextPorts = null)
{
var node = Create<SelectionNode>(graph, name, position, prePorts, nextPorts);
return node;
}
/// <summary>
/// 创建剧本段落
/// </summary>
[BoxGroup("Option", order:20, showLabel:false)]
[Button("剧本段落")]
public void ChooseScriptParagraph()
{
// 记录动作缓存
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(graph, "Delete Node");
#endif
// 在本节点位置创建剧本段落节点
var node = ScriptParagraphNode.Create(graph, position: position);
GiveEnterPortToOtherPort(node.GetInputPort("enter"), true);
GiveExitPortToOtherPort(node.GetOutputPort("exit"), true);
// 移除本节点
DestroySelf();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 79ef9a28ecfd1384a96569da56b2022b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: