首发于 Fusion Gamers
『Clickteam Fusion插件开发基础』

『Clickteam Fusion插件开发基础』

引言

虽然Fusion提供了上百个第三方插件,但总会遇到你的需求所不能满足的情形。

虽然Fusion提供了一套功能强大的事件系统,但总会遇到使用事件处理会比较繁琐的复杂逻辑。

虽然……

……

自己动手,丰衣足食。与其回避问题或等待他人帮忙解决,不如动手开发属于你自己的插件。 Fusion能够帮你回避具体的代码实现,但是不能够帮你回避思考的过程 。所以,不必害怕满屏的程序代码,时至今日,你已经具有了 最基本的编程知识 。只要你有耐心和信心,以及一台连接了网络的电脑,就能胜任最基本的开发工作。


环境配置

安装Visual Studio

首先,我们需要安装Visual Studio。针对个人用户,使用Community版本即可绝大部分满足需求。自Visual Studio 2017起,微软提供了全新的Visual Studio Installer,允许你在线安装所需要的部分,而非像过去那样使用镜像进行完整安装。

开发Windows平台的插件需要配置C++与Windows SDK这两个工作负载。正确选择并指定安装路径后,点击安装,剩下的动作交给Visual Studio Installer就好了。需要注意的是,尽管新版本允许你指定安装路径,但仍旧会有大量的文件需要安装至C盘,在安装前请务必确认C盘与目标磁盘中有充足的空间。

需要C++桌面开发与Windows SDK的工作负载

下载SDK

我们可以在Clickteam官网上下载到针对Windows、iOS、Android、H5等平台的Extension SDK。本文以Windows SDK为例,简单说明插件的开发流程。如果有进一步研究其他平台的需求,请自行查阅相关文档。

除了官网外,你还可以在GitHub上获取SDK:

Gitub上还托管有部分开源插件与第三方SDK,例如DarkEDIF系列,如果有需要可自行参考学习:

配置项目

SDK解压缩后共有三个文件夹,其中Extensions为插件模板,Inc为头文件库,Lib为库文件。除空模板Template外,SDK中还有提供有Control、Data Buffer、Picture与Text四类常见模板。

Extension SDK

以Extensions/Template为例,进入该文件夹,双击打开解决方案(.sln),VS会自动将其迁移到最新版本的工程。转换结束后,会给出一个迁移报告。一般来说并不会出现阻塞性错误,我们可以简单的忽略它。

迁移报告

同时,我们需要将工程的Windows SDK版本更新为先前所安装的版本,否则工程在编译时会提示“找不到目标版本的SDK”。

更新Windows SDK版本

工程通常能够正确获取到头文件与库文件的路径,如果未能正确获取,编译时会提示错误,可以在项目属性中自行配置:

  • 在项目属性页→C++目录选项卡下,配置包含目录为头文件库目录,配置库目录为引擎库目录。
配置VC++目录
  • 在项目属性页→连接器→输入→附加依赖项中,添加库文件mmfs2.lib。
配置附加依赖项

一切顺利的话,按下F6启动编译,稍后片刻,我们就能获得一个崭新的空插件。

问题解决

通常来说,VS给出的警告并不会影响我们的开发工作。如果一定要移除它们的话:

1、“Gm”选项已否决

cl : 命令行 warning D9035: “Gm”选项已否决,并将在将来的版本中移除

在项目属性页→C++→代码生成中,禁用最小重新生成即可。

禁用最小重新生成

2、属性值不匹配

...\IDE\VC\VCTargets\Microsoft.CppBuild.targets(1216,5): 
warning MSB8012: TargetPath(...\Extensions\FG\.\Obj\Unicode\Debug\Template.dll) 
与 Linker 的 OutputFile 属性值(...\Extensions\FG\Obj\Unicode\Debug\Template.mfx)不匹配。这可能导致项目生成不正确。
若要更正此问题,请确保 $(OutDir)、$(TargetName) 和 $(TargetExt) 属性值与 %(Link.OutputFile) 中指定的值匹配。
...\IDE\VC\VCTargets\Microsoft.CppBuild.targets(1217,5): 
warning MSB8012: TargetExt(.dll) 与 Linker 的 OutputFile 属性值(.mfx)不匹配。这可能导致项目生成不正确。
若要更正此问题,请确保 $(OutDir)、$(TargetName) 和 $(TargetExt) 属性值与 %(Link.OutputFile) 中指定的值匹配。

