所以我在过去几个月里一直在实验 GM 的插件系统,想把我学到的一些东西分享出来。
插件仍然相对新颖,除了 beta 分支中的 Prefab 生成器外,几乎没有官方文档说明如何创建插件。因此你必须自己摸索,深入 GM 进行实验。请记住,这不是 GM 官方“认证”的路径,它只是我自己在实验时留下的笔记,整理成供他人阅读的内容,以便在 GM 开发者正式公开发布系统之前,大家就能先了解插件的工作原理。
我的实验直接导致了 Sundial Visual Editor(一个可以在 GameMaker 内部运行的日夜周期编辑器,用于 Sundial 的作者工具)以及其他一些尚未发布的东西。
请注意,阅读本文在我的实际站点可能更方便,因为排版更好,而且还有其他有趣的内容可以探索:Experiments with GM Plugins (or How I Learned to Stop Worrying and Love Reflection)
制作插件的过程很有意思,充满了未来可能实现的酷东西,也有很多咒骂和牙gnashing(咬牙)——因为很多东西不按我想的工作,或者奇怪的小缺陷层出不穷。
那么插件怎么工作?
大多数人想到扩展 GM 能力时,会想到插件(extensions),这些已经存在很久了,但插件本身是完全不同的东西。
插件的作用不是在游戏内增加功能,而是扩展 GM 本身。它们使用 C# 与 .NET 编写。
一个插件通常包含一个编译后的 DLL、一个 .gmplugin manifest(描述文件),以及可能需要的其他资源,比如用于显示的 XML 布局或图片(例如 Sundial Visual Editor 的时间轴缩略图图标就是插件里的资源)。
GM 加载插件后,你可以注册命令、添加菜单项、创建编辑器窗口,甚至 hook 到各种 IDE 事件,总之,你可以“控制” GM 本身。
为了明确说明,你并不是在构建一个独立的系统,而是直接使用 GM 自身的界面系统,就像原生的 Sprite 编辑器、Object 窗口等。
构建界面
GM 使用 XML 布局来描述其界面(窗口、网格、按钮等),并用 C# 提供行为。你在插件中也可以如此实现,这正是 Sundial 为你提供的相当完整的编辑器,感觉就像 IDE 的原生组件。
Sundial 主要使用 GM 已有的控件,但在某些更复杂的部分(如时间轴和图片预览)则需要自定义渲染组件。
一个让我多次困扰的问题是,GM 有多种不同的窗口类型,它们 不 是可互换的。
例如,普通的浮动工具窗和作为工作区附带的编辑窗,以及通过 GM 那条细细的绿色线条关联的子编辑器,都是不同的窗口类型。
乍一看这似乎很明显,但你可以创建一个完全合法的窗口,却与所需的编辑器行为全然不同。系统不会提示你“伙计,你用错了窗口类型,请改用这种窗口”,它会直接不渲染或按自己的逻辑处理。
比如,在早期测试中,我创建了一个插件能够找到并交互的窗口,但它根本不会出现在屏幕上——这花费了我很久的时间才解决。
原因是 XML 布局的结构:我误把它当作嵌入组件,而不是独立窗口。
所以,如果你自己制作插件,请记住这一点。
与 IDE 交互
另一个有趣点是,你是在一个拥有自己资源管理、状态以及生命周期的应用内工作。GM 有其自己的行为,你需要做好准备。
例如,Sundial 为每个项目单独存储设置,并挂钩到 GM 的常规项目保存行为进行保存。
我在用户编辑时记录设置变更,若有改动则标记项目为“已修改”,并在用户保存时才实际写入改动数据。这看似简单直接,但需要考虑以下情况:用户在不保存的情况下关闭编辑器;切换项目时会发生什么;卸载插件会有什么后果?
插件的职责不仅仅是向用户展示自己,还必须正确参与 GM 的常规操作,包括在其生命周期结束时自行清理。
quelques conseils de développement
最大的收获是 在尝试复刻 GM 的功能之前,先弄清楚 GM 本身是如何实现这些功能的。
拥有对 GM 本身的了解至关重要:当我想要实现“下拉菜单”之类功能时,我会先思考“GM 中哪个原生窗口在实现类似需求?”然后 复制其实现,每次我自以为是的去直接实现而不先检查时,都会因为 GM 的某些细节、组件生命周期或其他 quirks 而吃面。
同样,当界面行为不符合预期时,也不要猜测自己能调用什么、该如何使用。进行小型探针实验(使用 C# 的反射)去获取类型、方法、字段等信息,这些在当前插件文档缺失的情况下尤为宝贵。我可以在运行时列出所有可用的组件,检查后确定哪些行为是合理的尝试,从而大幅减少早期编辑器实验的试错成本。
还需要记住,IDE 与运行时是完全分离的环境。
仅因为你的插件能在 IDE 中操作资源,并不意味着你自动扩展了 GM 的编译器或运行时,使其能够认识全新事物。我曾尝试自定义资源,能在 GM IDE 中创建自定义资产,但 将其在运行时的游戏中使用 却极其困难,因为 GM 在编译阶段有硬编码限制。
唯一能实现的办法是让项目在插件未加载时被 GM 判定为“已损坏”。这并不理想,哈哈,所以我希望将来 GM 能在资产和插件交互方面提供更灵活的机制,而不必把项目弄到破损的地步。随后我发现了一些其他“黑客式”手段,在不破坏项目的前提下把自定义资产塞进编译过程,这也算小技巧,但它们不是真正的自定义资产,和最初的实验不一样。
对于 Sundial,编辑器负责 IDE 中的可视化作者工具,而实际的日夜调度则由一个完全普通的 GML 代码库完成。
保存时,我会遍历项目并使用 project.FindResourceByName(name, type); 检查代码库所需的资源是否齐全;若齐全,则将预设数据写入库中某个对象的变量定义里。运行时的代码库读取这些变量,从而建立 IDE 与游戏之间的桥梁,使视觉编辑和代码库能够独立工作。
分发插件
GameMaker 的 Package Manager 与常规包管理器相似,支持 npm 兼容的注册表,并可自定义来源,这在后续将会用到。
打包并正确签名插件需要几个步骤,由插件作者自行处理。但使用插件的玩家只需指向相应的注册表下载即可,无需自行安装 .NET、npm 或其他开发工具(这与 beta Prefab 生成器的基本流程相同)。
在开发时,我使用本地注册表(如 verdaccio)来快速测试安装和更新流程,而不是每次手动拷贝文件。
另一个需要注意的点是:如果你把插件发布到标准 npm 注册表,它不会提供给 GM 正确的元数据,这意味着虽然插件仍能工作,但无法展示诸如图标等细节(我当时找不到让图标在包管理器中显示的方法),导致界面看起来比较丑陋或通用。为解决此问题,我自建一个中间页面(免费部署在 Cloudflare),该页面从 npm 获取包数据,并额外提供 GM 所需的元数据,使插件在包管理器中显示得更整洁。这虽然是个令人恼火的绕路办法,但我作为专业且固执的人,不能容忍插件缺少炫目的图标。
最后,要考虑版本兼容性。由于插件本身文档缺失,且依赖于 GM 的 IDE 接口,每次 GM 更新都有可能导致插件失效。直至 GM 官方正式公布插件交互的行为规范和接口,插件仍属于一种“黑客”手段,类似于在大引用更新之前对资源 ID 进行数学运算的做法,而那次更新同样破坏了许多工作流。
因此,你需要随时关注更新,并在每次新版发布后测试你打算使用的插件,确保它们没有被破坏。
Anyway!
插件很酷,能以 incredible 的方式扩展 GM,我在此过程中不断发现新玩法。
目前为止,我尝试过以下几方面:
- 扩展已有编辑器
- 创建新的工作区窗口
- 处理项目资源
- 构建更专业化的界面(Sundial 即是此类示例,拥有自定义绘制以匹配游戏内视觉)
我感觉我才刚刚触及表面。若有时间和耐心,能做的事情还很多。希望在官方终于提供官方、可靠的插件创建方式(比如 Sundial Visual Editor)后,所需的时间和耐心会大幅降低。
如果你读到这里,下面还有几篇教程可能感兴趣:
- Let's build a basic state machine
- How to use Signals in game dev
- How to (comfortably) deal with modifiable stats in RPGS (and other genres)
- A beginners series on procedural generation
A brief plug
在本文中,你已经多次看到 Sundial Visual Editor 的实际效果。它免费下载并试用,只需按照以下指南安装:
通过安装,你可以直观感受插件如何扩展 GM。
Also, all my libraries are 50% off for the itch Halloween Sale, so check them out. 我很少打折,这次是 huge saving,如果你对任何库感兴趣,请把握机会。
我的站点:RefresherTowel Games libraries
https://refreshertowel.games/libraries/
我刚刚发布了 Sundial,一个酷炫的日夜循环系统,让你能为一天中的不同时段打造特定且视觉 striking 的氛围,同时提供回调、遮罩系统(区分室内外区域并分别调光)等众多实用功能。
Sundial's Day/Night Cycle in my little bee sim game
最重要的是,你可以使用 Sundial Visual Editor 在 GM 中编辑整个外观,无需启动游戏即可预览,并且只需一行代码即可启动:
SundialStart();
想要停止时:
SundialStop();
如果你是初学者或讨厌代码的艺术家,这就足够了——简单、易用、质朴!
更多关于 Sundial 的信息请参阅:Sundial Docs
评论 (0)