MeteorCat / Fortress 11 - 开发说明

Created Wed, 10 Sep 2025 20:36:33 +0800 Modified Wed, 29 Oct 2025 23:24:54 +0800

开发说明

之前部分已经大大小小涵盖作为 RPC 日常用到的知识点, 后面就需要基于自己项目工程经验来处理编写对应业务代码.

这里面设计的分歧点十分巨大, 以至于没有通用的处理方式; 所以只能说下为什么要这么设计的原因方便日常来处理, 其他需要去自己通过业务分析出处理方式

会话节点

关于网关会话需要每次创建 Actor 节点的功能, 其实这里面实现方法有很多种, 如下就是常规能够做到:

  • 全局 ClusterProxy: 其实就是全局在加入集群的时候挂载 clusterSharding 代理地址, 所有会话共享该集群对象
  • 动态 ClusterProxy: WebSocket 会话 Open 的时候依靠 ActorSystem 动态生成 clusterSharding 代理
  • 动态 LocalActor 托管: 在本地动态生成 Actor, 消息和 WebSocket 都移交内部处理处理

按照 pekko 文档来说, actor 节点应该控制好数量避免生成数量过于庞大, 那么直接全局设置 clusterSharding 集群代理应该是最适合的吧?

其实恰恰相反, 动态 LocalActor 托管才是最安全高效的, 这里思考以下问题:

  • 会话隔离性: 需要防止某个网关会话传输时候奔溃, 连锁全部网关的其他运行中的会话连带崩溃 - 奔溃扩散
  • 传输效率性: 全局统一采用共享集群代理句柄, 是否意味着高并发的时候传输都是在同个通道上? - 单点规章
  • 节点可控性: 网关会话是无限制增长吗? 按照网络请求来说, 实际上单个端口服务最大会话数量为 65536 - 会话可控

所以这里面都是十分值得深思的问题, 甚至有的问题需要在生产环境下才能看出异常.

数据库选型

玩家信息落地数据库也是十分有争议的点, 我概括下面基本上遇到项目会采用以下方式:

  • MongoDB: 文档型内存数据库, 不过因为转为商业授权之后现在好像采用的不多
  • MySQL|MariaDB|PostgreSQL: 传统关系型数据库, 现在也支持 JSON 落地

这里面性能最好的就是 MongoDB, 因为本身作为内存数据库读取和写入方面都无可挑剔, 是很早以前项目立项采用的新技术, 但是后来转商业付费授权就不做太深入维护了.

其实还有更大的问题就是早期 MongoDB 做聚合计算的时候差得离谱, 后面发现难堪大用

这里面的的关系型数据库都是按照 Actor 设置个定时器异步入库处理, 其实 Quarkus 工具链已经足够丰富, 直接用 ORM 模型入库即可.

另外文档型数据库最大的问题就是没有强约束, 多人合作开发的时候很容易一股脑保存文档入库, 这种没有强约束很容易后面维护起来常常不知道某个字段是不是存在, 从而很容易就搞崩业务.

玩家数据落地一般是玩家挂载 Entity 之后定时保存数据库, 采用 NoSQL 主要让游戏数据能快查和快写, 不过这部分其实反而不怎么看重, 只要保持数据安全性落地数据库就行了, 其他基本上都是可以托管给云服务器优化处理.

主要涉及到数据库的 ORM 又是个人分歧点最大的地方, 主流采用的 ORM 方案:

  • Hibernate JPA: 海外主流的数据落地方案, 把 SQL 抽象成对应语言实现, 让SQL操作更加贴近开发语言处理
  • MyBatis Plus: 国内比较常用的数据库方案, 适配国内复杂多样查询业务
  • QueryDSL: 比较小众的SQL方案, 基本都是字符串拼接处理

这里其实都没什么区别, 按照个人习惯选择集成即可.

故障转移

