MMORPG 与单机游戏的区别,就在于您所见一切背后的那台服务器。在 Ancient Online: Remastered 开发日志的第一篇中,我们讲述如何用 Unreal Engine 5.8 打造一款 MMORPG:客户端与服务器如何分工、世界如何被模拟,以及我们如何做到实时编辑一个统一的完整世界。
两个部分:客户端与服务器
Ancient Online 由两个独立的软件组成:
- 客户端基于 Unreal Engine 5.8,用 C++ 编写。渲染、音频、界面和输入是它的工作。
- 服务器独立于 Unreal:我们用 C++23 从零编写。游戏的所有规则都在这里运行。
为什么我们把服务器单独编写,而不是放在引擎内部?因为 MMORPG 服务器的需求与游戏引擎的需求不同:它没有图形,却有成千上万的实体、持久化数据、长时间的持续运行,以及对可预测性能的要求。独立的服务器让我们只做必要的工作,并掌控每一个字节。
权威服务器:不信任客户端
我们的基本规则是:客户端提出主张,服务器作出决定。
- 移动:玩家的输入发往服务器,由服务器计算位置。客户端通过平滑插值来显示服务器告知的位置。所以,一个“把自己瞬移走”或穿墙而过的客户端,在服务器看来哪儿也没去。
- 战斗:伤害、命中、击退等结果都在服务器上计算。例如,一次连击是否把对手击倒由服务器决定,并同时把结果广播给所有人。
- 经济:物品、货币和商店交易只在服务器和数据库中发生变化。
这种做法的代价是必须把网络延迟处理好;回报则是一个作弊难得多、所有人都适用同一套规则的世界。
25 Hz 的世界循环
服务器每秒更新世界 25 次。每一步中,怪物 AI、移动、战斗、技能效果和天气都会向前推进。固定的步长让模拟保持可预测,并确保相同的输入总是产生相同的结果。
并非每位玩家都需要知道所有事情。服务器把世界划分成网格,只向每位玩家发送其关注区域内(也就是附近)实体的更新。这样即使在拥挤的城市里,网络流量也能保持在合理水平。
轻量的二进制协议
客户端与服务器之间没有 JSON 或 REST;每条消息都是布局固定的二进制数据包。这既节省带宽,也节省 CPU 时间。当我们加入一个新系统(例如天气、移动或游戏内账号服务)时,会为它编写一份独立的、带版本号的数据包契约,并通过共享头文件来保证客户端和服务器依据同一份契约工作。
持久化:保存数据而不让游戏等待
玩家数据存放在 PostgreSQL 数据库中。但让世界循环在每一步都等待数据库是不可接受的。因此保存是在后台进行的,采用后写式持久化:世界服务器把变更放入队列(Redis),后台写入进程再把它们分批提交到数据库。游戏的流程不依赖数据库的速度。
频道与登录服务器
玩家首先连接到登录服务器,它负责账号验证、角色选择和会话。随后他们进入四个游戏频道(CH1–CH4)之一。各频道的实时状态显示在游戏网站的服务器状态页面上。
一个统一的世界,实时编辑
Ancient Online 的世界被设计为一个统一的完整世界,而不是被加载画面隔开的一张张独立地图;它被划分为单元格,以流式方式加载。
世界的内容(地形、物件、怪物布置)不是保存在代码里,而是保存在数据库中,并且带有版本。我们可以用游戏内的世界编辑器来编辑这个世界:改动先在草稿中进行,经过检查,然后发布。每次发布都会生成一个新版本,客户端通过比对摘要来验证自己持有的世界数据与服务器的一致。如果不一致,它会等数据更新之后再继续进入游戏。
任务:以 Lua 作为脚本语言
任务和 NPC 对话在服务器上用 Lua 5.4 编写。这让我们无需重新编译服务器就能开发和测试任务。
玩家的消息绝不离开我们的服务器
游戏内聊天翻译不使用外部翻译服务。我们在自己的服务器上运行一个仅使用 CPU 的本地翻译模型;玩家的消息不会发送给任何外部 API。这样,说不同语言的玩家可以相互理解,而他们的消息始终不会离开工作室。
接下来
开发日志的第二篇讲述我们为了让同一个世界能用鼠标、触摸屏和手柄游玩而作出的决策。想了解工作室,请阅读 Astronaut Entertainment 是谁?;想了解游戏本身,请参阅游戏页面。


