Object Oriented Programming
UnigineScript lets the user define new types using user classes.
Classes
A class is an expanded concept of a data structure: instead of holding only data, it can hold both data and functions.
An object is an instantiation of a class. In terms of variables, a class would be the type, and an object would be the variable.
Classes are generally declared using the keyword class, with the following format:
class class_name {
access_specifier_1:
member1;
access_specifier_2:
member2;
// ...
} object_names;
// class_name - valid identifier for the class
// object_names - an optional list of names for objects of this class
The body of the declaration can contain members, that can be either data or function declarations, and optionally access specifiers.
UnigineScript Class Types
There are 3 types of classes applicable in UnigineScript:
- Engine Classes
- User-Defined Classes
- External Classes (exported from the C++ by a script)
Engine Classes
You can see the list of available Engine Classes here.
User-Defined Classes
User-Defined Classes are classes defined by user. Inheritance is supported.
/* user-defined class
*/
class Foo {
// member variables
int a;
int b;
// constructor
Foo(int a,int b) {
this.a = a;
this.b = b;
}
// destructor
~Foo() {}
// member function (method)
void print() {
log.message("a is %d, b is %d\n",a,b);
}
};
// create a new instance of the Foo class
Foo foo = new Foo(10,20);
// call a method of that instance
foo.print(); // Output: a is 10, b is 20
// force object destruction
delete foo;
Note that you always manipulate with references to objects but not with the objects themselves. That is why when you assign one object to another, it means that you simply copy the reference. Once the object is destroyed, its references aren't functional anymore.
Foo f0 = new Foo();
Foo f1 = f0;
f1.print();
delete f0;
f1.print(); // run-time error: the object is null
Methods are set up at the compile-time, therefore:
Foo foo = new Foo();
foo.print(); // ok
foo = 10;
foo.print(); // run-time error
int i = new Foo();
i.print(); // compile-time error
External Classes
External classes are classes exported from C++. You can read how to export a class from C++ here.
Access Specifiers
Access pecifiers modify the access rights that the members following them acquire. UnigineScript provides two different specifiers for accessing class members:
- public means that a member can be accessed from the outside of the class instance.
- private means that such member is accessible from within other members of the same class and members of the inherited classes.
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"); }
};
For example, in case if a class is inherited from Foo:
class Bazz : Foo {
void foo() {
Foo::public_foo();
Foo::private_foo();
}
};
Bazz b = new Bazz();
b.foo();
Public (no access specifier)
Private
Constructors and Destructors
Constructors and new
For a class to be used, it should be instantiated. This is done using constructors. A constructor creates an instance of a class and returns it. Here is how a constructor can be defined:
class Foo {
// member variable
int a;
// constructor
Foo(int a) {
// do something here
}
// other functions
};
- Has the same name as the class to which it belongs.
- Does not have a return type specifier in its declaration and does not use the return or yield statement.
If the constructor is defined outside the class declaration, it should be done like this:
class Foo {
// constructor declaration
Foo();
// other stuff
…
};
// constructor definition
void Foo::__Foo__() {
// do something here
}
To invoke a constructor, use the new operator as follows:
Foo fooInstance = new Foo(5);
Also the new operator can receive a string:
Foo fooInstance = new("Foo",5);
Destructors and delete
If a class instance is no longer needed and it's time for it to pass away, it should be properly freed. That is why a destructor is called before the occupied memory is marked as free. Here is how a destructor can be defined:
class Foo {
// member variable
int a;
// destructor
~Foo() {
// do something here
}
// other functions
…
};
- Has the same name as the class to which it belongs.
- Has a prefix ~ before its name.
- Does not have a return type specifier in its declaration and does not use the return or yield statement.
If the destructor is defined outside the class declaration, it should be done like this:
class Foo {
// destructor declaration
~Foo();
// other stuff
…
};
// destructor definition
void Foo::__~Foo__() {
// do something here
…
}
To invoke a destructor explicitly, you need to write something like this:
delete fooInstance;
Overloading
Overloading allows functions and operators to have the same name but with different parameters.
Function Overloading
UnigineScript allows Function Overloading.
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)); // the result is: 2*3=6
log.message("%d\n",foo(2,3,4)); // the result is: 2+3+4=9
Operator Overloading
Operator overloading allows operators to have different implementations depending on their arguments. You can provide your own operator to a class by overloading the built-in operator to perform some specific computation when the operator is used on objects of that class.
The name of an overloaded operator is operatorx, where x is the operator, available for overloading in UnigineScript:
- operator+
- operator-
- operator*
- operator/
- operator%
- operator<
- operator>
- operator==
- operator<=
- operator>=
- operator!=
- __set__ (for [])
- __get__ (for [])
The following example represents overloading of the operator+, operator- and operator* for the class Vec3:
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);
// the result is: 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");
// the result is: 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]);
// the result is: 10 10
Keywords
this Keyword
Sometimes it is important to reference the object, for which a method is invoked. In such a situation, this keyword is helpful; it is reference to an object itself, available for member functions of the class.
class Foo {
int a;
int b;
// usage in a constructor
Foo(int a,int b) {
this.a = a;
this.b = b;
}
// usage in a method
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 Keyword
Base class functions can be overridden in the derived class. However, you can call such functions from the derived class by using the super keyword. For example, the foo() function is overriden in the Bar class, so, the foo() function of the Foo class can be called using super.
class Foo {
void foo() { log.message("base\n"); }
};
class Bar : Foo{
void foo() { log.message("derived\n"); }
void bar() {
super.foo(); // prints "base"
this.foo(); // prints "derived"
}
};
Bar bar = new Bar();
bar.bar();
The result will be:
base
derived
Inheritance
In UnigineScript, derived classes can inherit all public methods and data members from one or multiple base classes.
There is a peculiarity of the class inheritance in UnigineScript:
- When a user class is inherited from another class, both automatically support virtual methods.
You can declare the virtual function by using the virtual keyword, but it's optional.
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 is initialized in the base class
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 is initialized in the base class
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);
The result will be:
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
Calling the Base Class Constructor
It is also possible to call the base class constructor in the inherited class.
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;
And the result will be:
Foo::Foo(): 4 7
Accessing the Overridden Base Class Function
If in the inherited class a method is overridden, you can still access this member function as it is implemented in the base class using the following syntax:
<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) {
// Overridden method implementation
log.message("Bar::foo(): %d\n",a + 3);
// Accessing the base class method implementation
Foo::foo(a);
}
};
Bar b = new Bar();
b.foo(2);
delete b;
And the result will be:
Bar::foo(): 5
Foo::foo(): 2
Creating Abstract Methods in a Base Class
You can create an abstract method in the base class and implement it in the derived one. However, in the base class method an error or, if necessary, an exception should be thrown. It is used to indicate that a method implementation is lacking in the derived class.
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;
And the result will be:
Bar::foo(): 7
Casting
An object of a parent polymorphic class can be cast to a child one, so as to access all its member functions.
class Foo {
void foo() { }
};
int foo = new Foo();
Foo(foo).foo(); // changes a variable type
foo.foo(); // this is invalid
Class Based Branching
The fast class based branching can be implemented by using the switch-case conditional statement.
If you have several classes defined in the script and their instances require to be processed differently, you can use the classid() function as a constant for the case within the switch to check if the passed value is an instance of the particular class.
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
Inheritance from C++ Classes
User classes can not only be inherited from other user classes, but also from C++ classes:
- Engine base classes (any classes from the core and UnigineScript libraries)
- Extern C++ classes accessed via C++ API
In both of these cases, class inheritance is performed in the same way (see an example below).
Inheritance from Engine Classes
Here is an example how to create MyObjectMesh class which inherits from the base engine ObjectMeshStatic class.
- Objects of the derived class can call methods of base class directly.
- When passing object of the derived class to function that takes an object of the base class, inherited class cannot be automatically converted to the base class. Upcasting should be done explicitly via accessing extern member of the derived class.
- To downcast the instance of base class to your custom derived class, use cast() function. This function is created automatically for all inherited classes and needs not to be implemented manually.
// Custom class inherited from base engine class.
class MyObjectMesh : ObjectMesh {
int number;
// Constructor.
MyObjectMesh(string name, int a) : ObjectMesh(name) {
number = a;
log.message("MyObjectMesh object constructed\n");
}
};
void foo(MyObjectMesh mesh) {
// Check if the argument is an instance of 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);
// You can call functions of base class.
log.message("Mesh name: %s\n",mesh.getMeshName());
// To upcast the object to the base class, use "extern".
// It's necessary when passing MyObjectMesh to a function that takes a base ObjectMesh or Node.
engine.world.isNode(mesh.extern));
// To downcast the object back to its derived class, use cast() function.
foo(MyObjectMesh::cast(mesh.extern));
The result will be:
MyObjectMesh object constructed
Mesh name: samples/common/meshes/box.mesh
Object is MyObjectMesh
Number = 42
Using Callbacks in Inherited Classes
To use callbacks in the inherited class, you need to pass a reference to this class (this) as an additional argument for the callback function.
For example, a callback in the class inherited from 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);
}
};
Here is another example with a class inherited from 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
In UnigineScript, function chaining enables to call a chain of functions in one statement. The return value of the first function is used to call the second function. Such approach is useful when there is no need to store intermediate objects. For example:
mesh.getBoundBox().getMin();
Function chaining can also be used when calling functions of user-defined classes. For example:
// the 1st user defined class
class Foo {
void print() { log.message(__FUNC__ + ": called\n"); }
};
// the 2nd user defined class
class Bar {
Foo get_foo() { return new Foo(); }
};
int init() {
// create an instance of the 2nd class
Bar b = new Bar();
// chain the function calls: call the get_foo() member function for the instance of the Bar class
// and then call the member function of the Foo class
b.get_foo().print();
}
// Output: Foo::print(): called