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();
}
}
}