【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,8 @@
fileFormatVersion: 2
guid: c40af5c425ac0874aabc6979f2a6e289
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7418c79b3f95ac9428a8836bccd4cb7a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
// ReSharper disable Unity.PerformanceCriticalCodeInvocation
namespace Stary.Evo.StoryEditor.Editor
{
public class GraphCreateWindow : EditorWindow
{
private int _selectedIndex;
private string _scriptName = "graph";
private bool _showTip;
private string _tip;
private string[] _options = Array.Empty<string>();
private Dictionary<string, string> _paths = new();
[MenuItem("Evo/剧本编辑器/创建剧本")]
private static void Open()
{
// 打开一个浮动窗口
GetWindow<GraphCreateWindow>( "创建配置" ) ;
}
private void OnEnable()
{
minSize = maxSize = new Vector2(300, 200);
_options = GetPackages();
}
private void OnGUI()
{
GUILayout.Space(15);
EditorGUILayout.BeginHorizontal();
GUILayout.Space(10);
EditorGUILayout.BeginVertical();
// Package选项
EditorGUILayout.LabelField( "请选择剧本所在的Package" , EditorStyles.boldLabel ) ;
var newIndex = EditorGUILayout.Popup( _selectedIndex , _options ) ;
_selectedIndex = newIndex;
GUILayout.Space(15);
EditorGUILayout.LabelField( "请输入剧本名称" , EditorStyles.boldLabel ) ;
var newName = EditorGUILayout.TextField(_scriptName);
if (_scriptName != newName)
{
_scriptName = newName;
_showTip = false;
}
if (_showTip)
{
GUIStyle redBold = new GUIStyle(EditorStyles.boldLabel)
{
normal = { textColor = Color.red }
};
EditorGUILayout.LabelField(_tip, redBold) ;
}
GUILayout.Space(15);
// 创建按钮
if (GUILayout.Button("创建剧本"))
{
try
{
CreateScriptGraph(_options[newIndex]);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
EditorGUILayout.EndVertical();
GUILayout.Space(10);
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 获取所有Module Package
/// </summary>
private string[] GetPackages()
{
// 查找Modules目录
List<string> modules = new();
GetDirectoryPaths(Application.dataPath, modules, "Modules");
if (modules.Count == 0)
{
Debug.LogError("未找到任何Modules目录");
return Array.Empty<string>();
}
// 查找package
List<string> packages = new();
GetDirectoryPaths(modules[0], packages, "com.");
if (packages.Count == 0)
{
Debug.LogError("未找到任何Package");
return Array.Empty<string>();
}
// 记录Package地址并返回选项
_paths.Clear();
packages.ForEach(path => _paths.Add(Path.GetFileName(path), path));
return _paths.Keys.ToArray();
}
/// <summary>
/// 获取符合条件的目录
/// </summary>
/// <param name="root">查找起始目录</param>
/// <param name="result">查找结果</param>
/// <param name="title">筛选条件title</param>
/// <param name="tail">筛选条件tail</param>
private static void GetDirectoryPaths(string root, List<string> result, string title = null, string tail = null)
{
foreach (var dir in Directory.GetDirectories(root))
{
// ReSharper disable once ReplaceWithSingleAssignment.True
var check = true;
// 匹配文件头
if (!string.IsNullOrEmpty(title) && !Path.GetFileName(dir).StartsWith(title))
{
check = false;
}
// 匹配文件尾
if (!string.IsNullOrEmpty(tail) && !Path.GetFileName(dir).EndsWith(tail))
{
check = false;
}
if(check)
result.Add(dir.Replace('\\', '/'));
// 继续往下找
GetDirectoryPaths(dir, result, title);
}
}
/// <summary>
/// 创建剧本
/// </summary>
/// <param name="packageID">包体ID</param>
private void CreateScriptGraph(string packageID)
{
// 检查剧本名称是否有效
if (string.IsNullOrEmpty(_scriptName))
{
_tip = "剧本名称不能为空";
_showTip = true;
return;
}
if (_paths.TryGetValue(packageID, out var path))
{
// 包体目录排空
var graphDir = Path.Combine(path, "Main","Res","Graphs");
if (!Directory.Exists(graphDir))
Directory.CreateDirectory(graphDir);
// 检查资源存在性
var graphPath = Path.Combine(graphDir, $"{_scriptName}.asset").Replace(Application.dataPath, "").Replace('\\', '/');
graphPath = graphPath[1..];
var graphFilePath = Path.Combine("Assets", graphPath);
var existAsset = AssetDatabase.LoadAssetAtPath<ScriptGraph>(graphFilePath);
if (existAsset)
{
_tip = "该名称的剧本已存在";
_showTip = true;
return;
}
// 创建剧本图表
var graph = CreateInstance<ScriptGraph>();
graph.packageID = packageID;
graph.graphPath = graphPath;
AssetDatabase.CreateAsset(graph, graphFilePath);
}
else
{
Debug.LogError($"未记录Package路径{packageID}");
}
}
}
}

View File

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

View File

@@ -0,0 +1,14 @@
using XNodeEditor;
namespace Stary.Evo.StoryEditor.Editor
{
[CustomNodeGraphEditor(typeof(ScriptGraph))]
public class ScriptGraphEditor : NodeGraphEditor
{
public override void OnOpen()
{
base.OnOpen();
NodeEditorWindow.current.panOffset = new(-350, -75);
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b06721b22b647a540a75c6f31a275c04
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
using UnityEditor;
using XNodeEditor;
namespace Stary.Evo.StoryEditor.Editor
{
[CustomNodeEditor(typeof(BeginNode))]
public class BeginNodeEditor : NodeEditor
{
public override void OnBodyGUI()
{
serializedObject.Update();
EditorGUILayout.LabelField("剧本开始");
base.OnBodyGUI();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

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

View File

@@ -0,0 +1,19 @@
using UnityEditor;
using XNodeEditor;
namespace Stary.Evo.StoryEditor.Editor
{
[CustomNodeEditor(typeof(EndNode))]
public class EndNodeEditor : NodeEditor
{
public override void OnBodyGUI()
{
serializedObject.Update();
EditorGUILayout.LabelField("剧本结束");
base.OnBodyGUI();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

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

View File

@@ -0,0 +1,30 @@
<h1>可视化剧本编辑器</h1>
<div style="line-height:2.5">
<h3>创建剧本</h3>
1. 点击Unity顶部菜单栏`Evo/剧本编辑器/创建剧本`
2. 在打开的小窗口中选择要创建剧本的包,然后点击`创建剧本`
3. 新创建的剧本会在`*/Modules/[包体ID]/Main/Res/Graphs`路径下生成
<h3>编辑剧本</h3>
1. 双击剧本打开可视化编辑窗口
2. 右键空白处可以新建节点(节点类型详见后续附录)
3. 按住Exit端口或其他Output端口可以拖拽出一条连接线将其拖到任一节点的Enter端口或其他Input端口可以在两个节点之间建立连接原则上禁止将一个节点的Exit端口与其自身的Enter端口相连其造成的死循环问题自负
4. 原则上剧本需要从`Begin节点`连接到`End节点`
5. 剧本会自动保存但建议在关闭窗口前Ctrl+S进行手动保存以防万一
<h3>导出剧本</h3>
1. 导出剧本前需要先选择`资源加载方式`,为此需要创建一个继承`IResource`接口的类实现接口后选中剧本即可在Inspector面板选择资源加载方式
2. 选择资源加载方式后,点击`导出`即可在同级目录下生成剧本的json文件
<h3>使用剧本</h3>
1. 使用前需要先调用ScriptPlayer.Init初始化剧本执行模块并根据需要将字幕组件SpriteRenderer和音频组件AudioSource传入加载器IResource是必传项
2. 确保剧本的json文件能够被加载到例如热更环境下需要将其打包到对应的AB包中随后调用ScriptPlayer.Play即可播放剧本
3. 在场景结束时需要调用ScriptPlayer.Release释放资源以防止内存泄漏
</div>

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c353f6ac45228214bad134444f74f490
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9652a98bfb5f6774ebed08cc80657c5c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 63c081c2eb3009043870d3700881058e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 52e52a16b8e597e48a3c645e65005b09
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 剧本数据
/// </summary>
[Serializable]
public class GraphData
{
/// <summary>
/// 剧本名称
/// </summary>
public string name;
/// <summary>
/// 节点
/// </summary>
public List<NodeData> nodes = new();
/// <summary>
/// 起始节点索引
/// </summary>
public int startNodeIndex;
/// <summary>
/// 资源加载类型
/// </summary>
public string resourceType;
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 77eaac9f599722345a3a3ae21a895fbd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace Stary.Evo.StoryEditor
{
[Serializable]
public class BeginNodeData : NodeData
{
public List<int> next = new();
public BeginNodeData()
{
}
public BeginNodeData(NodeData data)
{
name = data.name;
type = NodeType.Begin;
}
public new BeginNodePlayer GetPlayer(GraphPlayer graph)=>
new(graph, this);
}
}

View File

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

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace Stary.Evo.StoryEditor
{
[Serializable]
public class EndNodeData : NodeData
{
public List<int> pre = new();
public EndNodeData()
{
}
public EndNodeData(NodeData data)
{
name = data.name;
type = NodeType.End;
}
public new EndNodePlayer GetPlayer(GraphPlayer graph) =>
new(graph, this);
}
}

View File

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

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 空节点(流程节点)数据
/// </summary>
[Serializable]
public class FlowNodeData : NodeData
{
public List<int> pre = new();
public List<int> next = new();
/// <summary>
/// 后续连接执行类型
/// </summary>
public NodeExecuteType executeType = NodeExecuteType.Async;
public FlowNodeData()
{
}
public FlowNodeData(NodeData data)
{
name = data.name;
type = NodeType.Empty;
}
public new FlowNodePlayer GetPlayer(GraphPlayer graph)=>
new(graph, this);
}
}

View File

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

View File

@@ -0,0 +1,23 @@
using System;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 节点数据
/// </summary>
[Serializable]
public class NodeData
{
/// <summary>
/// 节点名称
/// </summary>
public string name = "Unnamed Node";
/// <summary>
/// 节点类型
/// </summary>
public NodeType type = NodeType.Empty;
public NodePlayer GetPlayer(GraphPlayer graph)=>
new(graph, this);
}
}

View File

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

View File

@@ -0,0 +1,34 @@
using System;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 剧本段落节点数据
/// </summary>
[Serializable]
public class ScriptParagraphNodeData : FlowNodeData
{
/// <summary>
/// 字幕路径
/// </summary>
public ResourcePathData captionPath;
/// <summary>
/// 语音路径
/// </summary>
public ResourcePathData audioPath;
public ScriptParagraphNodeData()
{
}
public ScriptParagraphNodeData(FlowNodeData data) : base(data)
{
type = NodeType.Paragraph;
executeType = data.executeType;
}
public new ScriptParagraphNodePlayer GetPlayer(GraphPlayer graph) =>
new(graph,this);
}
}

View File

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

View File

@@ -0,0 +1,42 @@
using Sirenix.OdinInspector;
using UnityEngine;
namespace Stary.Evo.StoryEditor
{
public enum NodeType
{
/// <summary>
/// 空节点(流程节点)
/// </summary>
Empty,
/// <summary>
/// 起始节点
/// </summary>
Begin,
/// <summary>
/// 结束节点
/// </summary>
End,
/// <summary>
/// 剧本段落
/// </summary>
Paragraph
}
/// <summary>
/// 节点执行类型
/// </summary>
public enum NodeExecuteType
{
/// <summary>
/// 异步
/// </summary>
[LabelText("异步"), Tooltip("异步执行意味着Exit连接的多条支路将同时进行")]
Async,
/// <summary>
/// 同步
/// </summary>
[LabelText("同步"), Tooltip("同步执行意味着Exit连接的多条支路将依次进行")]
Sync
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: edf21b4cd79b9cf459596d41bb2a1da2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5a786bcde0f40a044b8e37ee058a125d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Stary.Evo.StoryEditor
{
public class GraphPlayer
{
/// <summary>
/// 剧本名称
/// </summary>
public string Name;
/// <summary>
/// 节点
/// </summary>
public List<NodePlayer> Nodes = new();
/// <summary>
/// 起始节点
/// </summary>
public BeginNodePlayer BeginNode;
/// <summary>
/// 结束节点
/// </summary>
public EndNodePlayer EndNode;
/// <summary>
/// 当前进行中的节点
/// </summary>
public List<NodePlayer> CurrentNodes = new();
/// <summary>
/// 播放完成
/// </summary>
public bool Finished;
/// <summary>
/// CTS
/// </summary>
public CancellationTokenSource Cts = new();
public GraphPlayer(GraphData data, IResource loader)
{
// 检查加载方式
if (loader.GetType().ToString() != data.resourceType)
{
Debug.LogError($"加载方式不匹配,剧本无法加载:{data.name}\n需要的加载方式: {data.resourceType}");
return;
}
Name = data.name;
data.nodes.ForEach(node =>
{
switch (node)
{
case BeginNodeData beginNode:
Nodes.Add(beginNode.GetPlayer(this));
break;
case EndNodeData endNode:
Nodes.Add(endNode.GetPlayer(this));
break;
case ScriptParagraphNodeData paraNode:
Nodes.Add(paraNode.GetPlayer(this));
break;
case FlowNodeData flowNode:
Nodes.Add(flowNode.GetPlayer(this));
break;
}
});
BeginNode = (BeginNodePlayer)Nodes[data.startNodeIndex];
BeginNode.Connect();
}
/// <summary>
/// 执行剧本
/// </summary>
public async UniTask Execute()
{
// 初始化结束标志
Finished = false;
// 执行剧本
_ = BeginNode.Execute();
// 等待剧本完成
await UniTask.WaitUntil(() => Finished);
}
/// <summary>
/// 停止剧本
/// </summary>
public void Stop()
{
// 取消并重置CancelToken
Cts.Cancel();
Cts = new();
// 标记剧本结束
Finished = true;
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: adab23e0caf805e45b47a9a0c88ec076
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Stary.Evo.StoryEditor
{
public class BeginNodePlayer : NodePlayer
{
public new BeginNodeData Data;
public List<NodePlayer> Next = new();
public BeginNodePlayer(GraphPlayer graph, BeginNodeData data) : base(graph, data)
{
Data = data;
}
public override bool Connect()
{
Debug.Log("BeginNodePlayer: Connect");
if (!base.Connect())
return false;
Data.next.ForEach(index => Next.Add(Graph.Nodes[index]));
Next.ForEach(node => node.Connect());
return true;
}
public override async UniTask Execute()
{
Init();
Debug.Log($"开始执行剧本: {Graph.Name}");
await base.Execute();
await MoveNext();
}
public override UniTask MoveNext()
{
Next.ForEach(node => node?.Execute());
return base.MoveNext();
}
}
}

View File

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

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Stary.Evo.StoryEditor
{
public class EndNodePlayer : NodePlayer
{
public new EndNodeData Data;
public List<NodePlayer> Pre = new();
public EndNodePlayer(GraphPlayer graph, EndNodeData data) : base(graph, data)
{
Data = data;
}
public override bool Connect()
{
if(!base.Connect())
return false;
Data.pre.ForEach(index => Pre.Add(Graph.Nodes[index]));
return true;
}
public override UniTask Execute()
{
Init();
Debug.Log($"剧本执行完成: {Graph.Name}");
// 标记剧本完成
Graph.Finished = true;
ScriptPlayer.ReleaseGraph();
return base.Execute();
}
}
}

View File

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

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
namespace Stary.Evo.StoryEditor
{
public class FlowNodePlayer : NodePlayer
{
public new FlowNodeData Data;
public List<NodePlayer> Pre = new();
public List<NodePlayer> Next = new();
/// <summary>
/// 后续连接执行类型
/// </summary>
public NodeExecuteType ExecuteType;
public FlowNodePlayer(GraphPlayer graph, FlowNodeData data) : base(graph, data)
{
Data = data;
ExecuteType = data.executeType;
}
public override bool Connect()
{
if(!base.Connect())
return false;
Data.pre.ForEach(index => Pre.Add(Graph.Nodes[index]));
Data.next.ForEach(index => Next.Add(Graph.Nodes[index]));
Next.ForEach(node => node.Connect());
return true;
}
/// <summary>
/// 开始执行
/// </summary>
public override async UniTask Execute()
{
await base.Execute();
await MoveNext();
}
/// <summary>
/// 向下继续执行
/// </summary>
public override async UniTask MoveNext()
{
// 异步执行(并行)
if (ExecuteType == NodeExecuteType.Async)
{
Next.ForEach(node => node?.Execute());
}
// 同步执行(串行)
else
{
foreach (var node in Next.Where(node => node != null))
{
await node.Execute();
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,65 @@
using Cysharp.Threading.Tasks;
namespace Stary.Evo.StoryEditor
{
public class NodePlayer
{
/// <summary>
/// 节点名称
/// </summary>
protected string Name;
/// <summary>
/// 图表
/// </summary>
protected GraphPlayer Graph;
/// <summary>
/// 数据
/// </summary>
protected NodeData Data;
/// <summary>
/// 初始化标志
/// </summary>
protected bool Initialized;
public NodePlayer(GraphPlayer graph, NodeData data)
{
Graph = graph;
Name = data.name;
Data = data;
}
/// <summary>
/// 连接节点
/// </summary>
public virtual bool Connect()
{
if(Initialized)
return false;
Initialized = true;
return true;
}
/// <summary>
/// 初始化
/// </summary>
public virtual void Init() => Graph.CurrentNodes.Add(this);
/// <summary>
/// 开始执行
/// </summary>
public virtual UniTask Execute()
{
Graph.CurrentNodes.Remove(this);
return UniTask.CompletedTask;
}
/// <summary>
/// 向下继续执行
/// </summary>
public virtual UniTask MoveNext() => UniTask.CompletedTask;
/// <summary>
/// 停止执行
/// </summary>
public virtual UniTask Stop() => UniTask.CompletedTask;
}
}

View File

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

View File

@@ -0,0 +1,32 @@
using Cysharp.Threading.Tasks;
namespace Stary.Evo.StoryEditor
{
public class ScriptParagraphNodePlayer : FlowNodePlayer
{
public new ScriptParagraphNodeData Data;
/// <summary>
/// 字幕路径
/// </summary>
public ResourcePathData CaptionPath;
/// <summary>
/// 语音路径
/// </summary>
public ResourcePathData AudioPath;
public ScriptParagraphNodePlayer(GraphPlayer graph, ScriptParagraphNodeData data) : base(graph, data)
{
Data = data;
CaptionPath = data.captionPath;
AudioPath = data.audioPath;
}
public override async UniTask Execute()
{
Init();
await ScriptPlayer.PlayScriptPara(this, Graph.Cts.Token);
await base.Execute();
}
}
}

View File

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

View File

@@ -0,0 +1,149 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using Newtonsoft.Json;
using UnityEngine;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 剧本播放器
/// </summary>
public static class ScriptPlayer
{
/// <summary>
/// 资源加载工具
/// </summary>
private static IResource _loader;
/// <summary>
/// 动画单次时长
/// </summary>
private const float AnimationTime = 0.2f;
/// <summary>
/// 字幕组件
/// </summary>
private static SpriteRenderer _caption;
/// <summary>
/// 音频组件
/// </summary>
private static AudioSource _audio;
/// <summary>
/// 当前执行的剧本
/// </summary>
private static GraphPlayer _graph;
/// <summary>
/// 初始化剧本播放器
/// </summary>
/// <param name="loader">资源加载工具</param>
/// <param name="audio">音频组件</param>
/// <param name="caption">字幕组件</param>
public static void Init(IResource loader, AudioSource audio = null, SpriteRenderer caption = null)
{
_loader = loader;
_caption = caption;
_audio = audio;
}
/// <summary>
/// 释放资源
/// </summary>
public static void Release()
{
_loader = null;
_caption = null;
_audio = null;
Resources.UnloadUnusedAssets();
}
/// <summary>
/// 播放剧本
/// </summary>
/// <param name="packageID">包体ID</param>
/// <param name="scriptName">剧本名称不用加_txt后缀</param>
public static async UniTask Play(string packageID, string scriptName)
{
// 资源加载工具排空
if (_loader == null)
{
Debug.LogError("资源加载工具未准备好");
return;
}
// 加载剧本
ResourcePathData path = new(packageID,scriptName);
var json = await _loader.Load<TextAsset>(path);
var scriptData = JsonConvert.DeserializeObject<GraphData>(json.text, new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All });
if (scriptData == null)
{
Debug.LogError($"剧本加载失败:[{path.packageID}]{path.path}");
return;
}
// 解析并执行剧本
_graph = new(scriptData, _loader);
await _graph.Execute();
}
/// <summary>
/// 播放剧本段落
/// </summary>
/// <param name="para">段落数据</param>
/// <param name="ct">CT</param>
public static async UniTask PlayScriptPara(ScriptParagraphNodePlayer para, CancellationToken ct)
{
// 设置变量
if (_caption)
_caption.sprite = await _loader.Load<Sprite>(para.CaptionPath);
if (_audio)
_audio.clip = await _loader.Load<AudioClip>(para.AudioPath);
// 淡入
_caption?.DOColor(Color.white, AnimationTime);
_audio?.DOFade(1, AnimationTime);
await UniTask.Delay(TimeSpan.FromSeconds(AnimationTime), cancellationToken:ct);
// 等音频播放完
if (_audio)
{
_audio.Play();
await UniTask.Delay(TimeSpan.FromSeconds(_audio.clip.length), cancellationToken:ct);
}
// 淡出
_caption?.DOColor(Color.clear, AnimationTime);
_audio?.DOFade(0, AnimationTime);
await UniTask.Delay(TimeSpan.FromSeconds(AnimationTime), cancellationToken:ct);
}
/// <summary>
/// 停止剧本
/// </summary>
public static async UniTask Stop()
{
_graph.Stop();
var hasAnim = false;
if (_caption && _caption.color != Color.clear)
{
_caption?.DOColor(Color.clear, AnimationTime);
hasAnim = true;
}
if (_audio && _audio.volume > 0)
{
_audio?.DOFade(0, AnimationTime);
hasAnim = true;
}
if(hasAnim)
await UniTask.Delay(TimeSpan.FromSeconds(AnimationTime));
ReleaseGraph();
}
public static void ReleaseGraph() => _graph = null;
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e59434304b735b44b6f025c51615cfa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Stary.Evo.StoryEditor
{
public interface IResource
{
/// <summary>
/// 加载资源
/// Json => 资源
/// </summary>
/// <param name="pathData">资源路径</param>
/// <typeparam name="T">资源类型</typeparam>
UniTask<T> Load<T>(ResourcePathData pathData) where T : Object;
/// <summary>
/// 保存资源
/// 资源 => Json
/// </summary>
/// <param name="asset">资源</param>
/// <param name="packageID">包体ID</param>
UniTask<ResourcePathData> Save<T>(T asset, string packageID = null) where T : Object;
}
}

View File

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

View File

@@ -0,0 +1,38 @@
using System;
namespace Stary.Evo.StoryEditor
{
/// <summary>
/// 资源路径数据
/// </summary>
[Serializable]
public struct ResourcePathData
{
/// <summary>
/// 包体ID
/// </summary>
public string packageID;
/// <summary>
/// 资源路径
/// </summary>
public string path;
public ResourcePathData(string packageID, string path)
{
this.packageID = packageID;
this.path = path;
}
public void AddPath(params string[] tail)
{
if(tail == null || tail.Length == 0)
return;
foreach (var t in tail)
{
path = string.IsNullOrEmpty(path) ? t : System.IO.Path.Combine(path, t);
}
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 48bedbe80b198ef4d8811fac3b41d204
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4f25ed558908de741b709bb8f3df2427
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,272 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using Sirenix.OdinInspector;
using Sirenix.Utilities;
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
/// <summary>
/// 资源加载方式
/// </summary>
[BoxGroup("Export", centerLabel:true)]
[LabelText("资源加载方式"), ValueDropdown(nameof(_iResourceTypes)),SerializeField]
private string loaderType;
#if UNITY_EDITOR
/// <summary>
/// 获取继承 IResource 的所有类
/// </summary>
private HashSet<string> _iResourceTypes = AssemblyUtilities.GetTypes(AssemblyCategory.Scripts)
.Where(t => t.IsClass && typeof(IResource).IsAssignableFrom(t)).Select(t => t.ToString()).ToHashSet();
#else
private HashSet<string> _iResourceTypes = new();
#endif
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
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 90e4f51c40e7d9340a88df558072b353
folderAsset: yes
DefaultImporter:
externalObjects: {}
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 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:

View File

@@ -0,0 +1,693 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &354453078
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 354453079}
- component: {fileID: 354453080}
m_Layer: 0
m_Name: Audio
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &354453079
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 354453078}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1732823841}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!82 &354453080
AudioSource:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 354453078}
m_Enabled: 1
serializedVersion: 4
OutputAudioMixerGroup: {fileID: 0}
m_audioClip: {fileID: 0}
m_PlayOnAwake: 1
m_Volume: 1
m_Pitch: 1
Loop: 0
Mute: 0
Spatialize: 0
SpatializePostEffects: 0
Priority: 128
DopplerLevel: 1
MinDistance: 1
MaxDistance: 500
Pan2D: 0
rolloffMode: 0
BypassEffects: 0
BypassListenerEffects: 0
BypassReverbZones: 0
rolloffCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
panLevelCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
spreadCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
reverbZoneMixCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!1 &658829530
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 658829532}
- component: {fileID: 658829531}
- component: {fileID: 658829533}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &658829531
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 658829530}
m_Enabled: 1
serializedVersion: 10
m_Type: 1
m_Shape: 0
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ShadowRadius: 0
m_ShadowAngle: 0
--- !u!4 &658829532
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 658829530}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!114 &658829533
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 658829530}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Version: 3
m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2
m_LightLayerMask: 1
m_RenderingLayers: 1
m_CustomShadowLayers: 0
m_ShadowLayerMask: 1
m_ShadowRenderingLayers: 1
m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 0
--- !u!1 &948603383
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 948603387}
- component: {fileID: 948603386}
- component: {fileID: 948603385}
- component: {fileID: 948603384}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &948603384
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 948603383}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_VolumeFrameworkUpdateModeOption: 2
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_AllowHDROutput: 1
m_UseScreenCoordOverride: 0
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_Version: 2
m_TaaSettings:
m_Quality: 3
m_FrameInfluence: 0.1
m_JitterScale: 1
m_MipBias: 0
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
--- !u!81 &948603385
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 948603383}
m_Enabled: 1
--- !u!20 &948603386
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 948603383}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &948603387
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 948603383}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &962199775
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 962199776}
- component: {fileID: 962199777}
m_Layer: 0
m_Name: HybridClREntrance
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &962199776
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 962199775}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -0.6535072, y: -0.08580719, z: 1.3690248}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &962199777
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 962199775}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3397e2d382924ffea94a9ecbd16ab745, type: 3}
m_Name:
m_EditorClassIdentifier:
moduleName: com.sxkjg.main
m_moduleRootTrans: {fileID: 0}
--- !u!1 &1212764680
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1212764681}
- component: {fileID: 1212764682}
m_Layer: 0
m_Name: Caption
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1212764681
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1212764680}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1732823841}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &1212764682
SpriteRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1212764680}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 0
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 0
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_Sprite: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_FlipX: 0
m_FlipY: 0
m_DrawMode: 0
m_Size: {x: 1, y: 1}
m_AdaptiveModeThreshold: 0.5
m_SpriteTileMode: 0
m_WasSpriteAssigned: 0
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!1 &1732823839
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1732823841}
- component: {fileID: 1732823840}
m_Layer: 0
m_Name: ScriptPlayer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1732823840
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732823839}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 543f5e9055e16f045806981c5725e910, type: 3}
m_Name:
m_EditorClassIdentifier:
packageID: com.zjkjg.daopian
scriptName: graph.sg
--- !u!4 &1732823841
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1732823839}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1212764681}
- {fileID: 354453079}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 658829532}
- {fileID: 948603387}
- {fileID: 962199776}
- {fileID: 1732823841}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9dbefcb0c8b734346a6bf5cab23d09db
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7861aace72bef144da5e5349d49d214d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using Cysharp.Threading.Tasks;
using rokid.armaz.module;
using Stary.Evo.StoryEditor;
using UnityEngine;
public class ResourceLoader : IResource
{
public UniTask<T> Load<T>(ResourcePathData pathData) where T : Object
{
return AMP.ResourceLoader.LoadAssetAsync<T>(pathData.packageID, pathData.path);
}
public UniTask<ResourcePathData> Save<T>(T asset, string packageID) where T : Object
{
ResourcePathData pathData = new();
pathData.packageID = packageID;
pathData.AddPath(asset.name);
return UniTask.FromResult(pathData);
}
}

