多格式文本解析与渲染系统

Texturge 的多格式文本系统是一个完整的 解析 → 构建 → 渲染 管线,将带标签的多格式文本字符串转换为可逐字动画渲染的类型化渲染树。

管线总览

RichText String                    FTagNode Tree                   FRenderNode Tree
┌──────────────────┐      ┌────────────────────┐      ┌──────────────────────────┐
│ "Hello <Wave     │      │ Root               │      │ Root (Unknown)           │
│  id="bounce">    │ ───▶ │ ├─ "Hello " (text) │ ───▶ │ ├─ TextContent           │
│  Texturge</>!"   │      │ └─ Wave            │      │ │   "Hello "              │
│                  │      │     └─ "Texturge"  │      │ └─ AnimationLayer        │
└──────────────────┘      └────────────────────┘      │    TagName="Wave"        │
        FTagParser                  Parse               │    ├─ TextContent        │
                                                       │    │   "Texturge"       │
                                                       └──────────────────────────┘
                                                           FRenderTreeBuilder

标签解析

FTagParser 是一个单遍、非递归的迭代栈解析器,将带标签的多格式文本转换为 FTagNode 解析树和剥离标签后的 PlainText

支持的语法

语法示例说明
开始标签<tagname>标签名为 [A-Za-z0-9_-],最长 32 字符
带属性标签<tagname attr1=val1 attr2="val2">支持引号包裹和无引号两种值格式
自动关闭</>关闭最近打开的标签
显式关闭</tagname>关闭指定名称的标签,名称不匹配时产生警告
自闭合<tag />用于装饰器(如图片占位)
转义序列&lt; &gt; &amp;HTML/XML 实体解码

容错机制

  • 格式错误的标签产生 UE_LOG 警告,不崩溃
  • 输入结束时自动关闭所有未闭合的标签(每个自动闭合产生一条警告)
  • 无效的 < 字符(后跟非标签名)被视为纯文本

FTagNode 结构

struct FTagNode
{
    FString TagName;
    TMap<FString, FString> Attributes;  // 键值对属性
    TArray<TSharedPtr<FTagNode>> Children;  // 子节点
    TWeakPtr<FTagNode> Parent;
    int32 RangeBegin;  // 纯文本中的起始索引
    int32 RangeEnd;    // 纯文本中的结束索引(不含)
    bool bIsSelfClosing;
};

渲染树构建

FRenderTreeBuilder 将解析树转换为类型化渲染树 FRenderNode,核心任务是分类节点——区分动画标签、多格式文本样式标签和纯文本。

节点分类

类型识别方式用途
AnimationLayer标签名匹配 UTextAnimationDataAsset 中的 Entry标记需要独立动画的文本段落
StyleLayer标签名未匹配 DataAsset透传给引擎富文本(由 RichTextStyleSheet 定义的样式标签,如 <b>),保留在输出中
TextContent叶子节点纯文本内容
Decorator自闭合标签内嵌装饰器(图片等)

Entry 索引匹配

FindEntryIndex() 使用两层匹配策略:

  1. 优先:查找 id 属性(如 <anim id="Wave">),在 AnimationData->Entries[] 中按 TagName 匹配
  2. 回退:若 id 属性缺失,直接用标签名匹配(兼容旧 <Wave> 格式)

显示文本序列化

SerializeToDisplayText() 将渲染树转换为 UE5 兼容的显示多格式文本:

  • AnimationLayer → 跳过标签标记,递归输出子节点(剥离动画标签)
  • StyleLayer → 输出完整标签包裹(如 <b>text</>
  • TextContent → 直接输出文本
  • Decorator → 输出自闭合标签

SerializeSubstring() 按字符索引截取渲染树子串——这是逐字揭示时的关键方法,根据当前已揭示字符数 MaxCharIndex 动态生成带正确样式包裹的部分文本。

Slate 渲染

FTextRun — 逐字符动画渲染

FTextRun 继承 FSlateTextRun,重写 OnPaint() 实现逐字符动画渲染:

FTextRun::OnPaint()

  ├─ Animator 存在且正在播放?
  │   ├─ Yes → 对每个字符:
  │   │        1. 从 Animator 获取该字符的 FAnimationFrameData
  │   │        2. 应用变换:Translate → Rotate → Scale → Shear(以 Pivot 为中心)
  │   │        3. 应用颜色:Color × InWidgetStyle 染色
  │   │        4. 应用不透明度:Opacity × 原不透明度
  │   │        5. 绘制阴影(若 ShadowColor 非透明)
  │   │        6. 绘制字形
  │   └─ No  → 基类 FSlateTextRun::OnPaint()(快路径,无开销)

FRichTextMarshaller — 注入动画文字运行块

FRichTextMarshaller 继承 FRichTextLayoutMarshaller,在 AppendRunsForText() 中用 FTextRun 替换标准 FSlateTextRun,使所有文字运行块都带 UTextAnimator 引用。

Marshaller 维护 AccumulatedTextLengthPrecedingDecoratorCount 来正确计算每个 Run 在 PlainText 中的偏移量——这是将 ModelText 行索引映射回 PlainText 全局索引以查询帧数据的关键。

SAnimatedRichTextBlock

SAnimatedRichTextBlock 继承 SRichTextBlock,在 Tick() 中调用 Animator->TickAnimation(),并在动画器状态变化时动态更新控件文本。它使用 FRichTextMarshaller 来实现多格式文本解析与动画数据注入的统一管线。

SAnimatedTextBlock

SAnimatedTextBlock 继承 STextBlock,是纯文本路径的 Slate 控件。其 OnPaint()Animator->GetCurrentFrameDataArray() 读取帧数据并逐字形渲染。与 RichText 路径不同,此控件不需要 Marshaller 介入——它直接遍历 STextBlock 的文本布局行,对每个字形查找对应的帧数据。

images/rich-text-pipeline.png — 三栏管线示意:多格式文本输入 → FTagNode 解析树(树形嵌套)→ FRenderNode 渲染树(紫/蓝/绿/橙四色标注节点类型)
images/serialize-substring-progression.gif — 四帧序列 GIF:展示文字在字符索引 0/5/10/全部时的逐字揭示渲染,每帧标注 MaxCharIndex 与 SerializeSubstring 输出