【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: 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: