Godot 技能冷却与充能 UI:按钮转圈只是最后一步

围绕 Godot 技能冷却、充能、公共冷却、预测回滚和 UI 呈现,整理一套可落地的客户端实现方案。

冷却 UI 最怕看起来能按,实际按不出来

技能按钮的冷却转圈看上去简单:读一个剩余时间,画一个遮罩,时间到就亮。但真正上线后,玩家抱怨的往往不是“圆圈画错了”,而是“我看到它亮了,按下去却没有反应”。原因可能是公共冷却还没结束、充能层数只恢复到客户端预测值、角色处于沉默、目标不合法、服务器刚回滚了释放结果,或者动画前摇期间按钮应该显示排队而不是可释放。

因此,技能 UI 不能只订阅 cooldown_remaining。它要展示的是“玩家此刻对这个技能能有什么期待”:可以立即释放、可以排队、因为资源不足不能放、因为目标缺失不能放、因为服务器锁定暂不可用、正在恢复下一层充能。把这些状态拆清楚,按钮才不会用一个灰色遮罩承载所有信息。

数据模型要同时表达冷却、充能和限制

一个技能槽至少需要这些字段:skill_idchargesmax_chargesnext_charge_ready_atcooldown_end_atglobal_cooldown_end_atlocked_reasonqueued_intentprediction_seq。如果项目是纯单机,可以少掉服务器序列,但仍建议保留“绝对时间戳”而不是每帧递减的浮点数。Godot 的暂停、慢动作、切场景都可能影响帧时间,用统一时钟更容易恢复。

充能技能尤其容易写错。比如闪现最多 2 层,每 12 秒恢复一层。玩家有 2 层时释放一次,变成 1 层,并启动下一层恢复;如果恢复到 2 层,就不再继续计时。UI 上既要显示当前层数,又要显示下一层恢复进度。如果只用一个 cooldown,会在一层可用时把按钮显示成冷却中,玩家以为不能放。

状态机比一堆布尔值稳

技能按钮状态常常被写成 disabledcoolingnot_enough_manasilencedout_of_range 这样的布尔组合。组合一多,优先级就会混乱:蓝不够且沉默时显示哪个?有一层充能但公共冷却未结束时按钮亮不亮?客户端预测释放后服务器拒绝,按钮回退时要不要闪红?

更稳的是为 UI 暴露一个枚举状态和若干辅助字段。例如 ReadyQueuedCastingGlobalCooldownRechargingResourceBlockedTargetBlockedServerLocked。状态由 SkillRuntime 统一计算,UI 只负责呈现。复杂分支可以用状态机图和表格给策划确认,别让每个按钮脚本自己判断。

技能槽生命周期

下面这个状态机适合多数带充能和公共冷却的动作 RPG。它不直接绑定具体 UI 动画,而是描述技能槽在客户端的生命周期。

stateDiagram-v2
    [*] --> Ready
    Ready --> Casting: consume charge
    Casting --> GlobalCooldown: cast accepted
    Casting --> Ready: cast rejected
    GlobalCooldown --> Recharging: gcd end
    Recharging --> Ready: charges full
    Recharging --> Casting: has charge and player casts
    Recharging --> Locked: server correction or silence
    Locked --> Recharging: lock expired

Casting 不等于技能一定成功。单机里它可能被本地条件拒绝,联机里它可能先预测成功再被服务器修正。Locked 也不一定是错误,可能是沉默、缴械、过场禁止技能,或者服务器要求短暂冻结输入。UI 层要把锁定原因转成玩家能理解的反馈,比如图标加锁、资源条闪烁、目标边框变红,而不是永远灰掉。

UI 表现要分层

技能按钮建议拆成几层:底图是技能图标;冷却遮罩表现主冷却或公共冷却;充能角标表现层数;小进度环表现下一层恢复;边框表现可排队、选中或目标合法;覆盖图标表现沉默、锁定、资源不足。不要把所有信息挤进一个数字。玩家在战斗中只能扫一眼,颜色、形状和动效要各司其职。

Godot 的 Control 节点可以很容易组合这些层,但要注意不要让每层都每帧改材质。冷却遮罩可以用 TextureProgressBar 或自定义 Shader;数字变化只在整数秒或层数变化时刷新;不可用原因变化才触发动画。技能栏通常有 6 到 12 个按钮,移动端还可能更多,每帧全量刷新不是大问题,但不必要的 Layout 和文本重绘会放大 UI 峰值。

