《命运圣契》是一款百媚养成绝色卡牌手游,由极致游戏研发及发行。玩家需要在名为阿克迈斯大陆的奇幻世界里扮演橘屋冒险团团长,因意外获得神器,与百媚少女们缔结契约,共同抵御魔王,从而开启一段惊险刺激的冒险之旅。

作为首款转型 Unity 引擎的产品,极致游戏在开发《命运圣契》的过程中克服了多项技术难题并实现了开发提效。在 12 月 6 日 Unity Open Day 技术开放日厦门站,我们邀请到了极致游戏的技术经理汪兴分享《命运圣契》在架构、开发流程、工具等方面服务于提升开发效能的技术方案。以下为演讲实录。


大家下午好,很高兴来到这里和大家分享我们《命运圣契》在开发过程中的一些提效实践。


《命运圣契》是今年 9 月份上线的一款竖版 3D 养成卡牌游戏。这款游戏采用的是客户端 Unity + 服务端 Erlang 的技术栈

这款游戏作为我们团队转型 Unity 引擎的首款产品,也是我们公司首批 Unity 项目之一。因为团队是从其他的引擎转型过来,所以在前期属于摸着石头过河的状态,Codebase 比较混乱,框架和业务代码会比较耦合,通用性抽象比较欠缺一些,会存在一些重复的工作。在这种情况下,我们想和其他项目进行一些技术共享,也会比较困难。

针对这个情况,我们重新设计了我们项目的分层架构


在 Unity 引擎基础上,我们把项目分为四层。

第一层是 Package 层,这一层除了 Unity 和第三方的一些 package 之外,会把内部具有独立重用价值的模块进行抽象封装,从而让我们能够和其他的项目进行技术的复用。在 Package 之上,是我们的框架核心层,这一层是我们基于项目的特性、产品的需求,打造的一些核心的组件,这一层和我们的业务层进行了解耦。再之上是通用业务层,是对一些具有通用性业务的抽象,能够让我们在开发中能减少重复。最上面是游戏玩法层。

在这样一个分层结构下面,为了维持我们架构的整洁性,在 C# 侧,会把每一个 package 封装为一个程序级,Package 之上的每一层会作为一个程序级。通过程序级的依赖管理,能够去达到各个层次的解耦。同时因为我们项目组主要是用 Lua 开发,在 Lua 侧也有类似的分层,为了保证 Lua 侧分层的解耦,我们会在 Lua 侧实现一个分层依赖的检查机制,当我们识别到 Lua 有下层代码耦合调用上层代码的时候,我们会进行报错提示,通过这种方式来实现 Lua 侧分层的解耦。


我们通用业务抽象的例子——数据资源系统,将玩家的各种数据进行了资源化的抽象,除了常规的物品之外,有我们的英雄、经验等级,乃至于邮件、加成效果这些。把这些数据都进行了资源化抽象之后,可以进行统一的添加扣除、批量操作等等,从而来简化我们的业务代码。同时通过一些统一的安全检查、编辑处理,保障我们的业务安全。

总的来说,通过我们的分层架构来说,我们整个 Codebase 变得层次更加清晰,同时可以从不同维度上面去做代码的复用封装,从而提升我们的开发效率

UI模块


接下来讲一下 UI 模块,我们做一个数字卡牌游戏,UI 量级很大,并且我们的 UI 和 UX 也经过数次大的翻新。最开始通过人工制作 Unity UI Prefab 的方式,这种方式效率就会比较低,来回沟通核对的成本就会比较高。同时我们做出来的 UI,因为同一个控件在不同的 UI 上面重复编辑实现,导致我们统一性会比较差,细节品质上面体现出来会比较低一些。因为 UI 上的表现状态很多,一开始通过代码控制各个组合状态表现,这样导致我们代码比较繁琐,迭代优化效率比较低。


针对这些问题,首先是针对 PSD 自动生成 Prefab 的机制,借助这个工具,我们美术在设计好 UI 的 PSD 之后,直接通过这个工具转换成 Unity 里面的 Prefab,整个过程很高效,同时也节省了过程中大量的细节沟通成本。


