主题
开放局加入系统
概述
GameCore.LobbySystem.LobbyService 目前对外公开了两个客户端 API:
TryGetCurrentOpenGameRef(out OpenGameRef? openGame)LeaveAndJoinOpenGame(OpenGameRef openGame)
这两个 API 适合处理这样一类需求:
- 当前玩家已经在一个允许中途加入的游戏局内。
- 当前玩家通过你自己的项目逻辑,把该游戏局的引用提供给另一位玩家。
- 其他玩家收到该引用后,调用
LeaveAndJoinOpenGame切换加入同一个游戏局。
这套 API 只解决两件事:
- 从当前本地游戏局提取
OpenGameRef - 在已处于另一个游戏局内时,提交“退出当前局并加入目标开放局”的切换请求
它不负责帮你把 OpenGameRef 传递给其他游戏局里的玩家,也不包含好友发现、组队邀请或跨服分发能力。另一位玩家如何拿到这个 OpenGameRef,取决于你自己的项目逻辑。
这里的“开放局”通常来自创作者中心配置的“随进随出”大厅模式。模式 key、房间最大人数、地图玩家槽位和“大厅进入时创建全新的游戏局”的完整关系,见 随进随出模式与开放局。
如果目标玩家当前在另一个游戏局里,一个常见做法是:
- 发起方客户端把
OpenGameRef先交给自己当前游戏局的服务器。 - 当前服务器通过 跨服务器频道系统 把消息发到目标用户当前所在的服务器。
- 目标服务器再把
OpenGameRef转交给目标客户端。
> 后台配置注意:中途加入游戏系统依赖大厅支持“随进随出”能力。接入前,应在创作者中心的「运营配置 → 游戏基础配置 → 游戏大厅配置」中创建随进随出模式,并明确配置模式 key、房间最大人数和大厅进入行为。完整配置说明见 随进随出模式与开放局。
与大厅自动匹配的关系
后台“房间最大人数”控制的是大厅自动匹配容量:玩家从大厅点击该模式时,平台只会自动加入尚未达到该人数的开放局。
LeaveAndJoinOpenGame 是按 OpenGameRef 定向加入一个具体游戏局,不会重新执行大厅的模式分池和自动容量筛选。因此,定向加入可以让开放局实际人数超过后台房间最大人数,但不能超过地图配置的真人玩家槽位数。
模式 key 只隔离大厅自动匹配,不是 OpenGameRef 的版本校验。平台不会替项目判断目标开放局中的旧客户端/服务器逻辑是否能与当前版本共存;如果发布了不兼容的新版本,项目应为新版本使用新的模式 key,并在自己的邀请、队伍或引用分发逻辑中停止分发旧局引用,或自行完成版本兼容性检查。完整的版本隔离策略见随进随出模式与开放局。
例如,地图有 100 个真人玩家槽位,后台随进随出模式的房间最大人数设为 1:从大厅进入的玩家可以各自创建单人开放局;组队时,队员仍可通过队长分享的 OpenGameRef 定向进入队长局。这个结构适合“平时单人世界,组队时进入同一个副本服务器”的玩法。
后台自动匹配人数和地图硬上限的完整设计示例,见 随进随出模式与开放局。
核心类型
OpenGameRef
OpenGameRef 描述一个可以被主动加入的游戏局:
csharp
public sealed record OpenGameRef(
string MapName,
string JoinKey,
long? GameSessionId = null,
JoinPolicy JoinPolicy = JoinPolicy.Open);字段说明:
| 字段 | 说明 |
|---|---|
MapName | 目标地图名 |
JoinKey | 允许加入该局的 key |
GameSessionId | 可选的会话 ID,仅用于附带上下文 |
JoinPolicy | 当前仅支持 Closed 和 Open 两态 |
API 参考
TryGetCurrentOpenGameRef
csharp
public static bool TryGetCurrentOpenGameRef(out OpenGameRef? openGame)用途:
- 从当前本地活跃游戏局中提取一个可分享的开放局引用。
- 这是一个同步 getter,不会额外向服务器发请求。
典型失败原因:
- 当前没有活跃游戏局。
- 当前局不是允许中途加入的局。
- 当前局是 quick start 局。
- 当前局缺少可用的 join key。
LeaveAndJoinOpenGame
csharp
public static bool LeaveAndJoinOpenGame(OpenGameRef openGame)用途:
- 当当前调用方已经在另一个普通游戏局内时,提交“退出当前局并加入目标开放局”的切换请求。
行为说明:
- 这是 fire-and-forget API,只表示宿主是否接受了这次切换请求。
- 宿主会在内部处理退局、清理当前游戏状态,再发起加入目标开放局。
- 当前调用环境如果位于
StateGameWASM 中,退局过程可能直接结束当前 WASM 运行时,因此这个 API 不提供可等待的异步结果。 - 返回
true不表示已经进入目标局;目标局结束、JoinKey失效或地图真人玩家槽位耗尽时,最终加入仍可能失败。 - 切局流程由框架负责释放源局的开放局占位和真人
Player槽位;目标局只有在实际加入成功后才占用目标槽位。项目不要因为返回true就自行修改两边的人数。
不包含的能力
当前公开的 API 不包含下面这些能力:
- 查询其他玩家当前所在的开放局
- 邀请指定玩家加入你的游戏局
- 好友发现、组队关系或大厅房间管理
- 在不同游戏局、不同服务器或不同进程之间分发
OpenGameRef
如果你的玩法需要“让另一个当前不在本局里的玩家加入”,那么除了本篇介绍的两个 API 之外,你还需要自己提供一套能够把 OpenGameRef 交给对方的项目逻辑。
典型用法
场景:平时单人,组队时切入队长开放局
可以让后台大厅只负责创建单人开放局,再由项目自己的组队系统定向集结:
- 地图配置足够的真人玩家槽位。
- 随进随出模式的后台房间最大人数设为
1。 - 队长读取并分享自己的
OpenGameRef。 - 队员调用
LeaveAndJoinOpenGame切入队长局。
后台人数只限制大厅自动匹配,定向加入的人数最终由地图真人玩家槽位限制。完整配置见 随进随出模式与开放局。
场景:一方提供开放局引用,另一方主动加入
最小使用闭环是:
- 发起方调用
TryGetCurrentOpenGameRef。 - 你的项目逻辑把
OpenGameRef交给目标玩家。 - 目标玩家收到后,调用
LeaveAndJoinOpenGame。
发起方:读取当前开放局引用
csharp
#if CLIENT
using GameCore.LobbySystem;
public static class OpenGameHostExample
{
public static OpenGameRef? GetCurrentOpenGame()
{
if (!LobbyService.TryGetCurrentOpenGameRef(out var openGame) || openGame is null)
{
return null;
}
return openGame;
}
}
#endif接收方:切换加入目标开放局
csharp
#if CLIENT
using GameCore.LobbySystem;
public static class OpenGameSwitchExample
{
public static bool JoinBySwitching(OpenGameRef openGame)
{
return LobbyService.LeaveAndJoinOpenGame(openGame);
}
}
#endif场景:通过 ServerChannel 把 OpenGameRef 转发给另一个游戏局里的玩家
这一类需求的链路通常是:
- 发起方客户端调用
TryGetCurrentOpenGameRef。 - 发起方客户端把
TargetUserId + OpenGameRef发给自己当前游戏局的服务器。 - 当前服务器调用
ServerChannelMessage.PublishToUser(...),把消息发到目标用户当前所在的服务器。 - 目标服务器收到
ServerChannel消息后,再通过本局的客户端/服务端同步方式把OpenGameRef发给目标客户端。 - 目标客户端调用
LeaveAndJoinOpenGame。
这条链路里有两类通信:
下面给一个完整示例。
共享消息类型
csharp
using EngineInterface.BaseType;
public sealed class RequestCrossGameOpenGameInviteMessage
{
public long TargetUserId { get; set; }
public OpenGameRef? OpenGame { get; set; }
}
public sealed class OpenGameServerChannelEnvelope
{
public long FromUserId { get; set; }
public OpenGameRef? OpenGame { get; set; }
}
public sealed class NotifyOpenGameInviteMessage
{
public long FromUserId { get; set; }
public OpenGameRef? OpenGame { get; set; }
}发起方客户端:把目标用户和 OpenGameRef 交给本地服务器
> 这一步只是当前游戏局内部的客户端 → 服务端同步,不是跨服通信。
csharp
#if CLIENT
using GameCore.LobbySystem;
using TriggerEncapsulation.Messaging;
public static class OpenGameInviteClient
{
public static async Task<bool> InviteUserAsync(long targetUserId)
{
if (!LobbyService.TryGetCurrentOpenGameRef(out var openGame) || openGame is null)
{
return false;
}
return await MessageBuilder.Create(new RequestCrossGameOpenGameInviteMessage
{
TargetUserId = targetUserId,
OpenGame = openGame
})
.WithPriority(MessagePriority.High)
.SendToServerAsync();
}
}
#endif发起方所在服务器:通过 ServerChannel 把消息IncomingContext 发给目标用户
csharp
#if SERVER
using GameCore.ServerChannelSystem;
using GameCore.PlayerAndUsers;
using TriggerEncapsulation.Messaging;
public static class OpenGameInviteRelayServer
{
[MessageHandler]
public static async Task<bool> OnRequestCrossGameOpenGameInvite(
Player? sender,
RequestCrossGameOpenGameInviteMessage message)
{
if (sender?.User is null || message.OpenGame is null)
{
return false;
}
var envelope = new OpenGameServerChannelEnvelope
{
FromUserId = sender.User.UserId,
OpenGame = message.OpenGame
};
return await Task.FromResult(
ServerChannelMessage.PublishToUser(message.TargetUserId, "open-game-join", envelope));
}
}
#endif目标用户所在服务器:订阅频道并把消息转交给目标客户端
> 这一段里,ServerChannel 负责服务器之间的转发;真正投递到目标客户端时,仍然是目标游戏局内部自己的客户端/服务端同步。
csharp
#if SERVER
using Events;
using GameCore.Event;
using GameCore.PlayerAndUsers;
using GameCore.ServerChannelSystem;
using GameCore.ServerChannelSystem.Event;
using TriggerEncapsulation.Event;
using TriggerEncapsulation.Messaging;
public static class OpenGameInviteReceiverServer
{
public static void OnRegisterGameClass()
{
Game.OnGameInstanceInitialization += Initialize;
}
private static void Initialize()
{
var trigger = new Trigger<EventChannelMessage>(OnChannelMessageReceived, keepReference: true);
trigger.Register(Game.Instance);
var onConnected = new Trigger<EventPlayerUserConnected>(OnPlayerConnected, keepReference: true);
onConnected.Register(Game.Instance);
var onDisconnected = new Trigger<EventPlayerUserDisconnected>(OnPlayerDisconnected, keepReference: true);
onDisconnected.Register(Game.Instance);
}
private static async Task<bool> OnPlayerConnected(object sender, EventPlayerUserConnected eventArgs)
{
ServerChannelMessage.SubscribeUser(eventArgs.User.UserId, "open-game-join");
return await Task.FromResult(true);
}
private static async Task<bool> OnPlayerDisconnected(object sender, EventPlayerUserDisconnected eventArgs)
{
ServerChannelMessage.UnsubscribeUser(eventArgs.User.UserId, "open-game-join");
return await Task.FromResult(true);
}
private static async Task<bool> OnChannelMessageReceived(object sender, EventChannelMessage args)
{
if (!ServerChannelMessage.TryGetUserIdFromChannel(args.Channel, out var userId, out var subchannel) ||
subchannel != "open-game-join")
{
return false;
}
if (!ServerChannelMessage.TryParseJson<OpenGameServerChannelEnvelope>(args.Message, out var envelope) ||
envelope?.OpenGame is null)
{
return false;
}
var player = Player.AllPlayers.FirstOrDefault(p => p.User?.UserId == userId);
if (player is null)
{
return false;
}
return await MessageBuilder.Create(new NotifyOpenGameInviteMessage
{
FromUserId = envelope.FromUserId,
OpenGame = envelope.OpenGame
})
.WithPriority(MessagePriority.High)
.ToPlayer(player)
.SendAsync();
}
}
#endif目标客户端:收到 OpenGameRef 后调用 LeaveAndJoinOpenGame
csharp
#if CLIENT
using GameCore.LobbySystem;
using TriggerEncapsulation.Messaging;
public static class OpenGameInviteReceiverClient
{
[MessageHandler]
public static bool OnNotifyOpenGameInvite(
Player? sender,
NotifyOpenGameInviteMessage message)
{
if (message.OpenGame is null)
{
return false;
}
return LobbyService.LeaveAndJoinOpenGame(message.OpenGame);
}
}
#endif最佳实践
1. 把 OpenGameRef 当作短期有效的快照
OpenGameRef 适合即时分享和即时使用,不应长期缓存。目标局可能随时结束、满员或关闭加入。
2. 发起加入前给用户一个确认机会
如果你的项目有好友、队伍或邀请 UI,建议在真正调用 LeaveAndJoinOpenGame 前先让用户确认,而不是在拿到 OpenGameRef 后立即跳转。
3. 把 OpenGameRef 的分发视为你自己的项目逻辑
OpenGameRef 本身不负责做好友、队伍或跨服权限控制。谁可以拿到它、通过什么方式拿到它,都应该由你的项目逻辑决定。
4. 跨服分发和局内同步要分开设计
如果目标玩家在另一个游戏局里,通常至少会经过两段链路:
- 服务器 A → 服务器 B:跨服分发,适合用 跨服务器频道系统
- 服务器 B → 客户端 B:目标游戏局内部同步,适合用你自己的本局同步方案
不要把这两段链路混成同一种通信机制来设计。
5. 为失败结果提供 UI 反馈
加入请求失败时,通常要给用户明确提示,例如“目标游戏已结束”“目标游戏已满”或“当前不可加入”。
6. 让框架处理离开和槽位释放
客户端正常退出当前局时使用 LobbyService.QuitCurrentGame();服务端正常让玩家离开时使用 Player.Disconnect(),需要强制移出时使用 Player.Kick(...)。这些路径会按当前开放局规则清理房间占位和地图真人槽位。
普通网络断线仍可能保留重连名额,不能仅因为 Player.IsOnline 变为 false 就认为槽位已经空出。玩家放弃重连后,框架才会完成最终释放;未完成连接的进入预约也会在保护期结束后自动释放。完整的时间和状态表见 随进随出模式与开放局。
当前限制
- 当前公开的大厅 API 只覆盖“开放局加入”相关能力,不包含完整的好友、组队和邀请协议。
TryGetCurrentOpenGameRef目前只针对标准的开放游戏局生效。- quick start 局不会返回
OpenGameRef。 - 当前
LeaveAndJoinOpenGame仅覆盖普通GamePlay_Game类型,不包含 Lobby/Equipment 等其他局类型。