Godot Toast 通知队列:小提示也需要优先级和节奏

设计 Godot Toast、浮层提示和轻通知队列,处理优先级、合并、限流、遮挡和可访问性。

Toast 提示看起来很小:获得物品、网络重连、保存成功、队友上线、任务更新、背包已满,都弹一下。可一旦系统变多,小提示会迅速变成噪音。玩家刚进大厅,十几条 Toast 排队;战斗中提示挡住技能;同一条网络错误反复刷屏;重要失败被普通奖励提示挤掉。Godot 里做 Toast 动画很容易,但通知队列需要系统设计。

项目里的真实问题

一个项目里,背包系统、任务系统、网络系统、好友系统都直接调用 Toast.show(text)。内测时出现登录后连续 18 条提示,玩家根本看不清。网络断线时每个请求失败都弹一次,屏幕一直闪。后来团队临时加了“同文本 3 秒不重复”,但任务奖励和错误提示又被误合并。通知不应该只有文本,还应该有频道、优先级、合并 key、展示时长、是否可打断、是否需要声音、是否在战斗中延后。

设计目标

  • 语义化:通知包含频道、优先级、合并 key 和上下文。
  • 节奏控制:同屏数量、每分钟数量和战斗中展示策略可控。
  • 重要优先:错误、交易、存档等重要提示不被普通提示挤掉。
  • 可访问:提示可读时间、声音和字幕日志满足基本可访问性。

这些目标看起来像工程约束,实际是在保护玩家体验。Godot 的开发效率很高,很多功能几行脚本就能跑起来,但一旦进入多人协作和多平台发布,临时脚本会迅速变成隐性状态。这里的做法是把状态、输入、执行和反馈拆开,让每一步都能被测试、记录和回退。

推荐架构

flowchart TD
    A["玩家操作/场景事件"] --> B["ToastManager"]
    B --> C["去重合并"]
    B --> D["优先级队列"]
    B --> E["场景上下文"]
    B --> F["通知日志"]
    C --> Z["状态快照和日志"]
    D --> Z
    E --> Z
    Z --> Y["UI 反馈/运行时执行"]

架构图里的模块不要求都做成独立单例。小项目可以合并实现,大项目可以拆成服务和 Resource。真正重要的是调用方向:业务脚本提交意图,管理器做决策,执行层处理 Godot 节点和资源,最后把结果变成 UI 反馈和日志。只要这个方向稳定,后续替换实现不会牵动整个项目。

关键实现细节

ToastRequest 至少包含 channel、priority、message_id、params、merge_key、duration、sound_id、blocking_context 和 expire_time。message_id 对应本地化文本,params 填数量和名称。不要直接传拼好的字符串,否则本地化和合并都会变难。
频道用于区分奖励、错误、网络、好友、任务、系统。不同频道有不同策略。奖励可以合并,网络错误要限流,存档失败要优先,好友上线可以延后。
战斗中不适合弹大量提示。ToastManager 可以根据当前场景上下文决定立即显示、缩短显示、延后显示或写入通知日志。背包整理提示可以延后,存档失败必须立即显示,普通好友上线可以静默进入日志。
Toast 显示时间要足够。文字较长或玩家开启可访问性选项时,显示时间应该延长。不要所有提示固定 1.5 秒。重要 Toast 可以进入通知日志,让玩家稍后查看。

容易踩的坑

业务直接传字符串,会导致本地化、合并和日志都做不好。
同文本去重太粗糙,可能把不同任务或不同错误误合并。合并 key 要按频道设计。
提示音也要限流。网络错误如果每秒响一次,会非常烦。

GDScript 接口草图

class_name ToastManager
extends Node

var current_state := {}
var version := 0

func request(payload: Dictionary) -> void:
    version += 1
    var token := version
    current_state["phase"] = "pending"
    _run_async(payload, func(result):
        if token != version:
            return
        current_state = _normalize_result(result)
        emit_signal("state_changed", current_state)
    )

func _normalize_result(result: Dictionary) -> Dictionary:
    result["system"] = "godot-toast-notification-queue-2026"
    return result

这段代码展示的是接口边界,不是完整实现。真实项目里,payload 应该替换成具体 Resource 或 typed Dictionary,异步回调也要接入错误码、超时和取消。保留 version 或 token 的原因,是 Godot 客户端经常出现旧请求晚于新请求返回的问题,尤其在资源加载、网络和 UI 快速切换场景里。

分阶段落地

第一阶段把散落的 Toast.show 改为 ToastRequest,先统一入口。
第二阶段实现合并、限流和优先级队列,解决刷屏问题。
第三阶段接入场景上下文、通知日志和可访问性选项。

自动化验证和人工验收

登录、领奖、断网、背包满、任务完成同时触发时,确认显示顺序合理。
高频网络错误不会刷屏,合并文案正确。
战斗场景中低优先级 Toast 延后,高优先级错误仍可见。

观测指标

  • 每分钟 Toast 请求数、展示数、合并数和丢弃数。
  • 不同频道平均等待时间。
  • 高优先级 Toast 被延后或打断次数。
  • 玩家打开通知日志的次数。

指标不必全部做成线上埋点。开发包可以显示完整调试面板,内测包采样关键计数,正式包只保留错误码和聚合结果。关键是让问题出现时有证据,而不是靠“我感觉刚才卡了一下”这种描述反复猜。

上线前检查清单

  • 业务只提交 ToastRequest,不直接控制动画。
  • 通知有频道、优先级和合并 key。
  • 队列有同屏限制和限流策略。
  • 场景上下文能影响展示策略。
  • 可访问性选项能延长显示或降低动画。

