游戏系统
和客户端一样,服务器 SDK 也提供系统:暴露一个 tick 方法、在服务器自己的更新循环上运行的类。凡是应该在服务器上持续运行、不依赖某一次 RPC 调用的逻辑都适合放在这里。计时器、周期性的世界状态检查,以及根据在线玩家数量调整的缩放规则,都属于这一类。
声明系统
服务器端系统继承 ModSystemBase 并重写 OnUpdate:
public class MySystem : ModSystemBase
{
protected override void OnUpdate(UpdateTick tick)
{
// 每个服务器 tick 都会执行
}
}
传给 OnUpdate 的 UpdateTick 提供 deltaTime(距上一个 tick 的秒数)和 time(服务器启动以来的秒数)。
与客户端系统不同,服务器端系统不会被自动发现。请在模组的 Init 方法中逐个显式注册:
protected override void Init()
{
Services.RegisterSystem<MySystem>();
}
你可以通过构造函数注入依赖项,例如 EcsApi 或日志记录器,方式和从依赖注入容器解析的任何其他服务一样。
public class BountySystem(EcsApi ecs, ILogger logger) : ModSystemBase
{
protected override void OnUpdate(UpdateTick tick)
{
ecs.Query<BountyComponent>((ref bounty) =>
{
bounty.Multiplier = 1f + bounty.Kills * 0.05f;
});
}
}
响应玩家数量
用系统而不是 RPC 处理程序,最常见的理由是:答案取决于整个世界的状态,而不是某个客户端的请求。按在线玩家数量缩放 Boss 血量就是一个已发布的例子,取自联机合作模组:
public class ScaleHpSystem(EcsApi ecs, ILogger logger) : ModSystemBase
{
// 由 RPC 处理程序在网络线程上写入,在这里由服务器循环读取
public int ScalingPercent { get { ... } set { ... } }
protected override void OnUpdate(UpdateTick tick)
{
// 统计服务器上的所有玩家,而不只是本区域内的玩家
var players = 0;
ecs.Query<MainCharacterComponent, int>(ref players, static (ref _, ref players) => { players++; });
if (players == 0) return;
var targetScalingPercent = ScalingPercent * players;
ecs.Query<TamerComponent, HpComponent>((ref tamer, ref hp) =>
{
if (!tamer.IsBossOrElite)
return; // 不缩放普通敌人
if (hp is { HpMaxBase: 0, Hp: 0 })
return; // 尚未生成的怪物没有可缩放的 HP
var currentMultPercent = hp.HpMaxMulPercent == 0 ? 100 : hp.HpMaxMulPercent;
if (targetScalingPercent != currentMultPercent)
{
hp.Hp *= (float)targetScalingPercent / currentMultPercent;
hp.HpMaxMulPercent = targetScalingPercent;
}
});
}
}
请留意这个系统的写法:它每 tick 读取世界状态,与自己已经应用过的值比较,只有两者不一致时才写入。无条件写入的系统会在每个 tick 向每个客户端发送一次组件增量,这会立刻体现为网络统计里的出站流量。
系统与 RPC 处理程序配合
RegisterSystem<T> 同时会让该系统可以从容器解析,因此 RPC 处理程序可以把它作为构造函数依赖项。面向客户端的设置就是这样传到系统里的:处理程序负责校验并保存,系统负责执行。
protected override void Init()
{
Services.RegisterSystem<ScaleHpSystem>();
Services.RegisterSingleton<RpcHandlers>();
}
[ServerRpcFor(typeof(RpcContracts))]
public partial class RpcHandlers(ScaleHpSystem hpScaling) : ServerRpcHandlersBase
{
partial void OnScaleBossHp(RpcContext context, int scalingPercent)
{
hpScaling.ScalingPercent = scalingPercent;
}
}
RPC 处理程序运行在网络线程上,而系统在服务器循环上 tick。凡是处理程序写入、系统读取的字段都应当视为共享状态:把它限制为一个可以安全地被读到中间值的单一数值,或者用 Volatile 访问器或锁保护它。
系统的开销
你的模组注册的每个系统都在服务器的系统根更新中运行,而这正是网络统计里 服务器 tick 耗时 图表所测量的对象。一个用定时器扫描全部实体、而不是对变化作出反应的系统,通常就是 p99 上升而 p50 平稳的原因。要判断是不是你的系统在吃掉 tick 时间,那一页是最快的办法。