View File

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

View File

@@ -0,0 +1,166 @@
using Array = System.Array;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Stary.Evo.StoryEditor;
using Sirenix.OdinInspector;
using UnityEngine;
public class TestScriptPlayer : MonoBehaviour
{
[LabelText("指定包体ID"), SerializeField, ValueDropdown(nameof(GetPackages))]
private string packageID;
[LabelText("指定剧本名称"), SerializeField, ValueDropdown(nameof(GetScripts))]
private string scriptName;
private void Start()
{
var caption = GetComponentInChildren<SpriteRenderer>();
caption.color = Color.clear;
var audioClip = GetComponentInChildren<AudioSource>();
audioClip.volume = 0;
ScriptPlayer.Init(new ResourceLoader(), audioClip, caption);
}
private void OnDestroy()
{
ScriptPlayer.Release();
}
[Button]
public void Play()
{
_ = ScriptPlayer.Play(packageID, scriptName);
}
[Button]
public void Stop()
{
_ = ScriptPlayer.Stop();
}
#region
private Dictionary<string, string> _paths = new();
/// <summary>
/// 获取所有Module Package
/// </summary>
private string[] GetPackages()
{
// 查找Modules目录
List<string> modules = new();
GetDirectoryPaths(Application.dataPath, modules, "Modules");
if (modules.Count == 0)
{
Debug.LogError("未找到任何Modules目录");
return Array.Empty<string>();
}
// 查找package
List<string> packages = new();
GetDirectoryPaths(modules[0], packages, "com.");
if (packages.Count == 0)
{
return Array.Empty<string>();
}
// 记录Package地址并返回选项
_paths.Clear();
packages.ForEach(path => _paths.Add(Path.GetFileName(path), path));
return _paths.Keys.ToArray();
}
/// <summary>
/// 获取Package中所有Script
/// </summary>
private string[] GetScripts()
{
// 未选Package
if (string.IsNullOrEmpty(packageID) || !_paths.ContainsKey(packageID))
return Array.Empty<string>();
List<string> scripts = new();
GetFilePaths(_paths[packageID], scripts, tail: ".sg.json");
return scripts.Count == 0 ? Array.Empty<string>() : scripts.Select(path => Path.GetFileName(path).Replace(".json", "")).ToArray();
}
/// <summary>
/// 获取符合条件的目录
/// </summary>
/// <param name="root">查找起始目录</param>
/// <param name="result">查找结果</param>
/// <param name="title">筛选条件title</param>
/// <param name="tail">筛选条件tail</param>
private static void GetDirectoryPaths(string root, List<string> result, string title = null, string tail = null)
{
foreach (var dir in Directory.GetDirectories(root))
{
// ReSharper disable once ReplaceWithSingleAssignment.True
var check = true;
// 匹配文件头
if (!string.IsNullOrEmpty(title) && !Path.GetFileName(dir).StartsWith(title))
{
check = false;
}
// 匹配文件尾
if (!string.IsNullOrEmpty(tail) && !Path.GetFileName(dir).EndsWith(tail))
{
check = false;
}
if(check)
result.Add(dir.Replace('\\', '/'));
// 继续往下找
GetDirectoryPaths(dir, result, title);
}
}
/// <summary>
/// 获取符合条件的文件路径
/// </summary>
/// <param name="root">查找起始目录</param>
/// <param name="result">查找结果</param>
/// <param name="title">筛选条件title</param>
/// <param name="tail">筛选条件tail</param>
private static void GetFilePaths(string root, List<string> result, string title = null, string tail = null)
{
foreach (var file in Directory.GetFiles(root))
{
// ReSharper disable once ReplaceWithSingleAssignment.True
var check = true;
// 匹配文件头
if (!string.IsNullOrEmpty(title))
{
if(!Path.GetFileName(file).StartsWith(title))
check = false;
}
// 匹配文件尾
if (!string.IsNullOrEmpty(tail))
{
if (!Path.GetFileName(file).EndsWith(tail))
{
check = false;
}
}
if (check)
{
result.Add(file.Replace('\\', '/'));
}
}
// 继续往下找
foreach (var dir in Directory.GetDirectories(root))
{
GetFilePaths(dir, result, title, tail);
}
}
#endregion
}

View File

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

View File

@@ -0,0 +1,24 @@
{
"name": "com.staryevo.storyeditor",
"version": "1.0.0",
"displayName": "10.StoryEditor",
"description": "可视化剧本编辑器\n1.通过可视化图表编辑剧本内容\n2.将剧本导出为json\n3.解析剧本并执行",
"unity": "2021.3",
"unityRelease": "44f1c1",
"keywords": [
"unity",
"story",
"visual",
"editor",
"graph"
],
"author": {
"name": "staryEvo",
"url": "https://www.unity3d.com"
},
"dependencies": {
"com.github.siccity.xnode": "1.8.0",
"com.cysharp.unitask": "2.5.10",
"com.unity.nuget.newtonsoft-json": "3.2.1"
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e3e21fe172f8611499ecc724ca256dca
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: