Skip to content

玩家会话状态语义 ​

本文面向框架用户,说明登录态、游戏连接、主动退出、断线、顶号、重连和随进随出重新加入之间的区别。它重点回答两个问题:

  • 玩家到底还算不算在当前游戏局里?
  • 玩法代码应该用哪些事件和状态判断玩家是暂时离线、已经离开,还是重新连回来了?

最重要的原则是:断线不等于退出游戏,退出登录也不等于退出游戏。 只有主动退出、放弃重连、被踢、游戏结束或平台明确移除局内归属时,玩家才应被当作已经离开本局。

四层状态 ​

很多混淆来自把不同层的状态合成了一个“玩家不在了”。实际运行时至少有四层:

层级说明谁主要关心玩法代码能否直接判断
客户端登录态当前客户端是否仍持有平台账号登录。顶号会让旧客户端登录失效。客户端、平台登录系统通常不能。玩法服务器只会看到后续连接变化。
游戏连接态客户端和 Host 的网络连接是否还活着。网络超时、客户端崩溃、顶号后的旧连接关闭都会影响它。Host、玩法服务器可以,通过玩家连接/断线事件和 PlayerStatus 判断。
平台局内归属Lobby 是否认为该用户仍属于某个 game session。这决定下次是否能走重连路径。Lobby、重连流程间接可见。玩法不要自己维护平台归属。
玩法槽位状态当前 Player 槽位是否 Connected、Disconnected、Left,以及槽位上的单位、视野、资源是否还保留。gameplay / framework 用户可以,通过 Player.IsOnline、EventPlayerStatusChanged、玩家事件判断。

同一个用户可以出现这样的组合:

text
客户端登录态:旧客户端已失效
游戏连接态:旧连接断开
平台局内归属:仍保留,可重连
玩法槽位状态:Disconnected

这就是顶号、崩溃、网络断开后最容易误判的场景:旧客户端不再在线,但玩家并没有主动退出游戏。

状态语义速查 ​

场景玩法层通常看到平台是否保留本局归属下次是否走重连说明
短暂网络抖动,未达到底层超时通常无事件保留不需要重连传输层可能自行恢复,玩法层不一定感知。
网络断开、客户端崩溃、进程被杀EventPlayerUserDisconnected,状态变为 Disconnected保留是这是“暂时离线”。不要当作主动退出。
账号在其他地方登录旧客户端登录态失效;只有当 Host 侧游戏连接也断开、超时,或走 Entrance proxy 的 USER_DISCONNECT 链路时,玩法层才会收到 EventPlayerUserDisconnected保留是顶号不是主动退出游戏,也没有一个固定的“顶号后 N 秒必定断线”语义。旧客户端自身不会继续重连,但新登录端仍可能看到可重连局。
用户主动退出游戏EventPlayerUserActivelyQuit,状态变为 Left移除否这是明确离局。下次不应弹重连对话框。
用户取消/放弃重连EventPlayerUserAbandonReconnect,状态变为 Left移除否玩家明确表示不回到这局。
服务端 Player.Kick(...)客户端收到踢出提示;平台移除局内归属移除否如果需要记录业务原因,应在调用 Kick 的项目逻辑中记录。公开断线事件不携带 kick reason。
游戏结束Game.OnGameEnd / 游戏结束流程移除否游戏局结束后不再重连。
随进随出局主动退出后再加入先离开旧归属,再通过开放局加入接口进入退出后移除;再次加入时重新注册否,走加入这是“重新加入同一局”,不是“重连旧身份”。

重连对话框什么时候出现 ​

重连对话框依赖平台仍然保存“这个用户属于某个未结束游戏局”的信息。下面这些情况会保留归属,因此下次进入大厅或启动流程时可能出现重连入口:

  • 网络断开或客户端崩溃。
  • 顶号导致旧客户端退出登录后,旧游戏连接也断开、超时,或通过 Entrance proxy 链路通知到 Host。
  • 客户端进程被系统杀掉,Host 后续判定断线。

下面这些情况会移除归属,因此不应再弹重连对话框:

  • 用户主动退出游戏。
  • 用户在重连提示中选择放弃或取消重连。
  • 服务端踢出玩家。
  • 游戏局已经结束。

对随进随出局要特别区分:

  • 重连:回到自己没有主动离开的旧槽位和旧局内状态。
  • 重新加入:通过开放局/中途加入入口再次进入一个仍然存在的游戏局。

重新加入可以进到“原本那局”,但它不是重连路径。项目代码应把它当作一次新的进入流程处理。开放局加入 API 见 开放局加入系统。

时间判定 ​

这些时间来自当前平台和 Host 行为,实际线上值以部署配置和地图配置为准。玩法代码不应把它们写死成业务规则。

时间当前语义默认值或来源玩法侧建议
普通连接无包超时Host 多久没收到客户端包后先把 KCP 连接标为 paused / UnresponsiveHost 网络配置 base_config.netpeer_time_out_seconds,代码默认值为 10 秒,调试/部署可改这一步在 WasiCore 里表现为 EventPlayerConnectionUnresponsive / PlayerStatus.Unresponsive / Player.IsConnectionUnresponsive。
paused 连接超时KCP 连接已进入 paused 状态后,Host 最多继续保留多久地图配置 connection_timeout_when_paused,默认 120 秒超过这段时间仍未恢复,才进入真正断线流程,并触发 EventPlayerUserDisconnected。
全员离线后结束游戏局所有真人玩家都离线一段时间后,普通游戏局可被 Host 结束地图配置 end_game_when_offline_time,默认 300 秒;常驻局不按此自然结束这是游戏局回收条件,不是单个玩家从 Disconnected 变 Left 的时间。

一名玩家断线后,并不会因为“过了 N 秒”自动在玩法层变成主动退出。是否清理他的局内资源,应该由项目自己的玩法规则决定;但如果你清理了实体、队伍席位或临时资源,就要确保重连回来时能恢复或给出明确的不可恢复处理。

paused 的底层判定标准是连接状态,不是玩家主观上“离开键盘”。KCP 连接在两种情况下会进入 paused:客户端显式发送 CE1PAUSE / CE1PAUSELOADING,例如应用切后台或少数仍调用真实 Connection.Pause() 的加载流程;或者 Host 在普通连接无包超时窗口内收不到包,当前默认约 10 秒后先把连接自动标为 paused,而不是立刻断线。进入 paused 后,Host 会触发 connection pause 流程;WasiCore 当前公开语义是 EventPlayerConnectionUnresponsive 和 PlayerStatus.Unresponsive。此时不会触发 EventPlayerUserDisconnected。只要后续收到任意有效 KCP 业务包,就会恢复为 connected,并映射为 EventPlayerConnectionRestored / PlayerStatus.Connected。只有 paused 状态继续超过 connection_timeout_when_paused,Host 才会真正断开连接并进入玩家断线流程。

顶号本身也不是一个 gameplay 断线计时器。当前实现里,旧 Entrance channel 会被通知“账号在其他地方登录”并关闭;如果该游戏连接是通过 Entrance proxy 转发给 Host,Entrance 注销会广播 USER_DISCONNECT,Host 收到后会很快进入玩家断线流程。这个路径主要受服务端调度和网络往返影响,Entrance 侧还安排了 3 秒后的兜底关闭。若当前游戏是客户端直连 Host 的连接,顶号不会直接等价于 Host 连接断开;只有旧客户端实际关闭游戏连接、进程退出、网络不再发包并达到 Host 网络超时,玩法层才会收到断线事件。

玩法中如何判断 ​

服务端优先使用玩家事件和 PlayerStatus。常用事件见 玩家系统。

csharp
#if SERVER
using GameCore.Event;
using GameCore.GameSystem;
using GameCore.PlayerAndUsers;
using GameCore.PlayerAndUsers.Enum;
using Events;
using System.Collections.Generic;

public static class PlayerSessionStateExample : IGameClass
{
    private static readonly HashSet<long> DisconnectedUsers = new();

    public static void OnRegisterGameClass()
    {
        Game.OnGameTriggerInitialization += RegisterTriggers;
    }

    private static void RegisterTriggers()
    {
        new Trigger<EventPlayerUserDisconnected>(OnDisconnected).Register(Game.Instance);
        new Trigger<EventPlayerUserConnected>(OnConnected).Register(Game.Instance);
        new Trigger<EventPlayerConnectionUnresponsive>(OnConnectionUnresponsive).Register(Game.Instance);
        new Trigger<EventPlayerConnectionRestored>(OnConnectionRestored).Register(Game.Instance);
        new Trigger<EventPlayerUserActivelyQuit>(OnActivelyQuit).Register(Game.Instance);
        new Trigger<EventPlayerUserAbandonReconnect>(OnAbandonReconnect).Register(Game.Instance);
        new Trigger<EventPlayerStatusChanged>(OnStatusChanged).Register(Game.Instance);
    }

    private static void OnDisconnected(EventPlayerUserDisconnected e)
    {
        DisconnectedUsers.Add(e.User.UserId);

        Game.Logger.LogInformation(
            "玩家暂时断线: Player={PlayerId}, User={UserId}",
            e.Player.Id,
            e.User.UserId);

        // 推荐:暂停只依赖在线玩家的逻辑、保留可恢复状态。
        // 不推荐:立刻删除存档、结算失败、销毁不可恢复的关键对象。
    }