在项目属性页→配置属性→常规中更改目标文件扩展名为.mfx即可。

更改目标文件扩展名

3、忽略“/EDITANDCONTINUE”

Edittime.obj : warning LNK4075: 忽略“/EDITANDCONTINUE”(由于“/SAFESEH”规范)

在项目属性页→C++→常规中,修改“调试信息格式”即可。

修改“调试信息格式”

4、WARNING

************************ WARNING ****************************
***** Do not forget to change the IDENTIFIER in Main.h! *****
*************************************************************

更改Main.h中的标识符(详见下文)后,移除Pragma Message即可。

移除Pragma Message

至此,我们已经完成了基础环境的配置,下面便可以开始正式的开发工作。在此之前,我们需要了解插件的文件结构:


文件结构

除Visual Studio工程相关文件外,一个常规的插件由以下文件组成:

1、Common.h:

该头文件包含全局函数与变量的定义,并允许你限定插件所适用的引擎版本。

全局函数的函数原型必须声明于Common.h中,否则会出现“找不到标识符”错误。全局变量同理。与C/C++声明全局变量的方法一致,在需要头文件中以extern修饰,并在相应的Cpp中进行定义,否则会在链接时报错“无法解析的外部符号”与“无法解析的外部命令”。

你所需要的其他头文件(例如经典的iostream.h与vector.h)也需要从此处包含。

2、Edittime.cpp:

包含编辑时由场景和事件编辑器调用的功能。

3、Ext.def:

用于链接器的导出定义文件。因为Fusion仅通过.def文件,而非通过名称修饰调用方法,因此必须在此处列出引擎需要访问的所有功能。请确保在该文件中移除所有你调用的引擎内置方法前的分号。关于名称修饰,可参见MSDN。

4、Ext.rc:

工程的资源文件,包含该对象全部的字符串、图像与对话框。

5、General.cpp:

包含编辑时和运行时均被调用的流程。

6、Main.h:

插件的定义文件,包含编辑于执行对象所必须的EDITDATA与RUNDATA结构体。

7、Main.cpp:

包含插件对象中全部的动作、条件与表达式。

8、Runtime.cpp:

包含只在运行时调用的必须函数,包括调试器流程、对象绘制流程等。

9、Resource.h:

资源定义文件,包含资源文件中不同元素的识别符。

10、Res:

  • ExtIcon.bmp: 用于在场景和事件编辑器的对象窗格中显示对象图标。若使用MakeIconEx函数手动绘制图标,则可以移除该文件。
  • ExtImg.bmp: 用于在场景编辑器中显示对象图标。若使用EditorDisplay函数手动绘制图标,则可以移除该文件。

定义插件

定义适用版本:

在Common.h中,可以定义插件的适用版本。例如,可设定插件为仅适用于专业版;或限定插件仅允许在某个版本之上的Fusion中运行。

#define	MMFEXT	// 适用于标准版与专业版/开发者版
//#define PROEXT// 仅适用于专业版/开发者版
//允许的引擎最低构建版本
#ifdef _UNICODE
	#define	MINBUILD	292
#else
	#define	MINBUILD	292
#endif

定义 标识符:

每个插件都应该有 独立 唯一 的标识符,该标识符用于Fusion在用户复制粘贴与该对象有关的动作、条件与表达式时正确识别对象。如果两个插件使用相同的标识符,引擎会将两者混淆,无法正确对应复制的事件。因此,创建插件对象的第一步,便是设定一个 唯一的标识符

标识符定义于Main.h,默认的标识符为:

#define IDENTIFIER	MAKEID(S,A,M,2)	

修改位图资源:

图标以外部位图文件的形式定义

EXO_ICON (Exticon.bmp):场景编辑器和事件编辑器中对象窗口中显示的图标。第一个像素提供透明颜色。 
EXO_IMAGE (Extimg.bmp):用于在场景编辑器中显示对象。第一个像素提供透明颜色。

