游戏服务器托管
最后更新:2026 年 9 月 15 日
游戏服务器是你编写的程序,用来持有一个或多个活动房间——即玩家真正进行游戏的权威实例。服务器 SDK 提供 Room 基类承载你的逻辑,以及一个 RoomHost,它运行你的房间、把它们注册到 PlayServ、用心跳维持存活,并在结束时下线。把玩家送到房间是客户端的事(匹配与房间、连接到游戏);本页讲的是拥有房间的那个服务器。
这是服 务器端 SDK(PlayServ.Sdk.Rooms),由你部署为游戏服务器的程序使用,而不是游戏客户端。它以服务器身份认证,因此不受面向客户端的表权限限制。游戏服务器既可以是平台替你启动的**托管(managed)实例,也可以是你自己运行的外部(external)**进程;两者用同一个 SDK。
游戏服务器的构成
两个类型完成工作:Room 的子类承载你的游戏,RoomHost 运行该类型的房间。
一个最小房间
你用自己的玩家类型和配置类型继承 Room<TPlayer, TConfig>。基类管理花名册(AddPlayer、RemovePlayer、Players),并暴露供你重写的生命周期钩子。
using PlayServ.Sdk.Rooms;
public sealed class MyPlayer : RoomPlayer { }
public sealed record MyConfig;
public sealed class MyRoom : Room<MyPlayer, MyConfig>
{
protected override void OnPlayerJoined(MyPlayer p) { /* 安排座位 */ }
protected override void OnPlayerLeft(MyPlayer p) { /* 释放座位 */ }
protected override void OnTick(float dt) { /* 推进房间 */ }
}
RoomHost 通过一个工厂运行该类型的房间:
var host = new RoomHost<MyRoom, MyPlayer, MyConfig>(name => new MyRoom());
var room = host.GetOrCreate("lobby-1");
这已经是一个可用的权威服务器:host 会接好心跳、注册房间、应用从平台拉取的配置,并为你管理房间生命周期。
自注册与心跳
你不需要用单独的调用把房间告知平台——host 会自动注册每个房间,并以周期性心跳保持其最新,上报房间的占用与容量。这正是客户端房间浏览器所读取的信号,因此房间一运行就可被发现,而在服务器停止为其发送心跳后不久便会从浏览中消失。
房间配置由平台所有
房间设置——容量、预订有效期、房间生命周期、空闲超时,以及每服务器的 max_rooms 配额——都由平台所有、在 Backoffice 中编辑,而不是随代码或清单发布。host 会把实时配置拉下来并随其变化应用,因此运营者无需重新部署即可重新调优某个房间类型。
// 容量与上限来自平台,而非构造函数——
// 从房间读取它们,而不要假设你在代码里设的值
由于配置是拉取的,在 Backoffice 改容量或某个超时,会在一个心跳内对运行中的服务器生效。不要把这些值写死;把平台下发的当作事实来源。
上报房间在哪
客户端需要知道如何抵达房间。你的服务器用 DeclareConnect 声明它的连接信息——主机、端口、传输,以及可选的连接串与区域——平台会在客户端的预订中把它们返回给客户端。
DeclareConnect(new RoomConnect { Host = publicHost, Port = port, Transport = "udp" }, region: "eu");
当平台替你启动了服务器(托管实例)时,连接信息可以自动从编排器的环境填入;当你运行自己的进程时,由你来声明。
房间的生命周期
房间会经历一个小而明确的生命周期。大部分由 host 驱动;你通过钩子和少量声明来影响它。
| 阶段 | 发生了什么 |
|---|---|
| 开启 | 房间接受新玩家;浏览器可以展示它。 |
| 占用 | 玩家进出;OnPlayerJoined / OnPlayerLeft 触发;心跳上报实时人数。 |
| 空闲 | 所有人都离开了。OnIdle 触发;你可以暂停模拟,或让清理逻辑将房间下线。 |
| 排空 | 临近生命周期末尾,房间停止接纳新玩家,同时让现有玩家打完。 |
| 关闭 | 房间被下线。关闭会将房间标记为已关闭,而非删除,并作废指向它的预订票据。 |
有两项清理无需你做任何事:空闲清理下线无人房间,生命周期清理强制房间的最大存续时长。两者都遵循上面平台所有的配置。
掉线的玩家会在短暂的重连宽限内保留座位,之后房间才会将其视为离开,这样一次网络抖动不会让玩家丢掉位置。用 DeclareReconnect(...) 声明它。
接纳玩家
玩家携带着从客户端 SDK 拿到的预订抵达你的服务器。host 在玩家算作在场之前校验该预订:它必须有效、未被使用,且是发给这个房间的。只有这样 OnPlayerJoined 才会触发。
- 有效预订 → 玩家入座;
OnPlayerJoined(player)运行。 - 缺失、过期、房间错误或已被使用的预订 → 进入被拒绝。
这就是为什么客户端与服务器是一个系统的两半:客户端预订并连接,而你编写的 host 兑换预订。你无需手动校验票据——host 会做——但你要决定 OnPlayerJoined / OnPlayerLeft 时 发生什么:分配出生点、把玩家加入你广播的花名册、启动他们的计时器。
服务器在哪里运行
游戏服务器可以以几种方式运行,平台会为每个房间显示是哪种:
- 托管(Managed)——平台通过托管编排器替你启动服务器实例(PlayServ 集成了 Edgegap)。编排器令牌是你自己工作室的,按项目和环境保存。
- 外部(External)——你自己运行服务器(你自己的机群或机器)。你用
playserv functions declare把它作为游戏服务器声明给 PlayServ,它像其他服务器一样注册自己的房间;它永远不需要作为云函数部署。 - 池化(Pooled)——从预置的池中提供的房间。
无论哪种,房间模型都相同:注册、心跳、接受预订、上报在场。
容量与启动配额
有两个不同的限制。房间有容量——其中的座位,来自平台配置。游戏服务器有房间配额(max_rooms)——同时可运行多少房间。达到配额时,平台会在任何付费的编排器调用之前拒绝再启动房间,因此失控不会变成账单。
环境
托管服务器通过 PLAYSERV_* 环境变量获得它所需的东西——最重要的是 PLAYSERV_DEPLOYMENT_TOKEN,SDK 用它换取一个会话来与平台通信。托管启动时你无需手动设置这些;对于外部服务器,你提供对应项,好让 SDK 能认证并注册。