Introduction to GameMonkey Script Part 2
Embedding GameMonkey
More on Script ExecutionAll of the examples we've looked at so far have assumed that you want to execute a script immediately; this can often not be the case in games where data and scripts need to be loaded and compiled at a specific time, usually while a loading screen is presented. GameMonkey has two ways of preventing a compiled script from immediate execution; the first is to set the optional a_now parameter of the gmMachine::ExecuteString member function to false when you call it. This will compile the string but not execute it until you call the Execute member function on the gmMachine object. An example of this follows: #include "gmThread.h" int main() { gmMachine gm; // Compile a simple script without running it gm.ExecuteString( "print( \"Hello, world!\" );", 0, false ); // The text wasn't printed! // Wait for a key press getchar(); // The script will now execute! gm.Execute( 0 ); return 0; }
A task for you, the reader, is to create an enhanced version of the gmLoadAndExecuteScript function which allows you to specify when the code is actually executed by specifying the a_now parameter appropriately. The second method of controlling when a script is executed involves compiling it down to raw bytecode and executing the stream as a library. This method is useful in games as the script is converted into a form that isn't as easily readable as a script stored in a text file. One common use for scripting in games is to execute a script within a game level when the a certain event is triggered; by using a bytecode version of your scripts you can easily include compiled script code within your game level file. This is beyond the scope of this article and will not be explored further, however if you wish to explore this on your own, you should refer to the functions CompileStringToLib and ExecuteLib which are members of gmMachine. Like using text files, you are responsible for writing your own loading and saving routines for the gmStream objects that contain the compiled scripts. In the example code execution_1.cpp you were introduced to a new member function of the gmMachine object, namely Execute. This function effectively tells the machine to execute all active threads in turn. As you can appreciate, it is extremely powerful when it comes to running scripted threads, as you can now update the GameMonkey virtual machine along with the rest of your game logic. Using scripted threads, or co-routines, is out of this article's scope but I will introduce you to the basic concept of how they work. Everything within the GameMonkey virtual machine runs in a gmThread object, which has its own executable bytecode, stack and scope. The virtual machine can hold many of these threads in memory at any one time, preserving the states of them until the threads are next 'ticked' by the machine. What happens behind the scenes of the ExecuteString function is that GameMonkey will spawn a new GM Script thread, compile the script into the thread and populate its internal structures with references to the functions and variables contained within this script. If a_now is true, the thread will execute immediately on the gmMachine, causing the bytecode to be interpreted until there is no more code or the thread yields control back to the virtual machine. If a_now is false, the thread will be pushed into the queue along with the other threads and will only be updated on an Execute cycle, again executing until there is no more code or the thread yields. The GM Script ObjectsIf you look back to the first article you will recall that I said GameMonkey Script has several built-in types, namely integers, floats, strings, functions, tables and null. All variables within GMScript are held and accessed initially by gmVariable objects, which is a catch-all type. Because some GameMonkey Script objects are reference types we must allocate them through the virtual machine; this rule applies to all but the three basic types - integers, floats and the null type. Therefore, if you need to create a function, string, Table or a user-data type you must use request them from the gmMachine in the form of the gmFunctionObject, gmStringObject, gmTableObject and gmUserObject C++ objects. I am not going to detail exactly how to use these types because you can glean this information from the GM API docs, but I will say that every time you need GM to handle any of these types you must allocate them via the gmMachine object. For example, if you need to pass a string as a function parameter you cannot just pass the literal or a pointer to the null-terminated string data. Instead, you must allocate a new gmStringObject using gmMachine::AllocStringObject and populate the data accordingly. Likewise, if you are creating a bound type which needs to reference a native structure you must allocate a gmUserObject and set its data as your native pointer. You will see examples of these operations in the following section. |