定义 字符串:

在资源文件Ext.rc中,可以找到在引擎内显示于对象属性页关于选项卡中描述该对象的字符串。

KPX_NAME & IDST_OBJNAME:插件名称
IDST_AUTHOR:插件作者
IDST_COMMENT:对于插件的简短描述,显示于场景编辑器的插入新对象对话框中
IDST_HTTP:插件作者的网站地址

除定义上述字符串外,也应该修改资源文件中的“版本”项,编辑每一行并输入正确的信息。当发布新版本的插件时,应当增加插件的版本号。

定义对象标志:

定义插件对象时,应决定改对象是否应该运动,是否拥有动画,是否绘制窗口,是否允许控制,或只是简单的让它不可见。根据实际的需求,于Main.h文件中定义改对象要使用何种属性,同时根据定义的不同,也需要相应修改tagRDATA结构体中的内容。

typedef struct tagRDATA{
	//必须的主结构体标头
	headerObject	rHo;
	//可选结构体
	//rCom	rc;		// 用于运动与动画的共用结构体
	//rMvt	rm;		// 用于运动
	//rVal	rv;		// 用于可变值
} RUNDATA;
typedef	RUNDATA	*LPRDATA;

例如,需要运动与动画,则需要rCom结构体;运动还额外需要rMvt结构体;显示对象需要精灵rSpr结构体;拥有成员变量需要rVal结构体。

OEFLAGS:引擎使用该标志来确定将该对象视为精灵或背景对象,确定是否拥有运动属性等。
例如:对象包含成员变量,不跟随滚动,不会被销毁,允许在场景叠化前创建,允许手动定义是否休眠(即不参与事件)。
OEPREFS:引擎使用该标志来决定在对象属性页中显示何种属性。
例如:滚动选项(对应不跟随滚动),销毁选项(对应不会被销毁),休眠选项(对应手动定义休眠)。

若存在多个标志,则不同标志间使用与运算符连接。同时,部分标志在OEFLAGS与OEPREFS中均需要定义。一个典型的定义如下:

#define	OEFLAGS
OEFLAG_VALUES|OEFLAG_SCROLLINGINDEPENDANT|OEFLAG_NEVERKILL|OEFLAG_RUNBEFOREFADEIN|OEFLAG_MANUALSLEEP
#define	OEPREFS
OEPREFS_SCROLLINGINDEPENDANT|OEPREFS_KILL|OEPREFS_SLEEP

插件执行流程

完成基本信息定义只是第一步,在正式进行开发之前,我们还需要了解插件的执行流程:

插件中每个A/C/E必须都有自己的插件流程,这个流程在相应指令被执行时由引擎调用。由于这些流程可能在程序运行时被多次调用,因此应令其执行的尽可能快。下文以Action为例进行简要介绍,Condition、Expression与其类似。

一个典型的Action的函数原型如下,其返回值应为零:

short WINAPI DLLExport Action(LPRDATA rdPtr, long param1, long param2)

参数如下:

LPRDATA rdPtr:指向运行中对象的结构体指针
long param1:第一个可能的参数
long param2:第二个可能的参数

param1与param2仅在你于actionsInfos中声明了特定参数后才会被定义。在获取参数时,可以直接将param1与param2赋值给对应类型的变量中。对于超过两个参数的场合,可使用CNC_GetIntParameter、CNC_GetStringParameter与CNC_GetFloatParameter等宏来顺序获取参数。

一个Action流程不允许在屏幕上直接绘制任何内容。正确的方法是通过

callRun-timeFunction(rdPtrRFUNCTION_REDRAW0,0)

方法调用DisplayRunObject流程,让应用程序在绘制下一帧时完成绘制工作。

此外,在对象生命周期内,Fusion会根据其生灭与状态自动调用相关方法。

例如,在编辑器内加载Action菜单时会调用:

HMENU WINAPI DLLExport GetActionMenu(mv _far *mV, fpObjInfo oiPtr, LPEDATA edPtr)

