AngelScript进阶用法:在脚本中使用代码类

DinS          Written on 2018/1/16

本文介绍如何在脚本中使用代码中的类,确保阅读过《AngelScript基础用法:注册函数》。

一、重新回顾string

使用自定义类可能有些复杂,让我们先来是一个比较简单的但是很重要的东西:字符串。
AS的原始数据类型不包括char,而且也没有string,但是代码总是要处理字符串的,为之奈何?AS可以让用户自己注册字符串,但是一般而言我们可以使用标准库的string。在第一个例子里我们其实已经在脚本中使用了string,捷径是scriptstdstring.h,让我们继续沿着这个思路去探索。

其实传入和返回类的思路与原始数据类型一致。
对于原始数据类型,AS提供了SetArg…和GetReturn…来操作,其实质是按bit的大小来传参和返回。我们统一使用int SetArgObject(int arg, void *object)来设置参数对象,统一使用void *GetReturnObject()来获取返回对象。当然这里有使用小技巧。看一段代码。

大图点这里

脚本如下:

这段代码有说头了。
前面两步都相同,重点在第三部分。注意脚本中的函数其输入参数和返回值都是string,string其实就是一个自定义的类。
我们使用SetArgObject来将对象传入脚本。其第二个参数是void*,所以能够接收任何类型的对象。
参数设置好后,执行,判定是否执行成功是好习惯。
然后下面一行代码看起来有些可怕,实际不然。GetReturnObject返回void*,当然调用者知道具体是什么类型,于是强转成该类型,然后解引用赋值。一定要复制出来,不然ctx会释放掉内存。这个就是套路。

不过这里还有一些神奇的事情。
注意到脚本中我们使用了string的substr和size方法,但是我们并没有注册这些。实际上RegisterStdString(engine);这一句帮助我们做了很多工作。
默认的方法名称与stl有出入,所以要改一个值,在scriptstdstring.h中有相关说明。设置为1后就可以像stl中那样使用string了。

这个是很好的,string除了处理字符串外还能够装载byte。
运行看效果:

确实达到了我们的目标。

通过string可以给我们一些启示。
AS的SDK中提供了各种add-on,方便开发,比如scriptarray就相当于vector,而且也是模板,具体的一些成员函数可以去cpp中查看,比如:

也可以去官网查看文档。

有了这些,基本上可以像写c++代码那样写脚本了。
从这里我们也看到了一些注册自定义类的影子,注意这里出现了RegisterObjectMethod,猜想:通过这个注册类成员函数。

二、注册自定义类:POD

既然在脚本中使用已经注册的类没有问题了,让我们看看如何注册自定义类。类的话直接相关的是类名和接口,所以直觉上说要注册这两样东西。
提前说明,涉及到类会比你想的复杂,咱们循序渐进,先看简单的POD。

所谓POD,即plain old data,即成员都是基本数据类型。对于这种类,比较容易注册。

main里如下:

大图点这里

重要的事发生在第一阶段。
我们使用了RegisterObjectType来注册类,注意第二个参数是sizeof。脚本需要知道这个类占用了多少空间,这也说明了这个方式为什么只用于POD:因为没有动态内存分配。第三个参数是flag,有两个,一个是value,表明这个类是按值传送的,另一种是按引用传送,更复杂先忽略。第二个flag是POD,表明这是POD类,我们不需要提供额外的构造和析构函数。
然后我们使用了RegisterObjectMethod来注册类的成员函数,总体上应该能看懂,asMETHOD后面第一个是类名,第二个是函数名。最后一个是asCALL_THISCALL,专门用于注册类的成员函数,太长了没截全。

脚本如下:

运行尝试一下:

可见确实是成功了。

三、注册自定义类:复杂类

注册POD类倒是不难,但是用处有限,我们至少要能够处理字符串吧,就得有string。不过如果直接在Person里增加string但是不做别的修改,运行的时候会崩溃,原因出在sizeof上。

做一个实验,对现在的类使用sizeof()结果:

现在增加string成员。

再次使用sizeof(),结果:

为什么是40?尝试换个顺序:

再运行:

48?这说明什么?
string这个类是动态变化的,初始化后有一个容量,但是容量大概跟对齐方式有关,所以会变化。
然而POD的空间一定是固定的,所以如果有了string这种动态分配空间的成员,就不算POD了,用之前的注册方法就不行了。如果我们强制运行,会这样:

