This commit is contained in:
zhangzheng
2026-01-07 18:20:16 +08:00
180 changed files with 9863 additions and 0 deletions

View File

@@ -14,6 +14,8 @@ stages:
- 07.RKTools
- 08.UniTask
- 09.CodeChecker
- 10.StoryEditor
- 10.XNode
- 11.PointCloudTools
.template_job: &template_job
@@ -90,6 +92,16 @@ job_RKTools:
variables:
MODULE_NAME: "07.RKTools" # 定义模块名称变量
stage: 07.RKTools
job_StoryEditor:
<<: *template_job
variables:
MODULE_NAME: "10.StoryEditor" # 定义模块名称变量
stage: 10.StoryEditor
job_XNode:
<<: *template_job
variables:
MODULE_NAME: "10.XNode" # 定义模块名称变量
stage: 10.XNode
job_PointCloudTools:
<<: *template_job
variables:

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,20 @@
{
"name": "com.storyeditor.editor",
"rootNamespace": "",
"references": [
"GUID:4f15563d771239c44bec6b88c77db9f7",
"GUID:002c1bbed08fa44d282ef34fd5edb138",
"GUID:b8e24fd1eb19b4226afebb2810e3c19b"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c31e05a53145634458979bede822b24b
AssemblyDefinitionImporter:
externalObjects: {}
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,162 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
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 const float UpdateInterval = 0.02f;
/// <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);
// 淡入
await SetCaptionColor(1,ct);
// 等音频播放完
if (_audio)
{
_audio.Play();
await UniTask.Delay(TimeSpan.FromSeconds(_audio.clip.length), cancellationToken:ct);
}
// 淡出
await SetCaptionColor(0, ct);
await UniTask.Delay(TimeSpan.FromSeconds(AnimationTime), cancellationToken:ct);
}
/// <summary>
/// 停止剧本
/// </summary>
public static async UniTask Stop()
{
_graph.Stop();
await SetCaptionColor(0, CancellationToken.None);
ReleaseGraph();
}
public static void ReleaseGraph() => _graph = null;
private static async UniTask DoAnim(Action animAction, CancellationToken ct)
{
float time = 0;
while (time < AnimationTime)
{
animAction?.Invoke();
await UniTask.Delay(TimeSpan.FromSeconds(UpdateInterval), cancellationToken: ct);
time+=UpdateInterval;
}
}
private static async UniTask SetCaptionColor(int alpha, CancellationToken ct)
{
var isAdd = alpha > _caption.color.a;
var speed = Mathf.Abs(_caption.color.a - alpha) / AnimationTime * UpdateInterval;
if (speed == 0)
return;
await DoAnim(() =>
{
_caption.color = new(1, 1, 1, _caption.color.a + (isAdd ? speed : -speed));
}, ct);
}
}
}

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,17 @@
{
"name": "com.storyeditor.runtime",
"rootNamespace": "",
"references": [
"GUID:f51ebe6a0ceec4240a699833d6309b23",
"GUID:b8e24fd1eb19b4226afebb2810e3c19b"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4f15563d771239c44bec6b88c77db9f7
AssemblyDefinitionImporter:
externalObjects: {}
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.4",
"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:

View File

@@ -0,0 +1,8 @@
root = true
[*.cs]
indent_style = space
indent_size = 4
end_of_line = crlf
insert_final_newline = false
trim_trailing_whitespace = true

12
Assets/10.XNode/.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: thorbrigsted
open_collective: # Replace with a single Open Collective username
ko_fi: thorbrigsted
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -0,0 +1,40 @@
## Contributing to xNode
💙Thank you for taking the time to contribute💙
If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)!
## Pull Requests
Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs.
These are the main points to follow:
1) Use formatting which is consistent with the rest of xNode base (see below)
2) Keep _one feature_ per PR (see below)
3) xNode aims to be compatible with C# 4.x, do not use new language features
4) Avoid including irellevant whitespace or formatting changes
5) Comment your code
6) Spell check your code / comments
7) Use concrete types, not *var*
8) Use english language
## New features
xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings.
Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible.
If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel.
## Coding conventions
Using consistent formatting is key to having a clean git history. Skim through the code and you'll get the hang of it quickly.
* Methods, Types and properties PascalCase
* Variables camelCase
* Public methods XML commented. Params described if not obvious
* Explicit usage of brackets when doing multiple math operations on the same line
## Formatting
I use VSCode with the C# FixFormat extension and the following setting overrides:
```json
"csharpfixformat.style.spaces.beforeParenthesis": false,
"csharpfixformat.style.indent.regionIgnored": true
```
* Open braces on same line as condition
* 4 spaces for indentation.

