游戏客户端登录状态机:别让一次重连变成一串弹窗

从账号登录、鉴权、选服、进大厅和断线恢复出发,拆解游戏客户端登录状态机的设计边界。

登录流程为什么总是出问题

登录看起来是游戏里最普通的一段流程:点开始,拿 token,选服务器,进入大厅。可只要项目上线,登录链路往往会变成事故高发区。原因不是它代码量最大,而是它同时连接了账号 SDK、渠道包、资源更新、服务器列表、角色数据、公告、排队、隐私协议和新手流程。任何一个环节慢一点、失败一次、返回字段变一下,玩家看到的都可能是“卡在登录界面”。

早期很多团队把登录写成一段顺序脚本:先调 A,成功后调 B,再调 C。内网环境里这样很顺,一到真实用户环境就开始露馅。用户会切后台,会从弱网切到 Wi-Fi,会点两次登录按钮,会在资源更新完之后 token 过期,也会遇到渠道 SDK 返回成功但游戏服鉴权失败。顺序脚本缺少对“正在做什么”和“失败后回哪里”的表达,所以修到后面经常堆满 if。

更稳妥的方式是把登录当成状态机,而不是一条直线。

把状态说清楚

一个可维护的登录状态机至少应该区分这些阶段:初始化、等待用户授权、渠道鉴权、获取服务器列表、选择服务器、游戏服鉴权、拉角色、进入大厅、恢复会话、失败处理。它们不一定全部显示在界面上,但客户端内部必须知道自己处于哪一步。

stateDiagram-v2
    [*] --> Boot
    Boot --> AccountAuth: SDK ready
    AccountAuth --> ServerList: token ok
    AccountAuth --> LoginFailed: token error
    ServerList --> SelectServer: list ok
    SelectServer --> GameAuth: server chosen
    GameAuth --> LoadRole: session ok
    GameAuth --> Queue: server full
    Queue --> LoadRole: queue passed
    LoadRole --> EnterLobby: role ready
    EnterLobby --> [*]
    GameAuth --> LoginFailed: auth failed
    ServerList --> LoginFailed: list failed
    LoginFailed --> AccountAuth: retry

这张图的价值不在于好看,而在于它逼着团队回答几个问题:失败能不能重试,重试回到哪一步;排队属于登录失败还是登录中;切后台回来是否继续原状态;资源更新后是否要重新拉服务器列表;游客账号升级正式账号时是否复用原来的游戏会话。

UI 不要直接驱动业务

常见坏味道是登录按钮里直接写完整链路。按钮点击后禁用按钮、调 SDK、调 HTTP、切面板、弹公告、进大厅。这样做短期很快,但 UI 变成了流程控制器。等后面加排队、加未成年人认证、加协议弹窗、加账号切换,所有逻辑都会塞回按钮事件。

登录界面更适合只做两件事:展示当前状态,以及把用户意图提交给登录控制器。比如用户点击“开始游戏”,UI 发出 StartLogin;用户点“切换账号”,UI 发出 SwitchAccount;用户点“重试”,UI 发出 Retry。真正决定下一步的是状态机,不是某个面板。

这样还有一个好处:自动化测试更容易写。测试脚本可以直接向状态机喂事件,模拟 token 失效、服务器列表为空、排队超时、角色数据损坏,而不必真的点十几个 UI 控件。

失败文案要具体

登录失败最怕一句“网络错误”。玩家不知道该等、该重试、该切账号,客服也不知道怎么定位。客户端至少应该把失败分成几类:本地网络不可用、渠道授权失败、游戏服维护、版本过低、账号被封、服务器满员、会话过期、未知错误。

这些分类不需要把内部错误暴露给玩家,但要让文案有行动指向。版本过低就去更新,服务器维护就显示维护时间,会话过期就回到账号授权,网络不可用就允许重试。日志里则要记录内部错误码、状态机当前状态、账号类型、渠道、客户端版本、资源版本和服务器 ID。

重连不是重新登录

很多项目把断线恢复偷懒做成“回登录页重新走一遍”。这会带来两个问题:一是玩家正在战斗或结算时体验很差,二是重复走登录链路容易触发弹窗、公告、协议确认等非必要步骤。重连应该优先恢复游戏会话,而不是模拟一次冷启动。

恢复会话时,客户端要保留最小必要上下文:账号 token、选中服务器、角色 ID、最近一次场景、未完成请求序列号。重新连上后先向服务端确认会话是否仍然有效,如果有效就拉权威状态补齐;如果无效,再退回游戏服鉴权;只有账号 token 也失效时,才回到渠道授权。

登录链路的日志

登录日志要能复盘一条链路,而不是散落几条“请求成功”。比较实用的字段包括 traceId、state、event、serverId、accountType、networkType、duration、errorCode。每次状态转换都写一条轻量日志,线上用户遇到卡登录时,就能知道他最后停在哪个状态。

日志量也要控制。登录阶段是高频入口,不能把完整响应体都打出来,更不能写 token。建议只记录字段摘要、错误码和耗时。敏感信息做脱敏,客户端本地日志设置大小上限,上传时按用户授权和隐私策略执行。

小结

登录状态机的目标不是把流程复杂化,而是让复杂性有名字。只要状态清楚,UI、网络、SDK、资源更新和服务端鉴权就不会互相抢控制权。上线后的很多登录事故,本质都是客户端不知道自己现在到底在等谁,也不知道失败后该退到哪里。把这件事讲清楚,登录链路就会稳定很多。

上线前的复盘清单

