返回博客

构建零浪费服务器:Fortnite 服务器优化休眠提案深度分析

发布于 2026年2月25日
构建零浪费服务器:Fortnite 服务器优化休眠提案深度分析

每一位 Multiplayer 开发者最终都会撞上同一堵财务之墙:你的服务器基础设施正在为了模拟真空空间而烧钱。当你为大型 Battle Royale、生存游戏或 MMO 启动 Dedicated Server 时,CPU 周期严重偏向于闲置计算。你正在支付高昂的 Cloud Computing 费用,去计算没人看的石头的重力,处理没有目标的敌人的 AI 导航,并在完全没有玩家活动的区域维持 World States。

最近,Unreal Engine 社区出现了一份引人注目的技术提案,直指 Epic Games 领导层。其核心论点是?Fortnite 的巨大规模要求从中心化的高维护托管模式转型为“Zero-Waste Infrastructure”模式。作者认为,通过消除模拟浪费,Epic 可以减少 60-70% 的 Operating Expenses (Opex),理论上可以将高级货币的价格削减至 1,000 V-Bucks 仅售 1.99 美元。

虽然 V-Buck 定价的经济可行性是变现设计师讨论的话题,但该提案的技术支柱——Sector Physics Hibernation (SGH)——则是现代服务器架构的典范。

在这篇行业分析中,我们将拆解 Fortnite 服务器优化休眠提案的机制,探索 Logic-Side Culling 在 Unreal Engine 5 中是如何工作的,并演示如何在你的 Multiplayer 作品中实现零浪费基础设施。

模拟浪费的数学逻辑

要理解为什么 Sector Physics Hibernation 是必要的,我们必须看看 Dedicated Server 残酷的数学逻辑。

以一个标准的 100km² Battle Royale 地图为例。比赛开始时,100 名玩家降落在各个兴趣点。在最初的 5 分钟内,50% 的玩家被淘汰,幸存的玩家向不断缩小的 Safe Zone 汇聚。

到第 10 分钟时,地图总面积的 70% 以上已经没有活跃玩家。然而,在标准的 Authoritative Server 设置中,Dedicated Server 仍以 30Hz 的频率持续 Tick 整个世界状态。

  • Physics Calculations: Rigid Bodies、可破坏环境和弹道仍在内存中被追踪。
  • Actor Ticking: 数以千计的 AActor 实例每秒调用 30 次 Tick() 函数。
  • NavMesh Processing: 游荡的 AI 或动态障碍物继续查询 Navigation Mesh。

如果你在 AWS c5.2xlarge 实例上运行服务器,每台机器每小时大约需要支付 0.34 美元。如果一台机器因为 CPU 忙于计算真空空间而只能托管两个 100 人的游戏实例,那么你的规模将受到严重瓶颈限制。

该提案建议,通过回收这些浪费的 CPU 开销,开发者可以将 5-6 个游戏实例打包到同一台硬件上(从而削减约 60% 的服务器账单),或者将回收的处理能力用于将全局服务器 Tick Rate 从 30Hz 提升到 60Hz+,确保完美的 Hit Registration 和丝滑的游戏体验。

深度探索:UE5 中的 Sector Physics Hibernation

提议的技术解决方案依赖于利用 Unreal Engine 5 现有的 World Partition 系统,但将其主要用途从客户端内存管理转变为服务器端 CPU 管理。

默认 Dedicated Server 的问题

默认情况下,UE5 的 World Partition 根据与流送源(玩家摄像机)的距离为客户端流进和流出单元格。这对于保持客户端低内存占用和高帧率非常有效。

然而,Dedicated Server 通常将整个地图加载到内存中以维持权威性。如果狙击手穿过山谷射出一颗子弹,或者触发了全局事件,服务器需要碰撞数据和 Actor States 随时可用以验证动作。在服务器上从磁盘动态加载和卸载数据(Level Streaming)通常太慢且会导致严重卡顿,从而破坏 Tick Rate。

SGH 解决方案:Logic-Side Culling

Sector Physics Hibernation 建议使用 CPU-Sleep States,而不是从内存中卸载扇区(这会导致 IO 瓶颈)。

扇区保留在 RAM 中,但所有的 Tick、物理计算和状态更新都会被强制暂停。当一个扇区的 Spatial Grid 单元格检测到零活跃实体(玩家、玩家拥有的载具或活跃投射物)时,服务器会暂停该特定网格的 CPU 分配。

在 C++ 中实现休眠管理器