基本数据类型赋值成功,但是string失败。为了满足动态需求,我们要做额外的工作。
结合代码来逐一讲解,先来看类本身:

与之前的POD类相比,多了string,另外还出现了赋值=的重载。
为什么需要特意写出来?用默认的不是挺好的吗?这是因为我们要注册赋值。先来看看脚本:

在脚本中实际上有这么几个函数被调用:Person2的构造函数和析构函数,SetProperty,赋值。
赋值是在return的时候被调用的,换句话说,如果我们不注册这些函数,则解释器不会正常工作。为了注册=,我们必须提供一个成员函数这就是必须显式声明、定义=的原因。

接下来是重点:如何注册这些函数,看代码:

点击看大图

这里是重点。
我们使用了AS提供的默认字符串,所以Person2类里面出现的string不需要管了。然后注册Person2这个类。之前用的是POD的flag,现在变成了asGetTypeTraits,这个是AS提供的辅助函数,用以确定类的内存方面的问题。我们不必深入细节,调用就好。
注意,支持c++11标准的编译器才能够用这个函数,不过现在一般都是了。
注册完类后我们注册了构造和析构函数。不过我们在类里面用的是默认的,并没有声明啊?
AS里的构造和析构跟c++有些不同,我们必须用一个函数把构造和析构包装起来。如下:

这是套路,固定写即可。可能需要#include<new>才可以通过编译。其功能也是跟内存分配有关,AS需要通过这样的调用知道内存的状态。
另外的问题是那个void f()是什么意思?这是AS的保留字段,用以填充构造和析构函数,其他的参数从语义上理解即可。
还有一个问题:我们通常会重载构造函数,这里如何处理?
可以见sdk中的add_on里面的scriptmathcomplex.cpp。这是一个不错的参考样例。180行-185行提供了重载构造函数的方法,甚至还包括初始化列表的方式。
如果有需要可以去参考,这里不赘述了。

再之后是注册成员函数,那个SetProperty大同小异,不说了。看看重载运算符。
那个声明与实际的声明有出入,AS里使用保留字段来指代运算符。后面那个asMETHODPR是为了定位重载情况使用的,要提供参数和返回值,关于如何重载其他运算符也可以参考scriptmathcomplex.cpp第187-196行。

最后要说明的是一个subtle point。有没有发现AS中注册函数时凡是提供了带引用的,其名字都跟in或者out有关系,而不是实际在代码中声明的样子?
AS使用保留字段来处理参数中的引用,详见http://www.angelcode.com/angelscript/sdk/docs/manual/doc_as_vs_cpp_types.html。
当然最常用的是const &in,这个跟c++的用法一致,另一个是&out,用于返回多个值。比如return返回了一个东西,但是还需要返回其他东西,那么参数里就带一个&out。

难点已经解决,剩下的就是调用了:

大图点这里

设置参数和取回参数大同小异,然后运行看看结果。

成功!可以再试试看更长的字符串,看看动态分配内存有没有问题。

确实可以。另一扇大门又打开了。
我们的这个Person2类实际上使用了组合的方式,内部有double、int、string,换句话说,只要我们提供了内部类的相关操作,比如vector/map等等,那么就可以通过脚本去调用,实现的效果跟写代码是一模一样的,这是非常厉害的一个成就。
当然成员里还没有出现过指针,要知道脚本里是没有指针的。实际上AS也可以应付这种情况,但是属于高级用法,一般应该设法避免。

四、值类与引用类

到目前为止我们碰到的都是值类(value type),与C++中按值拷贝的意思相同,即传递值类对象会拷贝。
AS还提供了引用类(reference type),与c++中引用的意思相同,即传递引用类对象无需拷贝,且变化在离开函数后会保留。使用引用类需要做更多的工作,具体来说,需要提供工厂函数、引用增加、引用减少三个函数。

使用reference type的用意一是增加效率,二是与动态绑定/多态有关,属于OOP的范畴。SDK提供的string属于值类,其他的比如array、dictionary等都属于引用类。所有在脚本里定义的类都属于引用类。

二者还有一个重要区别:值类可以通过脚本返回给程序,但是引用类却没这么容易,有更多工作要做。对引用类感兴趣的读者可以去官网自行研究。

至此我们成功在脚本中使用了代码类,接下来看看如何在代码中使用脚本类,见《AngelScript进阶用法:在代码中使用脚本类》。