View File

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

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Thor Brigsted
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

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

121
Assets/10.XNode/README.md Normal file
View File

@@ -0,0 +1,121 @@
<img align="right" width="100" height="100" src="https://user-images.githubusercontent.com/37786733/41541140-71602302-731a-11e8-9434-79b3a57292b6.png">
[![Discord](https://img.shields.io/discord/361769369404964864.svg)](https://discord.gg/qgPrHv4)
[![GitHub issues](https://img.shields.io/github/issues/Siccity/xNode.svg)](https://github.com/Siccity/xNode/issues)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Siccity/xNode/master/LICENSE.md)
[![GitHub Wiki](https://img.shields.io/badge/wiki-available-brightgreen.svg)](https://github.com/Siccity/xNode/wiki)
[![openupm](https://img.shields.io/npm/v/com.github.siccity.xnode?label=openupm&registry_uri=https://package.openupm.com)](https://openupm.com/packages/com.github.siccity.xnode/)
[Downloads](https://github.com/Siccity/xNode/releases) / [Asset Store](http://u3d.as/108S) / [Documentation](https://github.com/Siccity/xNode/wiki)
Support xNode on [Ko-fi](https://ko-fi.com/Z8Z5DYWA) or [Patreon](https://www.patreon.com/thorbrigsted)
For full Odin support, consider using [KAJed82's fork](https://github.com/KAJed82/xNode)
### xNode
Thinking of developing a node-based plugin? Then this is for you. You can download it as an archive and unpack to a new unity project, or connect it as git submodule.
xNode is super userfriendly, intuitive and will help you reap the benefits of node graphs in no time.
With a minimal footprint, it is ideal as a base for custom state machines, dialogue systems, decision makers etc.
<p align="center">
<img src="https://user-images.githubusercontent.com/6402525/53689100-3821e680-3d4e-11e9-8440-e68bd802bfd9.png">
</p>
### Key features
* Lightweight in runtime
* Very little boilerplate code
* Strong separation of editor and runtime code
* No runtime reflection (unless you need to edit/build node graphs at runtime. In this case, all reflection is cached.)
* Does not rely on any 3rd party plugins
* Custom node inspector code is very similar to regular custom inspector code
* Supported from Unity 5.3 and up
### Wiki
* [Getting started](https://github.com/Siccity/xNode/wiki/Getting%20Started) - create your very first node node and graph
* [Examples branch](https://github.com/Siccity/xNode/tree/examples) - look at other small projects
### Installation
<details><summary>Instructions</summary>
### Installing with Unity Package Manager
***Via Git URL***
*(Requires Unity version 2018.3.0b7 or above)*
To install this project as a [Git dependency](https://docs.unity3d.com/Manual/upm-git.html) using the Unity Package Manager,
add the following line to your project's `manifest.json`:
```
"com.github.siccity.xnode": "https://github.com/siccity/xNode.git"
```
You will need to have Git installed and available in your system's PATH.
If you are using [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) in your project, you will need to add `XNode` and/or `XNodeEditor` as Assembly Definition References.
***Via OpenUPM***
The package is available on the [openupm registry](https://openupm.com). It's recommended to install it via [openupm-cli](https://github.com/openupm/openupm-cli).
```
openupm add com.github.siccity.xnode
```
### Installing with git
***Via Git Submodule***
To add xNode as a [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your existing git project,
run the following git command from your project root:
```
git submodule add git@github.com:Siccity/xNode.git Assets/Submodules/xNode
```
### Installing 'the old way'
If no source control or package manager is available to you, you can simply copy/paste the source files into your assets folder.
</details>
### Node example:
```csharp
// public classes deriving from Node are registered as nodes for use within a graph
public class MathNode : Node {
// Adding [Input] or [Output] is all you need to do to register a field as a valid port on your node
[Input] public float a;
[Input] public float b;
// The value of an output node field is not used for anything, but could be used for caching output results
[Output] public float result;
[Output] public float sum;
// The value of 'mathType' will be displayed on the node in an editable format, similar to the inspector
public MathType mathType = MathType.Add;
public enum MathType { Add, Subtract, Multiply, Divide}
// GetValue should be overridden to return a value for any specified output port
public override object GetValue(NodePort port) {
// Get new a and b values from input connections. Fallback to field values if input is not connected
float a = GetInputValue<float>("a", this.a);
float b = GetInputValue<float>("b", this.b);
// After you've gotten your input values, you can perform your calculations and return a value
if (port.fieldName == "result")
switch(mathType) {
case MathType.Add: default: return a + b;
case MathType.Subtract: return a - b;
case MathType.Multiply: return a * b;
case MathType.Divide: return a / b;
}
else if (port.fieldName == "sum") return a + b;
else return 0f;
}
}
```
### Plugins
Plugins are repositories that add functionality to xNode
* [xNodeGroups](https://github.com/Siccity/xNodeGroups): adds resizable groups
### Community
Join the [Discord](https://discord.gg/qgPrHv4 "Join Discord server") server to leave feedback or get support.
Feel free to also leave suggestions/requests in the [issues](https://github.com/Siccity/xNode/issues "Go to Issues") page.

View File

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

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 657b15cb3ec32a24ca80faebf094d0f4
folderAsset: yes
timeCreated: 1505418321
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 5644dfc7eed151045af664a9d4fd1906
folderAsset: yes
timeCreated: 1541633926
licenseType: Free
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,5 @@
using UnityEngine;
/// <summary> Draw enums correctly within nodes. Without it, enums show up at the wrong positions. </summary>
/// <remarks> Enums with this attribute are not detected by EditorGui.ChangeCheck due to waiting before executing </remarks>
public class NodeEnumAttribute : PropertyAttribute { }

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 10a8338f6c985854697b35459181af0a
timeCreated: 1541633942
licenseType: Free
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,12 @@
using System;
/// <summary> Overrides the ValueType of the Port, to have a ValueType different from the type of its serializable field </summary>
/// <remarks> Especially useful in Dynamic Port Lists to create Value-Port Pairs with different type. </remarks>
[AttributeUsage(AttributeTargets.Field)]
public class PortTypeOverrideAttribute : Attribute {
public Type type;
/// <summary> Overrides the ValueType of the Port </summary>
/// <param name="type">ValueType of the Port</param>
public PortTypeOverrideAttribute(Type type) {
this.type = type;
}
}

View File

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

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 94d4fd78d9120634ebe0e8717610c412
folderAsset: yes
timeCreated: 1505418345
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,213 @@
#if UNITY_2019_1_OR_NEWER
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using static UnityEditor.GenericMenu;
namespace XNodeEditor
{
public class AdvancedGenericMenu : AdvancedDropdown
{
public static float? DefaultMinWidth = 200f;
public static float? DefaultMaxWidth = 300f;
private class AdvancedGenericMenuItem : AdvancedDropdownItem
{
private MenuFunction func;
private MenuFunction2 func2;
private object userData;
public AdvancedGenericMenuItem( string name ) : base( name )
{
}
public AdvancedGenericMenuItem( string name, bool enabled, Texture2D icon, MenuFunction func ) : base( name )
{
Set( enabled, icon, func );
}
public AdvancedGenericMenuItem( string name, bool enabled, Texture2D icon, MenuFunction2 func, object userData ) : base( name )
{
Set( enabled, icon, func, userData );
}
public void Set( bool enabled, Texture2D icon, MenuFunction func )
{
this.enabled = enabled;
this.icon = icon;
this.func = func;
}
public void Set( bool enabled, Texture2D icon, MenuFunction2 func, object userData )
{
this.enabled = enabled;
this.icon = icon;
this.func2 = func;
this.userData = userData;
}
public void Run()
{
if ( func2 != null )
func2( userData );
else if ( func != null )
func();
}
}
private List<AdvancedGenericMenuItem> items = new List<AdvancedGenericMenuItem>();
private AdvancedGenericMenuItem FindOrCreateItem( string name, AdvancedGenericMenuItem currentRoot = null )
{
if ( string.IsNullOrWhiteSpace( name ) )
return null;
AdvancedGenericMenuItem item = null;
string[] paths = name.Split( '/' );
if ( currentRoot == null )
{
item = items.FirstOrDefault( x => x != null && x.name == paths[0] );
if ( item == null )
items.Add( item = new AdvancedGenericMenuItem( paths[0] ) );
}
else
{
item = currentRoot.children.OfType<AdvancedGenericMenuItem>().FirstOrDefault( x => x.name == paths[0] );
if ( item == null )
currentRoot.AddChild( item = new AdvancedGenericMenuItem( paths[0] ) );
}
if ( paths.Length > 1 )
return FindOrCreateItem( string.Join( "/", paths, 1, paths.Length - 1 ), item );
return item;
}
private AdvancedGenericMenuItem FindParent( string name )
{
string[] paths = name.Split( '/' );
return FindOrCreateItem( string.Join( "/", paths, 0, paths.Length - 1 ) );
}
private string Name { get; set; }
public AdvancedGenericMenu() : base( new AdvancedDropdownState() )
{
Name = "";
}
public AdvancedGenericMenu( string name, AdvancedDropdownState state ) : base( state )
{
Name = name;
}
//
// Summary:
// Add a disabled item to the menu.
//
// Parameters:
// content:
// The GUIContent to display as a disabled menu item.
public void AddDisabledItem( GUIContent content )
{
//var parent = FindParent( content.text );
var item = FindOrCreateItem( content.text );
item.Set( false, null, null );
}
//
// Summary:
// Add a disabled item to the menu.
//
// Parameters:
// content:
// The GUIContent to display as a disabled menu item.
//
// on:
// Specifies whether to show that the item is currently activated (i.e. a tick next
// to the item in the menu).
public void AddDisabledItem( GUIContent content, bool on )
{
}
public void AddItem( string name, bool on, MenuFunction func )
{
AddItem( new GUIContent( name ), on, func );
}
public void AddItem( GUIContent content, bool on, MenuFunction func )
{
//var parent = FindParent( content.text );
var item = FindOrCreateItem( content.text );
item.Set( true/*on*/, null, func );
}
public void AddItem( string name, bool on, MenuFunction2 func, object userData )
{
AddItem( new GUIContent( name ), on, func, userData );
}
public void AddItem( GUIContent content, bool on, MenuFunction2 func, object userData )
{
//var parent = FindParent( content.text );
var item = FindOrCreateItem( content.text );
item.Set( true/*on*/, null, func, userData );
}
//
// Summary:
// Add a seperator item to the menu.
//
// Parameters:
// path:
// The path to the submenu, if adding a separator to a submenu. When adding a separator
// to the top level of a menu, use an empty string as the path.
public void AddSeparator( string path = null )
{
var parent = string.IsNullOrWhiteSpace( path ) ? null : FindParent( path );
if ( parent == null )
items.Add( null );
else
parent.AddSeparator();
}
//
// Summary:
// Show the menu at the given screen rect.
//
// Parameters:
// position:
// The position at which to show the menu.
public void DropDown( Rect position )
{
position.width = Mathf.Clamp( position.width, DefaultMinWidth.HasValue ? DefaultMinWidth.Value : 1f, DefaultMaxWidth.HasValue ? DefaultMaxWidth.Value : Screen.width );
Show( position );
}
protected override AdvancedDropdownItem BuildRoot()
{
var root = new AdvancedDropdownItem( Name );
foreach ( var m in items )
{
if ( m == null )
root.AddSeparator();
else
root.AddChild( m );
}
return root;
}
protected override void ItemSelected( AdvancedDropdownItem item )
{
if ( item is AdvancedGenericMenuItem gmItem )
gmItem.Run();
}
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More