RUST - 多人游戏后台深度解析

RUST - 多人游戏后台深度解析

关键洞察

关键洞察

关键洞察

  • 审查你的联网中间件:Rust 采用 RakNet 的经历表明,对一个成熟库进行开源分支并不代表没有隐藏漏洞。要验证你实际发布的具体版本,而不只是库的声誉。

  • 一个代码库,两个目标:Facepunch 使用预处理器标志,从同一个共享代码库编译服务器和客户端,消除了两者之间的偏差,并降低了长期维护开销。

  • 尽早投入编辑器测试:为 Unity 编辑器构建一个监听服务器模式,消除了繁琐的多步骤测试流程,并让网络代码迭代速度大幅提升——这一原则同样适用于任何多人项目。

  • 部署基础设施不是可选项:当 Rust 爆红时,团队不得不在压力下重建整个构建流水线;这段经历直接说明,应在发布前而不是之后自动化部署。

  • PVS 剔除让大型世界保持可扩展性:Rust 基于网格的可见性系统确保玩家只会收到附近单元格的状态更新,使带宽和 CPU 成本与局部玩家密度成正比,而不是与服务器总规模成正比。

Garry Newman,Facepunch Studios 的创始人,在他个人博客 garry.net 上的一系列文章中记录了 Rust 的网络和后端架构。这些文章写于 2013 年至 2016 年之间——当时游戏正处于动荡的早期开发阶段,并意外地在病毒式传播中上线——内容涵盖了网络栈的选择、服务器/客户端代码架构、编辑器测试工作流,以及必须几乎在一夜之间重建的部署流水线。最相关的两篇文章是“Rust 的网络”和“Rust 后端”,并辅以 Facepunch 同期的官方开发博客条目。

间接地,它们揭示了任何多人游戏工作室——从小型独立团队到不断壮大的持续运营游戏——迟早都会面临的决策。

选择网络库(以及盲目信任它)

当 Newman 在构建 Rust 的网络层时,他选择了 RakNet。“Raknet 是一个久经考验的网络解决方案,”他写道,并提到它在已发售作品中有着漫长的使用记录。Oculus 收购了它并将其开源,这在当时看起来是个好消息。

但事实并非如此。

Newman 注意到公共仓库中的错误,并怀疑与支撑那些已发售游戏的内部版本相比,这个开源版本“肯定被删掉了一大堆东西”。一个在业界口碑很强的库,以社区版本的形式出现时却处于不确定的状态。

真正的经验教训不是要避免第三方网络库,而是要根据你实际要发布的那个具体版本,以及你的真实使用场景,去验证它,而不是依赖这个库的历史声誉。

开源的商业工具分支值得额外审查。Newman 自己也提到,最终会用 Valve 的 Steam Networking Sockets 取代 RakNet,这提醒我们:从一开始就应把网络栈视为可替换的组件。

还值得注意的是,这篇文章写于 2016 年。此后,局面已经发生了很大变化。尤其是 Unity 开发者如今可以从多种易于获取、维护良好的 netcode 方案中选择。想了解当前可用方案,Edgegap 关于 Unity 网络代码解决方案 的指南就是一个不错的起点。

在服务器和客户端之间共享代码

Rust 中更具持久性的架构决策之一,是把服务器和客户端视为同一套代码的两个编译目标,而不是两套独立代码库。代码库会在定义了 SERVER 或 CLIENT 的情况下编译,某一侧特有的逻辑则由这些预处理器标志来隔离。Newman 借用了 Valve 的 Source 引擎中的这种模式,他也是在那里成长为开发者的。

这绕开了一个常见痛点:两套代码库逐渐分化,随着游戏规模扩大而慢慢背离。共享系统保持共享。错误修复会同时传播到两个目标。对“一个游戏,两种视图”的心智模型,和多人系统实际的工作方式非常契合。

代价是需要纪律。

编写代码时必须始终清楚它运行在哪种上下文中。边缘情况需要特别小心:例如在编辑器监听服务器模式下,一个物理对象会同时以服务器端和客户端两种形式存在,必须防止它们彼此交互。这些问题是可以解决的,但它们不会自己解决。

监听服务器:消除测试摩擦

