为MMORPG添加离线模式和自定义服务器

Lobsters Hottest 新闻

摘要

一位开发者详细介绍了为其自制MMORPG Trolddom添加离线模式和自定义服务器支持所面临的技术挑战,灵感来源于Stop Killing Games运动,涵盖了架构和实现方面。

<p><a href="https://lobste.rs/s/ng3xwc/adding_offline_mode_custom_servers">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/31 22:24

# 为 MMORPG 添加离线模式和自定义服务器 来源:https://plantbasedgames.io/blog/posts/09-adding-offline-mode-and-custom-servers-to-an-mmorpg/ 最近,**Stop Killing Games** 运动获得了大量关注,也引发了许多争论(以及口水战)。虽然我并没有计划关闭我自制的 MMORPG *Trolddom*,但它启发我去研究如何让它“防关闭”——通过添加离线模式和支持自定义服务器。当时看来,这就像是一个有趣的小型夏日插曲项目。在这篇博客中,我将详细介绍最终达成目标所面临的挑战。 ## 我们自家的 WoW 先明确一下定义:所谓 *离线模式*,是指在没有联网的情况下,像单机游戏一样在本地 PC 上运行游戏。而 *自定义服务器*,则是指允许玩家自行运行整个服务器环境。 ## 先生,这可是个 MMORPG⌗ (https://plantbasedgames.io/blog/posts/09-adding-offline-mode-and-custom-servers-to-an-mmorpg/#sir-this-is-an-mmorpg) 如今,大多数多人游戏似乎都围绕着 *在线服务* 商业模式来设计。即使是那些本质上以短时多人对战为核心的游戏,现在也常常被做成强制在线游戏,不支持玩家自建服务器。这种情况已经持续很久了,因为在竞争日益激烈的游戏行业中,企业不断寻找盈利方式。随着这么多游戏与在线服务紧密绑定,许多玩家对整个局面感到相当失望。不过,我不想浪费宝贵的博客篇幅来过多抱怨这个话题,毕竟似乎已经被人说烂了。我的观点其实无关紧要。对我来说,这更多是一种技术挑战,触发了我“写一堆代码看看会怎样”的本能。 但为什么要在 MMORPG 中加上离线模式甚至自定义服务器呢?大多数人可能都会同意,MMORPG 正是那种强制在线才合理的一个游戏类型。从商业角度来看,很容易认为这是个相当愚蠢的主意。幸运(或不幸)的是,我并不太受“是否愚蠢”这种观念的束缚,所以让我们直接开始吧。 ## Trolddom 的架构⌗ (https://plantbasedgames.io/blog/posts/09-adding-offline-mode-and-custom-servers-to-an-mmorpg/#architecture-of-trolddom) 我从未打算让 Trolddom 能够“脱离在线服务”,因此在搭建整体结构时并未为此做准备。显然,这也适用于当今大多数在线游戏。根据各种因素,为现有在线游戏添加这些功能可能相当容易、几乎不可能,或者介于两者之间。我最初的假设是,它会更接近“容易”的那一端,主要是因为毕竟这只是个单人项目,复杂度远低于某些疯狂的 AAA 游戏。但结果通常比我想象的更复杂。 为了提供一个结构化的概述,我们先从 *在线服务* 的角度来看游戏的整体架构。 ### 微服务 整个流程最好通过(简化的)分步说明来解释:玩家(客户端)如何启动游戏并进入世界: 1. **客户端** 从 **Steam** 获取一个 *已加密的应用程序票据*。它包含玩家的唯一 *Steam ID*,并证明玩家拥有该游戏。 2. **客户端** 连接到 **网关**,使用 *已加密的应用程序票据* 进行身份验证。网关会解密票据,检查其有效性,并提取 *Steam ID*。然后它查询 **PostgreSQL** 数据库中的账户。如果账户不存在,则创建它。**网关** 创建一个加密的 *身份验证令牌*,其中包含与账户关联的 *账户 ID*,并将其返回给客户端。这个 *身份验证令牌* 用于客户端与其他微服务进行身份验证。 3. **客户端** 从 **网关** 获取领域列表,并展示给用户(如果只有一个领域,则自动选中)。 4. 当玩家选择了领域后,**客户端** 连接到托管该领域的 **领域** 服务。领域会话管理所有已连接客户端的列表。 5. 如果领域会话尚未激活,**领域** 会通过 **锁** 对领域数据施加锁定,然后从 **Blob** 加载数据。这些领域数据包含领域的持久状态,例如神祇的势力排名等。 6. **领域** 会在成功连接后通知 **客户端**(如果存在队列,可能不会立即通知)。 7. **客户端** 随后向 **网关** 请求与当前领域关联的角色列表。玩家可以看到选择或管理角色的选项。**客户端** 还会连接到 **消息** 交换中心,允许客户端之间传递异步消息(例如邀请)。 8. 玩家选择一个角色,这将使 **客户端** 向 **网关** 请求一个 *角色令牌*。该令牌包含关于角色的信息,例如当前地图和唯一的 *角色 ID*。 9. *角色令牌* 被提交给 **领域** 作为生成请求。服务器会尝试确定要将玩家分配到的请求地图的哪个实例。如果没有可用实例,或所有实例都已满载,则会在合适的 **游戏** 服务器上创建一个新实例。 10. **客户端** 连接到分配到的 **游戏** 服务器,并告知它想要的实例。为玩家创建一个会话。 11. 如果该实例当前未在 **游戏** 上激活且需要创建,服务器会先通过 **锁** 锁定实例数据,然后从 **Blob** 获取数据。这些实例数据包含持久信息,例如地下城的哪些首领已死亡。 12. **游戏** 通过 **锁** 锁定角色数据,并从 **Blob** 获取数据。 13. **客户端** 开始从 **游戏** 接收游戏更新。在玩家连接期间,服务器会定期将更新的角色数据保存回 **Blob**。 14. 当玩家登出时,**游戏** 会确保最新的角色数据被保存,然后通过 **锁** 解锁。 哇,即使大大简化,这也是一大堆内容。我还省略了一些用于监控和日志记录的微服务。主要结论是:我们有一堆微服务,以及遍布各处的连接。**Blob** 和 **Lock** 分别封装了 Jelly (https://github.com/demogorgon1/jelly) 的 blob 节点和锁节点。我之前在早些的博客文章 (https://plantbasedgames.io/blog/posts/01-mmorpg-data-storage-part-one/) 中详细讲述过数据存储。**Group** 处理玩家队伍、公会、团队等的持久状态。 如今使用微服务似乎是默认做法,但对于像 Trolddom 这样的游戏来说,很容易被认为是过度工程。另一种选择是只用一个不可扩展的单一服务器来处理一切,在同样的托管成本下,它仍然可以处理数千名并发玩家。不用说,我从未让这么多玩家同时在线过。不过,我喜欢用 *正确的方式* 做事,所以我把架构设计得相当可扩展。主要的限制因素是同一开放世界地图(或实例)上的并发玩家数量,这与其说是技术限制,不如说是游戏玩法限制。当一百名玩家试图争夺同一个怪物刷新点时,体验会很糟糕。 ## 微服务管理⌗ (https://plantbasedgames.io/blog/posts/09-adding-offline-mode-and-custom-servers-to-an-mmorpg/#microservice-management) 微服务究竟指什么?通常,一个微服务对应运行在云中某台虚拟机上的一个进程。通常你会在同一台虚拟机上运行多个微服务。如果你的玩家人数不多,你可能可以把所有东西都运行在同一台虚拟机上。后来,如果你足够幸运,玩家过多,你可以启动更多虚拟机并分散你的微服务。你可能还想运行同一个微服务的多个实例(例如更多的 **游戏** 服务器)。这都很好,但在部署微服务时,如果同一台机器上有大量不同的进程,可能会很麻烦。如果只有一个进程,事情会简单得多,而我喜欢简单,因为我懒。因此,我有了一个 *服务器模块管理器*,它实质上管理在同一个进程内运行的一组微服务。缺点是,如果一个微服务发生段错误,它会使所有其他微服务一起崩溃。如果我真的担心这个问题,我会把 **游戏**(最复杂也最容易崩溃的)微服务隔离到自己的进程中。到目前为止,这还不是什么大问题。 ### 单服务器进程 上面的图展示了这种 *单服务器进程* 方法,通过使用 *服务器模块管理器* 实现。那么,为什么这很重要?嗯,这样管理微服务的一个巨大好处是,在我的特殊 *开发模式* 游戏构建中,我可以在同一个进程中运行所有内容——客户端和所有后端。能够在 Visual Studio 中按 F5 启动并同时调试所有内容,这非常有用。 ## 那不就是离线模式吗?⌗ (https://plantbasedgames.io/blog/posts/09-adding-offline-mode-and-custom-servers-to-an-mmorpg/#isnt-that-already-an-offline-mode) 现在我们对工作原理有了一个不错的表面了解,是时候看看如何为游戏添加离线模式和自定义服务器支持了。 ### 开发模式客户端 乍一看,*开发模式* 构建的客户端就已经像是离线模式,但请注意,它需要一个 **PostgreSQL** 服务器才能运行。PostgreSQL 用于许多不同的数据,而不仅仅是账户:游戏内邮件、拍卖行、角色列表、公会名称、全局唯一 ID 的生成、实例锁定等等。我们不能要求想玩 Trolddom 的玩家都安装 **PostgreSQL**,所以我们需要以某种方式摆脱它。所有不同节点(进程内)之间的众多连接也有问题。虽然它仍然能工作,但一款单机游戏需要监听多个端口,这看起来有点奇怪。如果玩家有防火墙,可能会弹出令人困惑的提示,或者干脆静默地阻止连接。当然,有些微服务在单机环境下没有意义。你不会拥有公会成员,也不会有人在拍卖行购买你的东西。我们当然可以在离线模式下禁用很多功能,但不是全部。此外,这意味着需要更改或重构大量代码,并且需要到处进行“我们是否处于离线模式?”的检查。从一个懒惰的开发者角度来看,尽可能保持原样要方便得多。 ## 离线模式网络⌗ (https://plantbasedgames.io/blog/posts/09-adding-offline-mode-and-custom-servers-to-an-mmorpg/#offline-mode-networking) 然而,摆脱网络连接感觉是必不可少的。我们怎么做呢?如果我一开始就考虑到这一点,也许我会创建某种网络抽象层,网络上的节点可以通过它相互传递消息。对于像我这样喜欢过度工程化的人来说,这样的抽象层似乎有点酷。想象一下所有这些节点相互传递消息,却对实际传递方式一无所知。这对于没有真实网络的自动化测试也很有用。但话说回来,我在这里是为了拼凑一个完整的 MMORPG,而不仅仅是为了酷而写代码。 ### TCP 连接 大多数网络实现都是这样工作的。每个微服务都有对应的客户端代码,例如 **网关** 服务有一个 **GatewayClient**,它封装了与服务器通信所需的所有逻辑。**GatewayClient** 对象负责维护一个 **TCPConnection** 对象,该对象包装了一个消息流和一个连接到服务器的套接字。这非常直接简单。如果我们想抽象掉 **TCPConnection**,并用一个更高级的消息传递机制替换它,事情会变得复杂得多。但我们的离线模式呢?要消除这些真实连接,最小且最不侵入的更改是什么?对我来说,明显的解决方案是简单地将代码的最低层部分替换成其他东西。换句话说,使用某种假套接字。我四处看了看,没有找到我喜欢的,所以我最终创建了自己的小型 C 库:fake-socket (https://github.com/demogorgon1/fake-socket)。它基本上可以当作普通 Unix 套接字 API 的直接替代品。工作原理基本相同,但(假)套接字只能在同一个进程内连接。如果你想尝试这个库,请注意我只实现了 Trolddom 实际需要的功能(非阻塞 IPv4 TCP 套接字)。当我开始做这件事时,我天真地预测编写(和测试)*fake-socket* 会是整个过程中最耗时的一部分。当然,事实并非如此。 ## 数据库的乐趣⌗ (https://plantbasedgames.io/blog/posts/09-adding-offline-mode-and-custom-servers-to-an-mmorpg/#fun-with-databases) 嵌入 **SQLite** (https://www.sqlite.org/) 似乎是避免 **PostgreSQL** 的明显解决方案。它是一个非常简洁的小型数据库,可以直接嵌入到程序中。幸运的是,我已经将所有数据库查询抽象到单独的 C++ 模块中,所以我认为只需要两个实现:一个用于 **PostgreSQL**,一个用于 **SQLite** 就够了。然后我打开 SQL 表定义文件,发现里面有 14 个不同的表,包含各种奇怪的东西。如何将所有表转换为能在 SQLite 中工作的格式?一想到要费力地重新创建每个表为 SQLite 兼容的 SQL 定义,我就感到很不舒服。我还考虑到,游戏的后期版本可能需要对表定义进行更改。玩家会在本地 PC 上拥有离线模式的数据库,这基本上要求游戏能够自动将旧数据库升级到新的表定义。这将需要编写大量烦人的代码,我宁愿避免。是时候再走个捷径了。 基本上,*SQLite* 后端不需要那么优化。它不需要支持我对数千名并发玩家的不切实际的幻想。一个很好的简化方法是,对于所有列都使用文本字符串。这样,表定义可以(主要)简化为一个表示列名的字符串列表。在初始化时,它可以简单地添加任何尚未存在的列。*accounts* SQLite 表可以像这样在代码中定义: ``` m_sqliteAccounts = aSQLiteManager->GetTable("accounts"); m_sqliteAccounts->Init( "account_id", { "username", "steam_id", "create_time", "banned_until_time", "permaban", "ban_message", "super_user", "token_id", "password_hash", "password_salt" } ); ``` 主键列(此处为 *account_id*)得到特殊处理,总是自增整数。没有创建其他索引,因此某些查询可能会非常慢。不幸的是,我无法避免将每个查询转换为适用于 *SQLite* 的工作形式。有些查询非常……

相似文章

Crosstalk-Solutions/project-nomad

GitHub Trending (daily)

Project N.O.M.A.D. 是一个优先离线运行的服务器,它集成了 AI 聊天、本地知识库、离线版维基百科、可汗学院课程、地图以及其他教育工具,旨在任何基于 Debian 的系统上运行。

停止扼杀游戏

Hacker News Top

文章认为,'Stop Killing Games'运动正确地指出了游戏在服务器关闭后无法游玩的问题,但错误地将根本原因归咎于专有软件,认为其本质上剥夺了用户控制权,转而倡导自由软件原则。

@0xAikoDai: https://x.com/0xAikoDai/status/2057317742248931363

X AI KOLs Timeline

作者反思了对一款由AI Dungeon制作团队开发的全新AI原生RPG测试版的体验,指出了在战斗和系统可读性方面AI生成的沉浸感会崩溃的设计挑战,并呼吁为AI原生游戏制定新的设计原则。