pekko 本身自带高级的集群故障转移方案, 我对于 Actor 系统之中最看重的点就是 故障转移和修复, 因为大规模服务端架构当中节点故障是大概率发生的, 而故障之后怎么迅速对节点重建恢复就是保证系统稳定的核心关键.

建议查看 pekko 关于节点持久化的配置和使用

另外 skyent 其实被说 玩具 框架其实也是有道理的, 因为相比较正规的 RPC 框架, skyent 对于节点集群的故障转移和节点选举基本没支持, 也没办法做集群节点故障之后的操作重放和镜像复制功能, 很多时候节点崩溃就直接崩溃写个日志让开发者自己去处理, 玩家信息不用恢复发邮件补偿.

可以说这就是工业级 Actor 框架应该做的事, 不过现在很多游戏公司都对这部分也没这么看重, 基本上报错最大也是回滚数据发补偿邮件而已.

持久化节点状态除了崩溃转移之外还有保存重放功能, 也就是直接原生支持还原节点执行操作状态, 方便举报作弊时候直接还原状态做判罚封禁依据

另外从我个人角度出发, 其实也不怎么喜欢 skyent 框架, 这种完全把业务挂载脚本系统上导致了调试测试很多功能是半残废; 并且业务量大的时候, 那堆脚本文件完全没办法做功能模块的调用定位, 开发过程之中编辑器没办法支持项目对应跳转.

不过好处就是可以让客户端和服务端可以共享相同的逻辑代码, 并且也能实现服务端业务热更新, 可以节省花费游戏逻辑的时间

不过真要说基于 H5游戏 来说, 其实挂个 JS 脚本系统来替代 Lua 更好, 毕竟市场上面编写 JS 业务更容易找到.

脚本系统

对于服务器挂载脚本系统, 其实我个人是十分支持, 毕竟能和客户端共享代码确实能够节约大量开发成本; 但是对于 skyent 这种底层和业务结合在一起的是十分排斥的, 其本身落后的开发方式没办法和现代语言集成在一起.

最好的方式就是仅仅做 业务片断 来加载, 比如声明 skill_1001.lua:

-- 技能基类
Skill1001 = {}

-- 玩家调用技能
Skill.use = function(entity)
    entity.hp = entity.hp - 100 -- 让外部去判断是否 <= 0 已经死亡逻辑 
    return entity -- 重新填充玩家实体
end

而作为宿主的 Java 部分就是抽取这部分业务片段来执行, 而不是基础功能底层暴露给脚本然后全部移交给脚本处理初始化.

这样可以把功能业务控制成比较小的模块, 而不用将复杂的 gateway|cluster 功能, 由底层暴露过来的接口让脚本实现.

特别后期项目规格上来的之后, Lua 的底层功能和业务功能黏在一起有多难维护, 可能某个模块有同名功能, 编辑器都不好定位是哪个模块的功能

如果真的不想采用 Java 这种基于 JVM 的开发语言, 可以用 golang 这种方式套个脚本解析器按照上面思路实现, 整体思路其实也大差不差.

还有对于魔兽世界采用 Lua 的说法, 其实也是一样做片断代码片断来逻辑热更, 而不是暴露底层让 Lua 全面接管业务处理.

其实脚本系统更加推荐 JS 脚本(QuickJS)之类, 现代化特性支持更完全且兼容面向对象方便做系统组件(Lua面向对象是残缺的)

不过脚本并不是游戏当中标配, 如果你服务端支持快速重连其实也没什么问题; 但是脚本系统建议还是考虑集成, 因为大部分策划有时候死线疯狂上功能可能参数都没配好就上架了, 后面急冲冲又要频繁改动数值.

Headless Dedicated Server

Headless Dedicated Server 大部分都是叫 无头服务器, 这其实游戏引擎扩展出来游戏专用服务器.

也可以这么理解: 运行在服务端没有游戏界面的游戏客户端, 用于客户端逻辑放置到服务端运行(多人对战)

