Introduction to AI sandbox
AI sandbox is a framework that is designed to do away with the tedious job of application management, resource handling, memory management, and Lua binding, so that you can focus immediately on creating AI in Lua. While the sandbox does the dirty work of a small game engine, none of the internals of the sandbox are hidden. The internal code base is well documented and explained here so that you can expand any additional functionality your AI might require.
The design behind the sandbox is a culmination of open source libraries preassembled in order to rapidly prototype and debug Lua's scripted AIs. While C++ code maintains and manages the data of AI, Lua scripts manage AI's decision-making logic. With a clear separation of data and logic, the logic represented in Lua scripts can be rapidly iterated on without worrying about corrupting or invalidating the current state of AI.
Understanding the sandbox
Before diving head-first into creating AI, this chapter goes over the internals and setup of the sandbox. While the AI scripting will all take place in Lua, it is important to understand how Lua interfaces with the sandbox and what responsibilities remain in C++ compared to Lua.
The sandbox is laid out to easily support individual applications while sharing the same media resources between them. A key project, which is demo_framework
, provides all the common code used throughout the book. The only difference between each individual chapter's C++ code is the setup of which Lua sandbox script to execute. Even though the entire sandbox framework is available from the beginning of the book, each chapter will introduce new functionality within the sandbox incrementally:
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Now we'll take a look at what each folder within the sandbox project contains:
- The
bin
folder will contain all built executables, based on which build configuration is selected in Visual Studio. Both 32-bit and 64-bit versions of the sandbox can be built side by side without the need to rebuild the solution.Note
While the sandbox can be built as both a 32-bit and 64-bit application, debugging Lua scripts is only supported in Decoda with 32-bit builds.
- The
build
folder contains Visual Studio's solution file. The build/projects
folder contains each individual Visual Studio project along with the solution. It is safe to delete the build
folder at any time and regenerate the solution and project files with the vs2008.bat
, vs2010.bat
, vs2012.bat
, or vs2013.bat
batch files. Any changes made directly to the Visual Studio solution should be avoided, as regenerating the solution file will remove all the local changes. - The
decoda
folder contains the individual Decoda IDE project files that correspond to each chapter demo. These project files are not generated from build scripts. - The
lib
folder is an intermediate output folder where static libraries are compiled. It is safe to delete this folder, as Visual Studio will build any missing libraries during the next build of the sandbox. - The
media
folder contains all the shared assets used within the chapter demos. Assets used by the sandbox exist as both loose files and ZIP bundles. - The
premake
folder contains the build script used to configure the sandbox's solution and project files. Any desired changes to the Visual Studio solution or projects should be made in the Premake script.Note
The Premake script will detect any additional C++ or Lua files within the project's folder structure. When adding a new Lua script or C++ files, simply rerun the build scripts to update the Visual Studio solution.
- The
src
folder contains the source code for each open source library as well as the sandbox itself. Every project within the sandbox solution file will have a corresponding src
folder, with separate folders for header files and source files. Chapter demos have an additional script folder that contains every Lua script for that particular chapter.Note
Each open source library contains both a VERSION.txt
and LICENSE.txt
file, which states the version number of the open source library used as well as the license agreement that must be followed by all users.
- The
tools
folder contains the installer for the Decoda IDE as well as the Premake utility program that is used to create the Visual Studio solution.
Premake is a Lua-based build configuration tool. AI sandbox uses Premake in order to support multiple versions of Visual Studio without the need to maintain build configurations across solutions and build configurations.
Executing the vs2008.bat
batch file will create a Visual Studio 2008 solution within the build
folder. Correspondingly, the vs2010.bat
and vs2012.bat
batch files will create Visual Studio 2010 and Visual Studio 2012 solutions of AI sandbox.
Compiling the sandbox with Visual Studio 2008/2010/2012/2013
Building the sandbox requires only one dependency, which is the DirectX SDK. Make sure that your system has the DirectX SDK installed, or download the free SDK from Microsoft at http://www.microsoft.com/en-us/download/details.aspx?id=6812.
The sandbox solution file is located at build/Learning Game AI Programming.sln
once one of the vs2008.bat
, vs2010.bat
, vs2012.bat
, or vs2013.bat
batch files have been executed. The initial build of the sandbox might take several minutes to build all of the libraries the sandbox uses, and further builds will be much faster.
Lua 5.1.5 is used by the sandbox instead of the latest Lua 5.2.x library. The latest build of the Decoda IDE's debugger only supports up to 5.1.x of Lua. Newer versions of Lua can be substituted into the sandbox, but Lua's debugging support will no longer function.
At the time of writing this, Ogre3D 1.9.0 is the latest stable build of the Ogre3D graphics library. A minimal configuration of Ogre3D is used by the sandbox, which only requires the minimum library dependencies for image handling, font handling, ZIP compression, and DirectX graphics support.
The included dependencies required by Ogre3D are:
- FreeImage 3.15.4
- FreeType 2.4.12
- libjpeg 8d
- OpenJPEG 1.5.1
- libpng 1.5.13
- LibRaw 0.14.7
- LibTIFF 4.0.3
- OpenEXR 1.5.0
- Imbase 0.9.0
- zlib 1.2.8
- zzip 0.13.62
The sandbox was created with the DirectX SDK Version 9.29.1962, but any later version of DirectX SDK will work as well. Additional open source libraries providing debug graphics, input handling, physics, steering, and pathfinding are detailed as follows:
- Ogre3D Procedural 0.2: This is a procedural geometry library that provides easy creation of objects such as spheres, planes, cylinders, and capsules used for debug- and prototyping-level creation within the sandbox.
- OIS 1.3: This is a platform-agnostic library that is responsible for all the input handling and input device management used by the sandbox.
- Bullet Physics 2.81-rev2613: This is the physics engine library that drives the AI movement and collision detection within the sandbox.
- OpenSteer revision 190: This is a local steering library that is used to calculate steering forces of AI agents.
- Recast 1.4: This provides the sandbox with runtime navigation mesh generation.
- Detour 1.4: This provides A* pathfinding on top of generated navigation meshes.
Premake dev e7a41f90fb80 is a development build of Premake that is based on the Premake development branch. The sandbox's Premake configuration files utilize the latest features that are only present in the development branch of Premake.
Decoda 1.6 build 1034 provides seamless Lua debugging through an inspection of the sandbox's debug symbol files.
Decoda is a professional Lua integrated development environment (IDE) that was released as open source by Unknown Worlds Entertainment, makers of Natural Selection 2 (http://unknownworlds.com/decoda/).
Decoda takes a unique approach to Lua script debugging, which makes integrating with an application far superior to other Lua debuggers. Instead of using a networked-based approach where an internal Lua virtual machine must be configured to support debugging, Decoda uses the debug symbol files produced by Visual Studio in order to support Lua debugging. The key advantage of this approach is that it supports Lua debugging without requiring any changes to the original application. This key difference in Decoda allows for easy debug support in the sandbox's embedded Lua virtual machines.
Running AI sandbox inside Decoda
Begin by opening this chapter's Decoda project (decoda/chapter_1_movement.deproj
). Each sandbox Decoda project is already set up to run the correct corresponding sandbox executable. To run the sandbox within Decoda, press Ctrl + F5, or click on the Start Without Debugging option from the Debug menu.
Setting up a new Decoda project
Setting up a new Decoda project only requires a few initial steps to point Decoda to the correct executable and debug symbols.
A new Decoda project can be configured with the following steps:
- Open the Settings menu under the Project menu, as shown in the following screenshot:
- Set the Command textbox to point to the new sandbox executable.
- Set the Working Directory and Symbols Directory fields to the executable's directory.
Note
Decoda can only debug 32-bit applications where debug symbols are present. AI sandbox creates debug symbols in both Release and Debug build configurations.
The Project Settings screen can be seen as follows:
To begin debugging Lua scripts within the sandbox, press F5 within Decoda. F5 will launch the sandbox application and attach Decoda to the running process. Selecting Break from the Debug menu or setting a breakpoint over a running script will pause the sandbox for debugging.
If you're familiar with the Visual Studio Watch window, the Decoda Watch window is very similar. Type any variable within the Watch window to monitor that variable while debugging. Decoda also allows you to type in any arbitrary Lua statements within the Watch window. The Lua statement will be executed within the current scope of the debugger.
The stack window shows you the currently executing Lua call stack. Double-click on any line to jump to the caller. The Watch window will be automatically updated based on the current scope specified by the call stack.
The Decoda Virtual Machines window
The Virtual Machines window shows you each of the Lua virtual machines the sandbox is running. In this case, there is a separate virtual machine for the sandbox and a separate virtual machine for each of the running agents.
Simultaneous Lua and C++ debugging
To simultaneously debug both the C++ sandbox and any running Lua scripts, there are a few approaches available.
Visual Studio – Attach to Process
If the sandbox was launched from Decoda, you can always attach it to the running process from Visual Studio through the Debug menu's Attach to Process option.
Decoda – Attach to Process
Decoda can also attach itself to a running process through the Debug menu. If the sandbox is run through Visual Studio, you can attach Decoda at any time in the same fashion that Visual Studio attaches it to the sandbox.
Decoda – Attach System Debugger
To automatically attach both Decoda and Visual Studio when the sandbox launches from Decoda, select the Attach System Debugger option from the Debug menu. Upon running the application from Decoda, Windows will prompt you to immediately attach a Just-In-Time (JIT) debugger.
Note
If your installed version of Visual Studio doesn't show up in the Just-In-Time debugger as a selectable option, enable JIT debugging for native applications from Visual Studio by navigating to Tools | Options | Debugging | Just-In-Time.
The following screenshot shows you the Debug option that we are accessing in order to attach the system debugger:
Associating Lua scripts from code with Decoda
In order for Decoda to know which Lua file to associate the currently executing Lua script, the luaL_loadbuffer
Lua API function must pass in the Lua filename as chunkName
during the loading. The luaL_loadbuffer
function is an auxiliary Lua helper function provided in lauxlib.h
:
lauxlib.h
The Lua virtual machine is represented by lua_State struct
, which is defined in the lstate.h
header file. Lua states are self-contained structures without the use for any global data, making them practical for threaded applications:
lstate.h
The sandbox runs multiple Lua virtual machines simultaneously. One main virtual machine is assigned to the sandbox itself, while each spawned agent runs its own separate virtual machine. The use of individual virtual machines comes to the sandbox at the cost of performance and memory but allows for iterating Lua scripts in real time on a per-agent basis.
As Lua is a weakly typed language that supports functions that can receive an arbitrary amount of parameters as well as return an arbitrary number of return values, interfacing with C++ code is a bit tricky.
To get around the strong typing of C++, Lua uses a First In First Out (FIFO) stack to send and receive data from the Lua virtual machine. For example, when C++ wants to call a Lua function, the Lua function as well as the function parameters are pushed onto the stack and are then executed by the virtual machine. Any return values from the function are pushed back to the stack for the calling C++ code to handle.
The same process happens in reverse when the Lua code wants to call the C++ code. First, Lua pushes the C++ function onto the stack, followed by any parameters sent to the function. Once the code has finished executing, any return values are pushed back to the stack for the executing Lua script to handle.
When interfacing with Lua, stack values can either be retrieved bottom-up, or top-down. The top element in the stack can be retrieved with an index of -1
, while the bottom element of the stack can be retrieved with an index of 1
. Additional elements can be retrieved by indexing -2
, -3
, 2
, 3
, and so on.
Note
Lua differs from most programming languages by index values beginning from 1 instead of 0.
Lua has 8 basic primitives: nil, Boolean, number, string, function, userdata, thread, and table:
- Nil: Here, a value corresponds to a null value in C.
- Boolean: Here, values are equivalent to their C++ counterparts and represent either true or false.
- Number: This internally represent doubles that Lua uses to store integers, longs, floats, and doubles.
- String: This represents any sequence of characters.
- Function: Lua also considers functions as a primitive, which allows you to assign functions to variables.
- Userdata: This a special Lua type that maps a Lua variable to data managed by the C code.
- Thread: A thread primitive is used by Lua to implement coroutines.
- Table: This is an associative array that maps an index to a Lua primitive. A Lua table can be indexed by any Lua primitive.
A metatable in Lua is a table primitive that allows custom functions to override common operations such as addition, subtraction, assignment, and so on. The sandbox uses metatables extensively in order to provide common functionality to the userdata that C++ manages.
To retrieve a metatable within Lua, use the getmetatable
function on any object:
To set a metatable within Lua, use the setmetatable
function that passes both the object to set and the metatable:
Tip
As the sandbox heavily uses metatables for userdata, you can always use getmetatable
on the userdata to see which operations a specific userdata type supports.
A metamethod is a particular entry within a metatable that is called when an overriden operation is being requested by Lua. Typically, all Lua metamethods will begin with two underscores at the beginning of the function's name.
To add a metamethod to a metatable, assign a function to the metatable indexed by the metamethod's name. For example:
Userdata is an arbitrary block of data whose lifetime is managed by Lua's garbage collector. Whenever a code creates userdata to push into Lua, a block of memory managed by Lua is requested from lua_newuserdata
:
lua.h
While the sandbox makes extensive use of userdata, the construction and destruction of the allocated memory is still handled within the sandbox. This allows for Lua scripts to be easily iterated without worrying about Lua's internal memory management. For instance, when an agent is exposed to Lua through userdata, the only memory that Lua manages for the agent is a pointer to the agent. Lua is free to garbage collect the pointer at any time and has no effect on the agent itself.
C/C++ calling Lua functions
The sandbox hooks into the Lua script through three predefined global Lua functions: Sandbox_Initialize
, Sandbox_Cleanup
, and Sandbox_Update
. When the sandbox is first attached to the corresponding Lua script, the Sandbox_Initialize
function is called. Each update tick of the sandbox will also invoke the Sandbox_Update
function in the Lua script. When the sandbox is being destroyed or reloaded, the Sandbox_Cleanup
function will have an opportunity to perform any script-side cleanup.
In order for C++ to call a Lua function, the function must be retrieved from Lua and pushed onto the stack. Function parameters are then pushed on top of the stack, followed by a call to lua_pcall
, which executes the Lua function. The lua_pcall
function specifies the number of arguments the Lua function receives, the number of expected return values, and specifies how to handle errors:
lua.h
For example, the Agent_Initialize
Lua script function is called in the AgentUtilities
class in the following manner:
Agent.lua
First, the Lua function is retrieved from Lua by name and pushed onto the stack. Next, the agent itself is pushed as the only parameter to the Agent_Initialize
function. Lastly, lua_pcall
executes the function and checks whether it succeeded successfully; otherwise, an assertion is raised by the sandbox:
AgentUtilities.cpp
Lua calling C/C++ functions
Exposing C++ functions to Lua takes place through a process called function binding. Any bound functions exposed to Lua become accessible either as a global function or as a function available through a package. Packages in Lua are similar to namespaces in C++ and are implemented as a global table within Lua.
Any function exposed to Lua must fit the lua_CFunction
declaration. A lua_CFunction
declaration takes in the Lua virtual machine and returns the number of return values pushed onto the Lua stack:
lua.h
For example, the C++ GetRadius
function exposed in the sandbox is declared in the LuaScriptBindings.h
header file in the following manner:
LuaScriptBindings.h
The actual function implementation is defined within the LuaScriptBindings.cpp
file and contains the code for retrieving and pushing values back to the stack. The GetRadius
function expects an agent pointer as the first and only parameter from Lua and returns the radius of the agent. The Lua_Script_AgentGetRadius
function first checks the stack for the expected parameter count and then retrieves the userdata off the stack through a helper function within the AgentUtilities
class. An additional helper function performs the actual work of calculating the agent's radius and pushes the value back onto the stack:
LuaScriptBindings.cpp
To bind the function to Lua, we define a constant array that maps the function's name within Lua to the C function that should be called. The array of function mappings must always end with a null luaL_Reg
type struct. Lua uses a null luaL_Reg
type struct as a terminator when processing the function map:
AgentUtilities.cpp
The actual function binding to the Lua virtual machine takes place in the luaL_register
helper function. The register function binds the table of function names to their corresponding C callback function. The package name is specified at this step and will be associated with each function within the mapping:
AgentUtilities.cpp
Note
If NULL
is passed in as the package name, Lua requires that a table be at the top of the Lua stack. Lua will add the C functions to the Lua table at the top of the stack.
While the sandbox uses userdata to pass around agents and the sandbox itself, another use of userdata is to add basic primitives into the sandbox. These primitives are completely controlled by Lua's garbage collector.
The vector primitive added into the sandbox is a good example of using userdata that is completely controlled by Lua. As a vector is essentially a struct that only holds three values, it is a great choice for Lua to completely maintain the creation and destruction of the data. What this means to the C++ code interacting with Lua vectors is that code should never hold on to the memory address returned from Lua. Instead, the code should copy values retrieved from Lua and store them locally.
Looking at the vector data type
Elevating a vector into a basic Lua primitive means supporting all the expected operations users would like to perform on a vector variable in Lua. This means that vectors should support the addition, subtraction, multiplication, indexing, and any other basic operators supported by Lua.
To accomplish this, the vector data type uses metamethods to support basic arithmetic operators, as well as supporting the dot operator for the ".x", ".y", and ".z" style syntax:
LuaScriptUtilities.cpp
LuaScriptUtilities.h
To support this functionality from the code, we need to let Lua know what type of userdata it is working with when we allocate memory. The LuaScriptUtilities
header defines the metatable name of the vector type:
LuaScriptUtilities.cpp
When binding C++ functions to the Lua virtual machine, an additional step is added to support vectors. The luaL_newmetatable
function creates a new metatable, associating the table with the vector userdata type. Immediately after the metatable is created and pushed onto the Lua stack, a call to luaL_register
adds the metamethods listed in LuaVector3Metatable
to the metatable:
LuaScriptUtilities.cpp
Whenever a vector is created in Lua, memory is allocated from lua_newuserdata
and the vector metatable is retrieved from Lua and assigned to the userdata. This allows Lua to know what type of userdata it is dealing with and what functions are supported on the userdata.
The demo framework follows a very simple update, initialization, and cleanup design shared throughout many of the classes within the sandbox.
The following is a class overview of the BaseApplication.h
header:
The BaseApplication
class has the main responsibility to configure the application window, process input commands, as well as configure and interface with Ogre3D. The Cleanup
, Draw
, Initialize
, and Update
functions are stub functions with no implementation within the BaseApplication
class itself. Classes that inherit from BaseApplication
can overload any of these functions in order to plug in their own logic:
- In a derived class, the
Initialize
function is called once at the start of the application after Ogre has been initialized. - The
Cleanup
function is called when the application requests to be shut down right before Ogre itself gets cleaned up. - The
Draw
call is executed right before the graphics processing unit (GPU) renders the current application frame. - The
Update
function is called immediately after the GPU has queued up all the rendering calls in order to process the current frame. This allows the GPU to work simultaneously as the CPU begins to prepare the next draw frame.
Ogre3D handles the general update loop and window management of the sandbox. BaseApplication
implements the Ogre::FrameListener
interface in order to implement both the Update
and Draw
calls of the sandbox:
The following is a class overview of the OgreFrameListener.h
header:
The second interface, which is Ogre::WindowEventListener
, enables the sandbox to receive specific callbacks to window events, such as the window movement, resizing, closing, closed, and window focus change:
The following is a class overview of the OgreWindowEventListener.h
header:
Note
Both interfaces specify functions that are called from Ogre's main thread, so no race conditions exist to handle events.
Object-Oriented Input System
The Object-Oriented Input System (OIS) library is responsible for all the keyboard and mouse handling within the sandbox. The BaseApplication
class implements both OIS interfaces in order to receive calls for key presses, mouse presses, and mouse movements. Once the BaseApplication
class receives these events, it sends these events to the sandbox in turn.
The following is a class overview of the OISKeyboard.h
header:
The following is a class overview of the OISMouse.h
header:
The main AI sandbox class, which is SandboxApplication
, inherits from the BaseApplication
class that implements the Cleanup
, Draw
, Initialize
, and Update
functions. The CreateSandbox
function creates an instance of a sandbox and hooks up the Lua script specified by the filename parameter.
The following is a class overview of the SandboxApplication.h
header:
The sandbox class represents the sandbox data as well as handling calls into the Lua sandbox script. The creation of a sandbox requires an Ogre SceneNode instance to situate the sandbox in the world. The sandbox's SceneNode instance acts as the parent to any additional geometry SceneNodes used for rendering; this also includes the AI agents within the sandbox.
The following is a class overview of the Sandbox.h
header:
The agent class encapsulates the agent data and performs function calls to the corresponding Lua script assigned to the agent from the LoadScript
function. A SceneNode is required to construct an agent instance to maintain an orient and position the agent within the world.
The following is a class overview of the Agent.h
header:
AI sandbox heavily uses the utility pattern to separate logic and data. While both the sandbox and agent classes store their own relevant data, any manipulation of the data that interacts with the Lua virtual machine is handled through a utility class. This design separates the need for the agent and sandbox classes to know about the intricacies of interacting with the Lua stack. Instead, the Lua stack manipulation and data manipulation is left as the responsibility of a utility class.
For example, the AgentUtilities
class handles all actions that are performed on an AI agent from Lua, while the SandboxUtilities
class handles all actions that can be performed on the sandbox from Lua.
Any general purpose or miscellaneous interactions with the Lua virtual machine are handled by the LuaScriptUtilities
class.
The LuaScriptBindings.h
header file describes every C++ function that is exposed to the Lua virtual machine from the sandbox. You can always reference this file as the AI sandbox application programming interface (API). Each function contains a description, function parameters, return values, and examples of how the function should be called from Lua.