Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs
T

495 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
[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<string> EmptyCharacterPool =
new List<string>().AsReadOnly();
protected IReadOnlyList<string> defaultUnitPool = EmptyCharacterPool;
protected IReadOnlyList<string> interferenceUnitPool = EmptyCharacterPool;
protected IReadOnlyList<string> 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<TextMeshPro>();
if (textMesh == null)
{
var textObj = new GameObject("Text");
textObj.transform.SetParent(transform, false);
textObj.layer = gameObject.layer; // 继承父对象的 layerSetParent 后仍强制)
// 添加 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);
currentUnitText = 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)
{
currentUnitText = GetNextDisplayUnit();
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)
{
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);
}
/// <summary>
/// 从干扰短句(笑话等)的字符中随机取一个字(每次只显示一个字符)。
/// 如 "哈哈|嘿嘿|呵呵" 会从 哈、嘿、呵 中随机。
/// </summary>
protected string GetRandomCharFromPhrases()
{
if (interferenceUnitPool == null || interferenceUnitPool.Count == 0)
return GetRandomChar();
return GetRandomPoolElement(interferenceUnitPool);
}
/// <summary>
/// 获取下一次要显示的字符。子类可重写以区分目标粒子与非目标粒子(与红蓝颜色解绑)。
/// 默认从本轮本地化字符池随机返回一个 Unicode 文本元素。
/// </summary>
protected virtual string GetNextDisplayUnit()
{
if (TryGetOverridePoolChar(out string overrideChar))
return overrideChar;
return GetRandomChar();
}
/// <summary>
/// 设置换字字符池覆盖(老虎机演出用);传空/null 清除,恢复默认字符池。
/// </summary>
public void SetOverrideUnitPool(string pool)
{
SetOverrideUnitPool(
ExpressionTextTokenizer.GetVisibleElements(pool));
}
public void SetOverrideUnitPool(IReadOnlyList<string> pool)
{
overrideUnitPool = pool != null && pool.Count > 0
? pool
: EmptyCharacterPool;
}
[System.Obsolete("Use SetOverrideUnitPool instead.")]
public void SetOverrideCharacterPool(IReadOnlyList<string> characterPool)
{
SetOverrideUnitPool(characterPool);
}
protected bool TryGetOverridePoolChar(out string character)
{
if (!HasOverrideCharacterPool)
{
character = null;
return false;
}
character = GetRandomPoolElement(overrideUnitPool);
return true;
}
/// <summary>
/// 设置本轮默认与干扰字符池。调用方负责传入不可变的本轮快照。
/// </summary>
public void SetUnitPools(
IReadOnlyList<string> defaultPool,
IReadOnlyList<string> 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<string> defaultCharacters,
IReadOnlyList<string> 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<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 = 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;
}
/// <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;
InvalidateVisualBounds();
}
protected virtual void OnDestroy()
{
ClearScreenClip();
}
}
}