五子棋开发全记录:人机、联机、搬服务器和热更新


之前写过一篇五子棋最近的进展,只是个概览。这篇按时间顺序,把整个开发过程完整记下来:每一步为什么这么做、怎么做的、踩了什么坑。

先说现在的样子:一个用 Godot 写的五子棋,有电脑版和安卓版,能人机对战、用实战教练练习,也能和家里人、朋友联机。新版本不用重装,打开游戏就自动更新。

手机版首页:人机对战、实战教练、联机对战三张卡片,底部显示昵称

手机版首页

整体结构:房主和客人都连到服务器上的中转服务,客人发请求、房主发结果;游戏启动时从更新服务检查和下载更新包,发版电脑把签好名的更新包上传上去

起因

看到别人用 Claude 加 Godot 做出了完整的项目,想自己也复刻一个。Godot 正好有能让 AI 直接操作编辑器的插件,就选了它。

五子棋规则简单,适合当第一个项目。

一、人机对战和实战教练

第一版只有两个模式。

人机对战:和电脑下,有简单、普通、困难、大师四档难度。难度越高,电脑想得越久、算得越深:简单档每步只想 0.3 秒,还会故意带点随机,大师档每步最多想 4 秒,往后算 8 层。

电脑是怎么想的:

  • 认棋型:把每个空位四个方向上、两边各 4 格的情况编成一个编号,提前算好一张表,查表就知道在这里落子能形成活三、冲四还是活四。每下一步只更新受影响的那几条线,不用整盘重算。
  • 先找杀棋:看能不能靠连续冲四一路逼到赢,对方每一步都只能被动去堵。
  • 找不到再搜索:用 α-β 搜索往后算几步,只考虑最有威胁的候选点,节省时间。
  • 计算放在后台线程里跑,电脑思考的时候界面不会卡。

电脑版人机对战:右边提示建议下在 K9,能形成活三、活二,并显示已算出必胜;棋盘上用序号圈出后续步骤

电脑版人机对战:点“提示”后写出建议和理由,棋盘上标出后面几步

实战教练:适合跟别人下真实的对局时打开。轮到我时,教练告诉我最好下在哪、为什么,比如“这步能形成活三”“对方藏着一套 5 步的连续冲四,建议下在这里化解”,还会把对方的关键点用红色标出来。轮到对方时,我把对方实际下的位置点到棋盘上,教练顺便预测对方最好的应手。旁边还有一条胜率条。

教练用的是同一个引擎,只是把算出来的结果翻译成人话。

电脑版实战教练:建议下在 H7 形成双三,已算出我方必胜,还列出其他可选的位置

实战教练:双方的棋都由自己点进去,教练给出建议

手机上棋盘太小。 手机上的玩家想把棋盘放大看,所以加了双指缩放,最大放大到 3 倍。手指一按下就落子的问题也一起处理了:双指缩放、拖动都不会误落子,只有单指轻点才算。

二、联机:自己写一个中转服务器

只给家里人和朋友玩,不需要上 Steam 这类平台,就自己写了一个中转服务器(Python + WebSocket)。

为什么让房主判定,而不是服务器

棋局由建房的一方说了算:落子合不合法、谁赢了,都在房主那边判断;另一方只发“我想下在这里”的请求。服务器只管配对和转发,完全不看消息内容。

这么做有两个原因:

  1. 之前考虑过类似 Steam 那样的联机方式,就是由一个玩家当主机,其他人连过来。
  2. 想尽量减少服务器的对外流量。服务器只转发几十字节的小消息,不跑任何游戏逻辑。

客人想下一步棋,发给房主的消息长这样:

{"t": "req_move", "g": 3, "n": 12, "x": 7, "y": 7}

g 是第几局,n 是现在第几手。房主收到后检查:轮到他了吗、这格空吗、手数对得上吗,没问题就发回正式的落子,有问题就发回拒绝。两边手数对不上时,客人向房主要一份完整棋谱,从头重放一遍,棋盘就不会不一致。

一局联机的消息顺序:房主建房拿到房间号,客人加入后服务器通知双方配对成功;之后客人发出想下的位置,房主校验后发回正式落子或拒绝,有人连五时房主发出终局结果

中转服务器的规矩

谁能连:游戏里打包了一把共用的密钥,握手时对不上就断开。不给每个朋友单独发邀请码,因为只是几个熟人在玩;万一密钥泄露了,换一把新的,旧版本的游戏就全部连不上了。

