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; protected string overrideCharPool; protected Bounds movementBounds; protected bool useCircularBounds = false; protected Vector3 boundsCenter; protected float boundsRadius; protected TMP_FontAsset customFont; private Material customFontMaterial; private TMPRectClipper screenClipper; // 字符集 protected static string chineseChars = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞"; protected static string[] chineseWords = { "不安", "紧张", "焦虑", "担忧", "烦躁", "恐慌", "恐惧", "绝望", "痛苦", "悲伤", "愤怒", "孤独", "无助", "迷茫", "困惑", "压抑", "沉重", "疲惫", "空虚", "失落" }; // 动态配置的干扰短句(笑话/焦虑等,由 Yarn start_expression 传入,用于非目标粒子字符池) protected static List configuredAnxietyPhrases = new List(); 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 = GetNextDisplayChar(); 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(); // 字符变化:目标粒子用 chineseChars 随机,非目标粒子从干扰短句字符中随机 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 chineseChars[Random.Range(0, chineseChars.Length)].ToString(); } /// /// 从干扰短句(笑话等)的字符中随机取一个字(每次只显示一个字符)。 /// 如 "哈哈|嘿嘿|呵呵" 会从 哈、嘿、呵 中随机。 /// protected string GetRandomCharFromPhrases() { if (configuredAnxietyPhrases == null || configuredAnxietyPhrases.Count == 0) return GetRandomChar(); var chars = new List(); foreach (var phrase in configuredAnxietyPhrases) { if (string.IsNullOrEmpty(phrase)) continue; foreach (char c in phrase) chars.Add(c); } if (chars.Count == 0) return GetRandomChar(); return chars[Random.Range(0, chars.Count)].ToString(); } /// /// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。 /// 默认返回 chineseChars 随机单字。 /// protected virtual string GetNextDisplayChar() { if (TryGetOverridePoolChar(out string overrideChar)) return overrideChar; return GetRandomChar(); } /// /// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。 /// public void SetOverrideCharPool(string pool) { overrideCharPool = string.IsNullOrEmpty(pool) ? null : pool; } protected bool TryGetOverridePoolChar(out string character) { if (string.IsNullOrEmpty(overrideCharPool)) { character = null; return false; } character = overrideCharPool[Random.Range(0, overrideCharPool.Length)].ToString(); return true; } /// /// 设置全局干扰短句配置(笑话等,由 LanguageParticleManager 调用) /// public static void SetAnxietyPhrases(List phrases) { configuredAnxietyPhrases = phrases ?? new List(); } 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(); } } }