要在 Unreal Engine 中构建此功能,你需要一个子系统来监控 Spatial Grid 单元格并动态切换其中 Actor 的 Tick 状态。下面是一个如何实现 SectorHibernationManager 的简化架构原型。

#include "SectorHibernationManager.h"
#include "EngineUtils.h"
#include "GameFramework/Actor.h"
#include "GameFramework/PlayerController.h"

void USectorHibernationManager::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    HibernationCheckInterval = 2.0f; // 每 2 秒检查一次
    TimeSinceLastCheck = 0.0f;
    GridCellSize = 10000.0f; // 100 米网格单元
}

void USectorHibernationManager::Tick(float DeltaTime)
{
    TimeSinceLastCheck += DeltaTime;
    if (TimeSinceLastCheck >= HibernationCheckInterval)
    { 
        EvaluateSectors();
        TimeSinceLastCheck = 0.0f;
    }
}

void USectorHibernationManager::EvaluateSectors()
{
    UWorld* World = GetWorld();
    if (!World) return;

    // 步骤 1:将活跃玩家位置映射到网格单元
    TSet<FIntVector> ActiveCells;
    for (FConstPlayerControllerIterator Iterator = World->GetPlayerControllerIterator(); Iterator; ++Iterator)
    { 
        APlayerController* PC = Iterator->Get();
        if (PC && PC->GetPawn())
        { 
            FVector PlayerPos = PC->GetPawn()->GetActorLocation();
            FIntVector CellCoord = FIntVector(
                FMath::FloorToInt(PlayerPos.X / GridCellSize),
                FMath::FloorToInt(PlayerPos.Y / GridCellSize),
                FMath::FloorToInt(PlayerPos.Z / GridCellSize)
            );
            
            // 将此单元格及相邻单元格标记为活跃(缓冲区)
            MarkAdjacentCellsActive(CellCoord, ActiveCells);
        }
    }

    // 步骤 2:遍历可休眠的 Actor 并切换 Tick
    for (TActorIterator<AActor> ActorItr(World); ActorItr; ++ActorItr)
    { 
        AActor* Actor = *ActorItr;
        
        // 跳过核心基础设施 Actor
        if (!Actor->ActorHasTag(FName("Hibernatable"))) continue;

        FVector ActorPos = Actor->GetActorLocation();
        FIntVector ActorCell = FIntVector(
            FMath::FloorToInt(ActorPos.X / GridCellSize),
            FMath::FloorToInt(ActorPos.Y / GridCellSize),
            FMath::FloorToInt(ActorPos.Z / GridCellSize)
        );

        bool bShouldBeActive = ActiveCells.Contains(ActorCell);
        
        if (bShouldBeActive && !Actor->IsActorTickEnabled())
        { 
            // 唤醒
            Actor->SetActorTickEnabled(true);
            Actor->SetActorEnableCollision(true);
        }
        else if (!bShouldBeActive && Actor->IsActorTickEnabled())
        { 
            // 进入睡眠
            Actor->SetActorTickEnabled(false);
            // 可选:将碰撞降级为仅简单查询以节省物理线程时间
            Actor->SetActorEnableCollision(false); 
        }
    }
}

“唤醒”阶段的复杂性

上面的代码说明了核心概念,但真正的工程挑战在于唤醒阶段。如果玩家向一个处于睡眠状态的扇区发射高速狙击步枪子弹,投射物将在 2 秒的评估循环捕捉到它之前穿过边界。

如果扇区在子弹到达 之后 才唤醒,你将经历灾难性的 Desync。子弹可能会直接穿过一个正在休眠的载具,因为它的碰撞被禁用了。这种现象与我们在 The Unreal Engine Multiplayer Sync Bug Ruining Your World States And How To Fix It 指南中详述的问题密切相关,即服务器状态与客户端预测之间的时间差异会完全破坏模拟。

为了解决这个问题,零浪费基础设施需要 Predictive Wake-Ups。服务器不仅要追踪玩家位置,还必须计算所有活跃投射物和高速载具的前向速度矢量。如果矢量与睡眠中的网格单元相交,服务器必须在物体到达之前立即强制该特定单元格执行唤醒事件。

大规模编排零浪费服务器

在游戏引擎内部实现 Logic-Side Culling 只是成功了一半。另一半是基础设施编排。

如果你的 UE5 Dedicated Server 成功动态减少了 60% 的 CPU 占用,你的服务器托管环境需要足够智能,能够识别出资源使用的下降,并将新的游戏实例打包到同一个硬件节点上。