针对运行时,在创建对象时会调用:

short WINAPI DLLExport CreateRunObject(LPRDATA rdPtr, LPEDATA edPtr, fpcob cobPtr)

在销毁对象时会调用:

short WINAPI DLLExport DestroyRunObject(LPRDATA rdPtr, long fast)

每刷新一帧,针对对象处理,会调用:

short WINAPI DLLExport HandleRunObject(LPRDATA rdPtr)

包括上述方法在内,引擎SDK内提供了数十个调用入口以及上百个宏与回调函数,用于处理各种不同的状态。


A/C/E编程

说了这么多,终于到了实际操作的环节了。我们依旧以Action为例(Condition与Expression各自拥有与之不同的特殊环节,请自行查阅帮助文档),新建一个Action的流程如下:

新建对应方法

在Main.cpp中新建对应的方法

short WINAPI DLLExport ActionName(LPRDATA rdPtr, long param1, long param2)

并在其中进行功能的实现。

添加函数指针

在Main.cpp中对应的跳转表中添加新建方法的函数指针,即新建方法的方法名:

short (WINAPI * ActionJumps[])(LPRDATA rdPtr, long param1, long param2)
	{ActionName,0};

跳转表的结尾要求必定是零。

声明方法参数

在Main.cpp中对应的Action参数表中添加新建方法的参数。在事件编辑器内调用该动作时,引擎会根据此处的定义依次要求用户输入所需的参数。

short actionsInfos[]=
		{IDMN_ACTION,M_ACTION,ACT_ACTION,0, 2, PARAM_FILENAME2, PARAM_EXPSTRING,PARA_ACTION_1,PARA_ACTION_2,};

其顺序为:识别符、显示字符串、Action代码、方法标志位、参数个数、参数1类型、参数2类型、……、参数1字符串、参数2字符串。

定义参数

1、识别符: 识别符需要设定在对应的菜单选项中,确保引擎能够调用正确的指令。对于Action,识别符的值为25000~25999,Condition为26000~26999;Expression为27000~27999,定义于Resource.h中。

......
// Commands of action menu
#define IDMN_ACTION	25000
// Action strings
#define M_ACTION	5000
......

2、显示字符串: 显示字符串为定义了该Action后在显示于引擎事件编辑器中的字符串,定义于资源文件中的String Table内。字符串中支持通过%0、%1引用对应位置的参数。

实际显示效果

3、Action代码: Action代码定义于Main.h文件内,为当前Action对应的代码。需要注意的是,宏ACT_LAST的值必定为最后定义的Action Code+1,即Action总数。

4、方法标志位: 方法标志位用于Condition,例如EVFLAGS_NOTABLE代表可以使用该条件的非条件。

5、参数类型: 当前输入参数的类型,例如获取鼠标事件的PARAM_CLICK、获取数字表达式的PARAM_EXPRESSION、获取字符串的PARAM_EXPSTRING。你也可以选择自定义参数宏,引擎会根据宏定义自动引用你所定义的参数输入界面。

以PARAM_FILENAME2为例,在事件编辑器内获取该参数时,引擎会自动打开用以指定文件路径的对话框。

指定文件路径对话框

6、参数字符串: 输入参数时对话框标题栏中显示的内容。

参数字符串效果

编辑资源

1、切换到资源视图

在资源视图下,我们需要编辑Action所需的菜单项与字符串。在点击“解决方案资源管理器”选项卡旁的“资源视图”选项卡,即可切换至资源视图。

资源视图

需要注意的是,如果你在VS中以文本形式打开了某个资源定义文件,则VS无法打开资源视图,需要将所有定义文件关闭。

2、编辑菜单项

展开Menu菜单,双击MN_Action选项卡,即可非常轻松的完成菜单的编辑。

编辑菜单项

随后,我们还需要为其配置识别符,否则即使你在事件编辑器中选择了该动作,也不会有任何效果。

配置菜单项识别ID

3、编辑字符串

每个字符串都会对应一个唯一ID,而该ID通过宏扩展到一个可读的名称(即上文中的限定符,也就是我们在参数列表与菜单项中所引用的名称)

编辑字符串

