面向对象编程
UnigineScript能让用户使用用户类定义新的用户类型。
类#
class是数据结构的一种扩展概念:其可具有数据和函数,而不是仅仅拥有数据。
对象时类的实例化。用变量的话说就是类是一种类型而对象是变量。
通常使用关键字类及下列格式声明类:
class class_name {
access_specifier_1:
member1;
access_specifier_2:
member2;
// ...
} object_names;
// class_name - 用于类的有效标识符
// object_names - 用于此种类对象的可选名称列表
类声明的body部分可包括成员及可选存取说明符,而成员可以是数据也可以是对函数的声明。
UnigineScript类的类型#
有3种类型的类可在UnigineScript中使用:
- 引擎类
- 用户自定义类
- 外部类 (使用脚本从C++导出)
引擎类#
可在此处查看可用引擎类列表
用户自定义类#
用户自定义类是用户自己定义的类。支持 继承。
/* 用户自定义类
*/
class Foo {
// 变量成员
int a;
int b;
// 构造函数
Foo(int a,int b) {
this.a = a;
this.b = b;
}
// 析构函数
~Foo() {}
// 函数成员(方法)
void print() {
log.message("a is %d, b is %d\n",a,b);
}
};
// 为Foo类创建一个新的实例
Foo foo = new Foo(10,20);
// 调用此实例的一种方法
foo.print(); // 输出为: a is 10, b is 20
// 强制对象析构
delete foo;
请注意通常使用引用对对象进行操作而不使用对象自身。这也正是将某个对象分配到另一个对象上时,仅需将引用进行拷贝的原因。一旦此对象遭到损坏,其引用也不再具有任何功能。
Foo f0 = new Foo();
Foo f1 = f0;
f1.print();
delete f0;
f1.print(); // 运行错误:对象为空
编译时创建方法,因此:
Foo foo = new Foo();
foo.print(); // ok
foo = 10;
foo.print(); // 运行错误
int i = new Foo();
i.print(); // 编译错误
外部类#
外部类 是从C++中导出的类。可在此处了解如何从C++中导出类。
访问说明符#
访问说明符 可以修改后续成员的访问权限。UnigineScript提供两种对类成员进行访问的说明符:
- public(公用) 意味着可从类实例的外部访问此成员。
- private(私用)意味着看而从同一类中的其它成员或者继承类中的成员对这些标记的成员进行访问。
class Foo {
int public_i;
void public_foo() { log.message("Public (no access specifier)\n"); }
private:
int private_i;
void private_foo() { log.message("Private\n"); }
public:
int public_j;
void public_bar() { log.message("Public\n"); }
};
例如,如果某个类继承自Foo:
class Bazz : Foo {
void foo() {
Foo::public_foo();
Foo::private_foo();
}
};
Bazz b = new Bazz();
b.foo();
Public (no access specifier)
Private
构造函数与析构#
构造函数与 new方法#
对于将被使用的某种类,应将此类实例化。使用构造函数完成实例化操作。构造函数创建一个类的实例并返回一个值。下列为定义构造函数的方式:
class Foo {
//成员变量
int a;
// 构造函数
Foo(int a) {
// 在此处执行一些操作
}
// 其它函数
};
如果在类的声明外定义构造函数,应按照如下方式进行操作:
class Foo {
// 声明构造函数
Foo();
// 其它填充物
…
};
// 构造函数的定义
void Foo::__Foo__() {
// 在此处执行一些操作
}
要调用此构造函数,按照如下方式使用new 运算符:
Foo fooInstance = new Foo(5);
new 运算符可接收到一个字符串:
Foo fooInstance = new("Foo",5);
析构及delete方法#
如果不再需要某个类的实例应将这个类进行删除释放。这也正是在占用的内存被标记为闲置状态之前会调用析构函数的原因。下方是对析构函数定义的方式:
class Foo {
// 变量成员
int a;
// 析构函数
~Foo() {
// 在此处执行一些操作
}
// 其它函数
…
};
如果在声明类的外部定义了析构函数,应按如下方式进行操作:
class Foo {
// 声明解析函数
~Foo();
// 其它填充物
…
};
// 定义析构函数
void Foo::__~Foo__() {
// 在此处执行一些操作
…
}
要明确地调用析构函数,需按照如下方式进行编写:
delete fooInstance;
重载#
Overloading 允许函数和运算符具有相同的名称不同的参数。
函数重载#
UnigineScript允许函数重载。
int foo(int a,int b) {
return a * b;
}
int foo(int a,int b,int c) {
return a + b + c;
}
log.message("%d\n",foo(2,3)); // 结果为: 2*3=6
log.message("%d\n",foo(2,3,4)); // 结果为: 2+3+4=9
运算符重载#
运算符重载允许运算符根据参数具有不同的实施方法。通过对嵌入运算符重载可以为自己的类提供运算符,当在类的对象上使用此运算符时执行一些特定的计算。
重载运算符的名称是operatorx,此处x是运算符,在UnigineScript内重载时可用:
- operator+
- operator-
- operator*
- operator/
- operator%
- operator<
- operator>
- operator==
- operator<=
- operator>=
- operator!=
- __set__ (for [])
- __get__ (for [])
下列示例代表用于Vec3类operator+, operator- 和operator*的重载:
class Vec3 {
float x,y,z;
Vec3(float x,float y,float z) {
this.x = x; this.y = y; this.z = z;
}
Vec3 operator+(Vec3 v0,Vec3 v1) {
return new Vec3(v0.x + v1.x,v0.y + v1.y,v0.z + v1.z);
}
Vec3 operator-(Vec3 v0,Vec3 v1) {
return new Vec3(v0.x - v1.x,v0.y - v1.y,v0.z - v1.z);
}
Vec3 operator*(Vec3 v,float f) {
return new Vec3(v.x * f,v.y * f,v.z * f);
}
};
Vec3 v0 = new Vec3(1,2,3);
Vec3 v1 = new Vec3(4,5,6);
Vec3 res = (v0 + v1) * 13.13 + (v1 - v0) * 31.31 + v0 * 0.5;
log.message("%f %f %f\n",res.x,res.y,res.z);
// 结果为: 160.080002 186.839996 213.600006
class Class {
string name;
Class(string name) {
this.name = name;
}
int operator<(Class c0,Class c1) {
return (c0.name < c1.name);
}
};
Class classes[0];
forloop(int i = 0; 8) {
classes.append(new Class(string(rand(0,100))));
}
classes.sort();
foreach(Class c; classes) {
log.message("%s ",c.name);
}
log.message("\n");
// 结果为: 15 35 77 83 86 86 92 93
class Foo {
mat4 m;
void __set__(Foo f,int index,int value) {
f.m[index] = value;
}
int __get__(Foo f,int index) {
return f.m[index];
}
};
Foo f = new Foo();
f[0] = 10;
f[1] = f[0];
log.message("%g %g\n",f[0],f[1]);
// 结果为: 10 10
关键字#
this关键字#
有时引用对象很重要,而方法又因为此对象得到调用。这种情况下,this关键字就十分有用。此关键字为对象本身提供参考,可用于类的成员函数。
class Foo {
int a;
int b;
// 在某个构造函数内的用法
Foo(int a,int b) {
this.a = a;
this.b = b;
}
// 在某种方法内的用法
void print() {
log.message("a is %d, b is %d, object is %s\n",a,b,typeinfo(this));
}
};
Foo foo = new Foo(10,20);
foo.print();
a is 10, b is 20, object is Foo (0:0)
super关键字#
基类函数可在派生类中被覆盖。然而可使用super 关键字调用派生类的这种函数。 例如,在Bar类中覆盖了foo()函数,因此Foo类的foo()函数可通过super进行调用。
class Foo {
void foo() { log.message("base\n"); }
};
class Bar : Foo{
void foo() { log.message("derived\n"); }
void bar() {
super.foo(); // 打印 "base"
this.foo(); // 打印"derived"
}
};
Bar bar = new Bar();
bar.bar();
结果为:
base
derived
继承#
在UnigineScript中,派生类可继承来自一个或多个基类的所有公用方法及数据成员。
在UnigineScript中出现了一种新的类继承特性:
- 当某种用户类继承自其它类时,两种类都支持虚拟方法。
可使用virtual关键字声明虚拟函数,但此选项为可选选项。
class Foo {
int f = -13;
int array[2] = ( 0, 1 );
Foo(int i) { f = i; }
void foo() { log.message("Foo::foo %d: %d %d\n",f,array[0],array[1]); }
};
class Bar : Foo {
// f 在基类中被初始化
int b = 2;
int array[2] = ( 2, 3 );
Bar(int i) { b = i; }
void bar() { log.message("Bar::bar %d %d: %d %d\n",f,b,array[0],array[1]); }
void foo() { log.message("Bar::foo %d %d: %d %d\n",f,b,array[0],array[1]); }
};
class Bazz : Bar {
// f 在基类中被初始化
int b = -23;
int array[2] = ( 4, 5 );
Bazz(int i) { b = i; }
void foo() { log.message("Bazz::foo %d %d: %d %d\n",f,b,array[0],array[1]); }
};
Foo f = new Foo(13);
Bar b = new Bar(133);
Bazz bz = new Bazz(1333);
void foo(Foo f) {
f.foo();
f.call("foo");
}
foo(f);
foo(b);
foo(bz);
结果将是:
Foo::foo 13: 0 1
Foo::foo 13: 0 1
Bar::foo -13 133: 2 3
Bar::foo -13 133: 2 3
Bazz::foo -13 1333: 4 5
Bazz::foo -13 1333: 4 5
调用基类构造函数#
也可在继承类中调用基类构造函数。
class Foo {
Foo(int a,int b) { log.message("Foo::Foo(): %d %d\n",a,b); }
};
class Bar : Foo {
Bar(int a,int b) : Foo(a + 3,b + 5) { }
};
Bar b = new Bar(1,2);
delete b;
结果将是:
Foo::Foo(): 4 7
访问被覆盖的基类函数#
如果继承的类被覆盖,由于其在基类中得到实施因此仍然可使用下列语法来访问此成员函数:
<base class name>::<overridden function name>
class Foo {
void foo(int a) {
log.message("Foo::foo(): %d\n",a);
}
};
class Bar : Foo {
void foo(int a) {
// 实施被覆盖的方法
log.message("Bar::foo(): %d\n",a + 3);
// 访问基类方法的实施
Foo::foo(a);
}
};
Bar b = new Bar();
b.foo(2);
delete b;
结果将是:
Bar::foo(): 5
Foo::foo(): 2
在基类中创建抽象方法#
可在基类中创建抽象方法并在派生类中实施此方法。然而在基类方法中如有必要应抛出一个错误或异常。这样做的目的是证明在派生类中缺少某种方法的实施。
class Foo {
void foo(int a) {
log.error("Foo::foo(): pure virtual function call\n");
}
};
class Bar : Foo {
void foo(int a) {
int num = a + 5;
log.message("Bar::foo(): %d\n",num);
}
};
Bar b = new Bar();
b.foo(2);
delete b;
结果将是:
Bar::foo(): 7
强制转换#
多态父类中的某个对象可被强制转换为子类,这样便能对其所有成员函数进行访问。
class Foo {
void foo() { }
};
int foo = new Foo();
Foo(foo).foo(); // 改变变量类型
foo.foo(); // 无效
基于类的分支#
可使用switch-case条件语句实施快速基于类的分支。
如果脚本中定义了数个类并要求对每个类的实例进行不同的处理,可将classid()函数作为常量在switch板块内用于case内,这样便可检查传递的值是否是特殊类的实例。
class Foo { };
class Bar { };
void foo(int a) {
switch(classid(a)) {
case classid(Foo): log.message("Foo\n"); break;
case classid(Bar): log.message("Bar\n"); break;
case classid(File): log.message("File\n"); break;
}
}
foo(new Foo());
foo(new Bar());
foo(new File());
Foo
Bar
File
log.message("%d %d\n",classid(new Foo()),classid(Foo));
log.message("%d %d\n",classid(new Bar()),classid(Bar));
log.message("%d %d\n",classid(new File()),classid(File));
0 0
65536 65536
-6 -6
对C++类的继承#
用户类不仅可继承自其它用户类还可以从C++类处继承:
- 引擎基类(来自核心库和UnigineScript库的所有类)
- 通过C++ API访问的外部C++类
两种情况中,类的继承以同样的方式进行(参看下方示例)
对引擎类的继承#
下面的示例演示创建MyObjectMesh类的方式,这种类继承自引擎基本的ObjectMeshStatic类。
- 派生类的对象可直接调用基本类的方法。
- 在将派生类的对象传递给采用基类对象的函数时,继承的类无法自动转换成基类。应通过访问派生类的外来成员明确地执行向上转型。
- 要将基类的实例向下转型为自定义派生类类型,使用cast()函数。此自动得以创建的函数会被用于所有继承类而且不需要手动执行。
// 继承自引擎基类的自定义函数。
class MyObjectMesh : ObjectMesh {
int number;
// 构造函数。
MyObjectMesh(string name, int a) : ObjectMesh(name) {
number = a;
log.message("MyObjectMesh object constructed\n");
}
};
void foo(MyObjectMesh mesh) {
// 检查参数是否为MyObjectMesh的实例。
if(is_base_class("MyObjectMesh",mesh) == 0) log.message("Object is not MyObjectMesh\n");
else log.message("Object is MyObjectMesh\nNumber = %d\n", mesh.number);
return;
};
/*
*/
MyObjectMesh mesh = new MyObjectMesh("samples/common/meshes/box.mesh",42);
// 可调用基类的函数。
log.message("Mesh name: %s\n",mesh.getMeshName());
// 将对象向上转型为基类,需使用"extern"。
// 在将MyObjectMesh传递给使用基本ObjectMesh或Node节点的函数时,这种操作很有必要。
engine.world.isNode(mesh.extern));
// 要将对象向下转型为其派生类,需使用cast()函数。
foo(MyObjectMesh::cast(mesh.extern));
结果将是:
MyObjectMesh object constructed
Mesh name: samples/common/meshes/box.mesh
Object is MyObjectMesh
Number = 42
在继承类中使用回调函数#
要在继承类中使用回调函数,需要将引用传递给此类(this) 并作为回调函数的额外参数。
例如继承自WidgetButton类中的回调函数:
class MyButton : WidgetButton {
private:
void on_click(MyButton button) {
log.message("on_click: %s\n",typeinfo(button));
}
public:
MyButton(Gui gui) {
extern = new WidgetButton(gui);
extern.setCallback(GUI_CLICKED,"::MyButton::on_click",this);
}
};
此处为另一个使用继承自WorldTrigger类的实例:
class MyTrigger : WorldTrigger {
private:
void on_enter(Node node,MyTrigger trigger) {
log.message("on_enter: %s %s\n",typeinfo(node),typeinfo(trigger));
}
void on_leave(Node node,MyTrigger trigger) {
log.message("on_leave: %s %s\n",typeinfo(node),typeinfo(trigger));
}
public:
MyTrigger(vec3 size) {
extern = new WorldTrigger(size);
extern.setEnterCallback("::MyTrigger::on_enter",this);
extern.setLeaveCallback("::MyTrigger::on_leave",this);
}
};
Function Chaining(函数链)#
在UnigineScript中,函数链可以在一个语句中调用函数链。 第一个函数的返回值会被用来调用第二个函数。在不需要对中间对象进行保存的情况下这种方法十分有用。例如:
mesh.getBoundBox().getMin();
在调用用户自定义类的函数时,也可以使用函数链。例如:
// 第一个用户自定义类
class Foo {
void print() { log.message(__FUNC__ + ": called\n"); }
};
// 第二个用户自定义类
class Bar {
Foo get_foo() { return new Foo(); }
};
int init() {
// 为第二个类创建一个实例
Bar b = new Bar();
// 链接函数调用:调用get_foo()成员函数将其用于Bar类的实例
// 接着调用Foo类的成员函数
b.get_foo().print();
}
// 输出为: Foo::print(): 被调用