using UnityEngine; using TMPro; using DG.Tweening; using System.Collections.Generic; namespace AibisDream.MiniGame.Language { /// /// 文字粒子基类 /// public class TextParticle : MonoBehaviour { [Header("组件")] public TextMeshPro textMesh; [Header("属性")] public Vector2 velocity; public float alpha; public float baseAlpha = 1f; public float size = 4f; public string currentChar; [Header("状态")] public bool isStatic = false; public bool isCalmed = false; public float ChangeInterval => changeInterval; protected float changeTimer; protected float changeInterval = 1f; private static readonly IReadOnlyList EmptyCharacterPool = new List().AsReadOnly(); protected IReadOnlyList defaultCharacterPool = EmptyCharacterPool; protected IReadOnlyList interferenceCharacterPool = EmptyCharacterPool; protected IReadOnlyList overrideCharacterPool = EmptyCharacterPool; protected Bounds movementBounds; protected bool useCircularBounds = false; protected Vector3 boundsCenter; protected float boundsRadius; protected TMP_FontAsset customFont; private Material customFontMaterial; private TMPRectClipper screenClipper; protected virtual void Awake() { if (textMesh == null) textMesh = GetComponentInChildren(); if (textMesh == null) { var textObj = new GameObject("Text"); textObj.transform.SetParent(transform, false); textObj.layer = gameObject.layer; // 继承父对象的 layer(SetParent 后仍强制) // 添加 RectTransform(在 Canvas 下必需) RectTransform rectTransform = textObj.AddComponent(); rectTransform.localPosition = Vector3.zero; rectTransform.localScale = Vector3.one; rectTransform.sizeDelta = new Vector2(10, 10); // 设置足够大的尺寸 rectTransform.anchorMin = new Vector2(0.5f, 0.5f); rectTransform.anchorMax = new Vector2(0.5f, 0.5f); rectTransform.pivot = new Vector2(0.5f, 0.5f); // 添加 TextMeshPro 组件 textMesh = textObj.AddComponent(); textMesh.alignment = TextAlignmentOptions.Center; textMesh.fontSize = size; textMesh.enableAutoSizing = false; textMesh.overflowMode = TextOverflowModes.Overflow; // 允许溢出,不裁剪 // 设置字体(优先使用自定义字体) SetupFont(); // 设置渲染层级,确保文字显示在正确的层 textMesh.sortingOrder = 10; } else { // 如果已存在 textMesh,也要确保设置字体 SetupFont(); } SyncLayerToChildren(); changeTimer = Random.Range(0.5f, 1.5f); currentChar = string.Empty; UpdateText(); // TMP 重建 mesh / SubMesh 后再对齐一次 layer SyncLayerToChildren(); } /// /// 将自身与所有子物体(Text / Glow / TMP SubMesh)设为同一 Layer。 /// public void SyncLayerToChildren() { int layer = gameObject.layer; var transforms = GetComponentsInChildren(true); for (int i = 0; i < transforms.Length; i++) transforms[i].gameObject.layer = layer; } protected virtual void Update() { if (!isStatic && !isCalmed) { // 更新位置 transform.position += (Vector3)velocity * Time.deltaTime; // 边界检测 CheckBounds(); // 字符变化:目标粒子用本轮默认池,非目标粒子从干扰短句字符池中随机 changeTimer -= Time.deltaTime; if (changeTimer <= 0) { currentChar = GetNextDisplayChar(); UpdateText(); changeTimer = changeInterval; } // 速度衰减 velocity *= 0.95f; } // 更新视觉效果 UpdateVisuals(); } public void SetMovementBounds(Bounds bounds) { movementBounds = bounds; useCircularBounds = false; } /// /// Registers the shared television-screen rect. Each particle owns a separate TMP /// material instance, so moving one particle never changes another particle's clip. /// public void SetScreenClipRect(RectTransform rect) { if (textMesh == null) return; if (screenClipper == null) screenClipper = textMesh.GetComponent() ?? textMesh.gameObject.AddComponent(); screenClipper.Initialize(textMesh, rect); } public void ClearScreenClip() { if (screenClipper != null) screenClipper.ReleaseMaterial(); } /// /// 设置圆形运动边界(与 SetMovementBounds 二选一,圆形模式下会覆盖矩形边界) /// public void SetCircularBounds(Vector3 center, float radius) { boundsCenter = center; boundsRadius = radius; useCircularBounds = true; } protected void CheckBounds() { Vector3 pos = transform.position; bool bounced = false; if (useCircularBounds) { Vector2 offset = new Vector2(pos.x - boundsCenter.x, pos.y - boundsCenter.y); float dist = offset.magnitude; if (dist > boundsRadius && boundsRadius > 0.0001f) { Vector2 norm = offset / dist; pos = boundsCenter + (Vector3)(norm * boundsRadius); pos.z = transform.position.z; // 反弹速度(沿径向反射) Vector2 vel = velocity; Vector2 reflect = vel - 2f * Vector2.Dot(vel, norm) * norm; velocity = reflect * 0.8f; bounced = true; } } else { if (pos.x < movementBounds.min.x || pos.x > movementBounds.max.x) { velocity.x *= -1; pos.x = Mathf.Clamp(pos.x, movementBounds.min.x, movementBounds.max.x); bounced = true; } if (pos.y < movementBounds.min.y || pos.y > movementBounds.max.y) { velocity.y *= -1; pos.y = Mathf.Clamp(pos.y, movementBounds.min.y, movementBounds.max.y); bounced = true; } } if (bounced) transform.position = pos; } protected string GetRandomChar() { return GetRandomPoolElement(defaultCharacterPool); } /// /// 从干扰短句(笑话等)的字符中随机取一个字(每次只显示一个字符)。 /// 如 "哈哈|嘿嘿|呵呵" 会从 哈、嘿、呵 中随机。 /// protected string GetRandomCharFromPhrases() { if (interferenceCharacterPool == null || interferenceCharacterPool.Count == 0) return GetRandomChar(); return GetRandomPoolElement(interferenceCharacterPool); } /// /// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。 /// 默认从本轮本地化字符池随机返回一个 Unicode 文本元素。 /// protected virtual string GetNextDisplayChar() { if (TryGetOverridePoolChar(out string overrideChar)) return overrideChar; return GetRandomChar(); } /// /// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。 /// public void SetOverrideCharPool(string pool) { SetOverrideCharacterPool( ExpressionTextTokenizer.GetVisibleElements(pool)); } public void SetOverrideCharacterPool(IReadOnlyList pool) { overrideCharacterPool = pool != null && pool.Count > 0 ? pool : EmptyCharacterPool; } protected bool TryGetOverridePoolChar(out string character) { if (!HasOverrideCharacterPool) { character = null; return false; } character = GetRandomPoolElement(overrideCharacterPool); return true; } /// /// 设置本轮默认与干扰字符池。调用方负责传入不可变的本轮快照。 /// public void SetCharacterPools( IReadOnlyList defaultPool, IReadOnlyList interferencePool) { defaultCharacterPool = defaultPool != null && defaultPool.Count > 0 ? defaultPool : EmptyCharacterPool; interferenceCharacterPool = interferencePool != null && interferencePool.Count > 0 ? interferencePool : EmptyCharacterPool; } public void InitializeRandomCharacter() { currentChar = GetNextDisplayChar(); UpdateText(); changeTimer = Random.Range(0.5f, 1.5f); } protected bool HasOverrideCharacterPool => overrideCharacterPool != null && overrideCharacterPool.Count > 0; private static string GetRandomPoolElement(IReadOnlyList pool) { if (pool == null || pool.Count == 0) return string.Empty; return pool[Random.Range(0, pool.Count)]; } protected void UpdateText() { if (textMesh != null) textMesh.text = currentChar; } public void ForceSetCharacter(string character) { currentChar = character; UpdateText(); } public void SetStaticState(bool value) { isStatic = value; } public void SetCalmedState(bool value) { isCalmed = value; } public void SetChangeInterval(float interval) { changeInterval = Mathf.Max(0.02f, interval); } public void ResetChangeTimer(float timer) { changeTimer = timer; } protected virtual void UpdateVisuals() { if (textMesh == null) return; Color color = textMesh.color; color.a = alpha; textMesh.color = color; } public void ApplyForce(Vector2 force) { velocity += force; } /// /// 设置字体资源 /// public void SetFont(TMP_FontAsset fontAsset) { SetFont(fontAsset, null); } /// /// 设置字体与可选的 Material Preset(须基于同一份 SDF Atlas) /// public void SetFont(TMP_FontAsset fontAsset, Material fontMaterialPreset) { customFont = fontAsset; customFontMaterial = fontMaterialPreset; SetupFont(); } /// /// 内部方法:设置字体 /// private void SetupFont() { if (textMesh == null) return; // 优先级:自定义字体 > 默认字体 > 警告 if (customFont != null) { textMesh.font = customFont; } else if (TMP_Settings.defaultFontAsset != null) { textMesh.font = TMP_Settings.defaultFontAsset; } else { Debug.LogWarning("[TextParticle] TextMeshPro 字体未设置!请在 LanguageParticleManager 中指定中文字体资源,或在 Project Settings > TextMesh Pro > Settings 中设置默认字体。"); } if (customFontMaterial != null) textMesh.fontSharedMaterial = customFontMaterial; } protected virtual void OnDestroy() { ClearScreenClip(); } } }