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; [SerializeField] [UnityEngine.Serialization.FormerlySerializedAs("currentChar")] private string currentUnitText; public string CurrentUnitText => currentUnitText; [System.Obsolete("Use CurrentUnitText instead.")] public string currentChar { get => currentUnitText; set { currentUnitText = value; UpdateText(); } } [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 defaultUnitPool = EmptyCharacterPool; protected IReadOnlyList interferenceUnitPool = EmptyCharacterPool; protected IReadOnlyList overrideUnitPool = EmptyCharacterPool; protected Bounds movementBounds; protected bool useCircularBounds = false; protected Vector3 boundsCenter; protected float boundsRadius; protected TMP_FontAsset customFont; private Material customFontMaterial; private TMPRectClipper screenClipper; private Vector2 preferredLocalSize; private bool preferredSizeDirty = true; 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); currentUnitText = 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) { currentUnitText = GetNextDisplayUnit(); 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) { Bounds visualBounds = GetVisualWorldBounds(); float visualRadius = Mathf.Max( visualBounds.extents.x, visualBounds.extents.y); float allowedRadius = Mathf.Max(0f, boundsRadius - visualRadius); Vector2 visualCenter = visualBounds.center; Vector2 offset = visualCenter - (Vector2)boundsCenter; float dist = offset.magnitude; if (dist > allowedRadius && boundsRadius > 0.0001f) { Vector2 norm = offset / dist; Vector2 correction = (Vector2)boundsCenter + norm * allowedRadius - visualCenter; pos += (Vector3)correction; // 反弹速度(沿径向反射) Vector2 vel = velocity; Vector2 reflect = vel - 2f * Vector2.Dot(vel, norm) * norm; velocity = reflect * 0.8f; bounced = true; } } else { Bounds visualBounds = GetVisualWorldBounds(); if (visualBounds.min.x < movementBounds.min.x || visualBounds.max.x > movementBounds.max.x) { velocity.x *= -1; if (visualBounds.min.x < movementBounds.min.x) pos.x += movementBounds.min.x - visualBounds.min.x; else pos.x -= visualBounds.max.x - movementBounds.max.x; bounced = true; } if (visualBounds.min.y < movementBounds.min.y || visualBounds.max.y > movementBounds.max.y) { velocity.y *= -1; if (visualBounds.min.y < movementBounds.min.y) pos.y += movementBounds.min.y - visualBounds.min.y; else pos.y -= visualBounds.max.y - movementBounds.max.y; bounced = true; } } if (bounced) transform.position = pos; } protected string GetRandomChar() { return GetRandomPoolElement(defaultUnitPool); } /// /// 从干扰短句(笑话等)的字符中随机取一个字(每次只显示一个字符)。 /// 如 "哈哈|嘿嘿|呵呵" 会从 哈、嘿、呵 中随机。 /// protected string GetRandomCharFromPhrases() { if (interferenceUnitPool == null || interferenceUnitPool.Count == 0) return GetRandomChar(); return GetRandomPoolElement(interferenceUnitPool); } /// /// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。 /// 默认从本轮本地化字符池随机返回一个 Unicode 文本元素。 /// protected virtual string GetNextDisplayUnit() { if (TryGetOverridePoolChar(out string overrideChar)) return overrideChar; return GetRandomChar(); } /// /// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。 /// public void SetOverrideUnitPool(string pool) { SetOverrideUnitPool( ExpressionTextTokenizer.GetVisibleElements(pool)); } public void SetOverrideUnitPool(IReadOnlyList pool) { overrideUnitPool = pool != null && pool.Count > 0 ? pool : EmptyCharacterPool; } [System.Obsolete("Use SetOverrideUnitPool instead.")] public void SetOverrideCharacterPool(IReadOnlyList characterPool) { SetOverrideUnitPool(characterPool); } protected bool TryGetOverridePoolChar(out string character) { if (!HasOverrideCharacterPool) { character = null; return false; } character = GetRandomPoolElement(overrideUnitPool); return true; } /// /// 设置本轮默认与干扰字符池。调用方负责传入不可变的本轮快照。 /// public void SetUnitPools( IReadOnlyList defaultPool, IReadOnlyList interferencePool) { defaultUnitPool = defaultPool != null && defaultPool.Count > 0 ? defaultPool : EmptyCharacterPool; interferenceUnitPool = interferencePool != null && interferencePool.Count > 0 ? interferencePool : EmptyCharacterPool; } [System.Obsolete("Use SetUnitPools instead.")] public void SetCharacterPools( IReadOnlyList defaultCharacters, IReadOnlyList interferenceCharacters) { SetUnitPools(defaultCharacters, interferenceCharacters); } public void InitializeRandomUnit() { currentUnitText = GetNextDisplayUnit(); UpdateText(); changeTimer = Random.Range(0.5f, 1.5f); } [System.Obsolete("Use InitializeRandomUnit instead.")] public void InitializeRandomCharacter() { InitializeRandomUnit(); } protected bool HasOverrideCharacterPool => overrideUnitPool != null && overrideUnitPool.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 = currentUnitText; preferredSizeDirty = true; } } public void ForceSetUnitText(string unitText) { currentUnitText = unitText; UpdateText(); } [System.Obsolete("Use ForceSetUnitText instead.")] public void ForceSetCharacter(string character) { ForceSetUnitText(character); } public Bounds GetVisualWorldBounds() { RefreshPreferredSize(); Transform textTransform = textMesh != null ? textMesh.transform : transform; Vector3 scale = textTransform.lossyScale; Vector3 size = new Vector3( Mathf.Max(0.001f, preferredLocalSize.x * Mathf.Abs(scale.x)), Mathf.Max(0.001f, preferredLocalSize.y * Mathf.Abs(scale.y)), 0.001f); return new Bounds(textTransform.position, size); } public float DistanceToVisualBounds(Vector2 worldPoint) { Bounds bounds = GetVisualWorldBounds(); float dx = Mathf.Max(bounds.min.x - worldPoint.x, 0f, worldPoint.x - bounds.max.x); float dy = Mathf.Max(bounds.min.y - worldPoint.y, 0f, worldPoint.y - bounds.max.y); return Mathf.Sqrt(dx * dx + dy * dy); } protected void InvalidateVisualBounds() { preferredSizeDirty = true; } private void RefreshPreferredSize() { if (!preferredSizeDirty) return; preferredSizeDirty = false; if (textMesh == null || string.IsNullOrEmpty(currentUnitText)) { preferredLocalSize = Vector2.zero; return; } preferredLocalSize = textMesh.GetPreferredValues(currentUnitText); } 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; InvalidateVisualBounds(); } protected virtual void OnDestroy() { ClearScreenClip(); } } }