清单要尽量和脚本结合。能自动检查的放进目录级验证,不能自动检查的写进验收步骤。每次事故后都应该补一条规则,哪怕一开始只是人工检查。这样系统会随着项目经验变厚,而不是只靠某个熟悉代码的人记在脑子里。

数据契约和本地化

ToastRequest 不应该直接携带最终文本,而应携带 message_id 和 params。比如 item.gained{name:"铁矿", count:3}。这样本地化、合并和日志都能工作。若直接传“获得 3 个铁矿”,英文、日文和复数规则都会变得难处理。

同一条 Toast 还应声明展示类型:普通、警告、错误、奖励、进度、系统。展示类型决定颜色、图标、声音和默认时长。频道决定调度策略,展示类型决定视觉样式。两者不要混用,否则“奖励频道里的错误提示”会找不到合适样式。

失败处理和队列保护

Toast 队列也需要保护上限。如果短时间涌入上百条通知,不能全部排队慢慢播完。低优先级过期后直接丢弃,高频同类合并,高优先级保留。每个请求可以有 expire_time,过期后不再展示,避免玩家五分钟后看到早已无意义的“队友上线”。

Presenter 被销毁或场景切换时,队列不能丢掉重要提示。可以把 ToastManager 做成 Autoload,Presenter 只是当前场景的显示层。切场景期间普通提示延后,重要错误进入通知日志。不要把队列放在某个页面节点下,页面一销毁提示全没了。

协作接口

业务系统提交 Toast 时,只能选择预定义 channel 和 message_id。新增 message_id 需要本地化文本和展示类型。这样 UI 团队能统一样式,策划能审查文案,程序也能避免随手拼字符串。

QA 验收时不只看是否出现,还要看节奏。一次操作触发多条 Toast 时,是否合并;战斗中是否遮挡关键 HUD;打开设置或弹窗时 Toast 是否换位置。小提示经常因为“能显示”就被放过,但真正影响体验的是它出现的时机和位置。

实战案例与复盘

网络抖动时 Toast 刷屏非常典型。某次弱网测试里,背包、任务、好友三个接口同时失败,每个接口都弹“网络错误”,十秒内出现二十多条提示。接入 ToastManager 后,同类网络错误合并为“网络不稳定,部分功能正在重试”,并把详细错误写入通知日志。玩家看到的是明确状态,开发仍然能查到具体接口。

奖励提示也需要合并。玩家扫荡副本获得十几种材料,如果逐条 Toast,会打断结算节奏。更好的做法是结算页展示完整奖励,Toast 只显示摘要:“获得 12 种材料,已放入背包”。如果背包满,错误提示优先级高于普通奖励摘要,并且不能被合并丢失。

复盘 Toast 体验时,要看玩家是否能记住关键信息。提示太多,等于没有提示。Toast 队列不是为了把每条业务消息都播完,而是为了筛选出当前时刻玩家需要知道的东西。

上线后的维护策略

Toast 队列上线后,维护重点是文案和频道治理。业务新增提示时,要问它属于哪个频道,是否可合并,是否会在战斗中出现,是否需要进入通知日志。没有这些信息的提示,不应该直接进入全局队列。

灰度开关也要提前准备。任何客户端系统只要影响加载、输入、UI 入口、平台权益或资源选择,都应该能在灰度阶段降低强度或回退到旧策略。回退不是简单关闭功能,而是要保证玩家路径仍然完整。例如系统异常时,可以停用高级策略、保留基础入口、显示降级文案,并把错误码写入日志。没有回退策略的功能,灰度时会让团队非常被动。

责任人要写清楚。一个系统上线后,谁维护配置,谁看指标,谁处理内容接入,谁判断是否回滚,都应该明确。否则问题出现时,大家会先讨论“这归谁管”。Godot 项目里的许多客户端系统横跨程序、策划、美术、运营和 QA,如果没有责任边界,维护成本会比实现成本更高。

文档也不需要写成很重的手册,但至少要有三部分:接入方式、常见错误、验收步骤。接入方式告诉后来的人怎么新增内容;常见错误记录已经踩过的坑;验收步骤保证每次改动都有同样的检查口径。文档越贴近项目真实问题,越不会变成没人看的摆设。

边界补充

Toast 还要和弹窗、字幕、教程提示协调。玩家正在看剧情字幕时,普通 Toast 可以进入延后队列;玩家正在新手教学步骤中,系统提示不应该遮挡教学高亮区域。ToastPresenter 可以从 UI LayoutService 获取当前占用区域,选择顶部、底部或侧边显示,而不是固定一个位置。

还有一种常见情况是同一业务同时需要 Toast 和持久入口。例如邮件到达时可以 Toast 一句,但真正的未读状态应该进入邮箱红点。Toast 负责即时反馈,红点负责后续可追踪状态。不要用 Toast 承担需要玩家稍后处理的信息。

小团队接入版本

小团队可以先做三个频道:奖励、错误、系统。奖励允许合并,错误优先显示,系统可延后。不要一开始做十几个复杂频道,先用最常见的刷屏问题验证队列价值。

交付边界

交付标准是玩家在高事件密度场景里仍能看懂关键提示。Toast 不应该抢走主要玩法,也不应该让重要错误消失。提示越轻,越需要节奏感。

现场演练

现场演练可以在登录后同时触发签到奖励、好友上线、网络重试、任务完成和背包已满。观察哪些提示立即显示,哪些合并,哪些进入日志。这个组合能验证优先级、限流、频道和可访问性时间。

结语

Toast 是小 UI,但不是小系统。Godot 客户端把通知请求语义化,再用队列控制优先级、合并和场景节奏,就能让反馈既及时又不吵。玩家需要的是清楚的提示,不是屏幕上永远排队的文字。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页