登录状态机最后容易输在细节。团队可以在提测前做一次十五分钟复盘:入口是否只有一个,失败路径是否能被重复触发,日志里是否能看到关键上下文,弱网、低内存、切后台、热更新后首次进入这些场景是否有人真正跑过。清单不需要很长,但要能挡住最常见的事故。

第一项是边界。哪些状态属于客户端暂存,哪些必须等服务端确认,哪些只是表现层效果,要写在需求文档或接口说明里。第二项是恢复。玩家断网、杀进程、锁屏、切换账号、更新资源后回来,客户端应该回到哪个画面,是否会重复扣道具或重复弹奖励。第三项是可观测。没有日志、没有埋点、没有版本号和配置号,线上问题只能靠猜。第四项是降级。低端机、老资源包、灰度配置错误时,系统能否退到朴素但可用的路径。

状态机设计不是为了把代码写得保守,而是为了让客户端在真实环境里少一点脆弱。玩家不会按测试用例玩游戏,他会在地铁里切网络,在战斗结算前接电话,在更新到一半时锁屏,也会在礼包倒计时最后几秒连续点击。能承受这些动作的系统,通常不是靠某个聪明函数撑起来的,而是靠清楚的状态、稳定的数据、可回放的日志和足够朴素的失败处理撑起来的。

和策划、美术、服务端对齐

很多登录状态机问题表面看是客户端实现,根上却是协作边界没有说清楚。策划需要知道哪些反馈可以立即出现,哪些反馈必须等待权威结果;美术需要知道资源尺寸、动画事件、特效峰值和加载时机的预算;服务端需要知道客户端会缓存什么、重试什么、放弃什么。只要这些假设没有写下来,后续迭代就会靠口头记忆运转。

比较有效的做法是把一页协作说明放在需求旁边,列出输入、输出、失败处理和验收方式。比如资源类需求要写明包体归属、依赖关系、是否允许边玩边下;战斗类需求要写明本地预演和服务端确认的差异;UI 类需求要写明列表规模、刷新频率和关闭后的状态保留。说明越具体,返工越少。

上线后也要保留一条反馈通道。客服截图、玩家录像、崩溃堆栈、埋点漏斗和灰度数据都能帮助团队判断问题在哪一层。客户端工程师不应该只等 bug 单,而要主动把现象翻译成可定位的问题:是资源缺失、状态跳转错误、请求重复、表现未降级,还是需求本身给了互相冲突的规则。

一个容易忽略的成本

登录状态机还有一个成本是新人理解成本。项目越到中后期,真正危险的不是某个类多了两百行,而是没人能说清一次完整流程经过哪些模块。新同事接手时,如果只能靠全局搜索和断点追踪,很容易在修一个小问题时改坏另一条路径。

因此我更偏向把关键流程画出来,并在代码里保留少量稳定的命名:状态名、事件名、错误码、资源阶段名尽量和文档一致。这样排查问题时,日志、配置、代码和运营后台看到的是同一套语言。语言统一以后,团队讨论会短很多,也更少出现“我以为你说的是另一个状态”的误会。

这类维护成本不会在第一周显现,但会在每次版本合入、每次活动复用、每次紧急修复里持续计息。早一点把结构讲清楚,后面就少一点靠资深同学记忆救火的依赖。

上线前的复盘清单

登录状态机最后容易输在细节。团队可以在提测前做一次十五分钟复盘:入口是否只有一个,失败路径是否能被重复触发,日志里是否能看到关键上下文,弱网、低内存、切后台、热更新后首次进入这些场景是否有人真正跑过。清单不需要很长,但要能挡住最常见的事故。

第一项是边界。哪些状态属于客户端暂存,哪些必须等服务端确认,哪些只是表现层效果,要写在需求文档或接口说明里。第二项是恢复。玩家断网、杀进程、锁屏、切换账号、更新资源后回来,客户端应该回到哪个画面,是否会重复扣道具或重复弹奖励。第三项是可观测。没有日志、没有埋点、没有版本号和配置号,线上问题只能靠猜。第四项是降级。低端机、老资源包、灰度配置错误时,系统能否退到朴素但可用的路径。

状态机设计不是为了把代码写得保守,而是为了让客户端在真实环境里少一点脆弱。玩家不会按测试用例玩游戏,他会在地铁里切网络,在战斗结算前接电话,在更新到一半时锁屏,也会在礼包倒计时最后几秒连续点击。能承受这些动作的系统,通常不是靠某个聪明函数撑起来的,而是靠清楚的状态、稳定的数据、可回放的日志和足够朴素的失败处理撑起来的。

和策划、美术、服务端对齐

很多登录状态机问题表面看是客户端实现,根上却是协作边界没有说清楚。策划需要知道哪些反馈可以立即出现,哪些反馈必须等待权威结果;美术需要知道资源尺寸、动画事件、特效峰值和加载时机的预算;服务端需要知道客户端会缓存什么、重试什么、放弃什么。只要这些假设没有写下来,后续迭代就会靠口头记忆运转。

比较有效的做法是把一页协作说明放在需求旁边,列出输入、输出、失败处理和验收方式。比如资源类需求要写明包体归属、依赖关系、是否允许边玩边下;战斗类需求要写明本地预演和服务端确认的差异;UI 类需求要写明列表规模、刷新频率和关闭后的状态保留。说明越具体,返工越少。

上线后也要保留一条反馈通道。客服截图、玩家录像、崩溃堆栈、埋点漏斗和灰度数据都能帮助团队判断问题在哪一层。客户端工程师不应该只等 bug 单,而要主动把现象翻译成可定位的问题:是资源缺失、状态跳转错误、请求重复、表现未降级,还是需求本身给了互相冲突的规则。

继续阅读

探索更多技术文章

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

全部文章 返回首页