上限按“只有几个熟人、服务器只有 2 GB 内存”来定,宁可紧一点:

  • 全服最多 5 个房间、50 个连接;每个 IP 最多 4 个连接、2 个房间
  • 单条消息最多 4 KB,每个连接每秒最多 20 条,防止有人刷消息
  • 每个 IP 每分钟最多尝试加入 10 次,超过就禁 5 分钟,防止有人猜房间号
  • 房间号 6 位,去掉了 0 和 O、1 和 I、L 这些容易看错的字符
  • 建房 10 分钟没人加入、对局中 30 分钟没有动静,房间自动关闭
  • 日志里不记任何消息内容,也不记密钥

联机是分两个阶段做的。第一阶段:建房、加入、落子同步、胜负判定。第二阶段:再来一局(交换颜色)、认输、离开房间要先确认。

为什么分三个分支

代码分成 main、desktop、mobile 三个分支。原因是希望手机和电脑的玩家能互相联机:联机逻辑、规则这些共用的东西都放在 main 上,电脑版和手机版只在界面上不一样,各自从 main 合并。这样两边说的是同一套“话”,不会出现手机版和电脑版对不上的情况。

手机版人机对战,和上面电脑版截图是同一个局面,右边信息栏更精简

同一个局面在手机版上的样子:规则和 AI 一样,只是界面不同

第一次自己管服务器

租了一台腾讯云的轻量服务器,做了这些基础设置:

  • 只允许密钥登录,关掉密码登录和 root 登录
  • 装了 fail2ban,反复登录失败的 IP 会被自动封掉
  • 防火墙只放行用得到的端口
  • 改动之前先拍快照,出问题能回滚

发给朋友之前,还把安卓包名改成了自己的,改用正式签名打包。包名和签名一旦发出去就不好再改,所以第一次分发前就定下来。

三、服务器从首尔搬回国内

首尔的服务器国内连不上。 一开始想租香港的,结果已经买不到了,离得近的只剩首尔,就先租了首尔的。部署好以后,国内的朋友连不上。用网站从全国各地的测试点做 TCPing(测某个端口能不能连上),确认是国内访问首尔的线路问题,换配置解决不了。

最后在国内重新租了一台,把中转服务搬过去。全国 314 个测试点 Ping 下来全部是通的,平均 43 毫秒。

测错了端口,以为新服务器也不通。 新服务器刚开好时测了一次 TCPing,全国都超时,吓了一跳。仔细一看,测试网站默认测的端口上根本没有程序在运行,当然连不上,不是网络的问题。改用普通 Ping 测,全国都是通的;等中转服务部署好,国内的朋友也顺利连上了。

搬家的同时,客户端改成支持一个服务器地址列表:国内的排第一,首尔的当备用,连不上或者 6 秒没反应就自动换下一台。后来发现这个备用没什么用:国内连不上首尔,海外连国内反而没问题,所以当天晚上就把首尔的服务器从游戏里去掉了。

错误提示总是“连接已关闭”。 密钥不对、服务暂停这类情况,服务器发完错误消息会马上断开,Godot 来不及读那条消息,玩家只能看到一句笼统的“连接已关闭”。改成根据断开时带的错误码来显示提示,玩家就能看到具体原因了。

四、热更新:不用再微信发安装包

以前每出一个新版本,都要把安装包通过微信发给家里人,让他们重新装一遍。为了省掉这一步,做了自动更新。

怎么做的

Godot 可以在运行时加载 pck 资源包,覆盖安装包里原有的脚本、场景和图片。所以把游戏改成先运行一个“启动器”:

启动器检查更新的流程:请求版本信息、验证签名、比较安装包版本、确认有新版本后下载到临时文件,校验 sha256,改名并记下版本号,再加载更新包进入游戏;任何一步失败都用本地已有的版本进入游戏

最重要的一条规则:任何一步失败,都用本地已有的版本进入游戏。 没网、超时、校验不对,玩家都能照常玩,最多是还没用上最新版。

服务器上的版本信息大概长这样:

{
  "pck_version": 4,
  "pck_file": "update_v4.pck",
  "pck_sha256": "…",
  "min_apk_version": 4,
  "godot": "4.7.1.stable",
  "sig": "…"
}

几条版本规则:

  • 手机版和电脑版界面不同,所以分成两个通道,各发各的更新包。
  • 更新包只给对应版本的安装包用。安装包太旧时,弹窗提示去下载新安装包。
  • Godot 版本必须完全一致才加载。
  • 版本号只增不减。回滚也是用新版本号重新发旧内容,客户端只会下载比自己新的版本。

