上一篇中,我记录了帧同步相关的技术点、设计思路与思考。
这一篇中,着重讲述项目开发过程中,所涉及到的自动化开发工具。这里向我刚入行时的制作人致敬,他曾不断告诫我,“提升效率最佳的手段就是实现自动化”。
开发工具框架
开发工具,实现它们的目的就是为了提升开发效率,无论这个工具介入的是游戏开发的哪个流程的哪个阶段。
很早之前,我们会习惯性把开发放到各个零散函数中,并绑定按钮。但是当这类功能变多之后,也会带来不便。比如有些功能仅名称无法完全获悉其含义,此时就需要额外的一些说明;还有一些使用UI展现关键信息会更合适,也需要进行此类扩展。正式基于这个诉求,我才开发了一个简易开发工具UI框架,便于这类辅助功能的开发与使用。
首先进行说明,这套框架内各种逻辑调用大多通过反射实现,这是因为在开发环境下,一切以最方便的方式进行开发,且反射机制要求将工具代码扩展自基础接口即可,方便工具代码文件的管理。
这里需要介绍几个接口与类:
ToolEditorWindow
是一个继承自EditorWindow
的类,核心作用就是在OnEnable
方法中通过反射获取所有继承自接口IEditorWindowItem
的类型,并创建实例放入队列中。队列中的实例则会在ToolEditorWindow
的OnEnable
与OnGUI
方法中被调用对应的方法。
继承自IEditorWindowItem
的类,核心负责的便是在OnGUI
中实现对应功能相关的UI数据显示。
以通讯层代码自动生成为例,它的OnGUI
方法中就包含了下面的几个部分:
- 目标代码生成路径显示
- 代码生成按钮
根据具体需求,这里还可以在UI代码中添加多种需要的数据。总之它的核心目的就是将我们开发的零碎工具进行整合,并进行相关信息数据的展示。
数据文件的远程增、删、改、查
在项目开发中,有很多需要围绕外部数据展开的辅助功能。比如战报,既可以存储战斗的过程信息,又可以作为战斗复盘依据,如果其他客户端出了战斗bug还可以通过战报进行复盘与检查。所以,这就需要我们建立一个中心,以便于收集这些数据,而客户端则可以通过开发工具去中心当中增删改查对应的数据文件(其实我也在思考如何实现去中心化,否则随着数据越来越多,对中心节点的数据存储容量会是一个严峻的挑战)。
基于此,我用go开发了一款中心节点程序部署在我们的开发服务器上,而客户端则开发有远程文件工具类。通过继承该类,就可以从中心服务器上指定的相对路径下上传、替换、显示、删除文件。
远程文件工具类提供了以下核心方法:
OnGUI,显示当前指定路径下的文件列表信息
显示指定路径下文件列表
刷新指定路径下文件列表
显示单个文件信息(虚方法,可重写)
借助以上几个方法,尤其再依据需求派生单个文件展示的功能,即可实现各文件模块的远程文件的显示、上传、下载、删除等操作。
但是工具毕竟不是万能的,核心是如何利用工具。正如战报文件,只有战报文件本身设计足够出色,这套远程文件的管理模块才能发挥出它最佳的作用,否则这些代码也只能是个摆设。
代码自动化生成
代码的自动化生成,本质其实是将我们平时需要重复编写的且遵循一定规律的代码进行自动化处理,使我们得到解放。
以通讯层代码为例,核心仅仅是数据发送与接收的方法,而各种消息协议实例的生成、回收、重置、拷贝代码本质其实是一样的,仅仅因内部变量不同而有所区别。这种情况下,就适合采用自动化生成代码的方式去协助我们开发。而且当协议发生变动或新增时,我们只需点击自动生成即可完成,而无需人工编写代码,省去了不少可能产生bug的点。
自动化生成原理简介
我这里所写的代码的自动化生成,还是以硬编码的形式去实现。通过编写代码模板,依据不同的内容生成对应的代码再插入模板既是一段自动化生成的代码。通过不断嵌套这一形式,我们就能完整实现一个类的自动化代码。
以通讯层代码自动化生成流程图为例,自动化的过程就是不断遍历协议类型、再在内部不断遍历字段类型的过程。通过不断将自动化生成的代码嵌入模板,即可得到最终的自动化生成类
我们需要着重强调C#的关键字partial
。基于partial
关键字,将自动化代码完全放在一个独立文件中才成为可能,这也便于我们对代码的开发与管理。还是以通讯层为例,发送与接收方法是实现后便不变的,那么他们就可以单独放在一个文件中;而自动化生成的代码则放在另一个文件中,这样就可以做到互不影响。
自动化生成需要用到大量的反射语句,尤其是获取类型。在通讯层代码的自动生成逻辑中,我们既需要通过反射获取所有协议类型,又需要通过反射获取单个协议内数据字段的类型以便自动补充其重置与拷贝的代码。
借助属性进行反射
借助属性是代码自动化生成的重要手段之一,而这里又分为两种形式。
标记自动化方法
通过属性标记自动化方法,可以通过反射在某个指定位置去获取这些自动化方法并进行统一执行。
以ECS代码的自动化生成为例:
ECS中核心需要手动维护的,是一个组件枚举,这个枚举中的每一个值都是一个组件类型。那么当我们新增一个组件时,我们会需要生成组件类型、生成实体与组件关联的方法、生成组件特定的缓存池等等代码。我们可以看到,由此引发的代码自动化生成已经包含了三个模块,即组件类自动化生成模块、实体获取组件自动化代码生成模块、组件缓存池代码自动化生成模块。
上述三个模块可以按照之前自动化代码生成策略去实现,但是我们需要他们各自实现一个自动化生成方法的入口,并标记为某个我们指定的属性。这样,我们就可以在某个统一的地方去通过反射获取这些方法并统一调用。当组件枚举发生变化时,就可以通过调用这个统一的方法去刷新组件相关的自动化代码,而无需人工参与。
此处也有一个细节,就是可以通过md5的形式去记录组件枚举版本,并在发生变化时自动执行这一方法。但是我在项目中并未实现,可以留待后续按照这种设计去完善。
标记字段或类,由自动化代码过滤
自动化生成代码的逻辑中,也可以通过判断类或字段的指定属性来完成我们的某些特定需求。
以ECS系统代码的自动化生成为例:
前面的文章我已记录了ECS的系统需要按照指定的顺序去执行,那么这就需要某些手段能够为系统排序,属性就是一个好的方法。
我设计了系统排序属性,内分两个字段,分别是系统所属的组,以及组内排序id。
当我们自动化生成系统的注册代码时,就可以获取所有系统的属性,按照先组后组内排序id的逻辑对系统进行排序,再一次生成注册代码。
这里顺带介绍系统排序的设计思路:核心就是系统所属的组,如果没有这个,单纯靠一个id对系统进行排序,从逻辑上是混乱的。我们可以将系统人为分为时间层、物理层、逻辑层、结束层等概念,将各个系统按需分配到所属组内,再在组内依据需求进行id排序。通过这种形式,就可以较为清晰地将系统进行排序,也便于我们对系统的管理。
如果系统排序出现冲突,即两个系统无论是组还是id都一致的情况,这时候就需要我们的自动化组件去进行异常抛出,并指出问题出处以便于我们修改。
再以组件回收自动化代码举例:
我们这时需要用指定属性去标记组件内的字段。当自动化代码去生成组件的回收代码时,会将标记有该属性的字段进行重置,没有的则不处理。
此外,每个字段重置也会有区分:同样是int字段,有些可能需要重置为0,而有些可能又需要重置为某个指定值。这时,这个字段的另一个作用便可以得到体现,就是说明该字段的重置方案。通过在自动化代码中对重置方案进行逐个判断,便可生成我们想要的组件重置代码。
以上,就是我对这些年卡牌战斗开发工作所总结的经验与思路。后续若有时间有精力则会继续内这里面的内容进行细化。
写这几篇记录文集的目的,也是希望有朝一日重新开发此类项目时,可以按图索骥,快速搭建起开发流程与架构,省去重复造轮子的设计时间成本。如果后续无论工作中还是交流中发现了内容的不足,我相信我会回来补足它们。
如果有人看到这些文章并对你有所启发,那么我相信我这些年的工作也不算全然无用了。
—— 卅川,完结于2022-6-28