比如很多游戏内部有的碰撞系统那些, 服务端编写其实很麻烦并且可能因为客户端和服务端做运算流程不同最后得出的浮动值也不同, 所以后来为了这种让客户端游戏逻辑开发更加简单, 就提供了这种剥离游戏界面仅仅做游戏计算的启动程序.

我至今接触到的这种处理方式就以下几种:

  • 在服务器当中挂起单独 Unity 服务, 内部自己启动 Socket 功能做监听传递
  • Unity 打包出来无头服务器, 通过网关转发数据到游戏服务端端口做游戏实体挂载
  • 开源框架集成, 基于 ET 之类框架做的前后端代码同步功能, 让客户端也参与服务端的开发工作

这里面大部分方案就是为了 妥协, 让客户端能够更好编写服务端代码, 从而达到两端代码复用目的.

就和为什么我选择 Java 编写服务端一样, 在市面上可能冷门游戏服务端编程语言不好找, 但是 Java 相关绝对好找

不过这也是理想状态双端复用, 如果客户端直接上手编写服务端代码的时候对服务端数据交换没概念, 可能会导致游戏模块出问题; 并且还有最大的问题就是一旦采用这种高度集成游戏引擎的服务端, 后面如果也不好切换游戏服务端技术栈(和引擎深度绑定).

所以基本上能够看到游戏服务端很少采用引擎自带的这种方案, 其实主要是更多是不想和游戏引擎绑定太深; 总不能直接全依赖 Unity 技术栈, 后续需要转 UE 的时候全部废弃从头开始构建服务端相关吧.

分区分服

之前看到其实依靠 Actor 技术就能保证游戏服务器不需要采用分区分服这种传统架构, 那么为什么还要有选区选服概念?

包括现在很多知名游戏的服务端也是直接输入账号登录直接不需要选服就能进入游戏

这其实不是技术原因, 你可以看到很多游戏都有专门的 渠道服, 比如 小米商店渠道服|B站游戏渠道服|抖音游戏渠道服 等, 更主要的是因为渠道服提成方案是不同的, 所以各自本来就不打算做成账号共享的服务.

比如 A 商店渠道充值的抽成是10块, 而 B 商店渠道充值抽成 9 块, 所以能够看到各自渠道上面的价格折扣都是不一样的.

比较典型就是苹果抽成30%, 玩家充值 Apple Pay 付费最后结算给研发公司的分成, 包括现在都是提倡网页支付甚至其他平台支付都是避免这笔抽成

这种也带来的不同渠道服不同的氪金福利, 如果不分服就会导致玩家去廉价的渠道服平台充值, 之后回热门的服务游玩碾压本地原折扣付费用户; 并且这种不同氪金待遇也会对游戏内部金融系统造成冲击, 可能策划专门为官方服务端出个付费活动, 最后玩家涌入低价渠道服付费不在官方服充值.

还有就是地区风俗问题, 因为每个地区风俗是不一样, 所以有的地区需要专门针对地方风俗出活动, 而其他地区不涉及这方面活动.

比如典型的圣诞节和春节这种地区不同节日, 可能需要单独策划不同的节日活动, 按照地区服务器来区分上架

所以现在分区分服更多并不是技术问题, 而且结合人文地理风俗从而不得不这样保留下来; 哪怕你进入游戏没看到选服页面, 其实内部服务器已经提交一个默认服务器标识当作某个服务器玩家.

为什么做不到统一服务器, 其实根本不是技术层面上的问题, 而是游戏运营的决策导致成必须做成这样方式.

服务端语言

虽然整个服务端流程基本采用的是 Java 来构建, 但实际上并不是最适配游戏服务端的开发语言, 除了基于 JVM 的老生常谈的 GC 问题, 还有值类型装箱问题.

包括还有 unsigned int 之类的值缺失, 需要注意 Java 默认是没有 int128 的, 最大只有到 long(int64) 支持, 除非采用官方支持的 BigInteger 支持(性能差).