有些东西更新包管不到,改了就必须发新安装包,比如启动器本身、Godot 引擎版本、安卓权限、项目设置。

为什么要签名

更新走的是 HTTP,不是 HTTPS。如果只靠 sha256 校验,校验值也是从同一个地方下载的,只能发现文件下坏了,挡不住有人在半路把版本信息和更新包一起换掉。更新包里是能执行的脚本,被换掉就麻烦了。

所以版本信息用 RSA 签名:私钥只在发版的电脑上,公钥写在启动器里。没有私钥,就做不出游戏会接受的更新。

更新服务也要限流

更新服务是 nginx,只提供下载,不能列目录。每个 IP 最多 2 个连接,每个连接限速 1 MB/s,每分钟最多 30 个请求。玩家只有几个人,正常更新一次几百 KB,这些限制完全够用,也不怕被人刷流量。

发版脚本

发版是一个脚本完成的:从对应分支建一个临时副本导出更新包(不碰正在写的代码),算 sha256、签名,然后上传。先传更新包,再传版本信息;版本信息先用临时文件名上传,传完再改名,客户端永远不会读到传了一半的文件。还支持回滚,以及只生成不上传,方便先检查。

踩的坑

更新包一开始有 7 MB。 导出时把只有编辑器里才用的插件也打包进去了。排除掉之后,更新包降到 426 KB。

本机往服务器上传只有每秒 35 KB 左右。 从澳洲往国内传文件非常慢。电脑版安装包 110 MB,只能先压成 39.6 MB 的 zip 再传。好在日常只发几百 KB 的更新包,几十秒就传完了。

上线前用真实导出的游戏跑了 16 项验收测试,包括:断网时 5 秒左右用本地版本进入、签名不对时拒绝加载、发布新版再回滚、本地文件损坏后重新下载等等。

这一版的安装包是 V1.5。从这以后,只要装了 V1.5,后面的更新都是打开游戏自动完成。

五、玩家反馈带来的改动

联机发给家里人和朋友以后,陆续收到了一些问题。下面几项都是因为真实玩的时候遇到的情况才加的。

切出去发房间号,就掉线了

手机切到微信发房间号,回来人就不在房间里了。 手机 App 切到后台,网络连接很快就会断。以前的规则是“断线就等于离开”,房间直接关掉。偏偏建房以后最常做的事,就是切到微信把房间号发给朋友。

于是做了断线重连,作为第一个热更新推送出去:

断线重连的流程:网络断开、手机切后台超过 5 秒、App 被杀后重开这三种情况,都会让服务器给座位保留 90 秒,对方的棋盘冻结并显示倒计时;掉线的一方带着房间号和座位凭证重新加入,回到原来的座位,同步棋谱接着下

  • 服务器给每个座位发一个凭证。断线后座位保留 90 秒,期间拿着凭证就能回到原来的座位。
  • 游戏自己每 5 秒发一次心跳,12 秒收不到任何消息就认为断线了,开始自动重连。
  • 手机切到后台超过 5 秒再回来,直接换一个新连接,不等旧连接慢慢超时。
  • 房间信息存在本地。App 被系统杀掉后重新打开,会问要不要“回到对局”。
  • 房主建完房切出去断线了,朋友照样能加入,房主回来后自动开局。
  • 回到联机菜单时,如果剪贴板里有房间号,会自动填上。

服务器同时兼容旧版游戏:旧版还是“断线就离开”,新版才有重连。

不想输房间号

有人提出,想直接看到有哪些空房间点进去,而不是每次输房间号。 于是加了联机大厅:

  • 建房时可以选公开或私密。公开房间会出现在大厅里,点“加入”就进;私密房间还是用房间号。
  • 大厅每 3 秒刷新一次,显示在线人数和正在进行的局数。
  • 有了大厅,就得知道对面是谁,所以也加了昵称。默认是“玩家”加 4 位随机数,随时能改,最多 10 个字,看不见的字符会被去掉。

手机版联机大厅:左边是公开房间列表和在线人数,右边选执子颜色、建公开或私密房间、输入房间号加入

手机版联机大厅

手机上容易误触

手机屏幕小,手指一碰就落子了。 注意到这个问题后,加了落子确认:点棋盘先出现一颗半透明的预览棋子,再点“确认”才真正落下。确认按钮放在棋子的另一侧,不挡住棋盘;放大或拖动棋盘时,按钮跟着棋子走。这个功能在设置里可以关,默认开着。

