using System;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Febucci.UI;
using Febucci.UI.Core;
namespace AibisDream.UI
{
///
/// Terminal风格的TMP_Text组件,自动滚动到底部并遮罩超出内容
///
[RequireComponent(typeof(RectTransform))]
public class TerminalText : MonoBehaviour
{
#region 字段
[SerializeField] private TMP_Text tmpText;
[SerializeField] private RectTransform viewportRectTransform;
[SerializeField] private TextAnimator_TMP textAnimator;
[SerializeField] private TypewriterCore typewriter;
private RectTransform rectTransform;
private RectTransform tmpTextRectTransform;
private Coroutine autoScrollCoroutine;
private float lastContentHeight = 0f;
public event Action OnTextShown;
#endregion
#region Unity生命周期
private void Awake()
{
InitializeComponentRefs();
ValidateMaskComponent();
}
private void OnRectTransformDimensionsChange()
{
if (tmpText != null && rectTransform != null)
{
// Viewport尺寸变化时,重新调整位置
UpdateBounds();
}
}
private void OnDestroy()
{
// 取消事件注册
if (typewriter != null)
{
typewriter.onTextShowed.RemoveListener(OnTextAnimationComplete);
typewriter.onCharacterVisible.RemoveListener(OnCharacterVisible);
}
}
#endregion
#region 初始化
private void InitializeComponentRefs()
{
// 获取Viewport的RectTransform
rectTransform = viewportRectTransform != null ? viewportRectTransform : transform as RectTransform;
// 如果没有指定TMP_Text,尝试从子对象获取
if (tmpText == null)
{
tmpText = GetComponentInChildren();
}
if (tmpText != null)
{
tmpTextRectTransform = tmpText.rectTransform;
// 初始化内容高度(使用GetWorldCorners方法)
tmpText.ForceMeshUpdate();
Vector3[] contentCorners = new Vector3[4];
tmpTextRectTransform.GetWorldCorners(contentCorners);
Matrix4x4 viewWorldToLocalMatrix = rectTransform.worldToLocalMatrix;
Vector3 vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int j = 0; j < 4; j++)
{
Vector3 v = viewWorldToLocalMatrix.MultiplyPoint3x4(contentCorners[j]);
vMin = Vector3.Min(v, vMin);
vMax = Vector3.Max(v, vMax);
}
Bounds contentBounds = new Bounds(vMin, Vector3.zero);
contentBounds.Encapsulate(vMax);
lastContentHeight = contentBounds.size.y;
}
else
{
Debug.LogError("TerminalText: 未找到TMP_Text组件!请在Inspector中指定或确保子对象中有TMP_Text组件。");
}
// 如果没有指定TextAnimator,尝试从TMP_Text的GameObject获取
if (textAnimator == null && tmpText != null)
{
textAnimator = tmpText.GetComponent();
}
// 如果没有指定Typewriter,尝试从TMP_Text的GameObject获取
if (typewriter == null && tmpText != null)
{
typewriter = tmpText.GetComponent();
}
// 如果找到了Typewriter,注册事件
if (typewriter != null)
{
typewriter.onTextShowed.AddListener(OnTextAnimationComplete);
// 监听字符显示事件,在每次显示新字符时更新滚动位置
typewriter.onCharacterVisible.AddListener(OnCharacterVisible);
}
}
private void ValidateMaskComponent()
{
Mask mask = GetComponent();
RectMask2D rectMask = GetComponent();
if (mask == null && rectMask == null)
{
Debug.LogWarning("TerminalText: Viewport上未找到Mask或RectMask2D组件,超出内容不会被遮罩!");
}
}
#endregion
#region 公共方法
///
/// 添加新文本(追加到现有文本)
///
public void AddText(string text)
{
if (string.IsNullOrEmpty(text))
return;
// 如果使用Typewriter和TextAnimator,使用ShowText方法追加文本并播放动画
if (typewriter != null && textAnimator != null)
{
// 获取当前完整文本
string currentText = textAnimator.textFull;
// 添加换行符(如果不是第一行)
string textToAppend = string.IsNullOrEmpty(currentText)
? text
: "\n" + text;
// 记录当前已显示的字符数
int previousVisibleCount = textAnimator.maxVisibleCharacters;
if (previousVisibleCount > 0)
{
// 已经有文本显示,使用AppendText追加新文本
// AppendText会保留已显示文本的可见性,并正确初始化新字符
textAnimator.AppendText(textToAppend, hideText: true);
// AppendText最后会设置maxVisibleCharacters = CharactersCount(所有字符可见)
// 我们需要重新设置maxVisibleCharacters,保留之前已显示的字符,新追加的文本暂时隐藏
// 这样Typewriter就可以继续显示新文本了
textAnimator.firstVisibleCharacter = 0;
textAnimator.maxVisibleCharacters = previousVisibleCount;
// 等待一帧,确保AppendText的更新完成,然后再启动Typewriter
StartCoroutine(DelayedStartTypewriter());
}
else
{
// 如果是第一次添加文本,使用ShowText自动启动Typewriter
typewriter.ShowText(text);
}
}
else if (textAnimator != null)
{
// 只有TextAnimator,没有Typewriter
// 添加换行符(如果不是第一行)
string textToAppend = string.IsNullOrEmpty(textAnimator.textWithoutTextAnimTags)
? text
: "\n" + text;
// 使用TextAnimator的AppendText方法追加文本
textAnimator.AppendText(textToAppend, hideText: false);
// 立即更新布局和滚动
tmpText.ForceMeshUpdate();
StartCoroutine(DelayedAutoScroll());
}
else
{
// 没有TextAnimator时,使用原来的方式
if (tmpText == null)
return;
if (string.IsNullOrEmpty(tmpText.text))
{
tmpText.text = text;
}
else
{
tmpText.text += "\n" + text; // 添加换行符分隔
}
// 强制更新布局以获取最新边界
tmpText.ForceMeshUpdate();
// 自动滚动到底部(显示最新内容)
AutoScrollToBottom();
}
}
///
/// 设置Typewriter速度
///
public void SetTypewriterSpeed(float speed)
{
if (typewriter != null)
{
typewriter.SetTypewriterSpeed(speed);
}
}
///
/// 文本动画完成回调
///
private void OnTextAnimationComplete()
{
// 文本动画完成后,更新布局并滚动到底部
if (tmpText != null)
{
tmpText.ForceMeshUpdate();
StartCoroutine(DelayedAutoScroll());
// 并触发文本显示事件
OnTextShown?.Invoke();
OnTextShown = null;
}
}
///
/// 字符显示事件回调(每次显示新字符时调用)
///
private void OnCharacterVisible(char character)
{
// 每次显示新字符时,延迟更新滚动位置
// 这样可以确保在文本增长过程中持续滚动到底部
// 使用协程延迟一帧,确保布局已更新
// 如果已经有协程在运行,先停止它
if (autoScrollCoroutine != null)
{
StopCoroutine(autoScrollCoroutine);
}
autoScrollCoroutine = StartCoroutine(DelayedAutoScroll());
}
///
/// 在LateUpdate中持续检查文本高度变化并更新滚动位置
///
private void LateUpdate()
{
if (tmpText == null || rectTransform == null || tmpTextRectTransform == null)
return;
// 如果使用Typewriter,在动画过程中持续检查
if (typewriter != null && typewriter.isShowingText)
{
tmpText.ForceMeshUpdate();
// 使用GetWorldCorners计算实际内容高度
Vector3[] contentCorners = new Vector3[4];
tmpTextRectTransform.GetWorldCorners(contentCorners);
Matrix4x4 viewWorldToLocalMatrix = rectTransform.worldToLocalMatrix;
Vector3 vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int j = 0; j < 4; j++)
{
Vector3 v = viewWorldToLocalMatrix.MultiplyPoint3x4(contentCorners[j]);
vMin = Vector3.Min(v, vMin);
vMax = Vector3.Max(v, vMax);
}
Bounds contentBounds = new Bounds(vMin, Vector3.zero);
contentBounds.Encapsulate(vMax);
float currentContentHeight = contentBounds.size.y;
// 如果内容高度发生变化,更新滚动位置
if (Mathf.Abs(currentContentHeight - lastContentHeight) > 0.01f)
{
lastContentHeight = currentContentHeight;
AutoScrollToBottom();
}
}
}
///
/// 延迟一帧后自动滚动(确保布局已更新)
///
private IEnumerator DelayedAutoScroll()
{
yield return null;
AutoScrollToBottom();
autoScrollCoroutine = null;
}
///
/// 延迟启动Typewriter(确保AppendText的更新完成)
///
private IEnumerator DelayedStartTypewriter()
{
yield return null;
if (typewriter != null && textAnimator != null)
{
// 临时设置maxVisibleCharacters为CharactersCount,让Typewriter能显示所有字符
// Typewriter的ShowTextRoutine会跳过已可见的字符,所以只会显示新追加的部分
int savedMaxVisible = textAnimator.maxVisibleCharacters;
textAnimator.maxVisibleCharacters = textAnimator.CharactersCount;
// 手动启动Typewriter继续显示新文本
typewriter.StartShowingText(false);
// 注意:Typewriter会在显示过程中更新maxVisibleCharacters,所以不需要恢复
}
}
///
/// 清空所有文本并重置位置
///
public void Clear()
{
// 先停止可能正在运行的Typewriter动画
if (typewriter != null)
{
typewriter.StopShowingText();
}
// 同时清除TextAnimator和底层TMP_Text
if (textAnimator != null)
{
textAnimator.SetText(string.Empty);
}
if (tmpText != null)
{
tmpText.text = string.Empty;
tmpText.ForceMeshUpdate();
}
// 重置TextAnimator的显示状态
if (textAnimator != null)
{
textAnimator.firstVisibleCharacter = 0;
textAnimator.maxVisibleCharacters = 0;
}
// 重置位置到顶部
if (tmpTextRectTransform != null)
{
Vector2 pos = tmpTextRectTransform.anchoredPosition;
pos.y = 0;
tmpTextRectTransform.anchoredPosition = pos;
}
lastContentHeight = 0f;
}
#endregion
#region 内部方法
///
/// 更新边界信息(类似ScrollRect)
///
private void UpdateBounds()
{
if (tmpText == null || rectTransform == null)
return;
// 强制更新布局以获取最新边界
tmpText.ForceMeshUpdate();
// 获取Viewport和Content的实际大小
float viewportHeight = rectTransform.rect.height;
float contentHeight = tmpText.bounds.size.y;
// 如果内容超出Viewport,调整位置显示底部
if (contentHeight > viewportHeight)
{
AutoScrollToBottom();
}
else
{
// 内容未满,重置到顶部
if (tmpTextRectTransform != null)
{
Vector2 pos = tmpTextRectTransform.anchoredPosition;
pos.y = 0;
tmpTextRectTransform.anchoredPosition = pos;
}
}
}
///
/// 自动滚动到底部(核心实现)
/// 使用类似ScrollRect的方法计算bounds和位置
///
private void AutoScrollToBottom()
{
if (tmpText == null || rectTransform == null || tmpTextRectTransform == null)
return;
// 确保布局已更新
tmpText.ForceMeshUpdate();
// 使用GetWorldCorners获取content的实际bounds(类似ScrollRect的GetBounds方法)
Vector3[] contentCorners = new Vector3[4];
tmpTextRectTransform.GetWorldCorners(contentCorners);
// 将content的world corners转换到viewport的本地空间
Matrix4x4 viewWorldToLocalMatrix = rectTransform.worldToLocalMatrix;
Vector3 vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
Vector3 vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int j = 0; j < 4; j++)
{
Vector3 v = viewWorldToLocalMatrix.MultiplyPoint3x4(contentCorners[j]);
vMin = Vector3.Min(v, vMin);
vMax = Vector3.Max(v, vMax);
}
Bounds contentBounds = new Bounds(vMin, Vector3.zero);
contentBounds.Encapsulate(vMax);
// 获取Viewport的bounds
Bounds viewBounds = new Bounds(rectTransform.rect.center, rectTransform.rect.size);
// 计算内容高度和Viewport高度
float contentHeight = contentBounds.size.y;
float viewportHeight = viewBounds.size.y;
// 如果内容高度超过Viewport,需要调整位置
if (contentHeight > viewportHeight)
{
// 计算偏移量:让内容底部对齐到Viewport底部
// contentBounds.min.y是内容底部在viewport本地空间的位置
// viewBounds.min.y是viewport底部的位置(通常是负值)
// 我们需要让contentBounds.min.y = viewBounds.min.y
float currentBottom = contentBounds.min.y;
float targetBottom = viewBounds.min.y;
float offsetY = tmpTextRectTransform.anchoredPosition.y + (targetBottom - currentBottom);
Vector2 pos = tmpTextRectTransform.anchoredPosition;
pos.y = offsetY;
tmpTextRectTransform.anchoredPosition = pos;
}
else
{
// 内容未满,重置到顶部
// 让内容顶部对齐到viewport顶部
float currentTop = contentBounds.max.y;
float targetTop = viewBounds.max.y;
float offsetY = tmpTextRectTransform.anchoredPosition.y + (targetTop - currentTop);
Vector2 pos = tmpTextRectTransform.anchoredPosition;
pos.y = offsetY;
tmpTextRectTransform.anchoredPosition = pos;
}
}
#endregion
#region Editor支持
#if UNITY_EDITOR
private void OnValidate()
{
// 在编辑器中验证组件引用
if (tmpText == null)
{
tmpText = GetComponentInChildren();
}
if (viewportRectTransform == null)
{
viewportRectTransform = transform as RectTransform;
}
// 自动查找TextAnimator和Typewriter
if (textAnimator == null && tmpText != null)
{
textAnimator = tmpText.GetComponent();
}
if (typewriter == null && tmpText != null)
{
typewriter = tmpText.GetComponent();
}
}
#endif
#endregion
}
}