Skip to content

Scripting

fredakilla edited this page Jul 9, 2018 · 11 revisions

The gplay framework has a full set of Lua script bindings allowing users to write the majority of their game completely from scripts. Also included is an open-source tool that can be used to generate bindings for user-defined classes (including classes that depend on features and technology of the gplay framework itself).

Writing Lua scripts

To write Lua scripts, you can use any text editor or IDE. If Visual Studio is your editor of choice, it is recommended that you install the Lua Language Support extension, which adds syntax highlighting to the editor.

To generate your own bindings, you will need to download and install Doxygen.

Lua Basics

For an introductory look at scripting with Lua, including basic language features, visit http://www.lua.org/pil/. Note that the documentation available at this link is aimed at Lua 5.0 and will contain some outdated APIs. However, the basic usage of the language remains the same.

There are two conventions that gplay uses within its Lua script bindings. First, in order to create a new object, the user calls the new function on the class and passes the correct arguments for the corresponding C++ constructor. e.g.

-- Create a new Vector2 object.
local v = Vector2.new(1.0, 3.7)

Game – Script Event Callbacks

In order to write a game primarily using Lua scripts, one must register for the main game events (initialize, update, render, and finalize), along with the desired input event handlers. To do this, you must edit the ‘scripts' section of the game's game.config file. If the user has a Lua script with functions for the four major events and functions that handle the key and touch input, the ‘scripts' section would look something like this:

scripts
{
    initialize = res/script.lua#initialize
    update = res/script.lua#update
    render = res/script.lua#render
    finalize = res/script.lua#finalize
    keyEvent = res/script.lua#keyEvent
    touchEvent = res/script.lua#touchEvent
}

Then on the C++ side, as usual, you must derive a class from Game and create a static instance on the stack. However, in the case of a script-based game, the .h file can simply contain the class definition with all empty implementations while the .cpp file would contain the static instance. For example,

MyScriptGame.h

class MyScriptGame: public Game
{
protected:
    void initialize() {};
    void finalize() {};
    void update(float elapsedTime) {};
    void render(float elapsedTime) {};
};

MyScriptGame.cpp

#include "MyScriptGame.h"
// Declare our game instance
MyScriptGame game;

The sample does not require any other C++ implementation. For a complete sample game using the techniques described above, see samples/lua as one-to-one mapping of sample00-mesh with a box instead of a duck. It also has some additional scripting code showcasing AIAgent and AIStates.

Extending ScriptTarget

To add scriptable events to your own class, you simply derive from the class gplay::ScriptTarget and add the required function calls. First, to define the scriptable events that are supported for the class, we call addScriptEvent with the name of the event and, optionally (depending on if the callback takes arguments or not), the parameter string for a valid script callback function for that event (the parameter string follows the format of the parameter string argument to ScriptController::executeFunction). For example, to add a ‘notify' event that passes an integer to the callback, we would do the following:

addScriptEvent("notify", "i");

The supported format identifiers for passed parameters are as follows:

Format Parameter Type (result)
c, h, i, l Signed Integer (int)
u Unsigned Integer (unsigned int)
b Boolean (bool)
f, d Floating Point (double)
s String (char*)
p Pointer (void*)
[x] Enum Value [x]
<x> Object References/Pointers <x>

This step is usually done either in the constructor of the class or in a class initialization function. Next, the class will want to fire the event so that the script callback functions are actually called. This code is placed wherever it makes sense (depending on what the actual event is) and looks like so:

fireScriptEvent<void>("notify", 14);

The template argument corresponds to the return type of the callback function, the first parameter is the name of the event to fire, and the remaining arguments are the parameters to the actual callback function: in this case, the integer that is passed to the notify callback. To see a full example of a class that derives from gplay::ScriptTarget within gplay, take a look at the gplay::Control class.

