This article series has since been combined, revised and updated
Get the source code
here
Introduction
This article will introduce you to the GameMonkey Script (abbreviated to
GM Script or simply
GM) language and API and how it can be used in your own games. It begins with a short introduction to the language and how it compares to Lua, the most similar language to GM Script and also a popular choice in game development. The article will then show you the basic features of the script language itself and, in the second part, teach you how to bind the virtual machine with your game applications. This article does not intend to be a comprehensive tutorial on using GameMonkey Script, nor will it cover the advanced topics and techniques that can be employed when scripting, however it will be enough to
whet your appetite for GM Script and provide you with enough information to explore the language on your own. You can follow along with the examples in the code by experimenting with the
gme.exe program that ships with standard GameMonkey distributions and running the example scripts that are supplied with this article.
Prerequisites
In order to get the most of this article and indeed GameMonkey script, it is assumed that:
- You have a working C++ compiler environment (an IDE or command-line, it doesn't matter)
- You are able to program to an average level in C++
- You are able to understand languages with a similar syntax to C/C++
- You have an interest or requirement in embedding scripting languages
What Is GameMonkey Script?
Scripting in games has long been an important technique for developers to employ. Scripting allows you to separate your game logic from your engine code and gives you the ability to tweak your content without time-consuming rebuilds of the entire engine. Some games, for example
Unreal, have vastly complicated scripting environments in place which allow you to create graphical effects and perform almost every action imaginable in your game. In the past it was popular for game developers to 'roll their own' scripting language that was tied directly into their game systems, however language parsing and virtual machine design is a complex subject, meaning that many developers are now choosing to use a third party scripting language.
Influenced by the popular embedded scripting language Lua, Matthew Riek and Greg Douglas set about creating their very own language for a game project whilst employment at Auran Software; and thus GameMonkey Script was born. Created specifically for game scripting, GM Script is written with speed and simplicity in mind. The original version of GM Script has been used in several commercial ventures, from PC games to console games, and is growing in popularity amongst hobby game developers. GameMonkey Script can be downloaded from
http://www.somedude.net/gamemonkey. At the time of writing, the most recent version is 1.24a.
Comparison to Lua
As GameMonkey script was originally inspired by Lua, it is useful to highlight the differences between the two environments.
GameMonkey Script Lua Script Syntax C-like C/BASIC hybrid Source Language C++ C Platform Cross-platform; has been compiled and used on PC, PS2, Xbox and GameCube Cross-platform; has been used on many platforms Numbers int, float double Array Start Index 0 1 Default Scope local global Multi-tasking Coroutines (called threads) Coroutines Garbage Collection Incremental Mark & Sweep Mark & Sweep (made incremental in 5.1beta) Parser Flex/Bison Custom Parser Data Access method Global table / function call params Stack access Statement terminator Semi-colon Optional semi-colon Ease of host binding:
without 3rd party tool Moderate Difficult Ease of host binding:
with 3rd party tool Simple/Moderate Simple Community size Small Large Syntax suited to: C/C++ programmers Beginners and non-programmers Availability of documentation / example code Low High Latest version 1.24a 5.1 (beta) License MIT MIT
Lua has been used successfully in many commercial and amateur game projects, however it can be difficult to use without a third party binding library as its stack-access method can be confusing for people new to scripting. GameMonkey Script aims to address the complexities of Lua whilst still maintaining its inherent power through flexibility.
Introduction to the Language
In the following section I will introduce you to the GameMonkey Script language. You will learn how to manipulate variables, how to create and call functions and how to use the powerful GM Script table type. This section is intended to be a primer to the language and it is assumed that you have some programming knowledge of C, C++, Java or other similar syntax styles. You can follow the simple examples I present here and experiment yourself using the gme executable that comes with standard GameMonkey Script distributions. gme is a stand-alone interpreter for GameMonkey Script which provides a basic GameMonkey Script environment for you to load your scripts into and test them. Because of this, gme will be a useful tool for your first steps into learning the GMScript language and syntax.
Using the gme interpreter is simple; from the command line you type 'gme path_to_script_file' (ensuring that gme is in your current directory). The script will then load and be executed, showing you the results in the console window.
Syntax Overview
This following section will give you an overview of the syntax used by the GameMonkey Script language. You will notice from the provided examples that the basic syntax of GM Script is very much like that of C; because of this GMScript may not be as instantly accessible as Lua or Python which aim to be simple for non-programmers to pick up.
GM Script features the common elements of most programming languages:
- Variables
- Comments
- Expressions
- Selection Statements (if / else)
- Loops (for, while, foreach)
- Functions
GameMonkey Script also features a built-in cooperative threading and state system, allowing you to create multiple threads within the virtual machine to run several parallel tasks at once. However, I will not venture into that territory in this introductory article.
GameMonkey Scripts: Defined
A script in GameMonkey is usually a plain-text ASCII file that contains a collection of functions, variable declarations and expressions. Scripts are usually loaded in from disk and compiled into a form that the GM environment can work with, namely bytecode. There is no standard form to a GameMonkey Script; you do not have to import modules like in Python, nor do you have to adhere to any indentation formatting (again, like Python). GameMonkey script does not have a native version of the include directive, so usually a script is self-contained (although you can load and execute multiple scripts on the same machine). The only requirement in GameMonkey Script is that function variables are declared before you try and call them; the ordering of such statements is important because GMScript code is compiled in a single-pass and also it does not have a pre-processor like in C/C++.
GameMonkey Variables
Unlike C/C++, GM Script is a
dynamically typed language; variables can assume any type at any time during the execution of a script. Also, variables do not need to be declared before use; one can simply make the variable assignment and the variable will be created and be of the type relevant to the data it holds. The basic types of GameMonkey Script are integers, floats, strings, functions,
tables and
null. Here is an example of some simple variables in use:
myNull = null; // has type null, which is a distinct type myNum = 50; // has type of int myString = "Hello!"; // has type of string myFloat = 3.14; // has type of float print( myNum ); print( myString ); print( myFloat ); As you will see, a variable declaration is as simple as assigning a value; variables that haven't been assigned will have the value and type of
null. You will notice that every line is terminated by a semi-colon. This denotes the end of a statement and unlike Lua, is non-optional in GameMonkey Script. To C/C++ developers this will come naturally and so shouldn't cause too many problems.
null is a distinct and important type in GMScript; a variable of null type is said to have no value. When referencing variables for the first time they automatically have the type null and will remain that way until an assignment is made. You should be careful when using nulls in comparisons and expressions; for example, concatenating a string with a null variable will result in the text 'null' being appended to the string. However in integer expressions, a null variable is interpreted as being
zero. This behaviour is different to some languages, such a T-SQL where operations involving null variables will themselves yield a null result. It should also be noted that assigning an existing variable be of type
null will allow it to be garbage collected by the GMScript machine; this consideration is especially important once you begin to create and bind your own types from C++ to the machine, but this shall be covered in the next article.
Comments
Comments in GM Script are exactly like those in C++; you can choose between C++ style line-comments or C-style block comments:
x = 5; // This is a line comment /* This is a block comment */ Like C++, block comments cannot be nested in GMScript. Because of this you can run into trouble if you are using the block comment features of the language to comment out sections of code. It is worth considering this when writing code as there is currently no GMScript-aware IDE that is intelligent enough detect this.
Expression and Conditional Syntax
GameMonkey Script can evaluate common expressions using the standard C-style expression syntax and as such, anyone familiar with C or C++ will feel comfortable using GameMonkey Script expressions.
x = 5; // Assign a value to x print( x ); y = (x * 10) + 56; // Assign a value to y print( y ); if (x == 10) // Comparison (equality) { print( "x is equal to 10" ); } if (x != 10) // Comparison (inequality) { print( "x is not equal to 10" ); } if (x < 10) // Comparison (less than) { print( "x is less than 10" ); } if (x > 10) // Comparison (greater than) { print( "x is greater than 10" ); } Example code: expressions_1.gm
GMScript also allows bitwise operations such as or ('|'), xor ('^'), and ('&') and bitwise complement ('~'). Logical operators include or ('||'), and ('&&') and logical complement ('!'). GMScript also allows the use of the keywords
and and
or to be used in place of && and || respectively.
One can employ conditional logic in GameMonkey script by using the
if statement.
if ( ) { // Do something if true } else if ( ) { // Do if 2nd condition passes } else { // Do if all are false } Unlike C/C++, GM Script does not contain a
switch statement so one must emulate the functionality by using blocks of if / else if tests. Also unlike C/C++, the body of conditional statements such as
if must be specified as block statements surrounded by curly braces, to do otherwise is an illegal syntax.
Loops and iterations
GameMonkey script has several methods of executing a loop. The first is the familiar for statement:
for (; ; ) { // repeated statement } A simple example to iterate a variable and print the number it contains would be:
for (it = 0; it <= 10; it = it + 1) { print( "it = ", it ); } Example code: loops_1.gm
The output will be the numbers from 0 to 10 printed on separate lines in the output console.
Like C/C++, for loops may contain empty statements (effectively simulating a while loop). However like the if statement, the body of the for loop must be surrounded by curly braces. For example:
for (; it != 10;) { print( "it = ", it ); it = it + 1; } The while statement is used in situations where the conditions around the loop aren't as certain; the most common use of the while loop is to loop until a particular flag is set. Again, like the for statement the body of the while statement needs to be enclosed in curly braces.
while ( ) { // repeated statement } For example, to repeat until the user has pressed the 'quit' button:-
while ( !quitButtonPressed ) { // do something in the game quitButtonPressed = host_check_for_quit(); } Note that the host_check_for_quit function is a hypothetical application-bound function. Similarly, an approximation of the for loop you saw previously would be:
it = 0; while (it <= 10) { print( "it = ", it ); it = it + 1; } Example code: loops_2.gm
The foreach loop allows you to iterate over the contents and keys of a table. I will cover this in more detail in the table section of this article.
That was a brief overview of the various statements, expressions and symbols used in the GM Script language; however it is far from exhaustive. For a full list of expressions and iterators along with their syntax you are advised to consult the GameMonkeyScriptReference which comes with the official GameMonkey Script releases. It is worth noting that the GMScript reference does contain a few errors; notably it claims that ~= is a valid assignment operation, which is not the case as it fails both to parse and make sense!
Scripted Functions
The GameMonkey Script machine has two forms of functions. The first is the scripted function, a function that is created and coded in script. A scripted function is specified in the same way as a normal variable:
myMultiply = function( x, y ) { return x * y; }; As functions are created and specified as variables, it is extremely important to remember the terminating semi-colon at the end of the function declaration. Calling a function is as you'd expect from a C-style syntax:
a = myMultiply( 100, 2 ); print( a ); Example code: functions_1.gm
The second type of function is that of the host-declared function. An example of a native function is the
print command which is contained within gmMachineLib and bound to every gmMachine you create. Only C-style functions or static class methods can be bound to the GameMonkey machine, but there are workarounds to this. I will cover the binding of native functions in the second part of this article.
The Table Type
The table is an important and powerful structure within GameMonkey script. At its most basic, it allows you to specify arrays of data; at its most complex, you can begin to create organised structures of data and functions for use in your games.
Tables as Arrays
A table can be used as a simple array which contains any type of data.
Initialisation Example:
myArray = table( 1, 2, 3.14, 4, 5, "dog", "cat", 100 ); Much like C/C++ arrays, you need to use indices to access the data within the table. All indices are zero-based when a table is initialised in this manner. Lua programmers should note this difference as in Lua, initialised tables begin at an index of 1.
Accessing the data:
myArray[0] = 50; print( myArray[1] ); myArray[100] = "dog_100"; // new item added print( myArray[100] ); Example code: tables_1.gm
Tables as Associative Arrays
A table can also be used as an associative array, much like the map structure in the C++ standard library. An associative array can be indexed using a non-numeric key, allowing for named data lookups. Again, the data items in an associative array can be of any type.
Initialisation:
myData = table( Name = "Test", Weight = 60 ); Accessing the data is as simple as the first example:
print( myData[ "Name" ] ); print( myData[ "Weight" ] ); myData["Name"] = "Albert"; myData["Test"] = "Some Text Here"; print( myData[ "Name" ] ); print( myData[ "Test" ] ); Example code: tables_2.gm
You will have noticed that we can assign new keys and indexes at any time as the tables have no defined bounds.
Tables as Mixed Arrays
You can use the table as a
mixed array, an array that contains both indexed data and keyed data (as in an associative array). This makes the table structure very flexible:
myTest = table( 1, 4, Test = "Text!", 7, 8 ); print( myTest[0] ); print( myTest[2] ); print( myTest["Test"] ); Example code: tables_3.gm
In the example above, the second print statement prints the number 7 and not as you may expect, the word 'Text!'. The reason for this is because GM Script keeps indexes and keys separate from each other within the table.
Because GMScript tables store their indexes as gmVariables, it is theoretically possible to index a table on
any type other than null (as indexing on null will return null). However, indexing tables on anything other than numbers or strings can be counter-intuitive, so it is best practise to avoid it.
Iterating Table Data - 'foreach'
The foreach statement allows you to iterate over the contents of a table in a loop. The most basic form of the foreach statement is to examine just the values within the table:
foreach ( in ) { // statements } An example of this follows:
fruits = table ( "apple", "pear", "orange" ); foreach ( frt in fruits ) { print(frt); } Example code: tables_4.gm
The code above will print the contents of the table to the console in no particular order. However, you may have noticed that the table key is often as important as the value it references and may wish to capture that data too:
foreach ( and in { // statements } An example:
fruits = table ( "apple", "pear", Juicy = "orange" ); foreach ( k and f in fruits ) { print( "The value at key '", k, "' is '", f, "'" ); } Example code: tables_5.gm
Will print something similar to:-
The value at key '0' is 'apple' The value at key 'Juicy' is 'orange' The value at key '1' is 'pear'
Simulation of 'structs' and simple classes with Tables
The final use of the table structure is to simulate C/C++ structs and classes. If you recall what I mentioned before, the GM Script table object can store
any type of data, including functions. Because of this, you can assign a scripted function to an index or key within a table. You should be aware that when declaring a function as a table member you
should not put the semi-colon line terminator as you do when declaring a function on its own.
myStruct = table( SayHello = function() { print( "Hello, world!" ); } ); myStruct.SayHello(); // Call table-bound function Example code: tables_6.gm
As you see in the example, you can access keyed table data using the period (dot) operator. This allows us to treat the table as a simple class structure, accessing the named elements in a familiar fashion.
myAlien = table( Name = "Alien", AttackPower = 20, Strength = 50, OnAttack = function( entity ) { entity.Damage( this.AttackPower ); } ); Example code: tables_7.gm
The slightly more complex example shows how simply a generic alien scripted object can be created using the basic GameMonkey Script types and how it is centred primarily around the use of the table object.
Unlike C++ classes, it is important to note that the GM Script table object has no constructor/destructor, cannot be inherited from and does not allow for custom operator overriding. However, you can achieve such behaviour through creating your own bound types (covered in the next instalment of this article). It should also be noted that GM tables have no concept of public, private and protected scoping as C++ presents for structs and classes. All table members are declared as being in the public scope and so can be accessed from anywhere. I will continue the scoping discussion in the next section.
Scoping
GameMonkey script has a range of scopes for variables (and hence functions). If you wish your functions or methods to be accessible from outside of the script (for example, to be read directly by the host application) you must declare them as being in the global scope. The global scope is accessible everywhere in the script; even within other functions. Without this declaration, the objects are implicitly within local scope, which means they're only accessible to within the current scope or lower.
// Create a variable in the global scope global myvar = 100; // parameter 'a_param' is in function local scope myfunc = function( a_param ) { // variable myvar is in local scope myvar = a_param; print( myvar ); }; print( myvar ); myfunc( 50 ); print( myvar ); Example code: scoping_1.gm
Hold up a minute; you will notice that I've created 2 variables called myvar, one in the function and the other in global scope. If you run this script you will notice that the value of the global myvar is unchanged, even though you set the value of myvar in the function. The reason for this is simple; they exist in different scopes! GameMonkey allows you to set global variables from within functions by explicitly specifying the scope of the variable. In this case, I add the global keyword to the myvar declaration in myfunc.
// Create a variable in the global scope global myvar = 100; // parameter 'a_param' is in function local scope myfunc = function( a_param ) { // Access variable myvar in global scope global myvar = a_param; print( myvar ); }; print( myvar ); myfunc( 50 ); print( myvar ); Example code: scoping_2.gm
Things can begin to become tricky, however, when using tables and the this operator. Whenever a variable is part of a table or user-defined object, it exists in the member scope of the parent object, or this. This concept will be familiar to you if you've done any work in C++, so I will not dwell on it. Let's have a look at the member scoping in use:
global mytable = table( myMember = 50, setMember = function( a_value ) { myMember = a_value; } ); print( mytable.myMember ); mytable.setMember( 100 ); print( mytable.myMember ); Example code: scoping_3.gm
The script above behaves similarly to the local scoping example; the myMember method isn't altered. However, when you include the member scoping keyword you will see a different result.
global mytable = table( myMember = 50, setMember = function( a_value ) { member myMember = a_value; } ); print( mytable.myMember ); mytable.setMember( 100 ); print( mytable.myMember ); Example code: scoping_4.gm
The this scoping is fairly complicated, but at the same time is very powerful. Using this scoping you can create generic delegates that can access the data of the object that is passed as this. Confused? Take a look at the following example:
myTable = table( myMember = 50 ); setMember = function( a_param ) { this.myMember = a_param; }; print( myTable.myMember ); myTable:setMember( 100 ); print( myTable.myMember ); Example code: scoping_5.gm
In this example the function setMember is completely external to the myTable object but is able to access its data and methods. The reason it is able to do this is though use of passing the myTable object as this when calling the setMember function. The body of setMember explicitly states that it will alter the data belonging to this without actually belonging to this at compile time. This allows you to create very powerful scripted functions which can exist in the global scope and be called from objects as if they were a member of that object itself. An abbreviation for typing this is to simply type a single period '.'. For a more complex example of this in action, please refer to scoping_6.gm which is included with this article.
It should be noted that this scoping is different to member scoping, although you could mistake the two if you're accustomed to C++. This scoping refers to the scope of the object passed as this. Member scoping allows you to specify that a variable is a member of this, making it of use in situations where a global or local member may conflict with your attempts to access a member variable of an object.
Further Exploration
By now you should have visited the most-often used aspects of the GameMonkey Scripting language and have hopefully experimented by running the example code through
gme and looking at the results. There is a fairly large amount of GameMonkey Script usage to cover; the most notable of the remaining topics are those of
script threads and the variable thread states that can be used in GameMonkey Script. There are also the vast machine library functions that allow you to control the virtual machine from within the script itself, but in order to keep this introduction simple I have left this up to you, the reader, to explore on your own. If there is enough interest I will happily cover the unexplored sections in a future article.
The next part of this two-part introduction to GameMonkey Script will focus solely on embedding the virtual machine in your game or application. By doing this you will be able to export and use your own functions, types and structures within GameMonkey and use it to control many aspects of your program.
Acknowledgements
The author would like to thank both Matthew Riek and Greg Douglas for their hard work in making GameMonkey Script what it is today. Thanks also to Jack Hoxley and Howard Jeng for their help in making this article worth reading.
References