WECHAT

扫码添加微信

YoungFan 微信二维码
← 返回全部项目
手机游戏Unity3DXLuaC#LuaUnity升级lua-protobufSDK性能优化AndroidIOS

第四纪元

《第四纪元》是一款欧美暗黑风格的2D放置卡牌手游,国服由360运营,海外由韩国Webzen发行(海外名《Awaken Legends: IDLE RPG》)。后期接任客户端主程,并主导了从 Unity 2019.4 到 2022 的引擎升级与 xLua 原生库重编译。

年份2024
第四纪元 项目封面

第四纪元

《第四纪元》是一款欧美暗黑风格的 2D 放置卡牌手游。国服由 360 运营,海外版本由韩国 Webzen 发行,海外名称为《Awaken Legends: IDLE RPG》。

客户端为 Unity + XLua 架构,业务逻辑以 Lua 为主:Lua 脚本约 2000 个文件、54 万行,C# 侧承担框架、SDK 与编辑器工具。项目上线后持续迭代,期间完成了引擎大版本升级与 xLua 原生库的重编译。

我的职责:后期接任客户端主程

项目初期,我主要负责客户端业务逻辑开发,并接手和维护已有战斗系统。后期接任客户端主程,统筹客户端技术方案、核心系统维护、性能优化、构建发布与发行技术对接,对客户端版本质量和上线交付负责。主要工作包括:

  • 开发和维护客户端业务及战斗系统
  • 主导 Unity 引擎升级(2019.4 → 2022)与 xLua / lua-protobuf 原生库重编译
  • 优化线上版本的性能与体验
  • 精简网络协议及通信数据
  • 优化 Jenkins 自动化构建流程
  • 与国内外发行团队进行技术对接
  • 负责正式版本的打包、更新与发布

确定性战斗与回放

我接手并持续维护已有的 Lua 战斗系统,客户端与服务端共享同一套战斗核心及配置。逻辑层负责行动顺序、技能、伤害与 Buff 结算,不依赖 Unity 的动画和渲染;客户端将结算过程转换为表现指令,驱动角色动画、特效和血条,服务端则独立推演并校验结果。

战斗以离散逻辑帧推进,而非依赖画面帧率或动画播放时间。随机事件统一使用基于 Wichmann–Hill 的三路组合线性同余生成器,由种子初始化独立随机序列,使相同输入在同版本代码与配置下能够重现。数值精度采用定点数的整数缩放思路:比例按万分比配置,统一换算并在伤害等结算处取整;部分中间计算仍使用浮点,并非全链路纯定点运算。

回放基于初始阵容、属性、随机种子和带逻辑帧信息的操作序列重新推演,而不是录制视频。相同输入也可交给服务端重算,用于结果校验;开发时则结合逐帧战斗日志复现异常、比对双端差异,让战斗问题能够回查到具体行动与结算节点。

Unity 引擎升级(2019.4 → 2022)

项目长期运营需要跟上引擎版本与工具链,我们把工程从 Unity 2019.4 升级到 Unity 2022(2022.3.62f3)。这不是改个版本号就能跑起来的事:引擎跨了三个大版本,业务代码、第三方插件、渲染管线和构建产物都要重新对齐。

工作分三条线推进:

一是过时接口替换。逐模块排查并替换废弃接口,对必须同时兼容旧版本的少数位置使用条件编译隔离差异。编辑器侧工具是重灾区,例如设备模拟器的按键注入接口、编辑器工具栏与 Prefab 编辑流程都按新版本接口做了适配,改动后回到编辑器与真机双端验证。

二是依赖包同步升级。整套包随引擎大版本一起升:URP 升到 14.0.12、Addressables 升到 1.22.3、TextMeshPro 升到 3.0.7、Timeline 升到 1.7.7、Cinemachine 升到 2.10.3,导航模块切到 Unity 官方的 AI Navigation 包。渲染相关的相机栈代码按新版 URP 接口维护(统一通过 UniversalAdditionalCameraData 设置 Base/Overlay 与 cameraStack)。第三方插件(Spine、DOTween、UIEffect、UniWebView、各类 SDK)也逐个验证到新引擎可用。

三是 XLua 与 Lua 层的适配。XLua 需要跟着引擎升级,其中一块是 Unity 2022 新增的 Span / ReadOnlySpan 一类成员要按版本过滤,否则绑定生成与反射会出问题;同时重新生成全部绑定代码,新生成的 wrap 里已经能看到 FindObjectsByType、FindAnyObjectByType 这类 2022 新增的接口。Lua 侧保持 Lua 5.3 不变,战斗等模块直接在 Lua 里断言版本,升级后行为与升级前一致。

