为什么这个问题要单独设计
折叠屏和平板不是把手机界面等比放大,而是重新分配信息层级和触控距离。很多团队会把它当成局部功能,在某个按钮、某个页面、某个脚本里补一段判断。短期看,这样最快;项目跑过几轮版本之后,就会出现同一件事在三个地方有三种解释的情况。玩家看到的是一个客户端,团队内部却把责任拆散了。
一次内测里,战斗 HUD 在普通手机上很顺,但平板横屏时技能按钮离右手太远,折叠屏半展开时背包列表又被中间铰链区域切开。测试同学没有报崩溃,只说“能玩但别扭”。这种反馈最容易被低估,因为它不是功能错误,却会持续消耗玩家。
所以本文把Godot 折叠屏与平板布局当成一个客户端系统来讨论,而不是把它写成零散技巧。系统化的好处是边界清晰、调试入口统一、QA 能复现、内容团队知道哪些配置可以改、哪些配置必须走评审。Godot 的节点和 Resource 很适合把运行时状态、配置资产和表现节点拆开;真正困难的是提前约定状态和数据,而不是写第一版脚本。
系统边界和责任划分
这个系统不应该直接替代玩法逻辑,也不应该把平台、网络、资源、UI 全部塞进同一个 Control 或 Node。建议把它拆成以下模块:DevicePostureProbe, LayoutBreakpointTable, SafeAreaResolver, DensityProfile, TouchReachAnalyzer, HudPresetApplier。每个模块只回答一个问题:数据从哪里来、是否可信、如何转换、谁来展示、失败后怎么恢复。
在 Godot 中,可以把稳定配置放在 Resource,把跨场景状态放在 autoload service,把页面表现放在普通场景节点。这样做的直接收益是切场景时状态不会被 UI 销毁,UI 重建时也不会重新发起危险请求。复杂系统最怕的是生命周期混在一起:按钮被隐藏了,请求还在;页面销毁了,回调还回来;配置热更了,旧实例继续按老规则运行。
落地时先写清四条边界规则:
- 把手机竖屏、手机横屏、平板横屏、折叠屏半展开拆成独立 breakpoint,不用单个 scale 值兜底。
- 关键操作按钮优先按手指可达区域布局,信息面板再吃剩余空间。
- 铰链、刘海、系统手势条只影响布局容器,不让业务控件自己读平台 API。
- 背包、地图、队伍列表这类高密度界面在宽屏时使用双栏或侧栏,不把单栏拉成很长的空白。
架构图
下面这张图强调的是数据和控制权的流向。图里的每个节点都应该能打日志,也应该能在开发包里看到当前状态。
flowchart TD
N0["Raw Display Metrics"] --> N1["DevicePostureProbe"]
N1["DevicePostureProbe"] --> N2["LayoutContext"]
N2["LayoutContext"] --> N3["Page Layout Preset"]
N3["Page Layout Preset"] --> N4["Focus Restore"]
N4["Focus Restore"] --> N5["HUD Render"]
如果图里的某个箭头在代码里找不到对应的函数或信号,后期排查时就会靠猜。反过来,如果代码里出现了图外的隐式通道,例如某个页面直接改全局配置、某个回调直接操作战斗对象,就要警惕它会绕过校验和恢复流程。
数据模型要先于表现
建议核心状态至少包含这些字段:posture, short_edge_dp, hinge_rect, safe_insets, density_bucket, dominant_hand, panel_mode, last_breakpoint。字段看起来多,但它们解决的是同一类问题:当现象出错时,我们能不能解释当前结果是怎么来的。没有字段,就只能从屏幕表现反推;有字段,QA 截图、日志、埋点和本地复现才能对齐。
字段命名要避免含糊的 ok、done、enabled。尤其是 enabled,它可能表示玩家开启、平台允许、服务端放行、资源可用、页面可见,这五种含义完全不同。状态字段宁可长一点,也要能让非作者读懂。对于会进入存档或远程配置的字段,还要写版本和默认值,避免旧玩家升级后走到未定义状态。
一个实用做法是把数据分成三层:原始输入层保存平台或内容原始值,归一化层转换成客户端统一语义,表现层只读归一化结果。表现层不应该知道某个值来自 Android API、远程配置还是本地 Resource,它只关心当前应显示什么、能不能交互、失败原因是什么。
具体实现骨架
下面的伪代码不是完整框架,只展示关键习惯:先归一化,再检查状态版本,最后通知表现。
var context := {
"breakpoint": current_breakpoint,
"safe_insets": safe_area,
"posture": posture,
"revision": revision + 1,
}
layout_context_changed.emit(context)
实际项目里还需要补 request_id、revision、owner 和错误码。request_id 用来丢弃旧回调,revision 用来判断状态是否被后来操作覆盖,owner 用来说明谁持有控制权,错误码用来把日志和 UI 文案连起来。很多偶现问题不是算法错,而是旧请求在新状态里继续生效。
典型事故和根因
最典型的事故是把 Control 的 anchor 设置成四边拉伸,然后用统一的 theme font size 解决所有设备。看上去适配了,实际变成平板上按钮巨大、列表信息稀疏、弹窗像海报。另一类事故是旋转屏幕后保留了旧的焦点路径,手柄或键盘导航还在访问已经隐藏的手机布局节点。
处理事故时不要只修表面现象。比如一个按钮灰掉,可能是权限不够、资源缺失、请求未完成、版本不兼容、焦点被弹窗抢走,也可能是状态机已经进入失败态但 UI 没更新。修复方案要让这些原因在数据里可区分,而不是继续新增一个 is_button_disabled。
我比较推荐把事故复盘写成三段:玩家看到什么、系统真实状态是什么、代码为什么没有表达出来。只要第三段写不清,说明模型还不够稳。复盘不是为了追责,而是把下一次同类问题挡在提交前。
实施步骤
按下面顺序推进比较稳:
- 建立 LayoutContext autoload,只输出当前姿态和断点,不直接操作 UI。
- 每个复杂页面提供 phone_compact、phone_wide、tablet_split、foldable_dual 四组布局资源。
- 页面切断点时先保存滚动位置、选中项、输入焦点和未提交文本,再应用新 preset。
- 在编辑器里提供断点预览按钮,让 UI 同事不用拿四台设备才能检查。
第一版不要追求把所有平台和所有玩法一次覆盖。先选一个高频页面或一条核心战斗链路,把状态模型、调试入口和 QA 样本跑通。第二版再扩到相邻场景。第三版才考虑编辑器工具、批量配置和自动化检查。越是基础系统,越不要在没有观测能力时大面积铺开。
还要提前约定谁能改配置。程序负责字段语义和运行时保护,策划或内容同学可以改阈值和映射,但不能临时新增未注册字段。美术可以调整表现资源,但不能绕过状态节点直接控制交互。权限边界写清楚,后期协作会少很多无效沟通。
失败恢复和降级
失败路径要和成功路径同等重要。资源缺失、弱网、平台接口失败、旧版本配置、玩家取消、切后台恢复、场景销毁,都要有明确的去向。能重试的进入重试队列,能降级的给降级结果,不能继续的要阻断并说明原因。
降级不是简单隐藏功能。隐藏会让玩家以为内容不存在,也会让 QA 以为没有触发。更好的方式是保留入口但改变状态:显示不可用原因、预计恢复条件、是否会自动重试。对于战斗、付费、存档、联机这类高风险链路,宁可少展示一点,也不要展示一个会误导玩家的半成功状态。
恢复时要避免“补偿过度”。例如网络恢复后不要把玩家之前连点的所有操作一次提交;资源重新可用后也不要强制把页面跳回顶部。恢复的目标是回到玩家可理解的最近状态,而不是机械地执行积压动作。
性能预算
任何客户端系统都要写预算,即使它看起来只是 UI 或配置。预算可以很朴素:每帧最多处理多少对象、每秒最多刷新多少次、缓存上限是多少、日志采样率是多少、一次状态切换允许耗时多少毫秒。没有预算,优化只能等到玩家觉得卡。
低端设备上要优先保留信息正确性,再削减动画、阴影、轮询频率、装饰效果和非关键刷新。不要为了省一点 CPU 把错误原因隐藏,也不要为了表现顺滑让主线程等待资源或网络。Godot 项目尤其要注意 Control 树重建、信号重复连接、Resource 同步加载和大列表刷新,这些问题经常在内容量上来后才显形。
建议上线后至少观察这些指标:breakpoint_switch_count, layout_apply_ms, safe_area_clamp_count, touch_miss_near_edge, focus_restore_failed。指标不是为了堆报表,而是为了在下一次内容扩展时知道哪条链路先逼近上限。
工具和调试
开发包里应该有一个小面板,显示当前配置版本、状态字段、最近一次状态变化、错误码、请求编号和 owner。面板不需要做得漂亮,但要能被 QA 截图。一个好截图应该让程序看到后马上知道系统卡在哪一步,而不是回头问“你刚才点了什么”。
对内容团队可触发的问题,最好再做编辑器检查。比如引用是否存在、标签是否合法、阈值是否越界、平台差异是否遗漏。能在提交前发现的问题,不要留到打包后。对于难复现的运行时问题,可以配合输入录制、状态快照和最近日志环形缓冲,把偶现变成可分析样本。
调试工具还要有关闭方式。正式包不应该暴露内部状态,也不应该因为调试面板引入额外资源和性能成本。可以用构建渠道、编译开关或远程白名单控制,但不要让正式玩家误触。
QA 清单
这类系统至少要覆盖以下用例:
- 普通手机竖屏、普通手机横屏、7 到 8 英寸小平板、11 英寸平板、内折和外折设备模拟都要覆盖。
- 检查右手模式和左手模式,尤其是攻击、闪避、确认、返回这些高频按钮。
- 旋转、折叠、切后台再回来后,当前弹窗和滚动位置不能丢。
QA 用例要尽量描述前置状态和预期结果,而不是只写“检查功能正常”。例如“在弱网中打开页面,等待请求超时,再切后台十秒后恢复,页面应保留当前选择并显示可重试状态”。这样的用例虽然长,但能逼迫系统说清状态。
同时要建立回归样本。每次修复一个线上或内测事故,就把最小复现步骤加入样本库。等到下一次改相关模块时,先跑样本库,再谈新功能。没有样本库,团队会反复修同一类问题,只是每次换个表象。
上线观察和回滚
上线不是终点,而是开始收集真实分布。内测设备、办公室网络、开发者习惯都太理想化,真实玩家会在低电量、弱网、旧存档、满磁盘、系统权限被关、后台恢复等条件下使用。指标要围绕这些真实条件设计。
回滚策略也要提前写好。哪些配置能远程关闭,哪些资源能退回上一版,哪些状态需要提示玩家重进,哪些数据一旦写入就不能回退,都要在发布前确认。没有回滚策略的灰度只是慢一点的全量,并不真正安全。
如果系统涉及公平性、付费、存档或社交关系,回滚还要考虑玩家感知。不要让玩家因为一次技术回退失去奖励、重复支付、错过队伍或看到矛盾状态。客户端能做的保护有限,但至少要避免展示错误承诺。
团队协作方式
这类系统横跨程序、策划、美术、QA、运营和客服。最容易出问题的不是代码,而是每个人对同一个字段的理解不同。建议把字段说明、状态图、错误码、配置入口和 QA 样本放在同一个文档或工具面板里,更新配置时同步更新说明。
客服也应该看到一部分可解释信息,比如玩家当前版本、配置修订、失败原因和是否命中降级。否则线上反馈只能转成“玩家说坏了”,程序还要从零开始猜。把客户端状态设计得可解释,本质上是在降低整个团队的沟通成本。
最小验收标准
我会用五条标准判断这个系统能不能进入主线:第一,状态字段能解释主要表现;第二,失败路径有明确 UI 和日志;第三,切场景、切后台、弱网和旧回调不会破坏状态;第四,QA 有可复现样本;第五,发布后有指标能观察。五条缺一条,都说明它还只是功能脚本,不是可靠系统。
做到这里之后,再去优化动效、视觉细节和操作节奏才有意义。很多体验问题看起来是手感或 UI,其实底层是状态不可解释。先把状态做稳,再调表现,团队会轻松很多。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。