Tips using Lua with gplay

  • To do integer like comparisons or casts on a number variable x in Lua, use math.floor(x).
  • Make sure all your member function calls use ':' instead of '.'
  • Remember to access all gplay variables, including static and global variables with '()' on the end of the name.
  • Primitive data type arrays and object arrays are both inefficient when created in Lua and passed to C++, so try to minimize this.
  • There is no reasonable way to unload a Lua script; thus, the recommended usage pattern is to put each script's variables and functions inside a table (see Lua technical note 7 at http://www.lua.org/notes/ltn007.html). i.e.
    -- If you want to load the module at most once, add a line like this.
    if Module then return end
    -- Declare the module Module.
    Module = {}
    
    -- Declare a variable within the module.
    Module.a = 47
    
    -- Declare a function within the module.
    function Module.myFunc()
        return Module.a + 17
    end
    
    -- Cleanup function; call when done with this module.
    function cleanupModule()
        Module = nil
    end
    Note: you can't pass an enumeration value to a function that doesn't explicitly take an enumeration type (i.e. Control::setTextColor, which takes an unsigned char). In these cases, you need to look up the enumeration values and pass them directly.

Generating user defined script bindings

The gplay-luagen tool is used to automatically generate all of gplay's built-in Lua script bindings. This tool takes doxygen xml files as in input and therefore requires doxygen to be installed.

Prior to generating custom script bindings for your project, check the following:

  • For any functions, variables, classes, structs, enums, etc. that are local to a .cpp file, declare them as static 1) because it is good practice and 2) so that Lua does not generate bindings for them.
  • For any remaining function, variable, class, struct, enum, etc. that should not be accessible from Lua, add @script{ignore} to its Doxygen comments.
  • For any function that allocates or returns an object that is owned (i.e. created/allocated) by the user calling that function, add @script{create} to its Doxygen comments. This ensures that Lua will automatically garbage collect the object when it goes out of scope, by calling Ref::release for an object that derives from Ref, or delete for all other objects.

To generate custom Lua script bindings for your own project:

  1. Copy the gplay-luagen.doxyfile file to your project's root directory (and rename it). Then, either manually using a text editor or using the Doxywizard tool, go to the INPUT section and ensure both that the path to gplay's 'src' folder is valid (relative to where the doxyfile is) and that your own source folder is added.

  2. Run doxygen using the above doxyfile from your project's root directory. For example, run doxygen my-project.doxyfile from the command line or run Doxygen using the Doxywizard application. This will generate an 'xml' folder containing xml doxygen files to be used by the script generator.

  3. Create a 'lua' folder inside your source folder.

  4. Run gplay-luagen using the following command (make sure you have a trailing '/' for the output directory (second) parameter):

    path-to-gplay/bin/your-platform/gplay-luagen.exe ./xml/ path-to-your-source/lua/ <your-namespace>
    

    Note: the parameter is used as the namespace that the bindings are generated within. This can be anything you want except for "gplay".

  5. Ensure your project has its 'src' folders in its include path to allow generated script bindings in the 'lua' folder to correctly resolve includes.

  6. Add the generated Lua script binding (.h/.cpp) files from path-to-your-source/lua to your project.

  7. From your Game class, #include the generated path-to-your-source/lua/lua_all_bindings.h and add a call to <your-namespace>::lua_RegisterAllBindings() in your Game's initialize method.

  8. Compile and run - now you can use your own classes from Lua scripts.

Note: Some complex code is not supported by the generator. If your custom bindings do not compile, review the problematic bindings and consider adding @script{ignore} to the original source and re-build the bindings.

Disabling the lua bindings

You can disable the built-in lua bindings by adding the #define directive NO_LUA_BINDINGS and deleting the directory gplay/src/lua.

  • Add NO_LUA_BINDINGS to the Preprocessor Definitions for the gplay project
  • Delete gplay/src/lua
  • Remove gplay/src/lua from your project (in Visual Studio and/or Xcode)

This will prevent the gplay lua binding functions from being registered on startup. However, the lua runtime will still be loaded and initialized. If you want to completely remove lua, you can remove _lua = luaL_newstate(); from ScriptController.cpp and resolve any compilation errors.