xLua 升级与原生库重编译

这个项目此前对 XLua 做过定制:为了性能,把 lua-protobuf 和 lua-rapidjson 两个 C 库直接编进了 XLua 的动态链接库,Lua 侧通过内置库方式加载(pb / pb.unsafe / pb.slice / rapidjson),协议解析全走这套。这样一来,升级 XLua 就不能只替换 C# 源码——原生库必须跟着一起重新编译,否则升级后 pb 与 rapidjson 直接不可用。

所以这里的做法是:把 xLua 单独 fork 出来维护,保留 lua_protobuf 的集成代码和各平台的构建工程,升到与 Unity 2022 匹配的版本后重新编译,再把产物替换回工程。最终覆盖原生库产物:

  • Android:arm64-v8a、armeabi-v7a、x86(libxlua.so)
  • iOS:libxlua.a
  • Windows:x86、x86_64(xlua.dll)

替换后将 Lua 侧协议解析、编辑器绑定生成、真机包体三处都做了回归验证,确认与新引擎组合可用。fork 的仓库与分支见页面顶部的 xLua 分支链接(lua_pb)。

Spine 性能优化:视锥剔除与 Job 并行化

项目里 Spine 对象数量很大(战斗、主城、界面同屏出现),而 Spine 每帧都要推进骨骼动画并用 MeshGenerator 重新生成蒙皮网格。不可见的角色也在算,主线程开销随同屏对象数量线性增长。Unity 自带的 OnBecameInvisible 是挂在单个 MeshRenderer 上的渲染回调,在本项目这种多相机(主相机 + UI 相机 + 相机栈)的场景里判定不够可控,也没法统一管理,所以改成自己做一套可见性判定。

第一层改造是给 Spine 运行时加一个开关。在 SkeletonRenderer 上维护一份静态注册表,所有渲染器在 Awake 时注册、销毁时移除,管理器因此可以随时枚举全场 Spine 对象;同时给渲染器增加一个 visibleToCameras 标记与公开的设置接口。然后在两个关键位置短路:SkeletonRenderer 的 LateUpdate 开头判断不可见就直接返回,整段蒙皮网格生成全部跳过;SkeletonAnimation 的 LateUpdate 里,不可见时跳过动画状态推进,但如果这一帧正好有动画切换请求,仍然放行一次——否则会出现“看不见的时候切了状态、重新可见时表现错乱”的问题。

第二层是逐帧的可见性判定,整块放到 Job 里并行计算。管理器的 Update 阶段先把上一帧的 Job 收掉,再收集所有启用相机的 view-projection 矩阵,用 GeometryUtility 算出每台相机的 6 个视锥平面写入 NativeArray;同时把全场 Spine 对象的包围盒与启用状态各写一份 NativeArray。随后调度一个 IJobParallelFor(批次 64)在 worker 线程上判定:每个对象对每台相机的 6 个平面做包围盒测试,用包围盒半长在平面法线方向上的投影作为半径,与中心到平面的有符号距离比较,任意一台相机可见即判定可见;未启用或没有 MeshRenderer 的对象直接判不可见。输入数组全部标 ReadOnly、结果写到独立数组,Job 之间没有依赖,因此可以立刻 ScheduleBatchedJobs 开始执行。

到 LateUpdate 阶段再 Complete 一次,把结果逐个体现在渲染器上,Spine 自身随后执行时就会按这个标记跳过骨骼更新、蒙皮计算与网格生成。所有 NativeArray 都按“相机数量或对象数量变化才重建”的方式复用(Persistent 分配),运行期只在数量变化时 Dispose 重建,避免每帧产生 GC。

在目标测试场景中,这套剔除使 CPU 单帧耗时降低约 10 ms;对象数量越多收益越明显,主线程基本只剩收集数据与回写结果的开销。

工程与上线

接任客户端主程后,我还参与网络协议减负和 Jenkins 打包流程优化,并负责与发行方协调版本接入、问题排查和上线交付。

打包与发布流程沉淀成了一批脚本:资源包体积对比与增量分析、资源上传、配置与工程备份、iOS 工程预处理(Xcode 工程 / Workspace 生成),以及构建结果推送到钉钉。项目上线后,我持续负责版本构建、更新及发布,保障国服与海外版本的迭代。