Introduction
Article Source CodeBy reading my Introduction to GameMonkey Script articles you have been introduced to the basic features of the GameMonkey Script (GM Script or simply aeGMAE herein) language and how to embed it into your game or application. This article will teach you some of the more advanced aspects of the GM language and the virtual machine API. I will begin by describing the functionality and provide examples in GameMonkey Script code itself as this allows simple and quick demonstration of the features. I will then continue this discussion with examples in C++ code to demonstrate how to access this functionality from within your game engine itself.
In order to follow this article you are assumed to have read, understood and implemented the ideas and examples presented in the introductory articles. It is also expected that you are familiar with concepts such as simple messaging systems and event handlers so that you can follow some of the sample code without issue.
This article covers the following topics:
- Cooperative Threads
- Blocking / Signalling
- Threads and aethisAE
- Thread States
- The Structure of a Thread
- Creating Script-Extensible Entities
- Best practices for using GameMonkey
Scripting Cooperative Threads
Every script running within the GM virtual machine is executed within its own ?thread?. A ?thread? is standalone in that it has its own executable bytecode and stack but it runs within the overall context of the GameMonkey machine so can access data and functions from other threads. Unlike your operating systemAEs definition of a ?thread?, the GM virtual machine is based around a co-operative threading model, so that upon every execution of thegmMachine::Execute()
method each thread must either complete (terminate) or yield to the machine to allow other threads a chance to execute.
When a thread runs, it executes a function, a sequence of bytecode that is contained within its own stackframe. Whenever you execute a script in GameMonkey the bytecode is compiled into a function and is actually created within a thread to be executed. As a result, any explicit creation of your own threads will need a function to actually execute. This concept will be covered in more detail in a later section of this article.
Creating Threads from Script
There are two ways of creating a thread in GM; the first is via the scripting language itself ? there is an in-built function called thread()
that takes the thread function and the values to be passed to this function as arguments. The following example demonstrates how to create a new thread from script:
global thread_1 = function( a_count )
{
print( "[1] Starting..." );
for (i = 0; i < a_count; i = i + 1)
{
print( "[1] iteration: " + i );
};
print( "[1] Finishing..." );
};
print( "[0] Ready to execute..." );
thread( thread_1, 100 );
sleep(1);
print( "[0] Thread created..." );
Example: threads_01.gm
[0] Ready to execute...
[1] Starting...
[1] Iteration 0
...
[1] Iteration 99
[1] Finishing...
[0] Thread Created...
Output: threads_01.gm
In this example, a new thread is created within the machine which executes a function to count from 0 to 99. It will continue until the function is completed, hogging the machineAEs runtime until it is done. In this example, the sleep()
function is called to yield control from the main thread and into the new thread we create. Use of sleep
will be discussed later on in this article.
Yielding Thread Execution
Now that you are able to spawn a new scripted thread you can begin to run processes co-operatively. As mentioned before, GM is not based upon a pre-emptive threading environment so each thread needs to yield to others after it has finished a portion of work; it is up to the script writer to define what a portion of aeworkAE for a thread and should be.In the following example we create two threads, each counting from 0 to a specified number. This example does not use yield()
? what do you think will happen when you run it?
global thread_func = function( a_name, a_count )
{
print( "["+a_name+"] Starting..." );
for (i = 0; i < a_count; i = i + 1)
{
print( "["+a_name+"] iteration: " + i );
};
print( "["+a_name+"] Finishing..." );
};
print( "[0] Ready to execute..." );
thread( thread_func, 1, 100 );
thread( thread_func, 2, 100 );
sleep(1);
print( "[0] Thread created..." );
Example: threads_02.gm
[0] Ready to execute...
[1] Starting...
[1] Iteration 0
...
[1] Iteration 99
[1] Finishing...
[2] Starting...
[2] Iteration 0
...
[2] Iteration 99
[2] Finishing...
[0] Thread Created...
Output: threads_02.gm
As you see from the output of the script, the two threads ran consecutively and not concurrently as you might have expected. The reason for this is that although two threads were created, the first was executed which blocked the second thread until it had completed. Once complete, the second thread was free to run until completion. If you intended to run a single cycle of each thread in turn you need to tell the GM machine when to yield execution to other threads. In order to tell a thread to yield, you simply call the scripted function yield()
with no parameters.
The following example is the same script but with a yield()
in the loop of each function.
global thread_func = function( a_name, a_count )
{
print( "["+a_name+"] Starting..." );
for (i = 0; i < a_count; i = i + 1)
{
print( "["+a_name+"] iteration: " + i );
yield();
};
print( "["+a_name+"] Finishing..." );
};
print( "[0] Ready to execute..." );
thread( thread_func, 1, 100 );
thread( thread_func, 2, 100 );
sleep(1);
print( "[0] Thread created..." );
Example: threads_03.gm
[0] Ready to execute...
[1] Starting...
[1] Iteration 0
[2] Starting...
[2] Iteration 0
...
[1] Iteration 99
[2] Iteration 99
[1] Finishing...
[2] Finishing...
[0] Thread Created...
Output: threads_03.gm
After running the above script, you will see that instead of running consecutively as witnessed in the first example the two threads appeared to run concurrently. Internally, the GM machine ran only one thread at a time but the yield()
command instructed the virtual machine to switch contexts and execute the next thread.
Sometimes you may want to pause a thread for a specific length of time, for example if you had an NPC that needed to wait for 10 seconds at a waypoint before moving on. This can be achieved using the script command sleep()
, which takes a numeric parameter of how many seconds the thread needs to sleep for. A sleeping thread yields until the sleep duration has passed, after which it resumes execution of the next command. Try experimenting with the samples above and replace a yield()
with sleep to see the effects it has on execution.
Blocking and Signalling
In the real world context of a game you will not be using long loops andyield()
very often; instead thread execution and context switching can be controlled by the more powerful blocking and signalling mechanism provided by GameMonkey. In games you would use a scripted thread to act as the aebrainAE of an entity which would aethinkAE on every game cycle and perform an action based on an event or stimulus; the majority of the time it may be sitting waiting for that trigger, potentially by checking a loop. The following example demonstrates the traditional and extremely inefficient way of doing this:
global WakeUp = false;
global thread_1 = function( a_count )
{
while (WakeUp == false)
{
// Snooze
print("zzz");
yield();
}
print("I just woke up, boy was I tired!");
};
// Launch thread
thread( thread_1 );
// Sleep for 1 secs then set the wakeup variable
sleep(1);
WakeUp = true;
Example: blocking_01.gm
When running the script, you will see a screen full of ?zzz?AEs before the thread aewakes upAE. The thread is actually still running here, taking up CPU cycles and waiting for the WakeUp call. GM presents a much more efficient way of handling this scenario via its blocking and signalling mechanism.
A thread can block itself until a specific signal (or one of several signals) is received. Blocking effectively takes a thread out of the active thread pool, yielding until a signal wakes it up. A simple example is demonstrated below:
global thread_1 = function()
{
block("WAKEUP");
print("I just woke up, boy was I tired!");
};
// Launch thread
thread( thread_1 );
// Sleep for 1 secs then set the wakeup variable
sleep(1);
signal("WAKEUP");
Example: blocking_02.gm
The thread will block until it receives a signal string of ?WAKEUP?, which is sent to it after 1 second. This is more efficient as itAEs not taking up interpreted script cycles checking for a scripted variable, weAEre literally waiting upon a message from the GameMonkey machine to continue. It is important to remember that a thread can block or signal on any gmVariable
and not specifically strings, which I have used here as the most intuitive way to demonstrate the functionality.
LetAEs take a look a more complicated example involving two threads. Each thread will be created consecutively and will block on a different signal. The first signal will be thrown after the sleep command, which in turn will signal the other thread.
global thread_1 = function()
{
block("WAKEUP");
print("I just woke up, boy was I tired! You should wake up too!");
signal("YOUTOO");
};
global thread_2 = function()
{
block("YOUTOO");
print("What did you wake me up for?");
};
// Launch thread
thread( thread_2 );
thread( thread_1 );
// Sleep for 1 secs then set the wakeup variable
sleep(1);
signal("WAKEUP");
Example: blocking_03.gm
I just woke up, boy as I tired! You should wake up too!
What did you wake me up for?
Output: blocking_03.gm
Often it is desirable for a thread to be able to block until it receives one of a selection signals - for example, you may want your scripted entity to walk to an area and then wait there until it receives a command to move to another point, attack an entity or indeed simply defend itself. The block and signal mechanism offers support for this by allowing you to block on multiple signals. The signal which resumes the thread is returned from the block command allowing you to act in appropriate manner.
global blockfunc = function()
{
print( "Waiting for instruction, sir!" );
signal_received = block("attack", "move", "defend");
if(signal_received == "attack")
{
print("Attacking!");
}
else if (signal_received == "move")
{
print("Moving to position!");
}
else if (signal_received == "defend")
{
print("Defending til the death!");
}
};
thread( blockfunc );
sleep(1);
signal("attack");
Example: blocking_04.gm
Waiting for instruction, sir!
Attacking!
Output: blocking_04.gm
In the example above, the thread will block upon 3 signals, attack, move or defend. The signal received will determine what the thread then proceeds to do ? in this case the attack signal is received so the entity attacks.
Each of the signalling examples presented until now have relied upon the signal being global, meaning that if two threads were waiting on the same signal, they would both be activated together. In games this would mean that all of your game units waiting for a wakeup call will spring into action at once. You will be relieved to know that it is possible to send a signal to a single thread rather than globally. To achieve this you must know the Thread Id of the thread youAEre signalling, this is returned by the normal thread
function when you first create the thread. The following example is adapted to demonstrate the same block function being used with multiple threads.
global blockfunc = function(name)
{
print( name + ", waiting for instruction, sir!" );
signal_received = block("attack", "move", "defend");
if(signal_received == "attack")
{
print(name + " is attacking!");
}
else if (signal_received == "move")
{
print(name + " is moving to position!");
}
else if (signal_received == "defend")
{
print(name + " is defending til the death!");
}
};
thread_1 = thread( blockfunc, "woob" );
thread_2 = thread( blockfunc, "foo" );
sleep(1);
signal("attack", thread_1);
signal("defend", thread_2);
Example: blocking_05.gm
woob, waiting for instruction, sir!
foo, waiting for instruction, sir!
woob is attacking!
foo is defending til the death!
Output: blocking_05.gm
As you have seen, the blocking and signalling mechanism in GameMonkey Script allows you to create multiple threads within the virtual machine and have them remain dormant and taking up no execution cycles until a signal is received to call it back into action. This allows you to design complex behaviours within your entities that respond to the signals that are sent back and forth between objects.
Script Threads and aethisAE
Now that youAEve explored the topics of GameMonkeyAEs threading model, usingblock
to pause them and signal to resume execution, itAEs time to look at how we can use the concept of this
with threads to open up a lot of power for scripting in your game.
Each thread has a special gmVariable
associated with it - the this
variable. The concept of this
allows you to effectively run a thread against an object and always have that object in scope. If you recall in the introductory article, I demonstrated how you could use this
to reference specific objects in function calls. In GameMonkeyAEs threading system, this
can be used in exactly the same way, except that it can be accessed in every function. See the example that follows:
global my_entity = {
x = 50, y = 100, name = "test"
};
global move_ent_left = function()
{
print( "Starting thread - this.name = " + .name );
while( this.x > 0 )
{
this.x = this.x - 10;
print( this.name + " - x = " + this.x );
yield();
}
print( "Ending thread - this.name = " + .name );
};
my_entity:thread( move_ent_left );
sleep(1);
Example: threads_this_01.gm
Starting thread ? this.name = test
test ? x = 40
test ? x = 30
...
Ending thread ? this.name = test
Output: threads_this_01.gm
The code demonstrated above creates a simple table called my_entity, which has members x, y and name. The function move_ent_left, which will simply decrement the x position of this by 10 units, is created in the global scope and accepts no parameters, so we canAEt aecheatAE by passing an object instance to the function.
The thread itself is created as normal using the thread()
function, but with one key difference ? the my_entity table is passed as this
via the syntax my_entity:thread( func );
The next example will show the move_ent_left function being used for multiple objects and on multiple threads.
global robot = {
x = 50, y = 100, name = "robot"
};
global player = {
x = 150, y = 200, name = "player"
};
global move_ent_left = function()
{
print( "Starting thread - this.name = " + .name );
while( this.x > 0 )
{
this.x = this.x - 10;
print( this.name + " - x = " + this.x );
yield();
}
print( "Ending thread - this.name = " + .name );
};
robot:thread( move_ent_left );
player:thread( move_ent_left );
sleep(1);
Example: threads_this_02.gm
Starting thread ? this.name = robot
robot ? x = 40
Starting thread ? this.name = player
player ? x = 140
...
Ending thread ? this.name = robot
player ? x = 90
player ? x = 80
...
Ending thread ? this.name = player
Output: threads_this_02.gm
Clever use of threads and this
is an extremely useful way of giving different entities different behaviours, even if they have the same properties; all you would need to do is pass different functions on the thread. In the example that follows, we define 5 ?robots? that each posses a different behaviour ? this behaviour is executed at runtime.
global create_robot = function(x, y, name)
{
return { x = x, y = y, name = name, id, class="robot" };
};
global behaviour_stupid = function()
{
print( .name + " is acting stupidly" );
};
global behaviour_seek = function()
{
print( .name + " is seeking resources" );
};
global behaviour_rest = function()
{
print( .name + " is resting" );
};
global robot_def = {
{"tom", behaviour_seek},
{"mike", behaviour_rest},
{"jane", behaviour_stupid},
{"bob", behaviour_stupid},
{"sarah", behaviour_seek}
};
for(id = 0; id < 5; id = id + 1)
{
robot = create_robot(1 * id, 10 * id, robot_def[id][0]);
robot:thread(robot_def[id][1]);
}
sleep(1);
Example: threads_this_03.gm
tom is seeking resources
mike is resting
jane is acting stupidly
bob is acting stupidly
sarah is seeking resources
Output: threads_this_03.gm
If the robot were your normal game entity that is displayed on screen, IAEm sure you could appreciate how being able to script them in this way is extremely powerful.
Thread States
Games often utilise finite state machines to control the behaviour and state of an object. State machines are often used in game entity artificial intelligence scripts which may want to act upon a specific stimulus and then return to their previous state. Consider the PacMan ghosts who change into to a aepanicAE state when the PacMan eats a power-up and then revert back to aeroamingAE once the power-up has worn off; or the patrolling unit in an RTS game that encounters an enemy, intercepts to attack it and then resumes its patrol once the threat has passed.I have demonstrated how you can effectively use different functions to interact with a scripted object through the use of threads and this
. As you have seen, GameMonkey Script threads execute a function - the state binding allows you to change the function that is running on a thread. The thread will keep the same Thread Id, but will lose any locally defined variables in the change of execution context. The following code will demonstrate how to initiate a state and change to another:
global awake = function()
{
print("I'm awake!");
};
global resting = function()
{
print("I am resting");
sig = block("wakeup");
if (sig == "wakeup")
{
stateSet( awake );
}
};
global init_state = function()
{
// Set a state on the thread
// we're now using states
stateSet( resting );
};
thread( init_state );
sleep(1);
signal("wakeup");
Example: states_01.gm
I am resting
IAEm awake!
Output: states_01.gm
The state of a thread is set by the first call to the in-built global function stateSet()
that accepts the new state function and any additionally required parameters. You will notice that this
behaves almost exactly how you created the thread in the first place, except this time you are still using the current thread and just changing the function that is being executed. If you want to pass this to the new state, you must explicitly do so when calling stateSet()
.Once you have set a thread state you can transition to a new state at any time by a subsequent call to stateSet()
.
Any subsequent changes in state allow you to query the current state function by calling stateGet()
or the previous state by calling stateGetLast()
. This is useful as GameMonkey allows you to do comparisons on functions, letting you react according to the previous state or simply just resume the previous behaviour by switching the state back to what it was before. If stateGet()
returns null
, the thread isnAEt engaged in state behaviour; likewise if stateGetLast()
returns null
the thread hasnAEt had a previous state.
The example that follows will demonstrate an object that is created, blocks until a condition is met, performs an action and then resumes the previous action. In one object, it will start as asleep, stir and then go back to sleep ? another will start asleep, get panicked at a loud noise and then cause all sorts of chaos for the player.
global ent_state_panic = function()
{
print(.name + " is panicking, firing off alrams and attracting attention to you");
};
global ent_state_awake = function()
{
print(.name + " is waking up");
this:stateSet( stateGetLast() ); // revert to previous state
};
global ent_state_sleeping = function()
{
print(.name + " is sleeping");
sig = block("quiet_noise", "loud_bang", "kill");
if (sig == "quiet_noise")
{
this:stateSet( ent_state_awake );
}
else if (sig == "loud_bang")
{
this:stateSet( ent_state_panic );
}
else
{
print( .name + " killed" );
}
};
/// Initialise the state on the entity
global ent_state_init = function(func)
{
print( .name, " state initialised");
this:stateSet(func);
};
global ent_1 = { name = "roboguard 1000" };
global ent_2 = { name = "old ticker" };
// Create two threads, one for each entity and initialise them in the sleeping state
ent_1.threadid = ent_1:thread( ent_state_init, ent_state_sleeping );
ent_2.threadid = ent_2:thread( ent_state_init, ent_state_sleeping );
sleep(1);
print( "You stand on a twig");
signal("quiet_noise");
sleep(1);
print( "You fire a gun at " + ent_1.name + " causing a loud noise");
signal("loud_bang", ent_1.threadid);
// Tell the entity to die
signal("kill", ent_2.threadid);
Example: states_02.gm
roboguard 1000 state initialised
roboguard 1000 is sleeping
old ticker state initialised
old ticker is sleeping
You stand on a twig
old ticker is waking up
old ticker is sleeping
roboguard 1000 is waking up
roboguard 1000 is sleeping
You fire a gun at roboguard 1000 causing a loud noise
roboguard 1000 is panicking, firing off alrams and attracting attention to you
old ticker killed
Output: states_02.gm
The state transitions involved above are described in this diagram:
Sometimes itAEs useful to know about a state change before it happens to allow you to run cleanup code or trigger another behaviour. To achieve this you can call the function stateSetExitFunction()
and pass a function object. The function you pass will be called just before the state of the thread changes, allowing you to run whatever code you need to. When the function completes the state will transition as expected; you could use this to play a sound effect before the real event happens, for example.
global awake = function()
{
print("I'm awake!");
};
global waking = function()
{
print("I am stirring...");
};
global resting = function()
{
print("I am resting");
sig = block("wakeup");
if (sig == "wakeup")
{
stateSetExitFunction( waking );
stateSet( awake );
}
};
global init_func = function()
{
// set state on thread
stateSet( resting );
};
thread( init_func );
sleep(1);
signal("wakeup");
Example: states_03.gm
I am resting
I am stirring...
IAEm awake!
Output: states_03.gm
All state changes happen on the currently operating thread, but sometimes it would be useful to change the state of another thread in the machine. Imagine you have a thread blocking on a signal for the player to move out of cover; if the cover explodes, you may want to force the players taking cover into a different state, such as the one that decides the next action. The stateSetOnThread()
function allows you to do just this and requires a thread id and a state function.
global state_advance = function()
{
print("Leaving cover and advancing");
};
global state_decide_action = function()
{
print("I have to decide on a next action");
};
global state_hiding = function()
{
print("Behind cover, waiting to advance");
sig = block("advance");
if (sig == "advance")
{
stateSet( awake );
}
};
global init_state = function()
{
stateSet( state_hiding );
};
tid = thread( init_state );
sleep(1);
// Signal isn't thrown, tell this thread to change state
print("Cover explodes!");
stateSetOnThread( tid, state_decide_action );
Example: states_04.gm
Behind cover, waiting to advance
Cover explodes!
I have to decide on a next action
Example: states_04.gm
As you have seen, the threading functionality built into GameMonkey Script provides a useful toolset to control behaviours of your entities and offers flexible solutions to achieve your scripting needs.
Integrating GameMonkey with your Game
As you have seen, GameMonkey Script offers a lot of advanced functionality to script your game entities which can become quite complex for your script writers. In a real-world usage scenario you may wish to simplify your scripting interface and use many of GameMonkey ScriptAEs threading features invisibly. For example, if you create an NPC entity that has an update routine it is often more intuitive for a script writer to simply write a script such as:
npcupdatefunc = function() { ... do stuff... };
npc = createNPC(npcupdatefunction);
Rather than:
npcupdatefunc = function() { ... do stuff... };
npc = createNPC();
npc:thread(npcupdatefunction);
Your game may also feature asynchronous events such as dialog boxes or entity movement instructions that use the blocking functionality.
This section will describe how to work with the GameMonkey Script API to take advantage of the threading functionality and offer some simple methods by which you can simplify the interface you provide to your script writers without compromising on the flexibility offered by the scripting language.
Creation of gmThreads
A new thread can be created at any time in the virtual machine by calling thegmMachine::CreateThread()
method. The result of this action is that a new gmThread
object is created and a unique thread id is returned. You can call this method passing a gmVariable
to act as this and importantly, a gmVariable
containing the gmFunctionObject
that the thread needs to execute. The following code demonstrates the creation of a simple thread via the API that calls an existing function within script ? for simplicityAEs sake this script is embedded in the application.
#include
#include "gmThread.h"
int main(int argc, char* argv[])
{
// Create gm virtual machine
gmMachine gm;
// A test script which creates the function we're testing
const char *testscript = "global threadfunc = function() { print(\"Hello, threads!\"); };";
// Execute the script to create the function in the VM
if (gm.ExecuteString( testscript, NULL, true ))
{
bool first = true;
std::cout << gm.GetLog().GetEntry(first);
return 1;
}
int new_threadId = 0;
// Allocate a thread within the machine
gm.CreateThread( gmVariable::s_null, gm.GetGlobals()->Get(&gm, "threadfunc"), &new_threadId );
// Execute the machine
gm.Execute(1);
return 0;
}
Example: threads_01a.cpp
The code is simple; a script is executed to create the scripted function (a global named threadfunc) and a new thread is created using the reference to the script function obtained by reading from the global table. One thing to note is that as weAEve created a scripted function using script, we need to call gmMachine::Execute()
to run the thread because the original ExecuteString()
call actually created a thread to run in and it is still seen as the active thread in the machine.
To demonstrate the passing of this
to a thread upon creation we simply pass an actual variable installed of gmVariable::s_null
(the static null
value variable). An example of this follows, you will notice that the string I pass as this
is available in the script function we call.
#include
#include "gmThread.h"
int main(int argc, char* argv[])
{
// Create gm virtual machine
gmMachine gm;
// A test script which creates the function we're testing
const char *testscript = "global threadfunc = function() { print(\"'this' passed as - \", this); };";
// Execute the script to create the function in the VM
if (gm.ExecuteString( testscript, NULL, true ))
{
bool first = true;
std::cout << gm.GetLog().GetEntry(first);
return 1;
}
int new_threadId = 0;
// Allocate a thread within the machine
gm.CreateThread( gmVariable(gm.AllocStringObject("Hello, this!")), gm.GetGlobals()->Get(&gm, "threadfunc"), &new_threadId );
// Execute the machine
gm.Execute(1);
return 0;
}
Example: threads_01b.cpp
As you expect, itAEs entirely possible to execute a native function as the thread function, you simply bind the function as normal and use the gmFunctionObject
pointer wrapped up by a gmVariable
. The following code demonstrates the same result as the first, this time binding a function and calling that.
int GM_CDECL gmMyThreadFunc(gmThread *a_thread)
{
std::cout << "Hello, threads!" << std::endl;
return GM_OK;
}
int main(int argc, char* argv[])
{
// Create gm virtual machine
gmMachine gm;
// Bind the function to use by creating a gmFunctionObject
gmFunctionObject *threadfunc = gm.AllocFunctionObject( gmMyThreadFunc );
int new_threadId = 0;
// Allocate a thread within the machine
gm.CreateThread( gmVariable::s_null, gmVariable(threadfunc), &new_threadId );
return 0;
}
Example: threads_02.cpp
Any object can be passed to the thread as this if it has been registered within the gmMachine
as a gmType
and wrapped in a gmVariable
. The following example shows how you can access this
in the callback via the gmThread::GetThis()
method. In this case I expect it to be a string type, but in your code youAEd most likely use your own entity types.
int GM_CDECL gmMyThreadFunc(gmThread *a_thread)
{
GM_ASSERT( a_thread->GetThis()->m_type == GM_STRING );
gmStringObject *thisstr = reinterpret_cast(a_thread->GetThis()->m_value.m_ref);
std::cout << "'this' passed as " << thisstr->GetString() << std::endl;
return GM_OK;
}
int main(int argc, char* argv[])
{
// Create gm virtual machine
gmMachine gm;
// Bind the function to use by creating a gmFunctionObject
gmFunctionObject *threadfunc = gm.AllocFunctionObject( gmMyThreadFunc );
// Add function to the global table so scripts can access it
gm.GetGlobals()->Set(&gm, "threadfunc", gmVariable(threadfunc) );
// Call script to make callback, pass a variable containing "hello" as this
const char *script = "text = \"hello\"; text:thread( threadfunc );";
gm.ExecuteString( script );
return 0;
}
Example: threads_03.cpp
Working with aethisAE
So far, most of the C++ binding examples weAEve been using have relied on us passing the objects to work on as a parameter to the callback functions. However youAEre more likely to want to passthis
to functions explicitly or implicitly by binding them to an instance of a type ? this is how weAEve been doing it in many of the actual scripts youAEve seen. This next section will demonstrate how to access and use this
in your native callbacks and continue to provide a simple example of some code to extend your bound types.
As you have seen previously, every gmThread
has a stack slot allocated for the this
variable. This can be accessed in raw gmVariable
form by calling gmThread::GetThis()
. The main difference between GetThis()
and Param()
is that GetThis()
returns the gmVariable
via a const pointer and not by value like the Param()
functions. In the previous example (threads_03.cpp), you were accessing a string variable passed as this
. It is often required that you validate the type of this variable ? the gmThread.h
file defines a series of useful macros for checking function parameters, there are also a few for checking the type of this
, such as GM_CHECK_THIS_STRING
, which will return a thread exception if this
is passed as anything other than a string type.
Like the Param()
functions, there are several helpers for automatically and safely converting this back to the various native types associated with the GameMonkey type. So, for example ThisString()
returns as a const char*
and ThisInt()
returns an int
.
int GM_CDECL gmThisTest(gmThread *a_thread)
{
GM_CHECK_THIS_STRING;
std::cout << "'this' passed as " << a_thread->ThisString() << std::endl;
return GM_OK;
}
Example: this_01.cpp
There are three methods for returning your user types, ThisUser()
returns a void*
and the gmType of the object it holds; ThisUserCheckType()
will only return a non-NULL
pointer to the object if the type you pass in matches the object type and finally ThisUser_NoChecks()
passes you just the pointer to the data held in the user object. Of course, these are simply common conversions ? you are encouraged to create your own routines should you require anything else, such as converting back and forth to your entity type automatically.
Script-Extensible Objects
The use ofthis
becomes incredibly important when it comes to working with your own user types and binding functions and data to them. The rest of this section will demonstrate a highly simplified game entity class that allows you to extend it with scripted properties.
The first thing we do is define our game entity. In this example we declare it to have the following attributes:
Variable Read/Write? Description Id Read Guid of the entity X Read, Write Write X position of the entity Y Read, Write Write Y position of the entity
If you think back to the simple Vector binding I demonstrated in the original articles, you will remember that we used the raw Vector*
pointer as the data tied to GameMonkey and bound the Dot/Ind operators to allow modification of the underlying data. This time along we want to take advantage of the flexibility of GameMonkey and effectively allow additional properties to be added to the object ? for example, allowing the script writer to add a ?name? property to the object.
struct Entity
{
Entity() : Id(0), X(0), Y(0) { }
Entity(int id) : Id(id), X(0), Y(0) { }
int Id, X, Y;
};
Example: ext_01.cpp
The first step to achieving this goal is to create a proxy object that holds both the raw Entity data and a gmTableObject that will be used to hold our additional properties; this proxy is represented by a ScriptEntity class. We have a choice to make at this stage; do we want to be invasive to the Entity class and allow it to hold a pointer to the ScriptEntity or do we want the ScriptEntity to wrap the Entity object? I have chosen to add a pointer to the ScriptEntity into the Entity class; this enables any other consumers of the Entity class to be aware of the additional properties and create their own or read any existing ones ? this allows for some nice data-driven entities to be created as you will see shortly.
struct ScriptEntity
{
ScriptEntity(Entity &ent) : EntityObject(ent), ScriptProperties(NULL) { }
Entity &EntityObject
gmTableObject *ScriptProperties;
};
Example: ext_01.cpp
Once the basic objects have been defined the type is bound as normal. I have bound a global function createEntity()
to the script environment which will allocate a new Entity from the EntityManager and also allocate a new ScriptObject and gmTableObject for it.
int GM_CDECL gmCreateEntity(gmThread *a_thread)
{
// Create an entity from the manager
int entityId = 0;
Entity &ent = s_EntityManager.CreateEntity(entityId);
// Allocate a script entity & construct it
ScriptEntity *sent = reinterpret_cast(s_scriptents.Alloc());
GM_PLACEMENT_NEW( ScriptEntity(ent), sent );
// Tie back to entity
ent.ScriptObject = sent;
// Create a property table
sent->ScriptProperties = a_thread->GetMachine()->AllocTableObject();
// Tell GM how much memory we're using
int memadjust = sizeof(gmTableObject) + sizeof(Entity) + sizeof(ScriptEntity);
a_thread->GetMachine()->AdjustKnownMemoryUsed(memadjust);
// return to client
a_thread->PushNewUser( sent, s_entitytype );
return GM_OK;
}
Example: ext_01.cpp
IAEve chosen to use some of the in-built GameMonkey memory allocation routines for the ScriptEntity creation (gmMemFixed); this is a fairly simple fixed memory allocator which uses a freelist for reallocations. You are free to use the normal new
and delete
operators or to rely on memory allocation routines from your own engine.
Once this function is bound, you can create entities in script ? but theyAEre not much use until you bind the operators. In this case I will bind the GetDot and SetDot operators only. They will both work in similar ways; the object is retrieved from the relevant operand and the text of the property is resolved from the gmStringObject it is passed as. If the property is resolved to either X, Y or Id (the properties of the Entity class), we simply access the relevant property on the Entity. Any other property accesses will go straight to the gmTableObject we created on the ScriptEntity, thus allowing us to get and set properties that didnAEt exist on the base class. The SetDot operator code is shown below as an example.
void GM_CDECL gmOpSetDot(gmThread *a_thread, gmVariable *a_operands)
{
if (a_operands[0].m_type != s_entitytype)
{
a_thread->GetMachine()->GetLog().LogEntry( "gmEntity:OpSetDot invalid type passed" );
a_operands[0] = gmVariable::s_null;
return;
}
// Get scriptentity back
ScriptEntity *sent = reinterpret_cast(a_operands[0].GetUserSafe(s_entitytype));
// Get name of 'get' prop
std::string propname = a_operands[2].GetCStringSafe();
// Test for our known properties and return their values back immediately
if (propname == "X")
{
sent->EntityObject.X = a_operands[1].GetIntSafe();
}
else if (propname == "Y")
{
sent->EntityObject.Y = a_operands[1].GetIntSafe();
}
else if (propname == "Id")
{
a_thread->GetMachine()->GetLog().LogEntry( "gmEntity:OpSetDot cannot set Id" );
a_operands[0] = gmVariable::s_null;
}
else
{
// Otherwise, store a value in the table
sent->ScriptProperties->Set(a_thread->GetMachine(), propname.c_str(), a_operands[1]);
}
}
>Example: ext_01.cpp
When you bear in mind that GameMonkey Script functions can also be stored as variables, you will see that you can start adding scripted functions to the base entity class with ease ? be it from the C++ API or from script itself. All functions invoked on the object have this
implicitly passed, so code such as the following is possible:
ent = createEntity();
ent.name = "somebody";
ent.sayname = function()
{
print( "My name is: ", this.name );
};
ent.sayname();
By using the C++ API and accessing this
, you can begin to add some script-bound functions to the ScriptEntityAEs ScriptProperties table with ease. The following example shows a simple function that displays the entityAEs position, id and name ? with name being a property added within script.
// The entity is passed as this
int GM_CDECL gmEntityInfo(gmThread *a_thread)
{
ScriptEntity *sent = reinterpret_cast(a_thread->ThisUserCheckType(s_entitytype));
if (sent == NULL)
{
a_thread->GetMachine()->GetLog().LogEntry("gmEntityInfo: Expected entity type as this");
return GM_EXCEPTION;
}
std::stringstream infotext;
infotext << "EntityId: ";
infotext << sent->EntityObject.Id;
infotext << " - Postition("; infotext << sent->EntityObject.X; infotext << ","; infotext << sent->EntityObject.Y; infotext << ") - Name: ";
// Get name from table and pass to object
gmVariable namevar = sent->ScriptProperties->Get(a_thread->GetMachine(), "name");
if (namevar.IsNull() || namevar.m_type != GM_STRING)
{
infotext << " [No name]";
}
else
{
infotext << namevar.GetStringObjectSafe()->GetString();
}
std::cout << infotext.str() << std::endl;
return GM_OK;
}
Example: ext_01.cpp
In the full example code ext_01.cpp
(not shown), I added an entry to the ScriptProperties table to automatically hold this function for each entity created by the script engine:
// Bind an additional method to the entity's prop table
sent->ScriptProperties->Set(a_thread->GetMachine(), "ShowInfo", gmVariable( a_thread->GetMachine()->AllocFunctionObject(gmEntityInfo) ) );
The script we run can be modified as follows to use the new function:
ent = createEntity();
ent.name = "somebody";
ent.ShowInfo();
And the output?
EntityId: 1 ? Position(100,0) ? Name: somebody
In this section youAEve learned how to use GameMonkey Script to extend an existing native class and attach new data and functions to it. Armed with this information you are free to create powerful entity types and allow your script writers to extend them without any changes to the engine code.
Threads, The Stack and Function Calls
As threads are an important part of GameMonkey Script, I will use this section to cover how they work in more detail. I will also discuss how a function is called within the GameMonkey Virtual Machine (VM) and how this is tied to threads.A thread on the VMis composed of the following components:
- A unique idenfier
- A stack
- A function to execute
- A pointer to the current instruction (if any)
- A list of blocks and signals pending on the thread
- State information about the thread
this
, any function parameters passed to the thread and then finally any working variables, including return values from functions.
The thread function is a gmFunctionObject
which can be either a native function callback or a scripted function. A scripted function is a series of bytecode instructions that are interpreted by the GameMonkey VM to create the behaviours you see in script. The execution of the VM bytecode is dependent on the structures in the thread and the VM cannot execute a function without it.
When a function is called in GameMonkey, what actually happens? The first thing pushed to the stack is a variable containing this
, it will contain null if there is no value for this
. A variable holding the function object is next to be pushed to the stack, followed by any function parameters in order. Finally, a stackframe is pushed to complete the setup and initialise all internal data and structures to ready the thread for execution on the VM. The stackframe is important as it allows the VM to maintain the correct pointers to instructions and data to, for example, allow nested function calls to be made within the system. The function is then executed by the virtual machine, either by calling a native function or by interpreting the bytecode involved with the scripted function. Any return value is pushed back to the stack and the stack frame is popped, allowing the script to continue where it left off before the function call.
We can see this in action by manually creating a thread and calling a native function. The first code we write is a simple function that accepts two integer parameters and writes them to cout
.
int GM_CDECL gmFunctionTest(gmThread *a_thread)
{
GM_CHECK_INT_PARAM( a_param1, 0 );
GM_CHECK_INT_PARAM( a_param2, 1 );
std::cout << "Param 1: " << a_param1 << std::endl;
std::cout << "Param 2: " << a_param2 << std::endl;
return GM_OK;
}
Next, we follow the steps detailed above to call the function via the GameMonkey API.
gmMachine gm;
int threadid = 0;
gmThread *thread = gm.CreateThread(&threadid);
thread->PushNull(); // Push 'this'
thread->PushFunction( gm.AllocFunctionObject(gmFunctionTest) ); // push function to call
thread->PushInt(50);
thread->PushInt(100);
thread->PushStackFrame(2);
Example: functioncall_01.cpp
As you can see, we create a thread, push the required information to it and then finally execute it by pushing the stack frame using gmThread::PushStackFrame
. If the function were a scripted function, you would need to run it by executing the thread using gmThread::Sys_Execute
or by using gmMachine::Execute()
to execute the whole machine.
gmMachine gm;
// Create a global scripted function
gm.ExecuteString("global func = function(a, b) { print(\"Param 1:\", a); print(\"Param 2:\", b); };");
gmFunctionObject *func = gm.GetGlobals()->Get(&gm, "func").GetFunctionObjectSafe();
int threadid = 0;
gmThread *thread = gm.CreateThread(&threadid);
thread->PushNull(); // Push 'this'
thread->PushFunction( func ); // push function to call
thread->PushInt(50);
thread->PushInt(100);
thread->PushStackFrame(2);
thread->Sys_Execute();
Example: functioncall_02.cpp
If weAEd have wanted to return a value from the function itAEs a simple case of pushing it to the stack before we return and retrieving it either by getting the top of the stack from the thread, or if it was a scripted function the call to Sys_Execute
accepts an optional pointer to a gmVariable
allowing you to capture this if you need to.
This section has shown you how to manually call functions and how they interact with the threads and stack. It is recommended that you use the gmCall
helper for most of your script calls as it wraps all of this functionality for you already and reduces the chance of errors occurring.
Callback Return Values
So far, all of the examples of bound functions youAEve seen have returnedGM_OK
or GM_EXCEPTION
. This section will describe how these are used by the virtual machine and discuss other values you can use.
When a function call is made or an operator callback is invoked, you have a chance to indicate success or failure to the VM execution engine. If the call is successful (GM_OK
), everything continues as normal ? but what happens if there is an error? An error could be that you received the wrong parameter type to a function callback, or maybe even an internal game error that requires your script environment to be notified. In such cases, you would return a value of GM_EXCEPTION
to the thread, which has the effect of causing it to terminate.
Usable values are as follows:
Value Meaning GM_OK Success GM_EXCEPTION An error occurred GM_SYS_YIELD Causes a thread to immediately yield execution to another thread GM_SYS_BLOCK A block has been set on the thread, see later for more detail GM_SYS_SLEEP Force the thread to sleep after setting the Sys_SetTimeStamp and Sys_SetStartTime values accordingly
There are other values available but they are internal and shouldnAEt be used for risk of corrupting the execution of the VM.
Signalling via the API
Like using signals in the script language you can signal threads from the API. This allows you to implement event-based systems that trigger game events as signals to the entire virtual machine as or to specific threads that are running on particular game entities. One use may be an RPG game that pops up a dialog box for a quest; in script youAEd tell the engine to open the quest dialog with a specific text and wait for the user to accept or decline it. An example script would look like:
ShowQuestDialog( Quests.SaveThePrincess );
response = block( QuestDialog.Accept, QuestDialog.Decline );
if (response == QuestDialog.Accept)
{
// do something
}
else
{
// do something else
}
In this example, the ShowQuestDialog()
function would be a native function that pops up a non-modal dialog that itself waits for a button response event from your GUI subsystem. As the dialog is non-modal, you need your script to respond from the userAEs input in some way, so you block on two signals to indicate accept or decline. Because the thread is waiting for a block it becomes dormant in the GameMonkey machine and therefore takes up no execution time until itAEs woken by a signal. The signal itself would be fired by your script subsystem in response to a GUI button press event for that specific dialog.
LetAEs look at some sample code:
enum QuestDialog
{
QD_DECLINE,
QD_ACCEPT
};
// This function simulates a modal quest dialog
int GM_CDECL gmShowQuestDialog(gmThread *a_thread)
{
std::cout << "A princess is in danger, do you wish to save her?" << std::endl << "[Accept | Decline]" << std::endl;
return GM_OK;
}
void HandleUserAcceptQuest(gmMachine &gm, int threadid, QuestDialog response)
{
// Fire signal to thread to indicate user's choice
gm.Signal( gmVariable( (int)response ), threadid, 0 );
}
int main(int argc, char* argv[])
{
// Create gm virtual machine
gmMachine gm;
// Bind ShowQuestDialog function to script
gm.GetGlobals()->Set( &gm, "ShowQuestDialog", gmVariable(gm.AllocFunctionObject( gmShowQuestDialog )) );
// Create a global table to hold Ids for QuestAccept/Decline
gmTableObject *dialogResponses = gm.AllocTableObject();
dialogResponses->Set( &gm, "Accept", gmVariable(QD_ACCEPT) ); // Accept = 0
dialogResponses->Set( &gm, "Decline", gmVariable(QD_DECLINE) ); // Decline = 0
gm.GetGlobals()->Set( &gm, "QuestDialog", gmVariable( dialogResponses ) );
// Run an example script that calls dialog and blocks on response
const char *script = "ShowQuestDialog(); \n"
"response = block( QuestDialog.Accept, QuestDialog.Decline ); \n"
"if (response == QuestDialog.Accept) { print(\"[Quest accepted]\"); } else { print(\"[Quest declined]\"); }";
int threadid = 0;
// Run script and capture thread it's running on
gm.ExecuteString( script, &threadid );
Sleep(5000); // Wait for 5 seconds to simulate user deciding to accept
HandleUserAcceptQuest(gm, threadid, QD_ACCEPT);
// Tick the machine along
gm.Execute(0);
return 0;
}
Example: signals_01.cpp
The majority of the code is actually setting up our global script variables and binding the demonstration quest dialog function. The sample script is run on the machine which immediately calls the quest dialog function (in your implementation youAEd open a real GUI window) and then immediately blocks for the response. The signal itself is sent via a call to gmMachine::Signal
, which accepts a gmVariable
and an optional thread Id ? you will see that this is identical to calling signal from script.
A second example is that of a door created in script. The door itself will remain closed until the game instructs it to open, perhaps by a person using a switch or firing some kind of trigger. The script for the door is simple; the door is created and immediately blocks for the signal to open. When this trigger is received, an animation game event is played asynchronously while the door script sleeps for 5 seconds. After it resumes, the close animation is played and the door goes back to blocking ? this behaviour will repeat forever or until the thread is terminated by the game, perhaps in response to the door becoming impassible or destroyed.
global DoorFunction = function()
{
while(true)
{
print(this, "waiting for use...");
block("usedoor");
this:startAnimation("dooropen");
sleep(5);
this:startAnimation("doorclose");
}
};
createDoor(DoorFunction);
Example: signals_02.cpp ? Door Script
The native callback createDoor()
will create a new door instance and run the function you pass it. Note that in this example I just demonstrate the object using a text string, your game will most likely feature an actual door entity being created and returned as a user object.
int GM_CDECL gmCreateDoor(gmThread *a_thread)
{
GM_CHECK_FUNCTION_PARAM(a_func, 0);
// Simple example, return "TESTDOOR" as the user object
gmStringObject *door = a_thread->GetMachine()->AllocStringObject("TESTDOOR");
// Call passed in function as a new thread (gmCall does this under the hood)
gmCall gmcall;
gmcall.BeginFunction(a_thread->GetMachine(), a_func, gmVariable(door), false);
gmcall.End();
a_thread->PushString(door);
return GM_OK;
}
Example: signals_02.cpp
To open the door at any point in the game, you fire the signal. IAEm executing the next frame of the VM manually here, but your game loop will be doing this each frame automatically.
// Open the door... fire global signal for now
gm.Signal(gmVariable( gm.AllocStringObject("usedoor") ), GM_INVALID_THREAD, 0);
Example: signals_02.cpp
The output of this whole example:
TESTDOOR waiting for use...
TESTDOOR starting animation: dooropen
TESTDOOR starting animation: doorclose
TESTDOOR waiting for use...
Output: signals_02.cpp
This example is an excellent way of showing how the thread that controls the door can be created within the game code itself, removing the need for a user to explicitly create them to run the door script. To the user, they are simply creating a door that will play out their behaviour, they donAEt even have to know about the signal being fired by the game.
Blocking via the API
It is possible to set a thread to block using the API and it allows more flexibility than blocking in script. When you block from within the GM Scripting language itself the currently executing thread is suspended immediately and is set to the status of ?blocked?. Blocking a thread from the API allows you to continue processing as normal; the blocked state of the thread is only registered upon the next execution cycle of the virtual machine. To block a thread, you need to use thegmMachine::Sys_Block
function; this function takes a thread Id and a list of gmVariables to block on. Although it is possible to block on a thread that isnAEt the current thread, it is not recommended as it may cause undesirable or unpredictable behaviour in the scripts running on the machine (such as a thread blocking for a signal that will never occur).
A quick example will show you how to set up a block via script:
enum
{
SIG_ZERO,
SIG_ONE
};
int GM_CDECL gmBlockTest(gmThread *a_thread)
{
gmVariable blocks[] = { gmVariable(SIG_ZERO), gmVariable(SIG_ONE) };
int ret = a_thread->GetMachine()->Sys_Block( a_thread, 2, blocks );
if (ret == -1)
{
return GM_SYS_BLOCK;
}
a_thread->Push(blocks[ret]);
return GM_OK;
}
Example: blocks_01.cpp
In this callback function I have set up two variables containing the values 0 and 1 and have instructed the GameMonkey machine to block the currently running thread on them. Under the hood, the gmMachine::Sys_Block
function will attempt to use up any signals already pending on the thread; if a matching signal is found for one of the blocks, it will return immediately with the index of the block variable you supplied. When this happens, I push the variable back to the thread stack as a return value so that it returns the block immediately to the user to allow them to check which block was signalled. If no corresponding signal is found, Sys_Block
returns a value of -1 ? when this happens you return a value of GM_SYS_BLOCK
to indicate to the VM that this thread should be suspended until a signal is received to wake it. When you bind the function above to the machine, it lets you run a script such as:
r = blocktest();
print( ?signal fired:?, r);
This will block until you signal the thread either using gmMachine::Signal
or the signal()
command from script.
Games often feature asynchronous actions, such as playing an animation or sound, actor pathfinding/movement and so on. In the real world, many of these actions will be kicked off with an event dropped onto a message queue which is picked by the relevant system. Imagine the case of an NPC being instructed to move across town; the entity management system will pick up the movement message and move the entity a little bit each frame until the NPC arrives at its destination many frames later. At this point the entity manager it will signal back to the game that the movement has completed.
Your scripts will often want to trigger asynchronous behaviours but will need to treat them as if they are synchronous by waiting for the result of the task before deciding how to proceed. If the machine-bound function returned immediately after queuing the message, the script would continue as normal without waiting for completion. In a previous section you achieved this in script by using GameMonkeyAEs blocking and signalling functionality; in this case you could fire your event and then immediately block for the signal back from the game engine to wake up the thread.
doasyncbehaviour();
ret = block( ?ok?, ?failed? );
// continue as normal
Although this works, it can be a little clunky in practice and requires that your script writers always remember to block after an asynchronous behaviour. In this next section I will demonstrate an example of working with asynchronous game events elegantly with GameMonkey Script.
For this example I will allow you to imagine a town guard in an RPG game that wanders back and forth between two towers. When he gets to a tower, heAEll inspect the area and move on if nothing is amiss. The guardAEs behaviour is described as the following series of states:
In the game engine this will be achieved by initiating the movement using a goto message to the entity manager. Upon arrival at the destination a goto_success message is sent back to the game system ? should anything happen such as the destination being unreachable, the entity being attacked or so on, a goto_failed message is sent back to the game, along with other messages for the next action (if any).
The sequence of events in this asynchronous call are as follows:
- Script calls
NPCWalkTo( dest )
- Game pushes
goto
message to message queue - Script call blocks thread and returns
- Game frame advances, entity manager picks up movement message
- Entity manager moves NPC each frame until destination is reached
- Message is sent back to script manager to inform destination reached
- Script manager signals thread of completion, script progresses as normal
blocking_02.cpp
for a simple yet realistic implementation of the above behaviour. In the code, we bind two functions to the GameMonkey machine, CreateNPC()
which is tied into the gameAEs entity manager and NPCGoto()
which accepts an entity and a location as a parameter. In this simple example, the location is either TOWER_ONE
or TOWER_TWO
in the global Location table. In a real game youAEd most likely specify a vector, or would bookmark real locations with names like TOWER_ONE
. The first thing we do in the script is create the guard NPC and start a thread for it to run in.
global GuardMove = function()
{
this:stateSet(GuardGotoTowerOne);
};
npc = CreateNPC();
npc:thread(GuardMove);
The guardAEs behaviour function immediately jumps into the state that tells it to go to the first tower.
global GuardGotoTowerOne = function()
{
print("Going to Tower One...");
res = NPCGoto(this, Location.TOWER_ONE);
if (res == Event.SUCCESS)
{
print("Arrived at tower One");
this:stateSet(GuardWait);
}
else
{
print("Couldn't get there");
}
};
This script function calls the NPCGoto()
function and passes TOWER_ONE
as a location for this entity to travel to. The native code behind this function fires off a message to the game system that indicates we want to move the entity to the specified location.
GM_CHECK_INT_PARAM(a_entity, 0);
GM_CHECK_INT_PARAM(a_location, 1);
// Push message to Game Message queue
NPCGotoMessage *msg = new NPCGotoMessage( a_entity, a_thread->GetId(), a_location );
s_Game.GetMessageSystem().PushMessage( msg );
The bound function then immediately sets up a block on the scripted thread that suspends it until weAEve received the success or failure result back from the game.
gmVariable blocks[] = { gmVariable(SM_SUCCESS), gmVariable(SM_FAILED) };
int res = a_thread->GetMachine()->Sys_Block( a_thread, 2, blocks );
if (res == -1)
{
return GM_SYS_BLOCK;
}
The game continues as normal, the entity manager picks up the NPCGotoMessage
we posted and processes the movement of the entity each frame. In this example weAEre using a time based trigger that fires after a few seconds, but in a real game you would move an entityAEs co-ordinates in the world.
if (_Dur >= 2500.0f) // Simulate time passing
{
NPCArrivalMessage *msg = new NPCArrivalMessage(ent._Id, ent._ScriptThreadId, ent._Destination);
s_Game.GetMessageSystem().PushMessage(msg);
ent._State = Entity::ES_WAITING; // Go back to waiting state
_Dur = 0;
}
The script manager system is set up to listen for an NPCArrivalMessage;
when it recieves one it will post the result of the movement request back on the entityAEs thread in the form of a signal.
if (a_msg->MessageType == MT_GOTO_SUCCESS)
{
NPCArrivalMessage *msg = reinterpret_cast(a_msg);
// Fire signal on thread
_Machine.Signal( gmVariable(SM_SUCCESS), msg->ScriptThreadId, 0 );
}
The GameMonkey Script machine processes the signal on the next update and the thread resumes op