预测释放和服务器修正

联机项目里,技能 UI 必须处理“客户端先亮、服务器后说不”的情况。玩家按下技能后,客户端可以立即扣一层预测充能并进入前摇,以保证响应。但请求要带 prediction_seq,服务器返回接受或拒绝。接受时对齐冷却结束时间;拒绝时根据原因回滚层数或进入锁定。最糟糕的做法是客户端完全等服务器,按钮按下后 100 毫秒没反应,玩家会以为输入丢了。

修正要温和。比如客户端预测闪现成功,服务器判定距离超限拒绝。角色位置回滚已经足够明显,技能按钮如果瞬间从冷却回满,玩家会困惑。可以让按钮播放一次短促的红色回弹,并显示“距离过远”的轻提示。重要的是区分网络拒绝和本地条件拒绝,日志里要能看到 sequence、客户端时间、服务器时间和拒绝原因。

可操作的实现步骤

第一步,写 SkillRuntime,只处理数据,不碰 UI。它接收角色状态、资源、目标、服务器修正,输出每个技能槽的 SkillViewState。第二步,写 SkillButton,只订阅 view state,不直接问角色能不能释放。第三步,把点击按钮和快捷键都转成同一个 CastIntent,交给技能系统处理。第四步,加调试面板,显示每个技能槽的时间戳、层数、状态、阻塞原因。

GDScript 里可以把 SkillViewState 做成 Resource 或轻量 Dictionary。若团队多人协作,Resource 更清晰,可以在 Inspector 里看到字段;若追求网络序列化,Dictionary 更方便。关键不是类型,而是保证 UI 不自行推断业务。UI 可以缓存上一次状态,只在变化时播放动画,避免每帧重启 Tween。

QA 场景

技能冷却要测很多边界:公共冷却结束前 50 毫秒连续按;有 1 层充能时切场景;暂停再恢复;时间缩放;沉默期间冷却继续还是暂停;资源不足时技能本身是否继续恢复;目标死亡瞬间按技能;服务器拒绝后层数是否回滚;快速换装备导致 max charges 改变;重连后服务端时间对齐。

还要测 UI 语言和平台。中文“冷却中”可能能放下,德语文本就会撑爆;手柄焦点停在冷却按钮上时,按确认应该给出反馈还是忽略;移动端按钮被按住时技能进入蓄力,冷却开始时间是按下还是松开。这些都不是视觉细节,而是技能系统和 UI 的契约。

落地建议

不要从炫酷按钮动效开始做冷却系统。先在纯文本调试面板里把状态算对,再给按钮套表现。每个技能按钮都应该能回答三件事:现在能不能放,不能放的最主要原因是什么,距离下一次能放还有多久。只要这三个答案稳定,转圈、闪光、数字和提示音都只是表现层。Godot 的 UI 系统足够灵活,真正需要纪律的是业务状态别散到每个 Control 脚本里。

公共冷却和个人冷却的优先级

技能系统里常见两个时间:技能自身冷却和公共冷却。玩家释放火球后,火球进入 8 秒冷却,所有主动技能进入 0.8 秒公共冷却。UI 呈现时,按钮上到底显示 8 秒还是 0.8 秒?我的经验是:如果技能自身不可用,主遮罩显示自身冷却;如果技能自身可用但公共冷却未结束,显示短促公共冷却效果,比如外圈扫过或按钮轻暗。这样玩家能区分“这个技能还要等很久”和“全局动作节奏还没开放”。

充能技能又多一层。假设位移有 2 层,目前剩 1 层,公共冷却中。按钮应该显示可用层数 1,同时用外圈表示公共冷却,而不是把整颗按钮灰掉。玩家需要知道公共冷却结束后马上能用。若 UI 把它显示成完全冷却,玩家会低估可用资源。

服务端时间对齐

联机模式下,服务器返回的冷却结束时间通常基于服务器时间。客户端要维护 server time offset,不能直接把本地 Time.get_ticks_msec() 当服务器时间。每次收到权威状态时,用平滑方式校准 offset。若 offset 突然跳变,按钮倒计时会回跳或加速,玩家非常容易感知。

