异星工厂(Factorio)是我在 Steam 上游戏时间最长的游戏,这是一个有关设计自动化流水线、观察流水线运行并不断地进行改进和扩容的游戏,在这样一个沙盒里可以发挥你的创造力来建造工厂、解决工厂内的物流需求,不断超越自己,The Factory Must Grow!
异星工厂也支持多人游戏,和朋友们一起联机建工厂就更加有趣了,但因为家用网络的多层 NAT,想要和朋友们在互联网上互相直连是一个比较麻烦也很不稳定的事情。刚好前一阵云引擎添加了 托管游戏后端 的能力,可以部署基于 UDP 的游戏(或其他类型的服务),于是我就开始尝试在云引擎上运行一个连接起来更稳定的服务器和大家一起游玩。
异星工厂的 多人模式 是一个非常典型的 Lockstep 架构(步进锁定,有时也称为帧同步),所有玩家连接到房主(可能是一个玩家也可能是一个服务器),通过 UDP 向房主发送自己的动作,房主会收集每个 tick(1/60 秒)中玩家的动作来确认先后顺序,然后广播给其他玩家,最后实现所有玩家的游戏状态完全同步的效果。
部署到云引擎
因为云引擎提供的是标准的 Linux 运行环境,并不限制运行在其中的进程使用什么技术栈,因此很容易将既有的游戏后端部署上去,云引擎提供了 leanengine.yaml
来自定义构建过程,所以我们只需要几行脚本就可以从异星工厂的官网下载到游戏的服务器版本:
install:
- curl -Lo factorio.tar.xz https://www.factorio.com/get-download/1.1.61/headless/linux64
- tar --strip-components 1 -xf factorio.tar.xz
- rm factorio.tar.xz
- use: default
extraPorts:
- name: factorio
protocol: udp
containerPort: 34197
云引擎支持通过 Git 或命令行工具等多种方式部署,这里不再展开介绍,详见 快速开始部署游戏后端 的文档页面。
leanengine.yaml
中的 extraPorts
部分用于定义游戏服务器监听的端口,在这里是 UDP 34197 端口,云引擎会将其映射至一个公网端口上。
因为云引擎被设计用来支持可横向扩展的游戏后端,所以游戏后端的每个实例都会被分配一个单独的公网地址和端口供客户端连接(尽管异星工厂只能单实例运行),我们需要通过一个 HTTP API 来获取公网连接地址(相关文档):
$ curl -H 'X-LC-Id: SJjoXHWuhewHKV4Ojw' \
'https://shared.cloud.tds1.tapapis.cn/1.1/engine/gateway/route?groupName=factorio&prod=1'
[
{
"name": "factorio",
"protocol": "udp",
"publicPort": 10280,
"publicIp": "106.75.48.157"
}
]
然后就可以在游戏中连接了:
存档
在游戏跑起来之后我们还要考虑存档的问题,游戏服务器会每隔几分钟将内存中的游戏状态导出写入为一个存档文件(如 first-game.zip
),然而云引擎当前还不提供持久的文件存储,实例重启或重新部署后,临时文件系统上的文件就会丢失。
于是我想到了检测存档文件的变化,使用 rclone 将文件同步到对象存储(如 S3)。相应地在每次重启后、游戏开始前再从 S3 上将存档下载回来。
我用一个简单的 Go 程序充当胶水代码,在启动游戏的同时,实现了创建、下载和同步存档的能力,完整的代码(以及 leanengine.yaml
)可以在 GitHub 看到。
存档的名字和对象存储的访问密钥可以设置在云引擎的环境变量中,而不必硬编码在代码中:
更适合游戏开发者
其实现在市面上已经有很多专门托管异星工厂等游戏服务器的云服务了,它们更加简单易用,也更有性价比。但不同于这些服务,云引擎提供的更多的是面向开发者的托管游戏后端的能力,如完善和易用的构建、部署、日志和统计等能力。利用这些能力游戏开发者们可以轻松地部署自行开发的游戏服务器上来,在玩家数量增加时还可以利用云引擎提供的横向扩展能力来应对。
就像前面演示的那样,云引擎对虽然对 Node.js、Python、Java、PHP、Go、C/C++、C# 等常用的技术栈都提供了支持,但并不限制实际使用的技术栈,也不存在供应商锁定(Vendor lock-in)。即使在项目的开发后期甚至完成后,也可以通过简单的适配运行在云引擎上,受益于云引擎提供的各种运维能力。