UI 公共组件机制,在美术的 PS 里面为它们实现了一个 UI 的公共组件插件,通过这个插件,美术在设计 UI 的时候,可以直接从公共组件库里面去选择想要的组件。比如说各式各样的按纽,通用物品节点,类表样式框等等。在 Unity 这边,每个公共组件会对应一个预制体,在 PSD 导出层 UI 的 Prefab 的时候,会把这些公共组件替换成 Unity 的预制体的引用。通过这种方式,公共组件同一个组件只需要设计开发一次,这样我们的开发效率就得到了提升,并且我们也很容易对这些公共组件做迭代优化,从而能够最终提高我们的 UI 统一性,提升细节的品质。


针对 UI 状态表现多的问题,我们实现了状态控制器,状态控制器能够让我们很方便在各个不同表现状态之间进行切换,每个状态下会去控制编辑我们的 transform、图片、文字等等一系列的组件、参数。代码中只需要在各个状态之间做简单的切换就行了。这种方式,原来需要写大量代码才能实现的表现效果,现在可以在 UI 上、编辑器上做可视化的编辑,这样所见即所得的编辑。


Coding 环节

接下来讲一下 Coding 环节,因为我们项目开发主要是用 Lua 语言,所以为了实现和 Unity 对象的高效交互,我们实现了变量绑定的机制,这个机制的核心在于根据我们设定的 Game Object 的命名规则,它会自动去搜集需要序列化的组件,搜集起来之后,序列化到场景或者是预制资源里面。同时生成右侧图里面的 Lua 自动绑定的代码,通过这种方式,我们在 Lua 侧很容易操控我们想要操作的组件。在这之外还支持自定义序列化字段,比如说 Int/Float 这种参数,从而帮助我们去在编辑器下面调节我们组件的参数。


在变量绑定基础上,结合 Lua 对 class 的模拟,以及 Unity 本身预制体变体的机制,实现Lua 组件的变体机制,通过这个变体机制,就能够比较容易对我们现有的 Lua 组件进行集成和复用扩展。


开发中还往往会面对场景和界面管理复杂的问题,比如说经常会跳转层级很深,循环跳转,以及还原状态等等的问题。

在面临这些问题,我们就实现了导航系统,在导航系统里面会把场景和界面进行分组管理。我们的导航系统整体呈现是一个树型的管理结构,通常一个玩法模块会做一个分组的节点进行管理,同时在复杂的玩法下面,也能够支持我们多层嵌套的分组管理。

在这种管理机制下面,当我们要从一个玩法跳转到另外一个玩法的时候,我们业务上只需要关心,我去调用哪一个玩法的打开接口,导航系统底层会根据我们画面的完整性以及内存的占用等等情况,来决定是否要对上一个玩法进行隐藏或者是卸载。同时导航系统会帮我们记录一下我们打开的场景和界面的信息,帮我们记录一下我们的跳转路径。通过这样一些信息,我们在返回的时候只需要做关闭当前的玩法,导航系统就可以帮我们重新加载或者是还原前一个玩法。

在这基础上为了提升玩家操作的流畅度,避免跳转过程中有反复加载的行为,我们也是实现了场景和界面的缓存机制。并且为了应对不同设备内存大小不一样的情况,也实现了内存的分级控制。


最后,我们可以看到的这是我们导航系统的状态面板,这个状态面板会详细的呈现整个导航系统,以及所有场景和截面的详细信息,包括调用堆栈各种之类的信息,来辅助我们排查复杂业务场景下面的问题。

Debug


接下来讲一下 Debug 环节,在日志模块首先是实现了Lua Debug 日志开关和发布剪裁机制。在开发文件下面,能够通过我们的可视化界面去对我们 Debug 日志做文件级和模块级的开关控制,从而排除其他模块的日志干扰。并且在正式发布的时候,会把所有的 Lua Debug 日志从我们的代码里面剪裁剔除掉,这样开发文件下面就可以添加很多的 Debug 日志来辅助排查问题,不用担心造成其他的影响。