Rust 最初用于测试网络代码的开发工作流,正如 Newman 所说,是“他妈的噩梦”。测试任何改动都需要编译一个独立的服务器构建,先在后台启动它,然后从编辑器客户端连接进去。它深深嵌在旧架构里,几乎无法轻易更改,这也是团队决定把游戏从头重做的主要原因之一。

在重建后的版本中,Unity 编辑器对监听服务器的支持从第一天起就被列为高优先级。解决方案是同时定义 SERVER 和 CLIENT,让编辑器在没有任何外部进程的情况下既托管又加入自己的游戏。Newman 特别指出,实现必须忠实模拟真实网络。那些让服务器和客户端直接共享变量的捷径,会掩盖只有在真实网络构建中才会暴露的 bug。

即使具体工具已经更新,这一基本原则仍然成立。如今,为本地测试编译并部署专用服务器,比 2013 年时容易得多。容器化和专用插件已经吸收了大部分痛苦。Edgegap 的 Unity 插件 包含一个构建工具,即使是大型项目,也能把专用服务器编译缩短到几分钟;而 Unreal Docker 扩展 则完全绕过了从源代码构建 Unreal 的过程,把原本需要 数小时的流程压缩到大约 8 分钟。目标与 Newman 的监听服务器相同:尽快把一个可工作的服务器摆到开发者面前,让 netcode 错误在迭代过程中而不是上线后暴露出来。

在你需要之前就构建好部署流水线

Rust 在 2013 年末走红后,团队花了一周时间从零开始重建整个部署流程。“此前,部署新版本又慢又充满 bug,”Newman 写道。构建包是在“某个家伙的电脑上”编译的,整个过程“像一场折磨”。

替代系统使用 Jenkins CI/CD,并由 SVN 提交自动触发。服务器和客户端分别有独立任务并行运行,将构建时间从大约 40 分钟缩短到不到 10 分钟。资源被拆分成 60 多个包,每个都控制在 5 MB 以下,这样浏览器就能像缓存图片一样缓存它们。每个包都使用 CRC 哈希命名,因此只有在内容真正发生变化时,文件才会触发重新下载,而不是每次部署都重新下载。正如 Newman 所解释的,“我们不想让玩家每次都得下载 300MB。”

系统奏效了。但它是在仓促中、在压力之下、在一次没人完全预料到的病毒式上线之后被搭建起来的。

Newman 对“之前状态”的描述和“之后状态”一样有启发性。构建包是在某个开发者的个人机器上编译的,整个过程就是一场折磨。取而代之的工具很简单:Jenkins、SVN、S3、CRC 哈希。没什么花哨的。真正起作用的是把它们部署好并自动运行起来,这样团队就能以局势所要求的速度发布更新。

PVS 剔除:只发送玩家看得见的内容

Facepunch 早期开发博客中最具技术启发性的一篇,描述了将潜在可见集(PVS)剔除系统引入 Rust 网络代码的过程。这个方法基于网格:世界被划分为若干单元,每个玩家只接收自己所在单元及周边单元的状态更新。

概念很直接。实现却不是。你需要追踪在单元之间移动的对象,向每个受影响玩家发送进入和离开通知,处理玩家一次跨越多个单元的情况,并管理实体移动时产生的各种边缘情况。正如 Newman 在开发博客中所写,“有很多需要协调的环节”。

收益非常显著。如果没有可见性剔除,服务器向每个已连接玩家广播完整世界状态,很快就会碰到带宽和 CPU 上限。PVS 让这些成本与本地密度成正比,而不是与服务器总人口成正比。这正是大型开放世界生存游戏能够在规模上可行的原因,而且这种技术的适用范围远不止 Rust 所属的特定类型。任何拥有大型可探索世界的多人游戏,在考虑更奇特的优化之前,都应该先有某种形式的状态相关性过滤。

本文基于并引用了 Garry Newman 在 garry.net 上发布的原始博客文章(https://garry.net/posts/rusts-networkinghttps://garry.net/posts/the-rust-backend)以及 Facepunch 官方 Rust 开发博客(https://rust.facepunch.com/news/friday-devblog-6)。

原始内容的所有权利归其各自所有者所有。

书写者

Edgegap团队

Get your Game Online Easily & in Minutes

立即开始集成!

轻松在线游戏
且在几分钟内