另外主要就是值类型, 比如比较大型 MMORPG 大部分要做很频繁的 Vector2/Vector3 运算, 这种情况下会产生类对象(主要是大部分这种游戏相关类型都是用类处理).

不过说是采用新版本垃圾回收器(ZGC)提升不少, 所以现代 Java 在这方面可能后续提升也挺不错的; 很多游戏公司喜欢采用 Java 来做 SLG 游戏服务端相关, 目前看应该问题不大.

主要 Java 部分的工具链太过完善了, 用人成本和第三方压力让其开发难度骤降, 底层构建好不用多久就能写业务代码

但是处理一些规模不大的 手机和H5游戏 方面足够了, 剩下大型游戏处理可能就需要单独做优化处理.

而如果游戏项目组对于 Unity 依赖比较深的情况, 采用 .net + C# 可能是个比较好的选择, 挂靠相同的 C# 编程语言可以让客户端一起参与服务端的流程开发.

而如果不想和 C# 绑定太深, 也可以考虑采用 golang 来做服务端处理, 事实上目前越来越多采用 golang 来做服务端开发.

服务器语言方面我更加偏向于以下方面考虑:

  • 第三方: 第三方插件和库丰富, 节约自己重复开发插件的时间
  • 招人难度: 招聘开发人员的难度低点, 避免招聘市场难以找到合作开发的人员
  • 活跃性: 尽可能找网上比较活跃的开发语言, 在 AI 时代热门语言的资料集更多, 出问题更加容易通过 AI 查询到答案
  • 严谨性: 优先找强类型的开发语言, 很反直觉的是: 弱类型对于开发者要求比强类型还高, 弱类型更容易写出无法维护的语言

直接接触过 Erlang 项目, 虽然是完全为了并发而生的语言但是社区资料少得可怜, 第三方支持稀缺(数据库驱动都不稳定要自己找)

至于 C/C++ 之类, 游戏规模一般到不了要这种靠近底层语言来处理; 而 Rust 之类目前没有经过大规模的市场检验, 不好直接作为主要开发技术选型.

技术更迭

这是我最想吐槽, 也有可能游戏代码是从别的地方 ‘参考拷贝’ 过来的, 底层就完全没有去细致的第三方管理, 然后问题就出来:

  • 第三方包必须锁定某个版本, 如果第三方包某些函数和方法如果有安全问题标记必须升级更新, 直接不知道升级怎么去修改依赖
  • 系统版本必须锁定某个发行版的版本, CentOS 必须锁定在 6 版本, 不管官方是不是标记已经不再维护
  • 数据库也是必须要锁定在低版本, 如果采用高版本可能会因为某些底层特性直接没办法导入进数据库, 存储过程和触发函数都用上

这些问题我不止一两次见到, 其中最过分的就是整个项目必须依赖 CentOS6 i386 版本, 那时候堪称顶级牢房的痛苦!

早期 CentOS 6 系统有分 32/64 位版本, 区分为 i386x86_64

要知道在 CentOS 7 官方都不支持的现在, 还要在游戏服务端跑在 32 位系统维护满是bug的系统, 并且还要在这上面继续开发构建游戏逻辑, 这简直就是 赛博酷刑.

这种陈年代码可以游戏圈的某些 隐藏规则 产物, 有时候甚至不如投入时间重写一遍整体的服务架构.


后面服务端设计因为有部分之前业务代码, 所以可能需要拆分出来单独重新编写来避免泄露部分内部逻辑代码; 到这里其实就差不多把游戏当中需要注意的点说明了, 剩下就是专门游戏逻辑业务方面的功能, 这部分可能需要单独设计个简单游戏来说明.

注: 建议学习相关的 Actor 设计知识, 内部的设计思想和消息投递方面知识和传统多线程/异步功能驱动有什么不同等, 都是很值得思考的