自行构建这种编排需要大量的 DevOps 工程工作。你需要部署 Kubernetes 集群,为游戏服务器生命周期管理配置 Agones,根据 CPU 利用率编写自定义缩放指标,并管理 Load Balancers 以将玩家路由到正确的实例。这很容易耗费 4-6 个月的专门基础设施工作——这些时间本可以直接用于开发游戏。

通过 horizOn,这些后端编排服务已预先配置好。该平台处理动态实例打包、基于实时服务器负载的 Auto-Scaling 以及 Dedicated Server 构建的自动化部署流水线。通过让专业的 Backend-as-a-Service 处理基础设施,你可以专注于发布你的 Multiplayer 游戏,而不是花半年时间与 Kubernetes 配置文件作斗争。

此外,当你将更多实例打包到单个节点时,会增加 Noisy Neighbor 问题影响网络线程的风险。保护你的 Netcode 免受这些瓶颈的影响至关重要,我们在 The Uefn Server Performance Exploit Explained Hard Armoring Your Unreal Engine Netcode 中广泛讨论了这一话题。

零浪费 Multiplayer 架构的最佳实践

无论你是构建 100 人的 Battle Royale 还是持久的开放世界生存游戏,实现休眠和零浪费技术都需要严格的架构纪律。以下是五个经过实战检验的最佳实践,可确保你的服务器 Opex 保持在低水平,同时不牺牲玩家体验:

1. 将 Game State 与 Tick 循环解耦

服务器性能的最大敌人是持续的数据轮询。永远不要使用 Tick() 来检查事件是否应该发生。完全转向 Event-Driven Architecture。如果营火需要在 5 分钟后熄灭,不要每一帧都 Tick 去减去时间。设置一个在 300 秒后仅触发一次的 Timer Delegate。这允许营火 Actor 在 4 分 59 秒内保持完全睡眠状态。

2. 实现激进的 NetCullDistanceSquared

Unreal Engine 根据 NetCullDistanceSquared 决定将哪些 Actor 同步给哪些客户端。许多开发者将其保留为默认值,迫使服务器为距离玩家数百米外的 Actor 序列化和压缩数据。审计你的裁剪距离。掉落的武器不需要在 5,000 个单位(50 米)之外进行同步。计算游戏循环所需的绝对最小半径并严格执行。

3. 使用 Spatial Hash Grids 进行 O(1) 查找

在计算哪些 Actor 应该进入睡眠时,如果你有 100,000 个实体,遍历世界中的每个 Actor (TActorIterator) 本身就会成为瓶颈。实现一个 Spatial Hash Grid。当 Actor 移动时,它会更新其在哈希映射中的位置。这允许你的休眠管理器以 O(1) 时间复杂度查询“网格单元 X 中有什么?”,使休眠评估对 CPU 而言几乎是免费的。

4. 利用 Buffer Zones 实现无缝唤醒

永远不要在玩家视野边缘直接休眠扇区。始终在任何活跃实体周围保持至少一个网格单元宽的活跃扇区“缓冲区”。如果你的网格单元宽 100 米,玩家在单元格 A 中,那么所有相邻单元格(3x3 网格)必须保持完全活跃。这保证了如果玩家突然冲过边界,目标单元格已经完全初始化并正在 Tick。

5. 定期对 Dedicated Server 构建进行 Profile

不要猜测是什么在消耗你的 CPU。在带有模拟负载的打包 Dedicated Server 环境中使用 Unreal Insights。特别关注 GameThread 的耗时。如果你在玩家静止时看到 PhysicsTickTime 占据了线程图的主导地位,那么你的休眠逻辑就失效了。遥测是验证你的零浪费架构在现实中(而非仅在理论上)运行的唯一方法。

服务器 Opex 的未来

Fortnite 社区的提案揭示了一个关键事实:目前行业通过昂贵的 Cloud Compute 暴力提升服务器性能的标准是不可持续的。随着世界变得越来越大,玩家数量不断增加,基础设施成本的线性增长将慢慢榨干 Live-Ops 预算。

Sector Physics Hibernation、Logic-Side Culling 和动态实例打包不再只是 AAA 工作室的优化手段;它们是各种规模的 Multiplayer 游戏的生存要求。通过在开发周期早期采用零浪费思维,你可以确保游戏的盈利能力随玩家基数的增长而同步增长。

如果你准备在没有 DevOps 烦恼的情况下实现动态服务器缩放,请免费试用 horizOn 或查看 API 文档,了解 Multiplayer 基础设施可以变得多么无缝。


来源:[Technical Proposal] Unified Operational Sovereignty: Decoupling Opex to Enable a $1.99 V-Buck Economy