    private static void OnConnected(EventPlayerUserConnected e)
    {
        var wasDisconnected = DisconnectedUsers.Remove(e.User.UserId);

        Game.Logger.LogInformation(
            "玩家连接: Player={PlayerId}, User={UserId}, WasDisconnected={WasDisconnected}",
            e.Player.Id,
            e.User.UserId,
            wasDisconnected);

        // 当前公开事件不直接暴露底层 isReconnect。
        // 如果需要业务上的“重连回来”,可用此前是否记录为断线来判断。
    }

    private static void OnConnectionUnresponsive(EventPlayerConnectionUnresponsive e)
    {
        Game.Logger.LogInformation(
            "玩家连接进入 paused/Unresponsive: Player={PlayerId}, User={UserId}",
            e.Player.Id,
            e.User.UserId);

        // 注意:这是底层连接 paused,不是真正的“用户挂机”。
        // 可能来自切后台、加载流程,或约 10 秒无包后的自动 pause。
        // 它不是主动退出,也不是正式断线。
    }

    private static void OnConnectionRestored(EventPlayerConnectionRestored e)
    {
        Game.Logger.LogInformation(
            "玩家连接从 paused/Unresponsive 恢复: Player={PlayerId}, User={UserId}",
            e.Player.Id,
            e.User.UserId);
    }

    private static void OnActivelyQuit(EventPlayerUserActivelyQuit e)
    {
        DisconnectedUsers.Remove(e.User.UserId);
        MarkUserLeft(e.Player, e.User.UserId, "actively quit");
    }

    private static void OnAbandonReconnect(EventPlayerUserAbandonReconnect e)
    {
        DisconnectedUsers.Remove(e.User.UserId);
        MarkUserLeft(e.Player, e.User.UserId, "abandon reconnect");
    }

    private static void OnStatusChanged(EventPlayerStatusChanged e)
    {
        Game.Logger.LogInformation(
            "玩家状态变化: Player={PlayerId}, {OldStatus} -> {Status}",
            e.Player.Id,
            e.OldStatus,
            e.Status);
    }

    private static void MarkUserLeft(Player player, long userId, string reason)
    {
        Game.Logger.LogInformation(
            "玩家离开本局: Player={PlayerId}, User={UserId}, Reason={Reason}",
            player.Id,
            userId,
            reason);

        // 这里再做最终离局清理、结算或释放不可重连资源。
    }
}
#endif

如果只需要判断当前是否在线,可以在服务端读:

csharp
#if SERVER
if (!player.IsOnline)
{
    // Disconnected 或 Left 都可能不在线。
    // 若需要区分,监听 EventPlayerStatusChanged,或读取 player.SlotController.Status。
}
#endif

如果需要判断“当前是否处于底层 connection paused / 暂时失联”,现有公开 API 可直接读 player.IsConnectionUnresponsive,或者读 PlayerStatus.Unresponsive:

csharp
#if SERVER
if (player.IsConnectionUnresponsive)
{
    // 当前 Host 连接处于 paused / 暂时失联状态。
    // 注意:player.IsOnline 仍会把 Unresponsive 视为在线。
}
#endif

设计建议 ​

  • 把 EventPlayerUserDisconnected 当作“暂时失联”,不要直接当作战败、退出或结算完成。
  • 主动退出、放弃重连、踢出、游戏结束才适合做最终离局清理。
  • 需要保存 CloudData 时,使用 User.UserId,不要用 Player.Id。Player.Id 是本局槽位,不是持久用户标识。
  • 断线期间应避免继续给该玩家发送必须即时确认的客户端消息;PlayerController.SendMessage 只会向 Connected 状态的真人玩家发送。
  • PlayerStatus.Unresponsive 当前对应底层 connection paused / 暂时失联。它仍被 Player.IsOnline 视为在线,但 PlayerController.SendMessage 不会向 Unresponsive 状态发送。
  • 顶号和网络断开在公开玩法事件里都表现为断线。项目不要依赖公开事件区分二者。
  • 如果项目允许玩家主动切换到另一个游戏局,并且旧局还会周期保存用户状态,阅读 同用户游戏局切换清理,避免旧局晚到写入覆盖新局状态。
  • 如果项目需要玩家全部离线后服务器继续运行,使用 常驻游戏局,不要依赖普通离线保活时间。

用户文案建议 ​

面向玩家时,尽量避免把平台技术状态直接暴露出来:

技术状态推荐文案方向避免
顶号“账号已在其他设备登录,本设备已退出登录。”“你已退出当前游戏。”
断线可重连“检测到上一局仍在进行,是否回到游戏?”“你刚刚退出了游戏,是否重进?”
主动退出后开放局再加入“加入游戏”“重连游戏”
放弃重连“放弃后本局将不再保留重连入口。”“稍后仍可恢复原局。”

相关文档 ​