可以在 SkillRuntime 里提供 now_server_msec(),所有冷却计算都用它。UI 数字刷新时,若剩余时间小于 1 秒,可以显示小数或只用动效,不要因为 offset 微调让数字 1、0、1 来回跳。服务器修正导致冷却明显延长时,给按钮一个柔和回弹,表示状态被修正,而不是静默变暗。

技能排队的显示

很多动作游戏允许在公共冷却或前摇末尾排队下一技能。此时按钮不应显示为普通冷却,而应该有“已排队”状态。比如边框变亮、按钮轻微脉冲、快捷键提示保持高亮。若玩家再次按同一技能,可以取消排队还是刷新队列,要按设计决定,但 UI 必须给出反馈。

排队技能要显示目标合法性。玩家排队冲锋,目标在队列期间死亡,按钮应从 Queued 变成 TargetBlocked,并清除队列或等待重新选目标。否则公共冷却结束时技能突然不放,玩家又会觉得输入丢失。队列系统和输入缓冲类似,但它属于技能层,时间更长、条件更多,需要单独状态。

数字显示规则

冷却数字看似小事,规则不统一会很烦。建议大于 10 秒显示整数向上取整,1 到 10 秒显示一位小数或整数,少于 1 秒隐藏数字只播放扫光。向上取整能避免显示 0 但技能还不能按。对于长冷却,例如 2 分钟,可以显示 2:00,小屏幕则显示 2m。这些规则最好集中在 CooldownFormatter,不要每个按钮自己格式化。

还要处理本地化。中文 2秒、英文 2s、俄文复数、阿拉伯数字方向都不同。战斗 UI 为了节省空间可以使用语言无关的数字加环形进度,但设置页或技能详情里的冷却说明应走翻译模板。

技能详情和战斗按钮使用同一份真相

很多项目技能详情页显示“冷却 8 秒”,战斗按钮却因为装备、天赋、Buff 显示 6.4 秒。玩家会问哪个是真的。SkillRuntime 应该提供两类接口:基础定义值和当前运行时值。详情页可以展示基础值,同时列出当前修正;战斗按钮只显示当前运行时值。不要让详情页自己重新计算天赋,也不要让按钮只读静态配置。

修正来源要可追踪。冷却减少可能来自装备、天赋、场景 Buff、服务器活动。调试面板里显示 base_cooldown=8000, modifier=-20%, final=6400,策划排查会快很多。线上玩家反馈某技能冷却不对时,客户端日志如果能打印修正链,服务端和客户端就能对齐。

图标变体和禁用态

技能按钮常见几种视觉:正常、冷却、资源不足、锁定、排队、正在蓄力、被替换。不要为每种状态准备完全不同的图标,维护成本很高。更好是图标本身稳定,状态通过遮罩、边框、角标和覆盖符号表达。只有技能被形态替换时,才换图标,例如变身后普通攻击变成爪击。

禁用态颜色要谨慎。把图标整体压到 20% 灰度,会让玩家认不出技能。资源不足可以偏蓝或闪资源条,沉默可以加锁链图标,距离过远可以红色边框,公共冷却可以短扫光。状态越具体,玩家越不需要读文字。

冷却和暂停

单机游戏暂停时,冷却是否继续取决于设计。打开系统菜单一般暂停,打开背包可能暂停也可能不暂停,联机游戏通常不能暂停。SkillRuntime 不应直接用 delta 递减,而应使用 time domain。每个冷却规则声明使用 gameplay time、real time 或 server time。暂停影响 gameplay time,不影响 real time。

例如每日奖励倒计时使用 real/server time,战斗技能使用 gameplay/server time。慢动作下技能冷却是否变慢也要明确。很多动作游戏慢动作只影响动画和物理,不影响真实冷却;有些单机游戏则让一切慢下来。时间域写清楚,UI 倒计时才不会在暂停和慢动作里乱跳。

自动化验证

可以给 SkillRuntime 写一批纯逻辑测试:释放技能后层数减少;公共冷却中不能释放其他技能但可排队;充能恢复到上限后停止计时;服务器拒绝后回滚;沉默结束后状态恢复;装备改变 max charges 时多余层数如何处理。UI 层再做少量截图测试,确认遮罩和数字显示。

这些测试不需要启动完整场景。把时间提供者注入 SkillRuntime,测试里手动推进时间,就能稳定覆盖边界。冷却系统一旦和真实帧时间绑死,测试会很难写,也更容易出偶现 Bug。

继续阅读

探索更多技术文章

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

全部文章 返回首页