手机版放大棋盘后的落子确认:半透明的预览棋子外面有一圈金色光环,下方是确认和取消两个按钮

放大棋盘后点一下,先出现预览棋子和确认按钮

六、把联机补完整

最后一轮一起做了这些:

  • 聊天:对局中能发文字。
  • 悔棋:只能撤回自己刚下的那一手,要对方同意。对方 30 秒不回应算拒绝,被拒绝后 5 秒内不能再请求。由房主执行撤回,再同步给对方。
  • 新局:对局中途也能请求重新开始,同样要对方同意;一局结束后是“再来一局”。
  • 先后手:建房时选执黑、执白或随机;下一局交换、随机还是不变,由房主决定。“随机”由房主算好再告诉对方,不能两边各自随机,否则结果会对不上。
  • 对局菜单:悔棋、新局、认输、离开都收进一个 ☰ 菜单里。认输和离开用红色标出来,还要再确认一次;暂时不能用的选项变灰,并写明原因,比如“轮到你时不能悔棋”。

手机版联机:对方请求悔棋,撤回 M4,剩 28 秒,下面有同意和拒绝按钮,再下面是聊天记录

对方请求悔棋时,信息栏里出现倒计时和同意、拒绝按钮

手机版对局菜单:请求悔棋变灰并注明轮到你时不能悔棋,下一局先后手只有房主能改,认输和离开房间是红色

对局菜单:不能用的选项变灰并写明原因

聊天按省流量来设计

聊天消息只有这么一点:

{"t": "c", "m": "好棋"}

不带昵称(配对时已经交换过了),不带时间(收到的一方用本地时间),也不做“正在输入”、已读回执这类会多产生流量的功能。单条最多 60 个字,每 2 秒最多发 1 条,只显示纯文本,防止有人发格式代码把界面搞乱。

做完实测了一下:一局 49 手加 20 条聊天,每人的消息数据大约 14 KB,加上网络包头和确认包,大约 50~65 KB。其中聊天只占 3 KB 左右,反而是心跳占了一半以上。

还在服务器上专门验证过:发一条带特殊标记的聊天,再去翻服务器日志,标记一次都没有出现。

手机版界面重做

手机版原来是一个塞满东西的大侧栏,这次拆成了几个页面:

  • 首页:三张大卡片选模式。如果上次的联机对局没结束,就跳过首页,直接问“回到对局?”
  • 对局页:左边棋盘,右边信息栏只显示当前模式需要的东西,不常用的设置收进 ⚙ 里。
  • 联机大厅:单独一个全屏页面。
  • 打字:横屏时软键盘会挡住大半个屏幕,所以点输入框后,输入框移到屏幕最上方,下面显示最近几条消息。

手机版打字时的输入模式:输入框、发送和收起按钮在屏幕最上方,下面显示最近三条聊天,其余部分变暗

打字时,输入框移到屏幕最上方

电脑版联机时,去掉了计时和棋谱,原来棋谱的位置换成了聊天区。

电脑版联机赢了一局:棋盘上连成五子的黑棋连成一条线,撒彩纸;右边显示再来一局按钮和下一局交换先后手,下面是聊天区

电脑版联机:棋谱的位置换成了聊天区,一局结束后可以“再来一局”

每次改动的流程

每一轮基本都是同一套流程:

  1. 先出设计:页面结构、消息格式、界面草图,列出需要我拍板的点。
  2. 确认后再写代码:共用的逻辑先写在 main 上,再合并到电脑版和手机版。
  3. 跑自动测试:在本机临时起一个中转服务器,开几个游戏实例互相联机,把断网、切后台、App 被杀这些情况都跑一遍。最后一轮电脑版 229 项、手机版 267 项,全部通过才发布。
  4. 发更新包:先在本机把更新包叠加到游戏上跑一遍,再发到服务器,最后用真实的安装包打开一次,确认自动更新成功。

结果和接下来

到现在,更新包已经发到第 4 版,从 V1.5 开始都是自动更新。每个更新包只有四五百 KB。

中转服务后来也被大富翁复用了:大富翁有自己的固定房间,两个游戏的数据完全分开,互不影响。

五子棋的开发先停在这里。概览里提过的“玩家记录”(看看都有谁在玩)还没有做。


本文由 Claude 根据我的开发记录整理。