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组件,超出内容不会被遮罩!"); } } private void Update() { if (Input.GetKeyDown(KeyCode.K)) { AddText("Hello, World!"); } } #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() { // 如果使用TextAnimator,使用SetText方法 if (textAnimator != null) { textAnimator.SetText(string.Empty); } else if (tmpText != null) { tmpText.text = string.Empty; } // 重置位置到顶部 if (tmpTextRectTransform != null) { Vector2 pos = tmpTextRectTransform.anchoredPosition; pos.y = 0; tmpTextRectTransform.anchoredPosition = pos; } if (tmpText != null) { tmpText.ForceMeshUpdate(); } } #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 } }