UE_Angelscript 绑定篇——综述:暴露更多内容给 AS 和蓝图#
前言#
此文不涉及 Angelscript(AS) 插件的过多底层原理,只涉及应用层面的理解,而且主要是整体的描述,不涉及具体代码。
cpp 代码里,只有声明为 UCLASS、USTRUCT 等内容才会被 UE 的反射系统识别,然后这些被反射系统识别到的内容很多都会暴露给蓝图。简单地理解是,要让蓝图认识到,必要条件是被 UE 的反射系统识别。(这是大多数情况,至于有没有特殊的,不深究了。)
AS 同样利用了 UE 的反射系统,所以就获得了和蓝图基本一致的绑定内容。简单地说,蓝图拥有的大部分类与函数,AS 同样绑定好了(注意,是大部分,有些是蓝图独享或 AS 暂时缺失的)。
但是,我们不仅仅想要拥有类似蓝图的功能,还想要更多——即想要获得更多自由度,即访问更多的引擎代码内容。
这就带来了一个问题,cpp里 未被反射系统识别的代码,该如何暴露给 AS 使用?
暴露的方法,总结就两个方向,一个方向是对已有的类进行绑定,包括手工绑定和 Mixin 绑定;另外一个方向是对已有的类进行继承、包装、转换,因为新建了类,改什么都相对方便。
以下都是建立在不想修改源码的情况下的处理方式。
手工绑定#
所有 AS 脚本里的 Bind_ 开头的文件,可以看脚本已经手工绑定的代码,学习脚本是如何手工绑定的。绑定的代码写法不算难,脚本还提供了一些宏进行封装。
下面说下注意的点。
问题一:什么时候必须手工绑定?#
1,这个类没有暴露给 UE 的反射系统,蓝图访问不到,但是我们想用它。
2,这个类暴露给蓝图了,但是你想访问的个别成员没有暴露给蓝图,你又不想继承一个类来专门访问这个成员,也不想专门写两个 Mixin版本 的 Get 和 Set 函数。
这里还有另外一种情况,这个成员暴露给蓝图的时候,是只读的、或者只能在构造时访问的,此时想要完全访问,还是得在手工绑定或者 Mixin 函数里写 Get 或 Set 函数。
3,想要让类在 AS 里也拥有 == 与 != 之类的运算符,目前来看,只能手工绑定或者 Mixin 绑定了。
AS 有专门的函数名与运算符一一对应,比如 opEquals 与 == 对应。
问题二:手工绑定有什么注意事项?#
1,绑定的顺序,有些类依赖于其他类,其他类如果没有先绑定,可能带来问题。
2,cpp 的 float 需要绑定为 float32。
3,AS 脚本里没有裸指针,对于UClass,一律绑定为可以为 nullptr 的对象了,但是对于 struct ,不能为 nullptr,都是引用,此时如果碰到 struct 的指针,注意进行绑定转换的时候看看是否需要判空,防止不小心访问了无效指针。
4,UE 里有继承关系的 struct,绑定不会被继承,如果都要用,需要都绑定。此时可以考虑用模板或者宏来批量处理。
5,UStruct 声明为 BlueprintType 了,如果有另外一个 Ustruct 继承了它,如果没有声明 BlueprintType,且没有新增暴露给蓝图的成员,那么这个 UStruct 不会被暴露给 AS,需要绑定。(我一般是给它加上 BlueprintType,加上不影响引擎源码。)
Mixin 绑定#
Mixin 绑定只能是函数。Mixin 绑定的原理,实际类似于写这么一种静态函数,传入类的指针,通过指针修改这个类。在 AS 的层面,经过一些处理,让其使用这个静态函数就像是使用了这个类的某个方法一样。单单就 Mixin 本身而言,它就能带来这么一个好处,可以用 . 号来调用一些本来不存在这个类的方法,相当于在不方便改变类的代码的前提下,给这个类进行了拓展。
问题一:什么时候需要 Mixin 绑定,好处是什么?#
1,UStruct 的方法都是不能声明为 UFUNCTION 的,如果想要给这个 UStruct 拓展功能,而且不方便更改源代码,那么推荐用 Mixin 的方式吧。(手工绑定也是能添加功能的,不过没那么直观。Mixin 的方式既然能拓展功能,自然也能将原本有的那些普通成员函数暴露出来了。)
2,某些类经过多次继承后,想要在中间某个类增加功能,那么用 Mixin 就很方便。
3,某些类有一大批静态函数,调用起来相当麻烦,不能用 . 号调用,那么用 Mixin 就可以轻松转换,在这里,直接用 AS 的 Mixin 会方便一些。
问题二:什么时候不方便用 Mixin 绑定?#
因为 Mixin 绑定只能是函数,所以,涉及到一些没有暴露给 AS 的类型时,就比较尴尬,可能就需要做一些包装和转换了。
因为 Mixin 绑定完全是在类的外部进行,自然不能访问 Protected 的内容了。
继承、包装、转换#
问题一:什么时候需要继承,好处是什么?#
AS 里常用的类,都建议在 cpp 里做一个中间层,这样可以很方便地拓展功能,特别是对虚函数进行 override ,可以暴露一个 BP 版本的函数或者广播一个暴露给蓝图的动态委托。
标记为被保护的成员,除了其自身以及继承的类,外部是访问不到的,此时想访问这些成员,不修改源码的情况下,只能继承。
另外,只要父类里标记了 UPROPERTY 的成员,可以在构造函数里通过反射系统修改它,进而暴露给 AS 。比如,有些蓝图只读的成员,可以修改为可读写。
问题二,什么时候需要包装,好处是什么?#
这些的包装,意思是新建一个 UStruct ,然后成员声明为想要访问的 cpp 原生 struct 或原生 class (cpp里,这两除了访问权限,其余都是一样的。)
新建的包装 UStruct ,可以通过手工绑定,或者 Mixin 的方式,将被包装类的成员与成员函数的访问提出来。
这样是不是多此一举?这种方案是有特定适用情况的。
1,有一些想要使用的函数的参数有 cpp 原生 struct,而你想保留基本的函数样式,那么这种包装就比较优雅一些,直接将参数类型替换一下,稍做转换就可以暴露出去了。
2,还有一些 AS 不支持的东西,比如 TSharedPtr 对 struct 的生命周期管理,遇到了只能包装起来。AS 不支持 TSharedPtr 的理由是什么呢——它不能声明为 UPROPERTY,这会造成设计师理解困惑,为了对设计师友好,进行了不少取舍的利弊权衡。
3,想要让一个 cpp 原生 struct 支持写进 TMap 作为 key。比如 FObjectKey,即使手工绑定了它,还是不能放进 TMap 里作为 key,可是这个天生就是要作为 key 使用的,无奈之下,我只好包装了一个来使用。(此情况存疑,不确定是否所有原生 struct 都如此。)
问题三,什么时候需要转换?#
这里的转换,主要是指新建一个相似但不同的东西,与原来的东西没有继承或者包含关系。碰到最多的转换是委托的转换。
只有两种委托会绑定到AS,一种是动态委托(绑定为AS的delegate),一种是动态多播委托 (绑定为AS的event),其中,动态委托可以作为 UFunction 函数的参数,而动态多播委托只能作为类的成员。
因为 UE 需要性能,需要节约内存,而动态委托是比普通委托需要更多资源的,所以 UE 里不打算给蓝图的,能用普通委托就用普通委托了。
1,有时候我们确实想要调用 cpp 的某个函数,里面有普通委托或者普通多播委托作为参数,此时,就需要委托的转换了。
方法就是声明一个相似的动态委托,在绑定的 Mixin 函数里,将原本函数的委托替换为我们的,然后在实现里写委托之间的转换。
2,还有一种情况是,某个作为类成员的普通委托,想要在 AS 里绑定函数给它,手工绑定又暴露不了这个委托,此时,可以考虑写一个 Mixin 函数,类似上方刚提到的委托转换方式,只不过这次需要自己手动写绑定逻辑。
总结#
想让 AS 访问更多的引擎代码,是需要付出一些绑定时间的。本文多为个人见解,有问题欢迎交流。
不是什么内容都容易绑定给 AS,从易到难排序,个人感觉大概是 UClass,UStruct,原生struct,委托转换。幸好,大部分的常用绑定,以及最难的那些绑定(涉及模板样式的),脚本已经绑好了。其他的,如果想比蓝图拥有更多,就可以考虑绑、绑、绑。