Jump to content

  • Log In with Google      Sign In   
  • Create Account


Like
0Likes
Dislike

Continuing GameMonkey Script: Advanced Use

By Oli Wilkinson | Published Aug 04 2009 12:17 AM in Game Programming

If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource




Introduction


Article Source Code

By reading my Introduction to GameMonkey Script articles you have been introduced to the basic features of the GameMonkey Script (GM Script or simply æGMÆ 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 æthisÆ
  • 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 systemÆs definition of a ôthreadö, the GM virtual machine is based around a co-operative threading model, so that upon every execution of the gmMachine::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 machineÆs 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 æworkÆ 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 and yield() 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 æbrainÆ of an entity which would æthinkÆ 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öÆs before the thread æwakes upÆ. 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 itÆs not taking up interpreted script cycles checking for a scripted variable, weÆre 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.

LetÆs 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 youÆre 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 æthisÆ


Now that youÆve explored the topics of GameMonkeyÆs threading model, using block to pause them and signal to resume execution, itÆs 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 GameMonkeyÆs 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 canÆt æcheatÆ 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, IÆm 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 æpanicÆ state when the PacMan eats a power-up and then revert back to æroamingÆ 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

IÆm 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 isnÆt engaged in state behaviour; likewise if stateGetLast() returns null the thread hasnÆt 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 itÆs 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...

IÆm 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 ScriptÆs 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 the gmMachine::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 simplicityÆs sake this script is embedded in the application.

#include <iostream>

#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 weÆve 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 <iostream>

#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, itÆs 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 youÆd 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<gmStringObject *>(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 æthisÆ


So far, most of the C++ binding examples weÆve been using have relied on us passing the objects to work on as a parameter to the callback functions. However youÆre more likely to want to pass this to functions explicitly or implicitly by binding them to an instance of a type û this is how weÆve been doing it in many of the actual scripts youÆve 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 of this 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:




VariableRead/Write?Description
IdReadGuid of the entity
XRead, WriteWrite X position of the entity
YRead, WriteWrite 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<ScriptEntity*>(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

IÆve 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 theyÆre 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 didnÆt 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<ScriptEntity*>(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 ScriptEntityÆs ScriptProperties table with ease. The following example shows a simple function that displays the entityÆs 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<ScriptEntity*>(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 youÆve 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
The thread stack is a effectively a list of gmVariable objects that is used to hold the function being executed, the variable for 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 weÆd have wanted to return a value from the function itÆs 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 youÆve seen have returned GM_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:




ValueMeaning
GM_OKSuccess
GM_EXCEPTIONAn error occurred
GM_SYS_YIELDCauses a thread to immediately yield execution to another thread
GM_SYS_BLOCKA block has been set on the thread, see later for more detail
GM_SYS_SLEEPForce the thread to sleep after setting the Sys_SetTimeStamp and Sys_SetStartTime values accordingly


There are other values available but they are internal and shouldnÆt 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 youÆd 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 userÆs 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 itÆs 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.

LetÆs 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 youÆd 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. IÆm 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 donÆt 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 the gmMachine::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 isnÆt 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 GameMonkeyÆs 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, heÆll inspect the area and move on if nothing is amiss. The guardÆs 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:

  1. Script calls NPCWalkTo( dest )
  2. Game pushes goto message to message queue
  3. Script call blocks thread and returns
  4. Game frame advances, entity manager picks up movement message
  5. Entity manager moves NPC each frame until destination is reached
  6. Message is sent back to script manager to inform destination reached
  7. Script manager signals thread of completion, script progresses as normal
Look at the example code 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 gameÆs 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 youÆd 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 guardÆs 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 weÆve 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 weÆre using a time based trigger that fires after a few seconds, but in a real game you would move an entityÆs 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 entityÆs thread in the form of a signal.

if (a_msg->MessageType == MT_GOTO_SUCCESS)

{

  NPCArrivalMessage *msg = reinterpret_cast<NPCArrivalMessage*>(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





Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS