392 lines
13 KiB
C#
392 lines
13 KiB
C#
using UnityEngine;
|
||
using TMPro;
|
||
using DG.Tweening;
|
||
using System.Collections.Generic;
|
||
|
||
namespace AibisDream.MiniGame.Language
|
||
{
|
||
/// <summary>
|
||
/// 文字粒子基类
|
||
/// </summary>
|
||
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<string> EmptyCharacterPool =
|
||
new List<string>().AsReadOnly();
|
||
protected IReadOnlyList<string> defaultCharacterPool = EmptyCharacterPool;
|
||
protected IReadOnlyList<string> interferenceCharacterPool = EmptyCharacterPool;
|
||
protected IReadOnlyList<string> 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<TextMeshPro>();
|
||
|
||
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>();
|
||
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<TextMeshPro>();
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将自身与所有子物体(Text / Glow / TMP SubMesh)设为同一 Layer。
|
||
/// </summary>
|
||
public void SyncLayerToChildren()
|
||
{
|
||
int layer = gameObject.layer;
|
||
var transforms = GetComponentsInChildren<Transform>(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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Registers the shared television-screen rect. Each particle owns a separate TMP
|
||
/// material instance, so moving one particle never changes another particle's clip.
|
||
/// </summary>
|
||
public void SetScreenClipRect(RectTransform rect)
|
||
{
|
||
if (textMesh == null)
|
||
return;
|
||
|
||
if (screenClipper == null)
|
||
screenClipper = textMesh.GetComponent<TMPRectClipper>() ?? textMesh.gameObject.AddComponent<TMPRectClipper>();
|
||
screenClipper.Initialize(textMesh, rect);
|
||
}
|
||
|
||
public void ClearScreenClip()
|
||
{
|
||
if (screenClipper != null)
|
||
screenClipper.ReleaseMaterial();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置圆形运动边界(与 SetMovementBounds 二选一,圆形模式下会覆盖矩形边界)
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从干扰短句(笑话等)的字符中随机取一个字(每次只显示一个字符)。
|
||
/// 如 "哈哈|嘿嘿|呵呵" 会从 哈、嘿、呵 中随机。
|
||
/// </summary>
|
||
protected string GetRandomCharFromPhrases()
|
||
{
|
||
if (interferenceCharacterPool == null || interferenceCharacterPool.Count == 0)
|
||
return GetRandomChar();
|
||
return GetRandomPoolElement(interferenceCharacterPool);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。
|
||
/// 默认从本轮本地化字符池随机返回一个 Unicode 文本元素。
|
||
/// </summary>
|
||
protected virtual string GetNextDisplayChar()
|
||
{
|
||
if (TryGetOverridePoolChar(out string overrideChar))
|
||
return overrideChar;
|
||
return GetRandomChar();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。
|
||
/// </summary>
|
||
public void SetOverrideCharPool(string pool)
|
||
{
|
||
SetOverrideCharacterPool(
|
||
ExpressionTextTokenizer.GetVisibleElements(pool));
|
||
}
|
||
|
||
public void SetOverrideCharacterPool(IReadOnlyList<string> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置本轮默认与干扰字符池。调用方负责传入不可变的本轮快照。
|
||
/// </summary>
|
||
public void SetCharacterPools(
|
||
IReadOnlyList<string> defaultPool,
|
||
IReadOnlyList<string> 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<string> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置字体资源
|
||
/// </summary>
|
||
public void SetFont(TMP_FontAsset fontAsset)
|
||
{
|
||
SetFont(fontAsset, null);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置字体与可选的 Material Preset(须基于同一份 SDF Atlas)
|
||
/// </summary>
|
||
public void SetFont(TMP_FontAsset fontAsset, Material fontMaterialPreset)
|
||
{
|
||
customFont = fontAsset;
|
||
customFontMaterial = fontMaterialPreset;
|
||
SetupFont();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 内部方法:设置字体
|
||
/// </summary>
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|