其次是我们的错误日志监控告警机制,对线上的错误都会进行告警和监控。当发生错误的时候,我们会第一时间通过我们的飞书进行告警,能够第一时间掌握这些线上的错误信息,同时也会在后台去把所有的错误信息统计起来,帮助我们去识别现在错误的影响范围有多大。

最后是我们的日志持久化机制,通过日志持久化机制,会持久化记录玩家的操作行为,以及相关的日志信息,结合我们的主动异常上报,玩家发生异常的时候可以主动上报客户端详细的状况,以及相关的日志,通过截图以及详细日志信息,就可以更好去定位解决我们线上的异常问题。


Debug 这一块我们还有一个时间机制工具,我们常常要去测试时间机制相关的玩法,这时候就实现了前后端一体时间修改和加速的工具,这个工具机制上面,首先是把逻辑时间和系统时间、表现时间进行了拆分,从而实现了我们去修改游戏时间或者是实现游戏的加速不会对系统时间和表现时间进行效果的影响。比如我们对游戏的逻辑时间加速 100 倍,这时候我们的表现不会有什么鬼畜的效果。

在这种情况下面,当我们要去测试一些周期性或者是持续性和时间相关的玩法,就可以对我们游戏逻辑时间做一些比较大的加速倍率,这种情况下可以比较快速测试一个游戏玩法周期,让它实现一个比较自然的过渡。


结合我们的数据备份还原工具,通过这个工具可以帮助我们快速还原问题的场景。比如说通过时间工具等,可以重现了我们的问题场景之后,我们能够把这个场景下面相关的一系列信息都备份起来,紧接着会做 Debug 调试以及做尝试性的代码修复。当我们需要再次去验证或者是调试这个问题的时候,我们只需要做一个一键的数据还原,就可以还原到我们的问题场景。

结合这两个工具,我们就能够比较高效的提升我们 Debug 效率。

性能


接下来讲一下关于性能方面提效的一些技术方案。

首先是渲染遮挡屏蔽机制,这个机制核心的作用是能够让我们不去渲染那些被遮挡、被覆盖的那些场景和界面。典型作用,当我们比如说打开一个 UI,去遮挡了下层 UI,或者是遮挡了下层场景之后,会去禁用下层 UI 或者是下层相机的渲染,从而减少性能消耗。

首先会在 UI 上挂载一个遮挡检测的组件,这个组件上面可以自动智能识别,也可以手动调整 UI 的遮挡和显示区域。运行时通过基于线段数的遮挡检测算法,从而去识别哪些 UI 现在被遮挡住了,我们就可以控制这些 UI,让它不去做显示。对场景上面,我们在场景相机上也会挂一个控制组件,通过这个组件发现场景当它被完全遮挡的时候,我们会去禁用或者是减少场景相机的渲染。


在这张动图可以看到,当场景上面打开了一个全屏 UI 之后,通过这种机制可以明显减少在这种情况下的渲染消耗。


在资源优化方面,实现了统一可视化的资源导入管理工具,通过这个工具我们在资源导入的时候可以做一些纹理尺寸合法性的检查,统一设置纹理压缩格式,mipmap 参数等等。同时对纹理质量我们提供了不同档位可供调节,这样在必要情况下可以把部分的纹理设置成更高的质量。我们也会对高质量的纹理做一些比例控制,防止这一块纹理的质量失控,从而取得我们调整的灵活性和纹理质量性能的平衡。


在压测这一块,我们实现了前后端一体化的压测工具,这个压测工具既能够实现后端性能的测试,也能够帮我们测试前端多人同屏这种性能热点情况下面的消耗。核心思路,基于流量回放的思路,采用 Locust 的性能框架测试框架来实现。最核心的两点是操作录制和操作回放

