我正在开发自己的游戏引擎。尽管这是一个个人爱好项目,但我已经成功发布了一款使用我的引擎的游戏。我的第一款游戏发布后,我想创建一些工具,使用户能够修改我的游戏并创建自己的游戏。我的愿景是为非技术用户创造一个易于使用且直观的用户界面:一个10英尺的UI和游戏控制器作为首选输入方法。所有免费和开源。

这变得成为一个单独的爱好项目的巨大挑战,特别是引擎支持嵌入式脚本、3D世界、模块化架构和完全数据驱动的功能。 我一定低估了让我的引擎超越仅仅是一个游戏运行时所需的工作量,但也非常值得看到一切都在一起。

然而,这个项目是非常孤独的经历。 我无法与我的女朋友、家人或朋友讨论我的项目,因为他们不会理解我在做什么或为什么在做。 他们不会欣赏我的设计的美感或我的架构的优雅,甚至不会对技术细节感兴趣。 除了展示给非技术人员之外,直到它准备好实际使用和演示之外,没有什么有趣的东西可以展示。

因此,当孤独感开始影响我时,我问Google Gemini进行一次模拟面试,感兴趣我的引擎。 与通常的“问题和答案”的角色倒转,使用LLM进行一次模拟面试是一种令人愉悦的体验,完全不考虑任何我从中学习的东西。 它使我在设计模式使用、架构决策、性能考虑、工具等方面感到紧迫。 它很有趣地自豪地告诉Gemini一些真正的人不理解的事情,并且有助于我对项目进行自己的思维框架。

但是,有一道特别的问题让我完全措手不及。 Gemini之前的问题是关于编辑/工具功能的抽象,所以我已经解释了我的组件属性接口。 Gemini的后续问题是这样(直接复制):

"您的getProperties模式是否双重作用于序列化?换句话说,是否可以使用相同的属性元数据来自动序列化定义数据到JSON时,当用户保存他们的级别时,还是您有一个单独的管道来保存/加载?"

在短暂的混乱和思考之后,突然之间我明白了。 为什么我以前没有想到这一点!

看起来,我已经实现了我的引擎的组件加载和保存接口。 加载发生在构造函数中,它们接受JSON对象,而保存则实现为函数,它们也接受JSON对象。 当它最终来到编辑时,我只是理解了我需要一个第三个接口函数来读取组件的值(以在UI中显示)并修改这些值(以允许用户更改它们)。 从这个需求自然而然地来到了我的属性接口。 但是我从来没有想到改变现有的load/save功能,或者我实际上已经创建了一个统一的属性反射系统而不自知。

但Gemini确实是正确的。 如果我有一个“组件加载器”来传递给我的“getProperties”函数,而不是我的当前“组件编辑器”,那么组件加载器可以从组件中取出每个属性并根据当前JSON对象的内容修改它们的值。 对应的“组件保存器”可以做相反的事情。

因此,我开始工作于我的新组件加载器和组件保存器,并且,我的引擎组件现在可以完全通过之前只用于编辑的属性接口来加载和保存(事实上,一些实现需要进行一些小的调整,但没有重大更改)。 这意味着我现在可以清除掉55个组件的冗余构造函数和保存函数。

但是,我决定进一步推进。 我开始注意到,我的所有组件类都没有任何提及或引用JSON。 通过加载和保存通过属性接口,我的组件已经脱离了文件格式。 这让我想起了如何将这个想法推到它的逻辑结论。

如果我可以在整个引擎的所有层面上实现我的属性接口,我可以完全脱离引擎和文件格式。 然后,如果有人想用我的引擎支持YAML、TOML、一个二进制格式或其他什么东西,他们可以创建自己的YAMLComponentLoader和YAMLComponentSaver实现来读写我的引擎的应用程序,而不需要在引擎内部改变一行代码。

因此,我继续推进这一点,并且,结果比我想象的还要好! 通过在引擎的所有层面上实现属性接口,我现在可以重用我的编辑工具的“组件属性菜单”来处理整个应用程序的编辑。 因此,相反于拥有特殊菜单类,如“应用程序菜单”、“模块菜单”和“组件类型菜单”,我现在可以简单地将主应用程序属性传递给组件属性菜单,然后我的编辑工具可以自然地遍历和操纵整个应用程序!

移除这些特殊菜单类的需求,使我能够清除更多的类和代码。

最终,我最终减少了我的代码库的大小约8,000行,约10%。

但这并不是(仅仅)关于减少代码库的大小。 开发新引擎组件变得更加容易,因为现在你只需要实现属性接口,然后你就可以免费获得加载和保存! 这意味着由于忘记或忽视实现构造函数、保存或属性函数而导致的潜在错误大大减少。

它还帮助改善了现有的组件:一些我使用较少的、更专业的组件还没有正确或工作的属性接口,所以将组件转换为ComponentLoader需要在我的现有项目不加载或工作正确之前修复这些实现。 一旦完成,那些组件也可以通过我的编辑工具由用户配置。

总体而言,我觉得这是一个巨大的成功,可能是自己没有想出来的。 我的引擎感觉干净、更少bug、更易于维护、更接近我的目标。

最终,通过这种方式,我不仅仅是清除了一些冗余的代码,还更重要的是,我更接近了我的目标。