Introduction to GameMonkey Script Part 2
Embedding GameMonkey
Creating a simple typeMany game developers will want to expose their own types to a scripting system, and GameMonkey Script is no exception. Imagine a simple class that is in most 3d applications, the Vector class. This can be as simple as a structure composed of 3 floats for x, y and z or can be as complex as a class which holds member functions to perform operations and calculations on the vector data. Sometimes it is possible to use the integral table object for custom-bound objects, but in some situations you require the application to have more control over the object - perhaps by providing extra validation, data conversion or updating another object in your game when the data is changed on the object. With a simple table this sort of control is impossible, hence the need for user-defined types. Choosing what to bindWhen choosing to bind your type you must consider how it will be used in script. For example, the simple Vector type could be easily represented as a fixed array of 3 floats, it could have the constituent items stored in named members (such as x, y and z), or it could be comprised of both. For example: v = Vector( 10, 20, 50 ); // Create vector // Vector could be represented using 3 floats v[0] = 20; v[2] = v[1]; // Access vector using named members v.X = 20; // Set X member to 20 v.Z = v.Y; You could choose to provide the exact same interface as your engine's vector class, you could simplify it somewhat, or you could even provide an entirely different interface. For example, some people may wish to access vector maths functions using the dot operator, whereas others may wish to keep the vector type as data-only and provide specialized functions to manipulate the data. For example: v = Vector( 10, 20, 50 ); // Create vector v.Normalise(); // Call normalise using dot operator NormaliseVector( v ); // Normalise using a specialist function Many game developers seem to think they need to expose their entire C++ class to the scripting environment. While this is possible, it is often unnecessary; there could be many member functions that are useless in a scripting context so it makes little sense to include them. Even more so, there may be member functions and data that you do not want your script to be able to access, so exposing the full C++ class would be undesirable. Unfortunately, the decisions about what to bind are entirely context dependant and will change for every game project, game system or even game class you wish to bind to a scripting language. In my personal experience, I have found it useful to provide a simplified and refactored interface to the scripting environment. The more complicated it is to use the scripted interface the more likely it is to confuse people and create problems for them whilst using it. If the interface is simple, you can also spend less time documenting it and writing code to debug it. Binding a simple Vector typeThe discussion around type binding will continue by expanding the Vector example examined previously. I will outline a simple 3d vector class and create a type binding for the GameMonkey Script environment. Imagine a typical game engine's vector type (simplified): class Vector { public: Vector() : x(0), y(0), z(0) { } Vector( float a_x, float a_y, float a_z ) : x(a_x), y(a_y), z(a_z) { } float x, y, z; }; There are three constructors; one default constructor, one taking the values to initialise the vector with and a copy constructor, which is implicitly created by the C++ compiler. The first thing to do to bind this vector type to GM Script is to specify the basic type library. The 'library' is nothing more than a global function declared within the machine global context and an instruction to the gmMachine to register this function with a specified type name. This global function effectively becomes the type's constructor and is the ideal place to begin creating our bound type. The constructor entry point is specified in a gmFunctionEntry table as such: namespace gmVector { // Declare a type ID gmType Type = GM_NULL; int GM_CDECL libentry( gmThread *a_thread ) { Vector *p = new Vector(); a_thread->PushNewUser( p, Type ); return GM_EXCEPTION; } gmFunctionEntry lib[] = { { "Vector", libentry } // type name, entry point }; }; // end of namespace gmVector
You will notice the global declaration of a gmType variable named Type; this is to hold the GM type ID once the type is registered with the gmMachine. You are free to name the constructor whatever you want, but for organisational reasons I have named it libentry and placed it within the gmVector namespace. The libentry function is where the first step of the binding is done; we create an instance of the native class object and return it to GM by pushing it onto the thread as a new gmUserObject but with the type ID stored within our library. Now that the initial structure of the library is laid out we can register it with the gmMachine. Initialisation is done with 2 simple function calls: namespace gmVector { void BindLib( gmMachine *a_machine ) { // Register one function (the entry point) a_machine->RegisterLibrary( lib, 1 ); Type = a_machine->CreateUserType( lib[0].m_name ); } }; // end of namespace gmVector
Just like function binding, you need to register the library with the machine. After doing so you create your type and store it away for use in the rest of the library. The CreateUserType member function simply takes the name of a type to register; in this case I've retrieved it directly from the library constructor's name as you should ensure that it corresponds with the function you just registered as the constructor. |