具体流程上来讲,我们会采用一个或多个录制用户进入我们的游戏,这个用户在游戏里面去进行正常游戏的行为,我们会把这个用户产生的网络相关的操作转发到我们的压测工具这边存储起来,形成我们需要回放的一个流量数据,压测工具产生的这些压测用户,他们会按时间线去把这些需要回放的操作、通信消息,提取出来,去按照时间线去执行,从而和我们的游戏服务器进行交互,进而产生压力。

这种实现方案的核心好处就在于,第一个我们大部分情况下面,通过这个工具不需要编写任何的代码就可以实现我们业务的压测,在业务改变的时候,也不需要维护我们的压测工具。第二个通过流量回放的方式,可以最大程度贴近我们真实用户的行为。


采用这个技术方案有两点核心的挑战。

第一个是随机性和全局数据的影响。像游戏里面的战斗一般都会有随机性,战斗这种随机性带来的影响可能导致比如说我录制的时候这个战斗是胜利的,但我们回放的时候这个战斗可能是失败了,这种失败可能导致我的压测用户没办法顺利按照我录制的消息,能够顺利执行下去。针对这个问题,核心解决思路是,我们要在服务端去做个人玩法业务逻辑的隔离,因为我们的服务端采用的是 Erlang 模型,Erlang 语言天然自带 Actor 模型,我们通过 Erlang 语言天然的 Actor 模型的支持,能够很好去隔离不同用户的后端逻辑。在这基础上,实现了各个用户的隔离之后,就可以对每个用户去设定他们的初始随机种子,当我们把压测用户和录制用户采用相同随机种子的时候,这时候就能控制这个随机性,从而达到我们想要控制随机性结果的目的。

全局数据的影响,典型就是全局的唯一 ID。唯一 ID 因为有全局性,所以通常分配一个唯一 ID 会影响到另一个唯一 ID。这种情况下我们的解决方案是这样的,我们以玩家抽卡为例子,当我们的录制用户去做一个抽卡行为的时候,服务端这个时候会生成一张卡牌,这个卡牌具有全局的唯一 ID,这时候会把录制用户的 ID 作为前缀去生成这张卡牌的唯一 ID,这样后续在录制用户再去发起升级这张卡牌请求的时候,我们消息里面就会带有卡牌的唯一 ID,去通信到服务端,服务端去查找这张卡牌,进而升级。

压测用户在去回放这整个操作过程中,首先会回放一个抽卡的操作,抽卡的操作也会在服务端生成一张卡牌。根据唯一 ID 生成规则,我们会把压测用户的 ID 作为新的这张卡牌的 ID 前缀。在这种情况下面,我们的压测用户再去回放卡牌升级这条消息的时候,我们会把消息里面的卡牌 ID 做一个规则的替换,把它替换成实际压测用户抽到这张卡牌的 ID,通过这种方式,就能够实现压测用户即使有受全局性的影响,也能够正常执行回放逻辑。

第二个挑战是交互玩法消息回放,对于交互玩法可能存在的问题是,比如说我们添加一个好友,录制的时候添加一个人为好友,回放的时候可能是所有的机器人,压测用户都会去添加同一个人为好友。这个问题对此我们的解决方案是,针对我们的交互类玩法会编写针对性的消息转换逻辑。这个转换逻辑可以选择在服务端测试环境下面去编写这种代码,通过这种代码可以实现我们压测用户随机选择不同的人去添加好友。


最后是线上性能监控,对于性能优化来讲,能够真实掌握线上用户的真实数据非常关键,所以我们对线上性能做了全盘的监控,既能够监控崩溃的情况,也能够掌握玩家的设备分布、质量分级等数据,以及更大数据层面的帧率、卡顿、加载耗时这些信息,通过这些信息我们能够设定我们的优先级,优先及时解决我们一些性能的热点问题。

好的,今天我的分享到此结束,谢谢大家!

Unity 官方微信

第一时间了解Unity引擎动向,学习进阶开发技能

每一个“点赞”、“在看”,都是我们前进的动力


ad1 webp
ad2 webp
ad1 webp
ad2 webp