插件调试

环境配置

为方便调试,修改输出目录为引擎插件目录路径\Extensions\Unicode下,即Release目录。

修改输出路径

设定参数

本节更新于2021年3月26日

命令为引擎内置的调试器所在的路径,本文中为“...\Data\Runtime\Unicode\edrt.exe”(如果你使用的是旧版2.5+且无法启动调试,请将edrt.exe替换为edrtex.exe,或更新引擎版本),工作目录为引擎根目录,命令参数为/f+CopyRun.bat提取的临时文件的位置,本文中为“/f D:\Temp\Rt\App0.ccn”

配置调试环境

编写CopyRun.bat

文件内容如下,具体功能为获取传入参数,并将该参数所表示路径下的全部文件复制到D:\Temp\Rt\路径下。

@copy %1\*.* D:\Temp\Rt\

将编写完毕的批处理文件放置于引擎根目录,回到Fusion,按住Shift并点击调试按钮,引擎会创建临时文件,并运行批处理,复制其到指定目录:

临时文件

返回Visual Studio,在对应的A/C/E方法内设定断点,并按F5启动调试,Fusion会自动运行目标文件夹内的临时文件。当执行到涉及到调试对应方法相关的事件时,就会转入Visual Studio。随后,我们就能够对当前调用的方法进行传统的单步调试。

在Visual Studio内进行调试

注意

  1. 在当前方法执行结束后,即单步至Return语句后,应让程序全速运行。否则,则会进入自动对引擎执行模块的调试,而这对于闭源软件来说是不可能的,Visual Studio会提示找不到对应的PDB文件。
  2. 在对应工程处于打开状态时,相应的.mfx文件会被占用,无法编辑。直接进行重新编译会提示错误。

插件发布

插件编译

插件工程的解决方案配置共有三种,Debug Unicode、Release Unicode与Run_Only Unicode,其中Debug Unicode用于调试,Release Unicode用于引擎编辑时使用,两者均安装于\Extensions\Unicode下,发布时仅需要后者;Run_Only Unicode用于编译独立可执行文件,安装于\Data\Runtime\Unicode路径下。是否包含源码等相关资源,通过在编译时判断RUN_ONLY宏是否被定义来实现。

需要编译两个版本的插件出于以下考量:

  1. 在编辑过程中使用的流程与资源并不需要在独立程序中使用,移除它们有助于节约资源。
  2. 当引擎编译独立可执行程序时,会将Run_Only的插件嵌入到应用程序中。运行时,这些插件会被提取到一个允许任何人访问的临时文件夹内,提供一个“ 不可编辑”版本的插件能够防止他人获取并在编辑器内使用。

根据应用程序目标平台的不同,Run_Only Unicode版本所需的文件格式也有不同的要求。例如*.mfx(Windows)、*.ext(iOS)、*.zip/.dat(Android)、*.js(HTML 5)。如果不存在对应的文件,Fusion则会在编译时提示“插件不支持目标平台”。若使用了版本不匹配的文件,则可能会导致运行独立程序时,新增的插件指令无效。

插件安装

通过Install Creator制作安装包

除直接向对应路径复制文件外,还可以使用Clickteam开发的Install Creator工具来进行安装包的制作。

在插件源文件根目录下的ToInstall目录内,已经提供了供Install Creator使用的临时文件。

通过Install Creator配置安装包

除两个版本的插件文件外,该目录下还存在Examples与Help两个文件夹。前者用于保存该插件的使用范例,后者用于保存该插件的帮助文件。插件帮助文件名与相对路径定义于GetHelpFileName方法内。若帮助文件可以被引擎主帮助文件加载,则返回不含路径的文件,默认返回"MyExt.chm"。如果你的帮助文件没有被引擎主帮助文件加载,则返回基于引擎根目录的帮助文件相对路径。根据实际情况,可对该函数进行删改。

// -----------------
// GetHelpFileName
// -----------------
LPCTSTR WINAPI GetHelpFileName(){
#ifndef RUN_ONLY
	return _T("Help\\WinAPI.chm");
#else
	return NULL;
#endif // !defined(RUN_ONLY)