diff --git a/Editor/AlicizaX.UI.Extension.Editor.asmdef b/Editor/AlicizaX.UI.Extension.Editor.asmdef index 917e3d4..76a8fbd 100644 --- a/Editor/AlicizaX.UI.Extension.Editor.asmdef +++ b/Editor/AlicizaX.UI.Extension.Editor.asmdef @@ -6,7 +6,8 @@ "GUID:760f1778adc613f49a4394fb41ff0bbc", "GUID:75b6f2078d190f14dbda4a5b747d709c", "GUID:a19b414bea3b97240a91aeab9a8eab36", - "GUID:83a193b118cfbef48a344187e07f53bb" + "GUID:83a193b118cfbef48a344187e07f53bb", + "GUID:acfef7cabed3b0a42b25edb1cd4fa259" ], "includePlatforms": [ "Editor" diff --git a/Runtime/AlicizaX.UI.Extension.asmdef b/Runtime/AlicizaX.UI.Extension.asmdef index 2ecf1a6..02063a3 100644 --- a/Runtime/AlicizaX.UI.Extension.asmdef +++ b/Runtime/AlicizaX.UI.Extension.asmdef @@ -7,7 +7,9 @@ "GUID:75b6f2078d190f14dbda4a5b747d709c", "GUID:a19b414bea3b97240a91aeab9a8eab36", "GUID:198eb6af143bbc4488e2779d96697e06", - "GUID:80ecb87cae9c44d19824e70ea7229748" + "GUID:80ecb87cae9c44d19824e70ea7229748", + "GUID:f51ebe6a0ceec4240a699833d6309b23", + "GUID:33661e06c33d31b4c9223810bf503247" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/Extension.meta b/Runtime/Extension.meta new file mode 100644 index 0000000..c6baa56 --- /dev/null +++ b/Runtime/Extension.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dc3fb1c1caf6fbd40a900ca46f204e07 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Extension/ZeroGCTypewriterPro.cs b/Runtime/Extension/ZeroGCTypewriterPro.cs new file mode 100644 index 0000000..84631bb --- /dev/null +++ b/Runtime/Extension/ZeroGCTypewriterPro.cs @@ -0,0 +1,236 @@ +// using UnityEngine; +// +// using System; +// using System.Collections.Generic; +// using Cysharp.Threading.Tasks; +// using Cysharp.Threading.Tasks.Triggers; +// using TMPro; +// using UnityEngine; +// using UnityEngine.Pool; +// using System.Threading; +// using System.Runtime.CompilerServices; +// using Cysharp.Text; +// +// namespace text +// { +// public static class ZeroGCTypewriterPro +// { +// #region 核心结构体 +// +// private struct TypewriterHandle : IEquatable +// { +// public int InstanceID; +// public int Version; +// +// public bool Equals(TypewriterHandle other) => +// InstanceID == other.InstanceID && Version == other.Version; +// } +// +// private struct TypewriterJob +// { +// public TypewriterHandle Handle; +// public TMP_Text Target; +// public string Content; +// public float Speed; +// public int LoopCount; +// public Action OnUpdate; +// public Action OnComplete; +// public CancellationToken ExternalToken; +// } +// +// #endregion +// +// #region 状态管理 +// +// private static readonly Dictionary activeJobs = +// new Dictionary(32); +// +// private struct ActiveJob +// { +// public int Version; +// public CancellationTokenSource Cts; +// } +// +// private static readonly ObjectPool ctsPool = +// ObjectPool( +// createFunc: () => new CancellationTokenSource(), +// actionOnRelease: cts => cts.Cancel(), +// collectionCheck: false +// ); +// +// #endregion +// +// #region 公开接口 +// +// [MethodImpl(MethodImplOptions.AggressiveInlining)] +// public static void Play( +// this TMP_Text text, +// string content, +// float charsPerSecond = 30, +// int loopCount = 0, +// Action onUpdate = null, +// Action onComplete = null, +// CancellationToken externalToken = default) +// { +// var instanceID = text.GetInstanceID(); +// +// // 停止当前任务(如果有) +// if (activeJobs.TryGetValue(instanceID, out var existingJob)) +// { +// ctsPool.Release(existingJob.Cts); +// activeJobs.Remove(instanceID); +// } +// +// // 从对象池获取CTS +// var cts = ctsPool.Get(); +// var linkedToken = CombineTokens( +// text.GetCancellationTokenOnDestroy(), +// externalToken, +// cts.Token +// ); +// +// // 创建新任务 +// var newVersion = activeJobs.TryGetValue(instanceID, out var job) ? job.Version + 1 : 1; +// +// activeJobs[instanceID] = new ActiveJob +// { +// Version = newVersion, +// Cts = cts +// }; +// +// RunJob(new TypewriterJob +// { +// Handle = new TypewriterHandle +// { +// InstanceID = instanceID, +// Version = newVersion +// }, +// Target = text, +// Content = content, +// Speed = Mathf.Max(0.01f, charsPerSecond), +// LoopCount = loopCount, +// OnUpdate = onUpdate, +// OnComplete = onComplete, +// ExternalToken = linkedToken +// }).Forget(); +// } +// +// [MethodImpl(MethodImplOptions.AggressiveInlining)] +// public static void Stop(this TMP_Text text) +// { +// var instanceID = text.GetInstanceID(); +// if (activeJobs.TryGetValue(instanceID, out var job)) +// { +// ctsPool.Release(job.Cts); +// activeJobs.Remove(instanceID); +// ResetTextState(text); +// } +// } +// +// #endregion +// +// #region 核心逻辑 +// +// private static async UniTaskVoid RunJob(TypewriterJob job) +// { +// try +// { +// var target = job.Target; +// using (var sb = ZString.CreateStringBuilder()) +// { +// sb.Append(job.Content); +// target.text = sb.ToString(); +// target.ForceMeshUpdate(); +// +// int currentLoop = 0; +// var chars = sb.AsSpan(); +// var interval = 1f / job.Speed; +// var timer = 0f; +// +// while (IsLoopValid(currentLoop, job.LoopCount)) +// { +// target.maxVisibleCharacters = 0; +// +// for (int i = 0; i < chars.Length; i++) +// { +// // 版本校验 +// if (!IsHandleValid(job.Handle)) return; +// +// // 基于时间的更新 +// timer += Time.deltaTime; +// var required = (int)(timer * job.Speed); +// +// if (required > i) +// { +// target.maxVisibleCharacters = required; +// job.OnUpdate?.Invoke(required); +// } +// +// await UniTask.Yield(PlayerLoopTiming.Update, job.ExternalToken); +// } +// +// currentLoop++; +// timer = 0f; +// job.OnComplete?.Invoke(); +// } +// } +// } +// finally +// { +// if (IsHandleValid(job.Handle)) +// { +// activeJobs.Remove(job.Handle.InstanceID); +// ResetTextState(job.Target); +// } +// } +// } +// +// #endregion +// +// #region 工具方法 +// +// [MethodImpl(MethodImplOptions.AggressiveInlining)] +// private static bool IsHandleValid(TypewriterHandle handle) +// { +// return activeJobs.TryGetValue(handle.InstanceID, out var job) && +// job.Version == handle.Version; +// } +// +// [MethodImpl(MethodImplOptions.AggressiveInlining)] +// private static bool IsLoopValid(int current, int max) => +// max < 0 || current <= max; +// +// private static CancellationToken CombineTokens( +// CancellationToken token1, +// CancellationToken token2, +// CancellationToken token3) +// { +// if (token1.CanBeCanceled && token2.CanBeCanceled && token3.CanBeCanceled) +// return CancellationTokenSource.CreateLinkedTokenSource( +// token1, token2, token3).Token; +// +// if (token1.CanBeCanceled && token2.CanBeCanceled) +// return CancellationTokenSource.CreateLinkedTokenSource( +// token1, token2).Token; +// +// return token1.CanBeCanceled ? token1 : token2; +// } +// +// private static void ResetTextState(TMP_Text text) +// { +// if (text != null) +// { +// text.maxVisibleCharacters = int.MaxValue; +// text.SetVerticesDirty(); +// } +// } +// +// private static CancellationToken GetCancellationTokenOnDestroy(this TMP_Text text) +// { +// return text.gameObject.GetCancellationTokenOnDestroy(); +// } +// +// #endregion +// } +// +// } diff --git a/Runtime/Extension/ZeroGCTypewriterPro.cs.meta b/Runtime/Extension/ZeroGCTypewriterPro.cs.meta new file mode 100644 index 0000000..80bda8c --- /dev/null +++ b/Runtime/Extension/ZeroGCTypewriterPro.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dd18a7ac84917984dbbf48f71872add9 \ No newline at end of file