Appearance
标题打字机
文档版本:基于 2026-06-01 代码分析
概述
标题打字机系统是一个全屏文字动画播放器,通过 JSON 配置文件定义分组动画序列,在目标玩家屏幕上以逐字打字效果展示标题文字。系统采用服务端配置加载 → 网络包发送 → 客户端状态机渲染的架构,每个分组可独立配置文字样式、速度、停留时间和跳转逻辑,支持循环和分支。
核心概念
分组动画序列
每个 JSON 文件包含多个 group,每个 group 由 index(>=1)唯一标识,播放完毕后通过 next 字段跳转到下一组。next=0 表示结束,next 可跳转到任意 index 形成有向图(可循环/分支),而非简单链表。
状态机五阶段
PRE_BLINK → LINE1_TYPING → LINE2_TYPING → STAY → BLANK → (next>0? 跳转下一组 : stop)| 阶段 | 屏幕显示 | 作用 |
|---|---|---|
PRE_BLINK | 闪烁下划线 _ | 预备阶段,给玩家注意力缓冲 |
LINE1_TYPING | 第一行逐字出现 + 闪烁光标 | 打字机效果(第一行) |
LINE2_TYPING | 第二行逐字出现 + 闪烁光标 | 打字机效果(第二行) |
STAY | 完整文字持续显示 | 玩家阅读时间 |
BLANK | 清屏 | 过渡间隔 |
跳转策略:
- 第一行
text为空 → 直接跳到LINE2_TYPING(若第二行也为空则跳到STAY) - 第二行
text为空 →LINE1_TYPING完成后跳过LINE2_TYPING直接进入STAY BLANK阶段不渲染任何内容
架构设计
服务端→客户端数据流时序
mermaid
sequenceDiagram
participant Admin as 管理员
participant Cmd as RfmTitleWriterStartCommand
participant Config as RfmTitleWriterConfigManager
participant Manager as RfmTitleWriterManager
participant Net as PacketDistributor
participant Client as RfmTitleWriterClientState
participant Render as ClientRfmTitleWriterEvents
Admin->>Cmd: /nfa rfm titlewriter start targets fileId group
Cmd->>Config: getFile(server, fileId)
Config-->>Cmd: TitleWriterFileConfig(groups)
Cmd->>Manager: start(server, targets, fileId, groupIndex)
Manager->>Manager: 遍历groups,构建RfmTitleWriterGroupData列表
alt 文件缺失
Manager-->>Cmd: StartResult.fileMissing()
else 组编号不存在
Manager-->>Cmd: StartResult.groupMissing()
else 成功
Manager->>Net: PlayRfmTitleWriterPayload(fileId, startGroup, allGroups)
Net->>Client: 发送到每个目标玩家
end
Client->>Client: start(payload) 清空GROUPS,填充数据
Client->>Client: beginGroup(initGroupIndex)
Note over Client: 状态机进入 PRE_BLINK
loop 每tick
Client->>Client: tick() 推进状态机
Client->>Render: render() 渲染当前帧
end
Note over Admin: /nfa rfm titlewriter stop targets
Admin->>Client: StopRfmTitleWriterPayload → 清空所有状态客户端状态机
mermaid
stateDiagram-v2
[*] --> PRE_BLINK : beginGroup
PRE_BLINK --> LINE1_TYPING : phaseTicks >= preBlinkTicks<br/>且 line1非空
PRE_BLINK --> LINE2_TYPING : phaseTicks >= preBlinkTicks<br/>且 line1为空 line2非空
PRE_BLINK --> STAY : phaseTicks >= preBlinkTicks<br/>且两行都为空
LINE1_TYPING --> LINE1_TYPING : 每charIntervalTicks<br/>visibleLine1CodePoints+1
LINE1_TYPING --> LINE2_TYPING : 第一行打字完成<br/>且 line2非空
LINE1_TYPING --> STAY : 第一行打字完成<br/>且 line2为空
LINE2_TYPING --> LINE2_TYPING : 每charIntervalTicks<br/>visibleLine2CodePoints+1
LINE2_TYPING --> STAY : 第二行打字完成
STAY --> BLANK : phaseTicks >= stayTicks
BLANK --> PRE_BLINK : phaseTicks >= blankAfterTicks<br/>且 next>0 → beginGroup(next)
BLANK --> [*] : next=0 → stop()
note right of PRE_BLINK : 显示闪烁下划线_
note right of LINE1_TYPING : 逐字显示+光标闪烁
note right of STAY : 完整文字停留
note right of BLANK : 清屏不渲染关键文件
服务端
| 文件 | 说明 |
|---|---|
com/chenxi/chenxi_rfm/server/config/rfm/RfmTitleWriterConfigManager.java | JSON 配置加载、解析、缓存(WeakHashMap 以 server 为 key) |
com/chenxi/chenxi_rfm/server/rfm/RfmTitleWriterManager.java | 将配置转化为网络 Payload 并发送 |
com/chenxi/chenxi_rfm/server/command/rfm/RfmTitleWriterCommand.java | /nfa rfm titlewriter 节点注册 |
com/chenxi/chenxi_rfm/server/command/rfm/RfmTitleWriterStartCommand.java | titlewriter start 指令实现(含 /suggest 自动补全) |
com/chenxi/chenxi_rfm/server/command/rfm/RfmTitleWriterStopCommand.java | titlewriter stop 指令实现 |
客户端
| 文件 | 说明 |
|---|---|
com/chenxi/chenxi_rfm/client/rfm/RfmTitleWriterClientState.java | 五阶段状态机核心(tick + render) |
com/chenxi/chenxi_rfm/client/event/ClientRfmTitleWriterEvents.java | tick 驱动 + GUI 渲染事件入口 |
网络包
| 文件 | 说明 |
|---|---|
com/chenxi/chenxi_rfm/common/network/payload/rfm/PlayRfmTitleWriterPayload.java | 启动播放包(含全部 groups 数据 + 起始 group) |
com/chenxi/chenxi_rfm/common/network/payload/rfm/RfmTitleWriterGroupData.java | 单个 group 数据 record(网络传输用) |
com/chenxi/chenxi_rfm/common/network/payload/rfm/StopRfmTitleWriterPayload.java | 停止播放包 |
关键流程
JSON 配置文件
存放于 <server>/nfa_rfm/typewriter_titles/*.json,文件 ID 取自文件名(去 .json 后缀并转小写)。
json
{
"id": "demo",
"groups": [
{
"index": 1,
"line1": { "text": "欢迎各位逃走者", "bold": true, "color": "gold" },
"line2": { "text": "欢迎来到第四领域", "bold": true, "color": "white" },
"next": 2,
"pre_blink_ticks": 16,
"char_interval_ticks": 10,
"cursor_blink_ticks": 6,
"stay_ticks": 40,
"blank_after_ticks": 20
},
{
"index": 2,
"line1": { "text": "祝你好运", "color": "green" },
"line2": { "text": "" },
"next": 0,
"pre_blink_ticks": 16,
"char_interval_ticks": 8,
"cursor_blink_ticks": 6,
"stay_ticks": 60,
"blank_after_ticks": 20
}
]
}配置字段说明
| 字段 | 类型 | 范围 | 默认值 | 说明 |
|---|---|---|---|---|
index | int | >=1 | — | 组编号(必填,用于识别和跳转) |
line1 / line2 | StyledText | — | "" | 文字内容及样式 |
next | int | >=0 | 0 | 播完后跳转的组 index;0=结束 |
pre_blink_ticks | int | >=0 | 0 | 光标闪烁预备 tick |
char_interval_ticks | int | >=1 | 2 | 每字间隔 tick(越小越快) |
cursor_blink_ticks | int | >=1 | 6 | 光标闪烁切换间隔 |
stay_ticks | int | >=0 | 0 | 完全显示后停留 tick |
blank_after_ticks | int | >=0 | 0 | 清屏后空屏 tick |
StyledText 样式支持
每行文字支持以下样式属性(全部可选):
| 属性 | 类型 | 说明 |
|---|---|---|
text | String | 文字内容 |
color | String | 颜色:white/gold/green/red 等 ChatFormatting 名称,或 #RRGGBB 十六进制 |
bold | bool | 加粗 |
italic | bool | 斜体 |
underlined | bool | 下划线 |
strikethrough | bool | 删除线 |
obfuscated | bool | 乱码效果 |
颜色解析优先级:先尝试 ChatFormatting.getByName()(如 "gold"→金色,仅限颜色类),再尝试 #RRGGBB 格式(如 "#FF6600")。非颜色类 Formatting(如 "bold" 写在 color 字段)无效。
字体渲染
- 缩放:第一行放大 4 倍(
FIRST_LINE_SCALE=4.0),第二行放大 2 倍(SECOND_LINE_SCALE=2.0) - 溢出自适应:
scaledWidth > maxWidth时等比缩小:scale *= maxWidth / scaledWidth - 居中:计算缩放后的总宽度,水平 + 垂直居中
- BLANK 阶段不渲染:
render()中Phase.BLANK直接 return
光标闪烁
shouldShowCursor() 计算逻辑:(phaseTicks / cursorBlinkTicks) % 2 == 0。仅在 PRE_BLINK、LINE1_TYPING、LINE2_TYPING 阶段显示(打字时附加在当前行末尾,预备时独立显示)。
JSON 配置加载时机
配置仅在服务器启动时加载一次(ServerStartedEvent 触发 reload())。如需加载新配置,需重启服务器或执行 /nfa reload(若 reload 命令已集成)。缓存使用 WeakHashMap<MinecraftServer, Map<String, TitleWriterFileConfig>>,停止服务器后自动释放。
指令
| 指令 | 权限 | 说明 |
|---|---|---|
/nfa rfm titlewriter start <targets> <fileId> <group> | MASTER_ADMIN | 向目标玩家播放指定文件的指定起始组 |
/nfa rfm titlewriter stop <targets> | MASTER_ADMIN | 立即停止目标玩家的打字机动画并清屏 |
start 指令会自动提供文件 ID 和组 index 的 Tab 补全。
注意事项 / 限制
- JSON 仅在启动时加载:
RfmTitleWriterConfigManager在ServerStartedEvent时执行reload(),无手动热重载接口。修改或新增 JSON 文件后需重启服务器。 - fileId 规范化为小写:
normalizeFileId()将文件名转小写并去首尾空格,因此Demo.json和demo.json视为同一文件。 - 全部 groups 一次性打包:
PlayRfmTitleWriterPayload包含文件中的所有 groups,而非仅起始组。next跳转在客户端本地执行,无需再向服务端请求。 - 停止包立即生效:
StopRfmTitleWriterPayload到达客户端后stop()方法清空所有状态和缓存数据,清屏。 - 各玩家独立渲染:每个玩家独立运行
RfmTitleWriterClientState,可同时向不同玩家播放不同文件。 - BLANK 阶段不渲染:进入
BLANK后屏幕完全清空,不显示任何文字,仅等待blankAfterTicks到期。 - 预览期间其他 GUI 仍正常渲染:打字机文字渲染在
RenderGuiLayerEvent.Post,叠加在已有 GUI 之上。 - style 可能为
Style.EMPTY:JSON 中未指定样式时使用空样式(白色、无格式),而非默认加粗。