Showing posts with label XACT. Show all posts
Showing posts with label XACT. Show all posts

Wednesday, January 15, 2014

Programming By Example - Adding AngelScript to a Game Part 3

For Part 1: Programming By Example - Adding AngelScript to a Game Part 1
For Part 2: Programming By Example - Adding AngelScript to a Game Part 2
For Part 3: Programming By Example - Adding AngelScript to a Game Part 3

Introduction

This is part 3 of the article "Adding AngelScript to an Existing Game". In this series, I've been adding AngelScript to the XACTGame sample that can be found in the Direct X SDK. I hope to explain many of the needed concepts so others will be able to add scripting to their games.

AngelScript Concepts Covered
  • Using Script Header Files
  • Getting the Value of Script Global Variables in C++
  • Exposing Different Interfaces
  • Calling Script Functions with Parameters From C++
What will be Scripted
In the final part of this series, I'll use AngelScript to script much of the game logic inside of the XACTGame sample application. Here's a list of the functions that I'll script:
  • void FireAmmo()
  • void HandleAmmoAI( float fElapsedTime )
  • void HandleDroidAI( float fElapsedTime )
  • void CreateDroid()
  • float GetDistFromWall( D3DXVECTOR3 P1, D3DXVECTOR3 P2, D3DXVECTOR3 P3, D3DXVECTOR3 N )
  • void DroidPickNewDirection( int A )
  • void DroidChooseNewTask( int A )
  • void CheckForAmmoToDroidCollision( int A )
  • void CheckForInterAmmoCollision( float fElapsedTime )
  • void CheckForAmmoToWallCollision( int A )
  • void CreateAmmo( int nIndex, D3DXVECTOR4 Pos, D3DXVECTOR4 Vel )
Of all of these functions, only the first four need to be accessed by C++. The others will only be called by other script functions. The code here is all runs while the game is being played. It's different from the InitApp() function that I scripted in part one. The InitApp function, also needed access to the camera and menu settings while these functions don't so instead of putting all of the script functions in one file, I'll divide it into two files--"initapp.as" and "gamelogic.as".

Using Script Header Files

Eventhough I use separate script files, there are some constants defined in "game.h" that I'd like to use in both files. AngelScript doesn't have built-in support for includes and can only parse scripts and doesn't have any functions to load files. However, if you remember from part 1, AngelScript has an optional add-on called Script Builder that can load files, handle includes and some C-sytle preprocessor directives, and removes meta data tags from the script before compilation. Now I will add the a new file "constants.as"

// constants.as
// This is an AngelScript File
//--------------------------------------------------------------------------------------
// Consts
//--------------------------------------------------------------------------------------

// Will carry over to C++ code if modified in script ----------------------------------------
const uint MAXANISOTROPY            = 8; // MAXANISOTROPY is the maximum anisotropy state value used when anisotropic filtering is enabled.
const float GROUND_ABSORBANCE       = 0.2f; // GROUND_ABSORBANCE is the percentage of the velocity absorbed by ground and walls when an ammo hits.
const float AMMO_ABSORBANCE         = 0.1f; // AMMO_ABSORBANCE is the percentage of the velocity absorbed by ammos when two collide.
const int MAX_AMMO                  = 10;  // MAX_AMMO is the maximum number of ammo that can exist in the world.
const int MAX_DROID                 = 50;
const uint DROID_HITPOINTS          = 20;
const float AMMO_SIZE               = 0.10f; // AMMO_SIZE is the diameter of the ball mesh in the world.
const float DROID_SIZE              = 0.5f;
const float DROID_MIN_HEIGHT        = 0.5f;
const float DROID_HEIGHT_FLUX       = 0.5f;
const uint DROID_TURN_AI_PERCENT    = 40;
const uint DROID_MOVE_AI_PERCENT    = 40;
const uint DROID_MOVE_TIME_MIN      = 2000;
const uint DROID_MOVE_TIME_FLUX     = 3000;
const uint DROID_CREATE_DELAY_FLUX  = 2500;
const float DROID_DEATH_SPEED       = 3.0f;
const float AUTOFIRE_DELAY          = 0.15f; // AUTOFIRE_DELAY is the period between two successive ammo firing.
const float CAMERA_SIZE             = 0.2f; // CAMERA_SIZE is used for clipping camera movement
const float GRAVITY                 = 3.0f; // GRAVITY defines the magnitude of the downward force applied to ammos.
const float DROID_VELOCITY          = 2.0f; // MIN_VOL_ADJUST is the minimum volume adjustment based on contact velocity.
const float BOUNCE_TRANSFER         = 0.8f; // BOUNCE_TRANSFER is the proportion of velocity transferred during a collision between 2 ammos.
const float BOUNCE_LOST             = 0.1f; // BOUNCE_LOST is the proportion of velocity lost during a collision between 2 ammos.
const float REST_THRESHOLD          = 0.005f; // REST_THRESHOLD is the energy below which the ball is flagged as laying on ground.  // It is defined as Gravity * Height_above_ground + 0.5 * Velocity * Velocity
const float PHYSICS_FRAMELENGTH     = 0.003f; // PHYSICS_FRAMELENGTH is the duration of a frame for physics handling when the graphics frame length is too long.

// Will not carry over to C++ code if modified in script ------------------------------------
const float PI                      = 3.14159f;

// MinBound and MaxBound are the bounding box representing the cell mesh.
const float GROUND_Y                = 3.0f; // -GROUND_Y is the Y coordinate of the ground.
const D3DXVECTOR3           g_MinBound( -6.0f, -GROUND_Y, -6.0f );
const D3DXVECTOR3           g_MaxBound( 6.0f, GROUND_Y, 6.0f );

Even though this file will be used as an AngelScript "header file", it doesn't need header guards. The Script Builder add-on will check for that automatically. Later when I want to include it inside my other AngelScript files, I can include it just as I would in C/C++.

// include common constant variable definitions
#include "constants.as"


Getting the Value of Script Global Variables in C++

You may have noticed in the 'constants.as' file, two comments one of which says, "Will carry over to C++ code if modified in script." The constants defined in 'constants.as' are the same as the ones defined in 'game.h'. It would be nice if I can change the values in 'constants.as' and have them carry over into the C++ code. This is possible in AngelScript. The asIScriptModule class contains the compiled script and it has methods that can be used to get the value stored in its global variables. To get a global variable from AngelScript, first we need to get the variables index. We can do this by calling one of the following methods: GetGlobalVarIndexByName() or GetGlobalVarIndexByDecl(). Once you have the index, you can retrieve a pointer to the variables memory location. I've written this short template function for retrieving a global variable from AngelScript. The function doesn't do any type checking; that's the responsibility of the programmer.

template <class t>
// returns -1 if not found
int GetGlobal(asIScriptModule *module, char *declaration, T &out_val)
{
 int index = module->GetGlobalVarIndexByDecl(declaration);
 if(index < 0) return -1;

 // get the variable from AngelScript and cast to our type
 T *var_ptr = (T *)module->GetAddressOfGlobalVar(index);

 // set the value
 out_val = *var_ptr;

 return 1;
}

From this, I wrote a class that will retrieve the values from AngelScript and provide functions to get the data. I'll store the class in the ScriptContextData struct and pass it to the functions that need it.

class CXACTGameScriptConstants
{
    public:
        void SetContantsFromScript(asIScriptModule *module)
        {
            int result;
            result = GetGlobal(module, "const uint MAXANISOTROPY", maxanisotropy); assert(result >= 0);
            result = GetGlobal(module, "const float GROUND_ABSORBANCE", ground_absorbance); assert(result >= 0);
            result = GetGlobal(module, "const float AMMO_ABSORBANCE", ammo_absorbance); assert(result >= 0);
            result = GetGlobal(module, "const int MAX_AMMO", max_ammo); assert(result >= 0);
            result = GetGlobal(module, "const int MAX_DROID", max_droid); assert(result >= 0);
            result = GetGlobal(module, "const uint DROID_HITPOINTS", droid_hitpoints); assert(result >= 0);
            result = GetGlobal(module, "const float AMMO_SIZE", ammo_size); assert(result >= 0);
            result = GetGlobal(module, "const float DROID_SIZE", droid_size); assert(result >= 0);
            result = GetGlobal(module, "const float DROID_MIN_HEIGHT", droid_min_height); assert(result >= 0);
            result = GetGlobal(module, "const float DROID_HEIGHT_FLUX", droid_height_flux); assert(result >= 0);
            result = GetGlobal(module, "const uint DROID_TURN_AI_PERCENT", droid_turn_ai_percent); assert(result >= 0);
            
            // Some parts ommitted because of length
            ....
        }

        unsigned int get_maxanisotropy() const { return maxanisotropy; }
        float get_ground_absorbance() const { return ground_absorbance; }
        float get_ammo_absorbance() const { return ammo_absorbance; }
        int get_max_ammo() const { return max_ammo; }
        int get_max_droid() const { return max_droid; }
        unsigned int get_droid_hitpoints() const { return droid_hitpoints; }
        float get_ammo_size() const { return ammo_size; }
        float get_droid_size() const { return droid_size; }
        float get_droid_min_height() const { return droid_min_height; }
        float get_droid_height_flux() const { return droid_height_flux; }
        unsigned int get_droid_turn_ai_percent() const { return droid_turn_ai_percent; }

        // Some parts ommitted because of length
        ....

    private:
        // helper function for getting global data from AngelScript
        template <class t>
        // returns -1 if not found
        int GetGlobal(asIScriptModule *module, char *declaration, T &out_val)
        {
            int index = module->GetGlobalVarIndexByDecl(declaration);
            if(index < 0) return -1;

            // get the variable from AngelScript and cast to our type
            T *var_ptr = (T *)module->GetAddressOfGlobalVar(index);

            // set the value
            out_val = *var_ptr;

            return 1;
        }

        unsigned int maxanisotropy;
        float ground_absorbance;
        float ammo_absorbance;
        int max_ammo;
        int max_droid;
        unsigned int droid_hitpoints;
        float ammo_size;
        float droid_size;
        float droid_min_height;
        float droid_height_flux;
        unsigned int droid_turn_ai_percent;
    
        // Some parts ommitted because of length
        ....
};


Exposing Different Interfaces

Now again, "initapp.as" and "gamelogic.as" don't need access to all of the same parts of the C++ interface. To limit access, AngelScript uses access mask. These are bit flags that we set when registering our interface and also when we loading our scripts. To simplify things, I'll use only two types.

const unsigned int ScriptInterfaceMask_SetupOnly = 0x01; // interface can only be called by modules with this mask
const unsigned int ScriptInterfaceMask_Gameplay  = 0x02; // interface can only be called by modules with this mask
const unsigned int ScriptInterfaceMask_All       = ScriptInterfaceMask_SetupOnly | ScriptInterfaceMask_Gameplay; // interface can only be called both modules types

To set the access mask for our interface, we use the SetDefaultAccessMask() method inside asIScriptEngine. To add this support, the following changes are needed to the RegisterGameInterface() function inside as_scripting.cpp

int RegisterGameInterface(asIScriptEngine *scriptengine)
{
 int result;

 // Set Acces Mask ---------------------------------------------------------------------
 scriptengine->SetDefaultAccessMask(ScriptInterfaceMask_All); 
 // Bindings made in this section are accessible to everything -------------------------

 // Register STD String (Needed to help me test my implementation)
 RegisterStdString(scriptengine);

 // Register Arrays
 RegisterScriptArray(scriptengine, true);

 // Register print function (Needed to help me test my implementation)
 result = scriptengine->RegisterGlobalFunction("void print(const string &in)", asFUNCTION(print), asCALL_CDECL);
 if(result < 0) return result;

 // Set Acces Mask ---------------------------------------------------------------------
 scriptengine->SetDefaultAccessMask(ScriptInterfaceMask_Gameplay); 
 // Bindings made in this section are accessible to game play related scripts ----------

 // Register enum GAME_MODE
 result = RegisterEnumGAME_MODE(scriptengine);
 if(result < 0) return result;

 result = RegisterD3DXMathFunctions(scriptengine);
 if(result < 0) return result;

 result = RegisterD3DXCOLOR(scriptengine);
 if(result < 0) return result;

 result = RegisterDROID_STATE(scriptengine);
 if(result < 0) return result;

 result = RegisterAMMO_STATE(scriptengine);
 if(result < 0) return result;

 result = RegisterGameStateInterface(scriptengine);
 if(result < 0) return result;

 result = RegisterAudioInterface(scriptengine);
 if(result < 0) return result;

 result = RegisterCameraInterface(scriptengine); // FireAmmo() needs to get the view
 if(result < 0) return result;

 // Set Acces Mask ---------------------------------------------------------------------
 scriptengine->SetDefaultAccessMask(ScriptInterfaceMask_SetupOnly); 
 // Bindings made in this section are accessible to setup related scripts --------------

 result = RegisterDialogInterface(scriptengine);

 return result;
}

The Script Builder add-on can only load one at a time module. A module can be made up of a collection of script files, but the functions in the module have same access mask. Since we want our modules to have different access masks we'll need to change the LoadScript() from part 1.

int LoadScript(asIScriptEngine *scriptengine, ScriptContextData &contextdata)
// Note in earlier articles I passed the Script Builder object as a reference, but this isn't
// needed as once the module has been created, the script builder object no longer has any use.
{
 int result;

 // The CScriptBuilder helper is an add-on that loads the file,
 // performs a pre-processing pass if necessary, and then tells
 // the engine to build a script module.
 CScriptBuilder builder;

 // load initapp.as into a module -----------------------------------------------------------------------------
 result = builder.StartNewModule(scriptengine, "InitAppModule"); 
 if( result < 0 ) 
 {
  // If the code fails here it is usually because there
  // is no more memory to allocate the module
  MessageBoxA(NULL, "Unrecoverable error while starting a new module.", "AngelScript Message", MB_OK);
  return result;
 }

 // set the modules access mask
 builder.GetModule()->SetAccessMask(ScriptInterfaceMask_All); // give init app full access

 // load the script
 result = builder.AddSectionFromFile("initapp.as");
 if( result < 0 ) 
 {
  // The builder wasn't able to load the file. Maybe the file
  // has been removed, or the wrong name was given, or some
  // preprocessing commands are incorrectly written.
  MessageBoxA(NULL,"Please correct the errors in the script and try again.", "AngelScript Message", MB_OK);
  return result;
 }
 result = builder.BuildModule();
 if( result < 0 ) 
 {
  // An error occurred. Instruct the script writer to fix the 
  // compilation errors that were listed in the output stream.
  MessageBoxA(NULL,"Please correct the errors in the script and try again.", "AngelScript Message", MB_OK);
  return result;
 }

 // load gamelogic.as into a module ----------------------------------------------------------------------
 result = builder.StartNewModule(scriptengine, "GameModule"); 
 if( result < 0 ) 
 {
  // If the code fails here it is usually because there
  // is no more memory to allocate the module
  MessageBoxA(NULL, "Unrecoverable error while starting a new module.", "AngelScript Message", MB_OK);
  return result;
 }

 // set the modules access mask
 builder.GetModule()->SetAccessMask(ScriptInterfaceMask_Gameplay); // only give gameplay access

 // load the script
 result = builder.AddSectionFromFile("gamelogic.as");
 if( result < 0 ) 
 {
  // The builder wasn't able to load the file. Maybe the file
  // has been removed, or the wrong name was given, or some
  // preprocessing commands are incorrectly written.
  MessageBoxA(NULL,"Please correct the errors in the script and try again.", "AngelScript Message", MB_OK);
  return result;
 }
 result = builder.BuildModule();
 if( result < 0 ) 
 {
  // An error occurred. Instruct the script writer to fix the 
  // compilation errors that were listed in the output stream.
  MessageBoxA(NULL,"Please correct the errors in the script and try again.", "AngelScript Message", MB_OK);
  return result;
 }

 ...
}


Calling Script Functions with Parameters From C++

Remember in part 1, I stored the AngelScript script function pointer for InitApp. I'll do the same with the new script function

// enumerations -------------------------------------------------------------
enum ScriptFunctionIDs
{
 Function_InitApp = 0,
 Function_FireAmmo,
 Function_HandleAmmoAI,
 Function_HandleDroidAI,
 Function_CreateDroid
};

const unsigned int max_script_functions = 5;

struct ScriptContextData
{
 asIScriptContext *ctx;
 asIScriptFunction *script_functions[max_script_functions];

 void ExecuteFunction(ScriptFunctionIDs func_id);
};

I've created a helper function for finding the script function and displaying a message if the script can't be found.

asIScriptFunction *GetScriptFunction(asIScriptModule *mod, char *declaration)
{
 asIScriptFunction *func = mod->GetFunctionByDecl(declaration);
 if(func == NULL)
 {
  MessageBoxA(NULL,"AngelScript Message - The script function missing. Please add it and try again.", declaration, MB_OK);
  return NULL;
 }
 return func;
}

Now with that function, I just need to add the following to the end of the LoadScript function.

// Find the function that is to be called. 
asIScriptModule *modInitApp = scriptengine->GetModule("InitAppModule");
contextdata.script_functions[Function_InitApp] = GetScriptFunction(modInitApp, "void InitApp()");
if( contextdata.script_functions[Function_InitApp] == 0 ) return -1;

asIScriptModule *modlogic = scriptengine->GetModule("GameModule");
contextdata.script_functions[Function_FireAmmo] = GetScriptFunction(modlogic, "void FireAmmo()");
if( contextdata.script_functions[Function_FireAmmo] == 0 ) return -1;

contextdata.script_functions[Function_HandleAmmoAI] = GetScriptFunction(modlogic, "void HandleAmmoAI( float fElapsedTime )");
if( contextdata.script_functions[Function_HandleAmmoAI] == 0 ) return -1;

contextdata.script_functions[Function_HandleDroidAI] = GetScriptFunction(modlogic, "void HandleDroidAI( float fElapsedTime )");
if( contextdata.script_functions[Function_HandleDroidAI] == 0 ) return -1;

contextdata.script_functions[Function_CreateDroid] = GetScriptFunction(modlogic, "void CreateDroid()");
if( contextdata.script_functions[Function_CreateDroid] == 0 ) return -1;

To call the scripts, I made an ExecuteFunction() which I created and put into the ScriptContextData structure. It takes a value from the ScriptFunctionIDs enum as a parameter. This worked well as the InitApp() function doesn't take any parameters, but now I want to support call script functions from C++ that have parameters. Executing scripts functions from C++ is a 4-step process. First, you should call Prepare() which will allow the script context to prepare the stack. Next, if there are parameters, the paremeters should be set. One of the following asIScriptContext methods can be used for primitive types:

 int SetArgDWord(int arg, asDWORD value);
 int SetArgQWord(int arg, asQWORD value);
 int SetArgFloat(int arg, float value);
 int SetArgDouble(int arg, double value);
 int SetArgByte(int arg, asBYTE value);
 int SetArgWord(int arg, asWORD value);

The 'arg' parameter is the index of the parameter in the functions paramter list. After the parameters have been set, you should call Execute(). Finally, to get the return value, use one of the following functions:

 asDWORD GetReturnDWord();
 asQWORD GetReturnQWord();
 float   GetReturnFloat();
 double  GetReturnDouble();
 asBYTE  GetReturnByte();
 asWORD  GetReturnWord();

To handle these changes in code, I'll divide the ExecuteFunction() into two separate methods--PrepareFunction() and ExecuteFunction(). If the function has paramters, one of the SetArg methods can be called between the prepare and execute calls.

int PrepareFunction(ScriptFunctionIDs func_id)
{
 // I'm no longer checking for a valid context here. It's up to the application writer to ensure 
 // that the context is valid before calling this
 return ctx->Prepare(script_functions[func_id]);
}

int ExecuteFunction()
{
 // I'm no longer checking for a valid context here. It's up to the application writer to ensure 
 // that the context is valid before calling this
 int result = ctx->Execute();
 if( result != asEXECUTION_FINISHED )
 {
  // The execution didn't complete as expected. Determine what happened.
  if( result == asEXECUTION_EXCEPTION )
  {
   // An exception occurred, let the script writer know what happened so it can be corrected.
   MessageBoxA(NULL, ctx->GetExceptionString(), "An exception occurred.", MB_OK);
   return -1;
  }
 }
 return result;
}


Converting the C++ code to AngelScript

All that's left to do is to convert the code from C++ to AngelScript. AngelScript and C++ have almost identical syntax so this isn't too much of a problem. When registering the C++ objects and bindings with AngelScript, I did change things. For example, to access the game state object, the C++ code directly access the g_GameState variable whereas in my bindings, I give limited through a namespace 'GAME_STATE'. Also, the DirectX math functions use pointers, but in my bindings, I use references. Here's an example of the FireAmmo()function in AngelScript:

void FireAmmo()
{
    // Check to see if there are already MAX_AMMO balls in the world.
    // Remove the oldest ammo to make room for the newest if necessary.
    double fOldest = GAME_STATE::AmmoQ[0].fTimeCreated;
    int nOldestIndex = 0;
    int nInactiveIndex = -1;
    for( int iAmmo = 0; iAmmo < MAX_AMMO; iAmmo++ )
    {
        if( !GAME_STATE::AmmoQ[iAmmo].bActive )
        {
            nInactiveIndex = iAmmo;
            break;
        }
        if( GAME_STATE::AmmoQ[iAmmo].fTimeCreated < fOldest )
        {
            fOldest = GAME_STATE::AmmoQ[iAmmo].fTimeCreated;
            nOldestIndex = iAmmo;
        }
    }

    if( nInactiveIndex < 0 )
    {
        GAME_STATE::AmmoQ[nOldestIndex].bActive = false;
        GAME_STATE::nAmmoCount--;
        nInactiveIndex = nOldestIndex;
    }

    int nNewAmmoIndex = nInactiveIndex;

    // Get inverse view matrix
    D3DXMATRIXA16 mInvView;
 float det = 0.0;
 D3DXMatrixInverse(mInvView, det, FirstPersonCamera::CameraGetViewMatrix());
    //D3DXMatrixInverse( &mInvView, NULL, g_Camera.GetViewMatrix() );

    // Compute initial velocity in world space from camera space
    D3DXVECTOR4 InitialVelocity( 0.0f, 0.0f, 6.0f, 0.0f );
 D3DXVec4Transform(InitialVelocity, InitialVelocity, mInvView);
    //D3DXVec4Transform( &InitialVelocity, &InitialVelocity, &mInvView );
    D3DXVECTOR4 InitialPosition( 0.0f, -0.15f, 0.0f, 1.0f );
 //D3DXVec4Transform(InitialPosition, InitialPosition, mInvView);
    D3DXVec4Transform( InitialPosition, InitialPosition, mInvView );

    AUDIO::PlayAudioCue(AUDIO::Cue_iAmmoFire);
 //PlayAudioCue( g_audioState.iAmmoFire );

    CreateAmmo( nNewAmmoIndex, InitialPosition, InitialVelocity );
}

AngelScript is almost like an extension to C++. This code is almost exactly like its C++ counterpart and because of the array add-on introduced in part 2 of this series, the arrays can be easily shared.

Results and Conclusion

The XACTGame sample runs almost exactly the same when using AngelScript to write some parts of it as opposed to coding the entire project in C++. The goal of this article was to show how to add AngelScript to a project and also to test out the languages capabilites. For the most part, the app runs without any slowdowns. When the project runs in "Release Mode", it runs exactly like it did when built 100% in C++, but there is a slowdown when the player fires too many projectiles in "Debug Mode". I traced this to the collision detection code which I also decided to do in AngelScript. By reducing the number of active projectiles, I was able to get the performance back in the range of the C++ levels. This is to be expected because of the usual debugging overhead. I was surprised at how nicely it worked in "Release Mode". Changing the constants in 'constants.as' successfully changes the project without recompiling. AngelScript is a language that should be considered if you're thinking about adding scripting to your C++ project. It's easy to learn if you have a C++ background and it binds with C++ very well. I hope that I was able to cover the major points for adding AngelScript to your game.

Coding style in this article

Listed in the best practices for AngelScript is to always check the return value for every function. In most of the AngelCode examples and in the manual an assert is used to check for errors. I don't use the assert, instead I've been using "if(result < 0) return result;". This can easily be replaced by "assert(r >= 0);" as is used in the AngelScript documentation. Also, my goal with this project was to change the XACTGame sample as little as possible. The XACTGame sample was designed to show certain techniques such as adding graphics and audio, and it uses a simple framework.

Getting AngelScript

You can download the latest version of the AngelScript SDK from the AngelCode website. http://www.angelcode.com/ You'll find an excellent manual that explains the API in detail.

Note on Microsoft Source Code

Because Microsoft code was used in this program, I want to state some terms from the Direct X SDK EULA. The XACTGame sample was created by Microsoft and Microsoft owns the copyright. Changes made by Dominque Douglas have been clearly marked. Use of the source code provided does not change the license agreement for using Microsoft code. Microsoft code cannot be modified to work on non-Microsoft operating systems and Microsoft is not responsible for any claims related to the distribution of this program. Refer to the license agreement in the Direct X SDK for details.

Downloading This Project

The source code can be downloaded here: XACTGameAngelScript-Part3.zip

 Download note: Because of the size, this does not include the media files needed by the project such as the audio files and graphics files. You'll need to copy the "media" folder from the XACTGame sample in the Direct X SDK. You may need to alter the project's include and library directories to match your system. For simplicity, the AngelScript add-ons that were used in this project have been included. The project is a Visual Studio 2010 solution.

With this final part of the series, I've decided to also include the binary versions of the project.

XACTGameAngelScript-Binaries.zip

XACTGameAngelScript-Binaries - With Media.zip

Wednesday, January 8, 2014

Programming By Example - Adding AngelScript to a Game Part 2

For Part 1: Programming By Example - Adding AngelScript to a Game Part 1
For Part 2: Programming By Example - Adding AngelScript to a Game Part 2
For Part 3: Programming By Example - Adding AngelScript to a Game Part 3

Introduction

This is part 2 of the article Adding AngelScript to an Existing Game. In the first installment, This article series focuses on how to add a scripting language to a game and goes beyond just teaching a scripting language's API. Using the XACTGame example from the Microsoft Direct X SDK, I will detail the entire process. In part 1, I discussed some AngelScript basics and I showed how to add some bindings so the script can communicate with the C++ code. Then I scripted the InitApp() function. This article will build on the concepts from part 1 and in it, I'll code the remaining bindings so the script can call the proper C++ functions. In part 3, I'll write the remaining scripts. AngelScript Concepts Covered
  • Binding a C++ Interface to the script
    • Registering a Scoped Reference Type
    • Registering a POD Vale Type
    • Registering an Array as a Global Property
    • Registering functions with overloads

Adding Scripting to More Functions

As we did in part 1, we need to continue to survey the code to see what the dependencies are. Then we'll be able to figure out what bindings we'll need. Here are the remaining functions that I'll script and their dependencies. void FireAmmo();
  • D3DXVECTOR4
  • D3DXMATRIXA16
  • D3DXMatrixInverse()
  • D3DXVec4Transform()
  • g_Camera.GetViewMatrix()
  • AMMO_STATE
  • PlayAudioCue( g_audioState.iAmmoFire );
void DroidPickNewDirection( int A );
  • D3DXQUATERNION
  • D3DXQuaternionRotationYawPitchRoll
  • D3DXMatrixRotationQuaternion
  • DROID_STATE
  • AI_STATE
  • GAME_STATE - DROID_STATE DroidQ[MAX_DROID];
void DroidChooseNewTask( int A );
  • rand()
  • GAME_STATE - DROID_STATE DroidQ[MAX_DROID];
  • D3DXMatrixRotationQuaternion
  • D3DXMATRIXA16
  • Play3DAudioCue( g_audioState.iDroidScan, &g_GameState.DroidQ[A].vPosition );
void HandleDroidAI( float fElapsedTime );
  • GAME_STATE - DROID_STATE DroidQ[MAX_DROID];
  • D3DXQuaternionSlerp
  • D3DXMATRIXA16
  • D3DXMatrixRotationQuaternion
  • D3DXQUATERNION
void HandleAmmoAI( float fElapsedTime ); CheckForInterAmmoCollision() CheckForAmmoToWallCollision( A ); CheckForAmmoToDroidCollision( A );
  • GAME_STATE
  • DROID_STATE
  • AMMO_STATE
While, I'd prefer to not write any more object types, after looking at that list, it seems like I'll need to make these new types
  • D3DXVECTOR4
  • D3DXMATRIXA16
  • D3DXQUATERNION
  • DROID_STATE
  • AMMO_STATE

Registering a Scoped Reference Type for D3DXMATRIXA16

Adding the D3DXVECTOR4 and D3DXQUATERNION won't be hard considering they're almost exactly like D3DXVECTOR3. All it will take is some copying and pasting and some searching and replacing. The XACTGame sample application uses D3DXMATRIXA16 this class inherits from D3DXMATRIX and overloads the new and delete operators to keep things 16 bit aligned. Because of these special memory requirements, we can't create the object as a value type, we'll need to create it as a reference type. A reference type is an object that resides in dynamic memory; however, to use reference types, we'd have to add reference counting to the object which is not something we want to add. Thankfully AngelScript provides a special type of reference type just for this situation called a scoped reference type. To create an scoped reference type we'll need a function call like this:
r = engine->RegisterObjectType("D3DXMATRIXA16", 0, asOBJ_REF | asOBJ_SCOPED);

With reference types, we don't register constructors. Instead we register factories. A factory should create the object using dynamic memory. We also need a release function that AngelScript will use to delete the object. This is what we'll need for the D3DXMATRIXA16 type.

static D3DXMATRIXA16 *D3DXMATRIXA16_Factory()
{
  return new D3DXMATRIXA16;
}

static D3DXMATRIXA16 *D3DXMATRIXA16_FactoryCopy(const D3DXMATRIXA16 &other)
{
  return new D3DXMATRIXA16(other);
}

static D3DXMATRIXA16 *D3DXMATRIXA16_FactoryFromfloats(float _11, float _12, float _13, float _14,
                                                      float _21, float _22, float _23, float _24,
                                                      float _31, float _32, float _33, float _34,
                                                      float _41, float _42, float _43, float _44)
{
  return new D3DXMATRIXA16(_11, _12, _13, _14,
                           _21, _22, _23, _24,
                           _31, _32, _33, _34,
                           _41, _42, _43, _44);
}

static void D3DXMATRIXA16_Release(D3DXMATRIXA16 *s)
{
  if( s ) delete s;
}

I'll register the operators the same way I did or D3DXVECTOR3 and D3DXVECTOR4 with one exception. When using scoped reference types, "any function that either takes or returns the type by value in C++ must be wrapped in order to permit AngelScript to manage the life time of the values."(from the AngelScript manual) This means that will need to changed the binary operators so they return a pointer in C++, and a handle in AngelScript.

static D3DXMATRIXA16 *Matrix_opMul /* operator * */ ( const D3DXMATRIXA16 &lhs, const D3DXMATRIXA16 &rhs)
{
 // (From AngelScript Manual) any function that either takes or returns the type by value in C++ must
 // be wrapped in order to permit AngelScript to manage the life time of the values
 return new D3DXMATRIXA16(lhs * rhs);
}

Here's the function for registering the D3DXMATRIXA16 type. Redundant parts of the code have been omitted.

int RegisterD3DXMATRIXA16(asIScriptEngine *engine)
{
 int r;

 // Register the string type
 r = engine->RegisterObjectType("D3DXMATRIXA16", 0, asOBJ_REF | asOBJ_SCOPED);
 if(r < 0) return r;

 // with reference types we register factores and not constructors
 r = engine->RegisterObjectBehaviour("D3DXMATRIXA16", asBEHAVE_FACTORY, "D3DXMATRIXA16 @f()", asFUNCTION(D3DXMATRIXA16_Factory), asCALL_CDECL);
 r = engine->RegisterObjectBehaviour("D3DXMATRIXA16", asBEHAVE_FACTORY, "D3DXMATRIXA16 @f(const D3DXMATRIXA16 &in)", asFUNCTION(D3DXMATRIXA16_FactoryCopy), asCALL_CDECL);
 r = engine->RegisterObjectBehaviour("D3DXMATRIXA16", asBEHAVE_FACTORY, 
  "D3DXMATRIXA16 @f(float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float)",
  asFUNCTION(D3DXMATRIXA16_FactoryFromfloats), asCALL_CDECL);
 r = engine->RegisterObjectBehaviour("D3DXMATRIXA16", asBEHAVE_RELEASE, "void f()", asFUNCTION(D3DXMATRIXA16_Release), asCALL_CDECL_OBJLAST);

 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 &opMulAssign( const D3DXMATRIXA16 &in)", asFUNCTION(Matrix_opMulAssign), asCALL_CDECL_OBJLAST);
 if(r < 0) return r;
 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 &opAddAssign( const D3DXMATRIXA16 &in)", asFUNCTION(Matrix_opAddAssign), asCALL_CDECL_OBJLAST);
 if(r < 0) return r;
 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 &opSubAssign( const D3DXMATRIXA16 &in)", asFUNCTION(Matrix_opSubAssign), asCALL_CDECL_OBJLAST);
 if(r < 0) return r;
 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 &opMulAssign( float)", asFUNCTION(Matrix_opMulAssignF), asCALL_CDECL_OBJLAST);
 if(r < 0) return r;
 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 &opDivAssign( float)", asFUNCTION(Matrix_opDivAssign), asCALL_CDECL_OBJLAST);
 if(r < 0) return r;

 // instead of returning a value, the operator must return a handle because this is a scoped reference type
 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 @Matrix_opMul(const D3DXMATRIXA16 &in)", asFUNCTION(Matrix_opMul), asCALL_CDECL_OBJFIRST);
 if(r < 0) return r;
 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 @Matrix_opAdd(const D3DXMATRIXA16 &in)", asFUNCTION(Matrix_opAdd), asCALL_CDECL_OBJFIRST);
 if(r < 0) return r;
 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 @Matrix_opSub(const D3DXMATRIXA16 &in)", asFUNCTION(Matrix_opSub), asCALL_CDECL_OBJFIRST);
 if(r < 0) return r;

 // some code omitted
 ...

 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "D3DXMATRIXA16 &opAssign(const D3DXMATRIXA16 &in)", asMETHODPR(D3DXMATRIXA16, operator =, (const D3DXMATRIXA16&), D3DXMATRIXA16&), asCALL_THISCALL);
 if(r < 0) return r;

 r = engine->RegisterObjectMethod("D3DXMATRIXA16", "bool opEquals(const D3DXMATRIXA16 &in) const", asFUNCTION(MatricesEqual), asCALL_CDECL_OBJFIRST);
 if(r < 0) return r;

 // register the properties
 r = engine->RegisterObjectProperty("D3DXMATRIXA16", "float _11", asOFFSET(D3DXMATRIXA16,_11));
 if(r < 0) return r;
 r = engine->RegisterObjectProperty("D3DXMATRIXA16", "float _12", asOFFSET(D3DXMATRIXA16,_12));
 if(r < 0) return r;
 r = engine->RegisterObjectProperty("D3DXMATRIXA16", "float _13", asOFFSET(D3DXMATRIXA16,_13));

 // some code omitted
 ...

 return r;
}

There's one more thing I want to mention about this type. The D3DXMATRIXA16 type is derived from D3DMATRIX which has a union so the data can be accessed through an array or through individual floats. When I add this to AngelScript, I won't add support for the array.

typedef struct _D3DMATRIX {
    union {
        struct {
            float        _11, _12, _13, _14;
            float        _21, _22, _23, _24;
            float        _31, _32, _33, _34;
            float        _41, _42, _43, _44;

        };
        float m[4][4];
    };
} D3DMATRIX;

Registering a POD Vale Type

The DROID_STATE struct is a little special in that it doesn't have any constructors, destructors, or pointers. Also, copying and assignment can be done using a bit-to-bit copy. Because of this, we can register it with the flag asOBJ_POD. When using this flag, we don't have to supply a constructor, destructor, or assignment operator.

int RegisterDROID_STATE(asIScriptEngine *scriptengine)
{
 int r;

 // register AI_STATE enum
 r = scriptengine->RegisterEnum("AI_STATE");
 if(r < 0) return r;

 r = scriptengine->RegisterEnumValue("AI_STATE", "AI_TURNING", (int)AI_TURNING);
 if(r < 0) return r;

 r = scriptengine->RegisterEnumValue("AI_STATE", "AI_MOVING", (int)AI_MOVING);
 if(r < 0) return r;

 r = scriptengine->RegisterEnumValue("AI_STATE", "AI_STOPPED", (int)AI_STOPPED);
 if(r < 0) return r;

 // Register DROID_STATE as POD Value type
 // Register a primitive type, that doesn't need any special management of the content
 r = scriptengine->RegisterObjectType("DROID_STATE", sizeof(DROID_STATE), asOBJ_VALUE | asOBJ_APP_CLASS | asOBJ_POD);

 r = scriptengine->RegisterObjectProperty("DROID_STATE", "bool bActive", asOFFSET(DROID_STATE,bActive));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "D3DXVECTOR3 vPosition", asOFFSET(DROID_STATE,vPosition));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "D3DXVECTOR3 vVelocity", asOFFSET(DROID_STATE,vVelocity));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "D3DXVECTOR3 vNudgeVelocity", asOFFSET(DROID_STATE,vNudgeVelocity));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "AI_STATE aiState", asOFFSET(DROID_STATE,aiState));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "float fTargetRotation", asOFFSET(DROID_STATE,fTargetRotation));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "D3DXQUATERNION qTarget", asOFFSET(DROID_STATE,qTarget));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "D3DXQUATERNION qStart", asOFFSET(DROID_STATE,qStart));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "D3DXQUATERNION qCurrent", asOFFSET(DROID_STATE,qCurrent));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "float fRotInterp", asOFFSET(DROID_STATE,fRotInterp));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "float fTaskTimer", asOFFSET(DROID_STATE,fTaskTimer));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "int nHitPoints", asOFFSET(DROID_STATE,nHitPoints));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "float fDeathAnimation", asOFFSET(DROID_STATE,fDeathAnimation));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "float fAlpha", asOFFSET(DROID_STATE,fAlpha));
 if(r < 0) return r;
 r = scriptengine->RegisterObjectProperty("DROID_STATE", "D3DXCOLOR Specular", asOFFSET(DROID_STATE,Specular));

 return r;
}

I'll handle the AMMO_STATE structure the same way. To see how this is coded, see "scriptdroidammostates.cpp" in the source code for this article.

Arrays - Getting The Droid State

Now there's one big thing to consider now.XACTGame sample apps's DROID_STATE and AMMO_STATE information are stored in arrays inside of the GAME_STATE struct. C/C++ static arrays cannot be registered directly with AngelScript because AngelScript can't garuntee the lifetime of the array. Applications that want to use arrays need to register an array type. The AngelScript SDK comes with an add-on that can be used to register arrays, the class CScriptArray. This is a very useful class, but because it's for generic arrays, elements can't be accessed without converting back and forth between the type and void pointers. This means I can't just plug it into my C++ code. To get around this, I've written a STL-style wrapper for the CScriptArray class that behaves much like std::vector. Now all I have to do is change the array declaration to this:

CScriptArraySTL<AMMO_STATE> AmmoQ;
CScriptArraySTL<DROID_STATE>  DroidQ;

You can download the CScriptArraySTL class, by checking out the following blog entry: Blogspot post: C++ STL-Style Array That's Compatible with AngelScript I'll also add a Reset() method to the GAME_STATE class and register it with AngelScript so the script will be able to reset the state. The Reset() method will also be important because the XACTGame sample previously just used ZeroMemory(),a macro that calls memset(), to clear the data. This will no longer work because of the CScriptArraySTL array. I've also added clear to the droid state

// Reset in GAME_STATE
void Reset()
{
 // Reset here so we don't have to use ZeroMemory which messes up the CScriptArraySTL<> arrays
 gameMode  = GAME_RUNNING;

 nAmmoCount  = 0;
 fAmmoColorLerp = 0.0f;
 BlendToColor = (DWORD)0;
 BlendFromColor = (DWORD)0;

 nDroidCount = 0;
 nMaxDroids = 0;

 bDroidCreate   = false;
 bMassDroidKill   = false;
 fDroidCreateCountdown = 0.0f;

 bDroidMove  = false;
 bAutoAddDroids = false;

 // clear the static arrays
 ZeroMemory(gamePad, sizeof(DXUT_GAMEPAD) * DXUT_MAX_CONTROLLERS);
    
 // reset Ammo array
 AmmoQ.resize(MAX_AMMO);
 for(auto it = AmmoQ.begin(); it < AmmoQ.end(); it++)
 {
  (*it).clear();
 }

 // reset the Droid array
 DroidQ.resize(MAX_DROID);
 for(auto it = DroidQ.begin(); it < DroidQ.end(); it++)
 {
  (*it).clear();
 }
}

// changes to RegisterGameStateInterface
...
result = scriptengine->RegisterGlobalFunction("void Reset()", asMETHOD(GAME_STATE, Reset), asCALL_THISCALL_ASGLOBAL, &g_GameState);
if(result < 0) return result;

// initialize the droid state and ammo state arrays
result = g_GameState.DroidQ.InitArray(scriptengine, "array");
if(result < 0) return result;

result = g_GameState.AmmoQ.InitArray(scriptengine, "array");
if(result < 0) return result;
 
// register the droid state and ammo state arrays
result = scriptengine->RegisterGlobalProperty("array DroidQ", g_GameState.DroidQ.GetRef());
if(result < 0) return result;

result = scriptengine->RegisterGlobalProperty("array AmmoQ", g_GameState.AmmoQ.GetRef());
if(result < 0) return result;
...

Registering the remaining math functions

While keeping each type in it's own source file, I've decided to clean up the DirectX math bindings by making one header that and function that will add all of the types and functions. I had to make wrappers for most of the functions because the Direct X math functions use pointers but I want to use references with my AngelScript value type objects, and the wrapped functions will just return void and not a pointer to the out parameter. Also, even though it's not a Direct X math function, I've also decided to add my rand binding here as well with one change. Instead of just using the normal rand(), I'm going to change it so it takes the min and max values as a parameter.

static int rand(int min, int max)
{
 int diff = max - min;
 return (rand() % diff) + min;
}

Now when I register the function, I'll have to do it a little differently because I just overloaded rand() so it now has two versions with different params. So AngelScript knows which version to use, we need to register it like this: result = scriptengine->RegisterGlobalFunction("int rand(int, int)", asFUNCTIONPR(rand, (int, int), int), asCALL_CDECL); The asFUNCTIONPR macro takes 3 parameters--the function name, the parameter list, and the return type. This function registers all of the needed math functions.

int RegisterD3DXMathFunctions(asIScriptEngine *scriptengine)
{
 int result;

 // register our types first
 result = RegisterD3DXVECTOR3(scriptengine);
 if(result < 0) return result;
 result = RegisterD3DXVECTOR4(scriptengine);
 if(result < 0) return result;
 result = RegisterD3DXQUATERNION(scriptengine);
 if(result < 0) return result;
 result = RegisterD3DXMATRIXA16(scriptengine);
 if(result < 0) return result;

 // register the global functions
 result = scriptengine->RegisterGlobalFunction("void D3DXMatrixInverse(D3DXMATRIXA16 &out, float &out, const D3DXMATRIXA16 &in)", asFUNCTION(MatrixInverse), asCALL_CDECL);
 if(result < 0) return result;
 result = scriptengine->RegisterGlobalFunction("void D3DXVec4Transform(D3DXVECTOR4 &out, const D3DXVECTOR4 &in, const D3DXMATRIXA16 &in)", asFUNCTION(Vec4Transform), asCALL_CDECL);
 if(result < 0) return result;
 result = scriptengine->RegisterGlobalFunction("void D3DXQuaternionRotationYawPitchRoll(D3DXQUATERNION &out, float, float, float)", asFUNCTION(QuaternionRotationYawPitchRoll), asCALL_CDECL);
 if(result < 0) return result;
 result = scriptengine->RegisterGlobalFunction("void D3DXMatrixRotationQuaternion(D3DXMATRIXA16 &out, const D3DXQUATERNION &in)", asFUNCTION(MatrixRotationQuaternion), asCALL_CDECL);
 if(result < 0) return result;
 result = scriptengine->RegisterGlobalFunction("void D3DXQuaternionSlerp(D3DXQUATERNION &out, const D3DXQUATERNION &in, const D3DXQUATERNION &in, float t)", asFUNCTION(QuaternionSlerp), asCALL_CDECL);
 if(result < 0) return result;

 // add rand() here
 result = scriptengine->RegisterGlobalFunction("int rand(int, int)", asFUNCTIONPR(rand, (int, int), int), asCALL_CDECL);

 return result;
}

Handling Audio

I won't script any of the audio functions, but some of the other functions that I will script need access to some of the audio functionality. I'll need to give access to the following functions:

 HRESULT PlayAudioCue( XACTINDEX iCueIndex );
 HRESULT Play3DAudioCue( XACTINDEX iCueIndex, D3DXVECTOR3* pvPosition );
 void SetNumDroidsForAudio( int nDroidCount );

Adding these should be easy enough, but I'll need to wrap PlayAudioCue() and Play3DAudioCue() because they take the cue index as a parameter. I don't want to provide access to those variables so instead I'll make an enum that's a list of all of the cue indices and the wrapped functions will take the enum as a parameter.

enum AudioCues
{
    Cue_iAmmoBounce = 0,
    Cue_iAmmoFire,
    Cue_iDroidDestroyed,
    Cue_iDroidScan,
    Cue_iBackgroundMusic,
    Cue_iRoomRumble
};

XACTINDEX GetCueIndex(AudioCues cue)
{
 switch(cue)
 {
  case Cue_iAmmoBounce:
   return g_audioState.iAmmoBounce;
  case Cue_iAmmoFire:
   return g_audioState.iAmmoFire;
  ... // some code omitted
 }

 return 0;
}

void PlayAudioCueWrapper( AudioCues cue )
{
 PlayAudioCue(GetCueIndex(cue));
}

void Play3DAudioCueWrapper( AudioCues cue, const D3DXVECTOR3 &vPosition )
{
 Play3DAudioCue(GetCueIndex(cue), (D3DXVECTOR3 *)&vPosition);
}

int RegisterAudioInterface(asIScriptEngine *scriptengine)
{
 int result;

 // set the namespace
 result = scriptengine->SetDefaultNamespace("AUDIO"); 
 if(result < 0) return result;

 // first register our enum
 result = scriptengine->RegisterEnum("AudioCues");
 if(result < 0) return result;

 result = scriptengine->RegisterEnumValue("AudioCues", "Cue_iAmmoBounce", (int)Cue_iAmmoBounce);
 if(result < 0) return result;

 result = scriptengine->RegisterEnumValue("AudioCues", "Cue_iAmmoFire", (int)Cue_iAmmoFire);
 if(result < 0) return result;

 ... // some code omitted

 result = scriptengine->RegisterGlobalFunction("void PlayAudioCue(AudioCues)", asFUNCTION(PlayAudioCueWrapper), asCALL_CDECL);
 if(result < 0) return result;

 result = scriptengine->RegisterGlobalFunction("void Play3DAudioCue(AudioCues, const D3DXVECTOR3 &in)", asFUNCTION(Play3DAudioCueWrapper), asCALL_CDECL);
 if(result < 0) return result;

 result = scriptengine->RegisterGlobalFunction("void SetNumDroidsForAudio(int)", asFUNCTION(SetNumDroidsForAudio), asCALL_CDECL);
 if(result < 0) return result;

 // reset back to global namespace
 result = scriptengine->SetDefaultNamespace(""); 

 return result;
}

Conclusion

It's taken some time, but now I have finished all of the needed bindings between AngelScript and C++ that I'll need. Now that all of the bindings are complete, I need to work on adding the scripts. AngelScript is a very powerful scripting language and I hope to show some of its features in part 3 of this article. In part 3 of this article, I'll write all of the scripts, I'll also show how to restrict bindings to some scripts.

Coding style in this article

Listed in the best practices for AngelScript is to always check the return value for every function. In most of the AngelCode examples and in the manual an assert is used to check for errors. I don't use the assert, instead I've been using [tt]if(result < 0) return result;[/tt]. This can easily be replaced by [tt]assert(r >= 0);[/tt] as is used in the AngelScript documentation. Also, my goal with this project was to change the XACTGame sample as little as possible. The XACTGame sample was designed to show certain techniques such as adding graphics and audio, and it uses a simple framework.

Getting AngelScript

You can download the latest version of the AngelScript SDK from the AngelCode website. http://www.angelcode.com/ You'll find an excellent manual that explains the API in detail.

Note on Microsoft Source Code

Because Microsoft code was used in this program, I want to state some terms from the Direct X SDK EULA. The XACTGame sample was created by Microsoft and Microsoft owns the copyright. Changes made by Dominque Douglas have been clearly marked. Use of the source code provided does not change the license agreement for using Microsoft code. Microsoft code cannot be modified to work on non-Microsoft operating systems and Microsoft is not responsible for any claims related to the distribution of this program. Refer to the license agreement in the Direct X SDK for details.

Downloading This Project

The source code can be downloaded here: XACTGameAngelScript-Part2.zip Download note: Because of the size, this does not include the media files needed by the project such as the audio files and graphics files. You'll need to copy the "media" folder from the XACTGame sample in the Direct X SDK. You may need to alter the project's include and library directories to match your system. For simplicity, the AngelScript add-ons that were used in this project have been included. The project is a Visual Studio 2010 solution.

Wednesday, December 25, 2013

Programming By Example - Adding AngelScript to a Game Part 1

For Part 1: Programming By Example - Adding AngelScript to a Game Part 1
For Part 2: Programming By Example - Adding AngelScript to a Game Part 2
For Part 3: Programming By Example - Adding AngelScript to a Game Part 3

Using interpreted scripting languages in games instead of compiling to native code has become very common in games. Using scripting languages allows developers to make functional changes to their programs without having to compile and link the program and they allow developers to be able to separate the game logic from the game engine. These are good things, but how can a developer new to scripting successfully use a scripting language in his or her project? This article will explain how to add AngelScript to a game by taking the XACTGame example program given in the Direct X SDK.

AngelScript is a scripting language with a syntax that's very similar to C++. It is a strictly typed language with many of the types being the same as in C++. This article will explain some concepts of how to use AngelScript, but a basic knowledge of the language will be needed to understand all of the concepts. See this page in the AngelScript documentation for details. Before you begin with this tutorial, make sure you have installed the Direct X SDK. I'll be using the June 2010 update. Also, you'll need to go to www.AngelCode.com to get the latest version of the AngelScript SDK and follow the instructions on how to set up AngelScript with your compiler. Here are some details on my current setup. I'm using Angel Script 2.28 and I'm using Microsoft Visual Studio 2010 Professional.

When adding scripting to an existing program, taking an accurate survey of the code is very important. Other than the Direct X files, the XACTGame example is made up of 5 source files -- audio.cpp, audio.h, game.cpp, game.h, and main.cpp. For now, I'll ignore the audio files for now. I could script some of the capabilities, but I'd rather not make this article too long. When examining the files, you'll notice that not all of the functions have forward declarations, but we'll need to know all of the functions so we can decide what to script.

Here's the complete list:

void InitApp();
bool CALLBACK IsDeviceAcceptable( D3DCAPS9* pCaps, D3DFORMAT AdapterFormat,
                                  D3DFORMAT BackBufferFormat, bool bWindowed, void* pUserContext );
static int __cdecl SortAspectRatios( const void* arg1, const void* arg2 );
bool CALLBACK ModifyDeviceSettings( DXUTDeviceSettings* pDeviceSettings, void* pUserContext );
HRESULT SplitIntoSeperateTriangles( IDirect3DDevice9* pd3dDevice, ID3DXMesh* pInMesh, CDXUTXFileMesh* pOutMesh );
HRESULT CALLBACK OnCreateDevice( IDirect3DDevice9* pd3dDevice, const D3DSURFACE_DESC* pBackBufferSurfaceDesc,
                                 void* pUserContext );
void ComputeMeshScaling( CDXUTXFileMesh& Mesh, D3DXMATRIX* pmScalingCenter, float fNewRadius );
void ComputeMeshScalingBox( CDXUTXFileMesh& Mesh, D3DXMATRIX* pmScalingCenter, D3DXVECTOR3 vNewMin,
                            D3DXVECTOR3 vNewMax );
void SetEffectTechnique();
HRESULT CALLBACK OnResetDevice( IDirect3DDevice9* pd3dDevice,
                                const D3DSURFACE_DESC* pBackBufferSurfaceDesc, void* pUserContext );
void FireAmmo();
float GetDistFromWall( D3DXVECTOR3 P1, D3DXVECTOR3 P2, D3DXVECTOR3 P3, D3DXVECTOR3 N );
void DroidPickNewDirection( int A );
void DroidChooseNewTask( int A );
void HandleDroidAI( float fElapsedTime );
void CheckForAmmoToDroidCollision( int A );
void CheckForInterAmmoCollision( float fElapsedTime );
void CheckForAmmoToWallCollision( int A );
void HandleAmmoAI( float fElapsedTime );
void CALLBACK OnFrameMove( double fTime, float fElapsedTime, void* pUserContext );
void CreateAmmo( int nIndex, D3DXVECTOR4 Pos, D3DXVECTOR4 Vel );
void RenderAmmo( int A, D3DXMATRIXA16& mView, D3DXMATRIXA16& mProj );
void CreateDroid();
void RenderDroid( IDirect3DDevice9* pd3dDevice, int A, D3DXMATRIXA16& mView, D3DXMATRIXA16& mProj, bool bExplode );
void CALLBACK OnFrameRender( IDirect3DDevice9* pd3dDevice, double fTime, float fElapsedTime, void* pUserContext );
void RenderText();
LRESULT CALLBACK MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, bool* pbNoFurtherProcessing,
                          void* pUserContext );
void UpdateAspectRatioList( DXUTDeviceSettings* pDS );
void UpdateResolutionList( DXUTDeviceSettings* pDS );
void CALLBACK OnGUIEvent( UINT nEvent, int nControlID, CDXUTControl* pControl, void* pUserContext );
void ToggleMenu();
void CALLBACK KeyboardProc( UINT nChar, bool bKeyDown, bool bAltDown, void* pUserContext );
void CALLBACK OnLostDevice( void* pUserContext );
void CALLBACK OnDestroyDevice( void* pUserContext );

Of all of these functions, we now to choose suitable candidates for scripting. Again to limit the size of this article, I'll only script functions related to the game's logic. That leaves the following functions that will either be partially scripted or completely scripted.

void InitApp(); 
void FireAmmo();
void DroidPickNewDirection( int A );
void DroidChooseNewTask( int A );
void HandleDroidAI( float fElapsedTime );
void HandleAmmoAI( float fElapsedTime );

While AngelScript is a C++-style language, we can't just write the script code and be done. Our C++ code will need to be able to communicate with AngelScript, and our scripts need to be informed of the data structures and classes that we'll use. Again, an accurate examination of the base code will be needed. Let's determine the dependencies that each of the above functions have. Then we'll be able to define how our script bindings.

void InitApp();

  • RENDER_STATE - This is a structure defined in game.h that details everything that needs to be rendered. Only parts of this structure are needed by InitApp()
  • CDXUTDialog - This class is defined in DXUTgui.h and it defines a GUI dialog. The following  methods will be needed:


HRESULT AddStatic( int ID, LPCWSTR strText, int x, int y, int width, int height, bool bIsDefault=false,
      CDXUTStatic** ppCreated=NULL );
HRESULT AddButton( int ID, LPCWSTR strText, int x, int y, int width, int height, UINT nHotkey=0,
      bool bIsDefault=false, CDXUTButton** ppCreated=NULL );
HRESULT AddCheckBox( int ID, LPCWSTR strText, int x, int y, int width, int height, bool bChecked=false,
      UINT nHotkey=0, bool bIsDefault=false, CDXUTCheckBox** ppCreated=NULL );
HRESULT AddRadioButton( int ID, UINT nButtonGroup, LPCWSTR strText, int x, int y, int width,
      int height, bool bChecked=false, UINT nHotkey=0, bool bIsDefault=false,
      CDXUTRadioButton** ppCreated=NULL );
HRESULT AddComboBox( int ID, int x, int y, int width, int height, UINT nHotKey=0, bool bIsDefault=
      false, CDXUTComboBox** ppCreated=NULL );
HRESULT AddSlider( int ID, int x, int y, int width, int height, int min=0, int max=100, int value=50,
      bool bIsDefault=false, CDXUTSlider** ppCreated=NULL );

  • GAME_STATE g_GameState; - The global game state. GAME_STATE is defined in game.h

int nAmmoCount;
float fAmmoColorLerp;
D3DXCOLOR BlendFromColor;
bool bDroidMove;
bool bAutoAddDroids;
GAME_MODE gameMode;

  • D3DXCOLOR - defined in d3dx9math.h
  • GAME_MODE - enum of game modes. Defined in game.h


Many things need to be done just to get all the bindings for InitApp(). So before proceeding to the other functions, let's first build a version of XACTGame that uses AngelScript for the InitApp() function.

First, I'll start by adding two new files to the project as_scripting.h and as_scripting.cpp. The XACTGame sample application does everything in free functions so for simplicity, I'll continue to use that style.

We'll need to add some includes to the as_scripting.h file. angelscript.h is the file we need for all of the basic angelscript classes. scriptbuilder.h is in the add_on directory and it's a extra class that will help us load our scripts.
// Include the definitions of the script library and the add-ons we'll use.
// The project settings may need to be configured to let the compiler where
// to find these headers. Don't forget to add the source modules for the
// add-ons to your project as well so that they will be compiled into the 
// application.
#include 
#include 

#include "game.h"

Initially, I was going to use globals for the sake of keeping the same style as the XACTGame sample, but I've found a much better approach. To do this, I'll provide a structure called ScriptContextData, that I'll pass to the game functions and DXUT callbacks that need it. It would be easier to just use globals, but I want to show this approach because AngelScript allows multiple modules and script context to be used. A module is a compiled(compiled into VM bytecode) script and a context is an instance of the virtual machine. Complex games will probably have multiple modules and may have multiple context.

For now, I'll keep the structure simple.

enum ScriptFunctionIDs
{
 Function_InitApp = 0
};

const unsigned int max_script_functions = 1;

struct ScriptContextData
{
 asIScriptContext *ctx;
 asIScriptFunction *script_functions[max_script_functions];

 void ExecuteFunction(ScriptFunctionIDs func_id);
};

The structure keeps the context (the virtual machine) and an array of the script functions that we
can call from C++. To simplify things, I'm also adding a function that will run the function and
check for exceptions.

And for now I'll write these two functions:

int StartScriptingSystem(asIScriptEngine *&scriptengine, CScriptBuilder &scriptbuilder, ScriptContextData &contextdata)
{
 int result;

 // Create the script engine
 scriptengine = asCreateScriptEngine(ANGELSCRIPT_VERSION);

 // Set the message callback to receive information on errors in human readable form.
 result = scriptengine->SetMessageCallback(asFUNCTION(MessageCallback), 0, asCALL_CDECL);
 if(result < 0) return result; // an error has occurred

 result = RegisterGameInterface(scriptengine);
 if(result < 0) return result; // an error has occurred

 ...
}

void ShutdownScriptingSystem(asIScriptEngine *&scriptengine, asIScriptContext *&ctx)
{
 // Why check to see if this is NULL? This function will be called at the end of the program
 // as a way to clean it up even if an error has occurred. If an error occurs during initialization
 // one or both of these variables may be null.
 if(ctx)
 {
  ctx->Release();
  ctx = NULL; // i don't like leaving old pointers that don't point to valid data
 }

 if(scriptengine)
 {
  scriptengine->Release();
  scriptengine = NULL; // i don't like leaving old pointers that don't point to valid data
 }
}

The RegisterGameInterface() function performs all of the bindings between the script engine and the C++ code. Let's start with the easiest binding, the GAME_MODE enum.
int RegisterEnumGAME_MODE(asIScriptEngine *scriptengine)
{
 int result;

 result = scriptengine->RegisterEnum("GAME_MODE");
 if(result < 0) return result;

 result = scriptengine->RegisterEnumValue("GAME_MODE", "GAME_RUNNING", (int)GAME_RUNNING);
 if(result < 0) return result;

    result = scriptengine->RegisterEnumValue("GAME_MODE", "GAME_MAIN_MENU", (int)GAME_MAIN_MENU);
 if(result < 0) return result;

    result = scriptengine->RegisterEnumValue("GAME_MODE", "GAME_AUDIO_MENU", (int)GAME_AUDIO_MENU);
 if(result < 0) return result;

    result = scriptengine->RegisterEnumValue("GAME_MODE", "GAME_VIDEO_MENU", (int)GAME_VIDEO_MENU);

 return result;
}

Adding enumerations are simple. First use the RegisterEnum() function to register the type. Then use the RegisterEnumValue() to add each value. Sorry, there's no shorter way, but writing the code is very straight forward.

Next, I'll add the D3DXVECTOR3 type. I'll register is as a value type; however, to not clutter up the as_scripting.cpp and angelscript.h files, I'll create separate files for this type. I'll do the same with D3DXCOLOR. The files are a little long so please check the included source files for details.

Next, since the render state is a global variable in XACT, I'm going to simplify my life a little and not create bindings for the CDXUTDialog. Instead, I'm going to create a new enum XACTGAMEDIALOG, and I'll make and register some free functions with the script engine.

enum XACTGAMEDIALOG
{
 IDMainMenuDlg,
 IDVideoMenuDlg,
 IDAudioMenuDlg
};

void AddStaticToDialog(XACTGAMEDIALOG dialogID, int ID, const std::string & strText, int x, int y, int width,
 int height, bool bIsDefault);
void AddButtonToDialog(XACTGAMEDIALOG dialogID, int ID, const std::string & strText, int x, int y, int width,
 int height, UINT nHotkey, bool bIsDefault);
void AddCheckBoxToDialog(XACTGAMEDIALOG dialogID, int ID, const std::string & strText, int x, int y, int width,
 int height, bool bChecked, UINT nHotkey, bool bIsDefault);
void AddRadioButtonToDialog(XACTGAMEDIALOG dialogID, int ID, UINT nButtonGroup, const std::string & strText,
 int x, int y, int width, int height, bool bChecked, UINT nHotkey, bool bIsDefault);
void AddComboBoxToDialog(XACTGAMEDIALOG dialogID, int ID, int x, int y, int width, int height,
 UINT nHotKey, bool bIsDefault);
void AddSliderToDialog(XACTGAMEDIALOG dialogID, int ID, int x, int y, int width, int height,
 int min, int max, int value,  bool bIsDefault);

There's no reason why the interface that you supply to AngelScript has to be exactly like the C++ one. The AngelScript interface also can't handle standard C-style strings which is what the CDXUTDialog methods require for text so we'd still have to wrap them in another function either way. To make the interface a little cleaner, I'll put it in a namespace. This can be done by calling the SetDefaultNamespace()method before we register the global functions.

int RegisterDialogInterface(asIScriptEngine *scriptengine)
{
 int result;

 // set the namespace
 result = scriptengine->SetDefaultNamespace("dialogs"); 
 if(result < 0) return result;

 // first register our enum
 result = scriptengine->RegisterEnum("XACTGAMEDIALOG");
 if(result < 0) return result;

 result = scriptengine->RegisterEnumValue("XACTGAMEDIALOG", "IDMainMenuDlg", (int)IDMainMenuDlg);
 if(result < 0) return result;

 ...

 // register the global functions
 result = scriptengine->RegisterGlobalFunction("void AddStaticToDialog(XACTGAMEDIALOG, int, const string &in, int, int, int, int, bool)",
  asFUNCTION(AddStaticToDialog), asCALL_CDECL);
 if(result < 0) return result;

 ...

 // reset back to global namespace
 result = scriptengine->SetDefaultNamespace(""); 
 
 return result;
}

We should register the enum just as before. Use the RegisterGlobalFunction() method to add each of the new functions. The method needs the declaration of the function, a function pointer, and the calling convention. When using reference parameters, you must use 'in' or 'out' as the parameter names in the declaration you supply to AngelScript so it will know how it can optimize how it uses the parameter.

Now, if we need anymore functionality from RENDER_STATE, we can just add more functions to the interface instead of giving the script direct access. This will be simple to do and safer as we can add checks to our wrapper functions. We'll do the same thing with the CFirstPersonCamera class.

// declare the global g_Camera variable as extern here so we can use the one defined in game.cpp
extern CFirstPersonCamera  g_Camera;

static void CameraSetViewParams( D3DXVECTOR3 &pvEyePt, D3DXVECTOR3 &pvLookatPt )
{
 g_Camera.SetViewParams(&pvEyePt, &pvLookatPt);
}

static void CameraSetEnableYAxisMovement( bool bEnableYAxisMovement )
{
 g_Camera.SetEnableYAxisMovement(bEnableYAxisMovement);
}

...

int RegisterCameraInterface(asIScriptEngine *scriptengine)
{
 int result;

 // set the namespace
 result = scriptengine->SetDefaultNamespace("FirstPersonCamera"); 
 if(result < 0) return result;

 // register the global functions
 result = scriptengine->RegisterGlobalFunction("void SetViewParams( D3DXVECTOR3 &in, D3DXVECTOR3 &in )", asFUNCTION(CameraSetViewParams), asCALL_CDECL);
 if(result < 0) return result;

 result = scriptengine->RegisterGlobalFunction("void SetEnableYAxisMovement( bool )", asFUNCTION(CameraSetEnableYAxisMovement), asCALL_CDECL);
 if(result < 0) return result;

 ...


 // reset back to global namespace
 result = scriptengine->SetDefaultNamespace(""); 
 
 return result;
}
Now the last thing we need to make an interface for so that we can script the InitApp() function is GAME_STATE. There are a few possible ways to do this. One way would be to make a GAME_STATE object type in AngelScript and then register g_GameState as a global property. A second way would be to provide a set of accessor (get/set) functions and register them as global functions in a namespace. A third option would be to use a namespace and then register individual member variables of the GAME_STATE struct as global properties in AngelScript. Since there is only one g_GameState in the XACTGame sample, I think registering a new type would be a waste. There's also no need to make getters and setters since I'm not going to add any checking so I'll use the third option.

int RegisterGameStateInterface(asIScriptEngine *scriptengine)
{
 int result;

 // set the namespace
 result = scriptengine->SetDefaultNamespace("GAME_STATE"); 
 if(result < 0) return result;

 // Register a primitive property that can be read and written to from the script.
 result = scriptengine->RegisterGlobalProperty("int nAmmoCount", &g_GameState.nAmmoCount);
 if(result < 0) return result;

 result = scriptengine->RegisterGlobalProperty("float fAmmoColorLerp", &g_GameState.fAmmoColorLerp);
 if(result < 0) return result;

 result = scriptengine->RegisterGlobalProperty("D3DXCOLOR BlendFromColor", &g_GameState.BlendFromColor);
 if(result < 0) return result;

 result = scriptengine->RegisterGlobalProperty("bool bDroidMove", &g_GameState.bDroidMove);
 if(result < 0) return result;

 result = scriptengine->RegisterGlobalProperty("bool bAutoAddDroids", &g_GameState.bAutoAddDroids);
 if(result < 0) return result;

 result = scriptengine->RegisterGlobalProperty("GAME_MODE gameMode", &g_GameState.gameMode);
 if(result < 0) return result;

 // reset back to global namespace
 result = scriptengine->SetDefaultNamespace(""); 

 return result;
}
That's it for the interface for now, I can add more properties later if needed. Now we have the entire interface that will be needed to script the InitApp() function with AngelScript. Now we need to be able to load the script. The following code will do that.

int LoadScript(asIScriptEngine *scriptengine, CScriptBuilder &scriptbuilder, ScriptContextData &contextdata)
{
 int result;

 // The CScriptBuilder helper is an add-on that loads the file,
 // performs a pre-processing pass if necessary, and then tells
 // the engine to build a script module.
 CScriptBuilder builder;
 result = builder.StartNewModule(scriptengine, "BasicModule"); 
 if( result < 0 ) 
 {
  // If the code fails here it is usually because there
  // is no more memory to allocate the module
  MessageBoxA(NULL, "Unrecoverable error while starting a new module.", "AngelScript Message", MB_OK);
  return result;
 }
 result = builder.AddSectionFromFile("xactgamescript.as");
 if( result < 0 ) 
 {
  // The builder wasn't able to load the file. Maybe the file
  // has been removed, or the wrong name was given, or some
  // preprocessing commands are incorrectly written.
  MessageBoxA(NULL,"Please correct the errors in the script and try again.", "AngelScript Message", MB_OK);
  return result;
 }
 result = builder.BuildModule();
 if( result < 0 ) 
 {
  // An error occurred. Instruct the script writer to fix the 
  // compilation errors that were listed in the output stream.
  MessageBoxA(NULL,"Please correct the errors in the script and try again.", "AngelScript Message", MB_OK);
  return result;
 }

 // Find the function that is to be called. 
 asIScriptModule *mod = scriptengine->GetModule("BasicModule");
 contextdata.script_functions[Function_InitApp] = mod->GetFunctionByDecl("void InitApp()");
 if( contextdata.script_functions[Function_InitApp] == 0 )
 {
  // The function couldn't be found. Instruct the script writer
  // to include the expected function in the script.
  MessageBoxA(NULL,"The script must have the function 'void InitApp()'. Please add it and try again.", "AngelScript Message", MB_OK);
  return -1;
 }

 return result;
}
This function uses the CScriptBuilder add-on to load a script. CScriptBuilder is a useful class because it not only loads the script, but it can also do some C-style preprocessor actions such as #include. After the script has been loaded, we use script builder to compile the script into AngelScript bytecode and build a module. In this function, I also get and stor the AngelScript function for our scripted InitApp() function so I'll be able to quickly call it later. Inside the ScriptContextData struct that I created earlier is an ExecuteFunction() method that will execute a function in the script.

// enumerations -------------------------------------------------------------
enum ScriptFunctionIDs
{
 Function_InitApp = 0
};

const unsigned int max_script_functions = 1;

// Structures ---------------------------------------------------------------
struct ScriptContextData
{
 asIScriptContext *ctx;
 asIScriptFunction *script_functions[max_script_functions];

 void ExecuteFunction(ScriptFunctionIDs func_id);
};

void ScriptContextData::ExecuteFunction(ScriptFunctionIDs func_id)
{
 if(ctx)
 {
  ctx->Prepare(script_functions[func_id]);
  int result = ctx->Execute();
  if( result != asEXECUTION_FINISHED )
  {
   // The execution didn't complete as expected. Determine what happened.
   if( result == asEXECUTION_EXCEPTION )
   {
    // An exception occurred, let the script writer know what happened so it can be corrected.
    MessageBoxA(NULL, ctx->GetExceptionString(), "An exception occurred.", MB_OK);
   }
  }
 }
}
So now let's script it. First let's take a look at the C++ version of the InitApp() function.

void InitApp()
{
    srand( 0 );

    g_Render.pEffect = NULL;
    g_Render.pDefaultTex = NULL;
    g_Render.UseFixedFunction = 0.0f;
    g_Render.ForceShader = 0;
    g_Render.MaximumResolution = 4096.0f;
    g_Render.DisableSpecular = 0.0f;
    g_Render.bDetectOptimalSettings = true;

    // Initialize dialogs
    g_Render.MainMenuDlg.Init( &g_Render.DialogResourceManager );
    g_Render.MainMenuDlg.SetCallback( OnGUIEvent ); int iY = ( ( 300 - 30 * 6 ) / 2 );
    g_Render.MainMenuDlg.AddButton( IDC_AUDIO, L"Audio", ( 250 - 125 ) / 2, iY += 30, 125, 22 );
    g_Render.MainMenuDlg.AddButton( IDC_VIDEO, L"Video", ( 250 - 125 ) / 2, iY += 30, 125, 22 );
    g_Render.MainMenuDlg.AddButton( IDC_RESUME, L"Resume", ( 250 - 125 ) / 2, iY += 30, 125, 22 );
    g_Render.MainMenuDlg.AddButton( IDC_QUIT, L"Quit", ( 250 - 125 ) / 2, iY += 60, 125, 22, 'Q' );

    g_Render.AudioMenuDlg.Init( &g_Render.DialogResourceManager );
    g_Render.AudioMenuDlg.SetCallback( OnGUIEvent ); iY = 60;
    g_Render.AudioMenuDlg.AddStatic( IDC_STATIC, L"Music Volume", ( 250 - 125 ) / 2, iY += 24, 125, 22 );
    g_Render.AudioMenuDlg.AddSlider( IDC_MUSIC_SCALE, ( 250 - 100 ) / 2, iY += 24, 100, 22, 0, 100, 100 );
    g_Render.AudioMenuDlg.AddStatic( IDC_STATIC, L"Sound Effects Volume", ( 250 - 125 ) / 2, iY += 35, 125, 22 );
    g_Render.AudioMenuDlg.AddSlider( IDC_SOUNDFX_SCALE, ( 250 - 100 ) / 2, iY += 24, 100, 22, 0, 100, 100 );
    g_Render.AudioMenuDlg.AddButton( IDC_BACK, L"Back", ( 250 - 125 ) / 2, iY += 40, 125, 22 );

    g_Render.VideoMenuDlg.Init( &g_Render.DialogResourceManager );
    g_Render.VideoMenuDlg.SetCallback( OnGUIEvent ); iY = 0;
    g_Render.VideoMenuDlg.AddCheckBox( IDC_FULLSCREEN, L"Full screen", ( 250 - 200 ) / 2, iY += 30, 200, 22, true );
    g_Render.VideoMenuDlg.AddStatic( IDC_STATIC, L"Aspect:", 50, iY += 22, 50, 22 );
    g_Render.VideoMenuDlg.AddComboBox( IDC_ASPECT, 100, iY, 100, 22 );
    g_Render.VideoMenuDlg.AddStatic( IDC_STATIC, L"Resolution:", 30, iY += 22, 75, 22 );
    g_Render.VideoMenuDlg.AddComboBox( IDC_RESOLUTION, 100, iY, 125, 22 );
    g_Render.VideoMenuDlg.AddCheckBox( IDC_ANTI_ALIASING, L"Anti-Aliasing", ( 250 - 200 ) / 2, iY += 26, 200, 22,
                                       false );
    g_Render.VideoMenuDlg.AddCheckBox( IDC_HIGH_MODEL_RES, L"High res models", ( 250 - 200 ) / 2, iY += 26, 200, 22,
                                       true );
    g_Render.VideoMenuDlg.AddStatic( IDC_MAX_DROIDS_TEXT, L"Max Droids", ( 250 - 125 ) / 2, iY += 26, 125, 22 );
    g_Render.VideoMenuDlg.AddSlider( IDC_MAX_DROIDS, ( 250 - 150 ) / 2, iY += 22, 150, 22, 1, MAX_DROID, 10 );
    g_Render.VideoMenuDlg.AddButton( IDC_APPLY, L"Apply", ( 250 - 125 ) / 2, iY += 35, 125, 22 );
    g_Render.VideoMenuDlg.AddButton( IDC_BACK, L"Back", ( 250 - 125 ) / 2, iY += 30, 125, 22 );

    // Setup the camera
    D3DXVECTOR3 MinBound( g_MinBound.x + CAMERA_SIZE, g_MinBound.y + CAMERA_SIZE, g_MinBound.z + CAMERA_SIZE );
    D3DXVECTOR3 MaxBound( g_MaxBound.x - CAMERA_SIZE, g_MaxBound.y - CAMERA_SIZE, g_MaxBound.z - CAMERA_SIZE );
    g_Camera.SetClipToBoundary( true, &MinBound, &MaxBound );
    g_Camera.SetEnableYAxisMovement( false );
    g_Camera.SetRotateButtons( false, false, true );
    g_Camera.SetScalers( 0.001f, 4.0f );
    D3DXVECTOR3 vecEye( 0.0f, -GROUND_Y + 0.7f, 0.0f );
    D3DXVECTOR3 vecAt ( 0.0f, -GROUND_Y + 0.7f, 1.0f );
    g_Camera.SetViewParams( &vecEye, &vecAt );

    ZeroMemory( &g_GameState, sizeof( GAME_STATE ) );
    g_GameState.gameMode = GAME_RUNNING;
    g_GameState.nAmmoCount = 0;
    g_GameState.fAmmoColorLerp = 1000.0f;
    g_GameState.BlendFromColor = D3DXCOLOR( 0.6f, 0, 0, 1 );
    g_GameState.bAutoAddDroids = false;
    g_GameState.bDroidMove = true;

    // Store the rcWork of each monitor before a fullscreen D3D device is created 
    // This is used later to ensure the supported window mode 
    // resolutions will fit inside the desktop
    IDirect3D9* pD3D = DXUTGetD3D9Object();
    UINT numAdapters = pD3D->GetAdapterCount();
    for( UINT adapterOrdinal = 0; adapterOrdinal < numAdapters && adapterOrdinal < 10; adapterOrdinal++ )
    {
        MONITORINFO miAdapter;
        miAdapter.cbSize = sizeof( MONITORINFO );
        DXUTGetMonitorInfo( pD3D->GetAdapterMonitor( adapterOrdinal ), &miAdapter );
        g_Render.rcAdapterWork[adapterOrdinal] = miAdapter.rcWork;
    }

    // Make a list of supported windowed mode resolutions.  
    // The list of fullscreen mode resolutions are gathered from the D3D device directly.
    D3DDISPLAYMODE dm = {0, 0, 0, D3DFMT_UNKNOWN};
    dm.Width = 640; dm.Height = 480; g_Render.aWindowedDMList.Add( dm ); // 4:3
    dm.Width = 800; dm.Height = 600; g_Render.aWindowedDMList.Add( dm ); // 4:3
    dm.Width = 1024; dm.Height = 768; g_Render.aWindowedDMList.Add( dm ); // 4:3
    dm.Width = 1280; dm.Height = 960; g_Render.aWindowedDMList.Add( dm ); // 4:3
    dm.Width = 1600; dm.Height = 1200; g_Render.aWindowedDMList.Add( dm ); // 4:3

    dm.Width = 852; dm.Height = 480; g_Render.aWindowedDMList.Add( dm ); // 16:9
    dm.Width = 1067; dm.Height = 600; g_Render.aWindowedDMList.Add( dm ); // 16:9
    dm.Width = 1280; dm.Height = 720; g_Render.aWindowedDMList.Add( dm ); // 16:9
    dm.Width = 1920; dm.Height = 1080; g_Render.aWindowedDMList.Add( dm ); // 16:9
}
Some things can be scripted, but some things would be better left done in C++. Now I'll rewrite it and only leave in the things that shouldn't be scripted.

void InitApp(ScriptContextData &context_data)
// Changed 2013-12-25 By Dominque Douglas for AngelScript Example
{
    srand( 0 );

    g_Render.pEffect = NULL;
    g_Render.pDefaultTex = NULL;
    g_Render.UseFixedFunction = 0.0f;
    g_Render.ForceShader = 0;
    g_Render.MaximumResolution = 4096.0f;
    g_Render.DisableSpecular = 0.0f;
    g_Render.bDetectOptimalSettings = true;

    // Initialize dialogs
    g_Render.MainMenuDlg.Init( &g_Render.DialogResourceManager );
    g_Render.MainMenuDlg.SetCallback( OnGUIEvent );
 // we'll script adding all the GUI elements

    g_Render.AudioMenuDlg.Init( &g_Render.DialogResourceManager );
    g_Render.AudioMenuDlg.SetCallback( OnGUIEvent );
 // we'll script adding all the GUI elements

    g_Render.VideoMenuDlg.Init( &g_Render.DialogResourceManager );
    g_Render.VideoMenuDlg.SetCallback( OnGUIEvent );
 // we'll script adding all the GUI elements

 // script setting up the camera

 // script setting the inital game state

    // Store the rcWork of each monitor before a fullscreen D3D device is created 
    // This is used later to ensure the supported window mode 
    // resolutions will fit inside the desktop
    IDirect3D9* pD3D = DXUTGetD3D9Object();
    UINT numAdapters = pD3D->GetAdapterCount();
    for( UINT adapterOrdinal = 0; adapterOrdinal < numAdapters && adapterOrdinal < 10; adapterOrdinal++ )
    {
        MONITORINFO miAdapter;
        miAdapter.cbSize = sizeof( MONITORINFO );
        DXUTGetMonitorInfo( pD3D->GetAdapterMonitor( adapterOrdinal ), &miAdapter );
        g_Render.rcAdapterWork[adapterOrdinal] = miAdapter.rcWork;
    }

    // Make a list of supported windowed mode resolutions.  
    // The list of fullscreen mode resolutions are gathered from the D3D device directly.
    D3DDISPLAYMODE dm = {0, 0, 0, D3DFMT_UNKNOWN};
    dm.Width = 640; dm.Height = 480; g_Render.aWindowedDMList.Add( dm ); // 4:3
    dm.Width = 800; dm.Height = 600; g_Render.aWindowedDMList.Add( dm ); // 4:3
    dm.Width = 1024; dm.Height = 768; g_Render.aWindowedDMList.Add( dm ); // 4:3
    dm.Width = 1280; dm.Height = 960; g_Render.aWindowedDMList.Add( dm ); // 4:3
    dm.Width = 1600; dm.Height = 1200; g_Render.aWindowedDMList.Add( dm ); // 4:3

    dm.Width = 852; dm.Height = 480; g_Render.aWindowedDMList.Add( dm ); // 16:9
    dm.Width = 1067; dm.Height = 600; g_Render.aWindowedDMList.Add( dm ); // 16:9
    dm.Width = 1280; dm.Height = 720; g_Render.aWindowedDMList.Add( dm ); // 16:9
    dm.Width = 1920; dm.Height = 1080; g_Render.aWindowedDMList.Add( dm ); // 16:9

 // execute the script here
 context_data.ExecuteFunction(Function_InitApp);
}

With that done, now we can write the AngelScript script for our InitApp() function. I'll also add some constants to the script that were defined in the game.h file. A possible future enhancement would be to allow setting these constants in AngelScript and the allowing the C++ code use the values.

const float GROUND_Y = 3.0f; // -GROUND_Y is the Y coordinate of the ground.
const float CAMERA_SIZE = 0.2f; // CAMERA_SIZE is used for clipping camera movement
const uint MAX_DROID = 50;

// MinBound and MaxBound are the bounding box representing the cell mesh.
const D3DXVECTOR3           g_MinBound( -6.0f, -GROUND_Y, -6.0f );
const D3DXVECTOR3           g_MaxBound( 6.0f, GROUND_Y, 6.0f );

//--------------------------------------------------------------------------------------
// UI control IDs
//--------------------------------------------------------------------------------------
const uint IDC_STATIC              = 1;
const uint IDC_AUDIO               = 2;
const uint IDC_VIDEO               = 3;
const uint IDC_RESUME              = 4;
const uint IDC_QUIT                = 5;
const uint IDC_BACK                = 8;
const uint IDC_SOUNDFX_SCALE       = 6;
const uint IDC_MUSIC_SCALE         = 7;
const uint IDC_RESOLUTION          = 9;
const uint IDC_ANTI_ALIASING       = 10;
const uint IDC_MAX_DROIDS          = 11;
const uint IDC_HIGH_MODEL_RES      = 12;
const uint IDC_MAX_DROIDS_TEXT     = 13;
const uint IDC_APPLY               = 14;
const uint IDC_FULLSCREEN          = 15;
const uint IDC_ASPECT              = 16;

void InitApp()
{
 int iY = ( ( 300 - 30 * 6 ) / 2 );
    dialogs::AddButtonToDialog(dialogs::IDMainMenuDlg, IDC_AUDIO, "Audio", ( 250 - 125 ) / 2, iY += 30, 125, 22 );
    dialogs::AddButtonToDialog(dialogs::IDMainMenuDlg, IDC_VIDEO, "Video", ( 250 - 125 ) / 2, iY += 30, 125, 22 );
    dialogs::AddButtonToDialog(dialogs::IDMainMenuDlg, IDC_RESUME, "Resume", ( 250 - 125 ) / 2, iY += 30, 125, 22 );
    dialogs::AddButtonToDialog(dialogs::IDMainMenuDlg, IDC_QUIT, "Quit", ( 250 - 125 ) / 2, iY += 60, 125, 22, 81/*'Q'*/ );

 iY = 60;
    dialogs::AddStaticToDialog(dialogs::IDAudioMenuDlg, IDC_STATIC, "Music Volume", ( 250 - 125 ) / 2, iY += 24, 125, 22 );
    dialogs::AddSliderToDialog(dialogs::IDAudioMenuDlg, IDC_MUSIC_SCALE, ( 250 - 100 ) / 2, iY += 24, 100, 22, 0, 100, 100 );
    dialogs::AddStaticToDialog(dialogs::IDAudioMenuDlg, IDC_STATIC, "Sound Effects Volume", ( 250 - 125 ) / 2, iY += 35, 125, 22 );
    dialogs::AddSliderToDialog(dialogs::IDAudioMenuDlg, IDC_SOUNDFX_SCALE, ( 250 - 100 ) / 2, iY += 24, 100, 22, 0, 100, 100 );
    dialogs::AddButtonToDialog(dialogs::IDAudioMenuDlg, IDC_BACK, "Back", ( 250 - 125 ) / 2, iY += 40, 125, 22 );

 iY = 0;
    dialogs::AddCheckBoxToDialog(dialogs::IDVideoMenuDlg, IDC_FULLSCREEN, "Full screen", ( 250 - 200 ) / 2, iY += 30, 200, 22, true );
    dialogs::AddStaticToDialog(dialogs::IDVideoMenuDlg, IDC_STATIC, "Aspect:", 50, iY += 22, 50, 22 );
    dialogs::AddComboBoxToDialog(dialogs::IDVideoMenuDlg, IDC_ASPECT, 100, iY, 100, 22 );
    dialogs::AddStaticToDialog(dialogs::IDVideoMenuDlg, IDC_STATIC, "Resolution:", 30, iY += 22, 75, 22 );
    dialogs::AddComboBoxToDialog(dialogs::IDVideoMenuDlg, IDC_RESOLUTION, 100, iY, 125, 22 );
    dialogs::AddCheckBoxToDialog(dialogs::IDVideoMenuDlg, IDC_ANTI_ALIASING, "Anti-Aliasing", ( 250 - 200 ) / 2, iY += 26, 200, 22,
                                       false );
    dialogs::AddCheckBoxToDialog(dialogs::IDVideoMenuDlg, IDC_HIGH_MODEL_RES, "High res models", ( 250 - 200 ) / 2, iY += 26, 200, 22,
                                       true );
    dialogs::AddStaticToDialog(dialogs::IDVideoMenuDlg, IDC_MAX_DROIDS_TEXT, "Max Droids", ( 250 - 125 ) / 2, iY += 26, 125, 22 );
    dialogs::AddSliderToDialog(dialogs::IDVideoMenuDlg, IDC_MAX_DROIDS, ( 250 - 150 ) / 2, iY += 22, 150, 22, 1, MAX_DROID, 10 );
    dialogs::AddButtonToDialog(dialogs::IDVideoMenuDlg, IDC_APPLY, "Apply", ( 250 - 125 ) / 2, iY += 35, 125, 22 );
    dialogs::AddButtonToDialog(dialogs::IDVideoMenuDlg, IDC_BACK, "Back", ( 250 - 125 ) / 2, iY += 30, 125, 22 );

    // Setup the camera
    D3DXVECTOR3 MinBound( g_MinBound.x + CAMERA_SIZE, g_MinBound.y + CAMERA_SIZE, g_MinBound.z + CAMERA_SIZE );
    D3DXVECTOR3 MaxBound( g_MaxBound.x - CAMERA_SIZE, g_MaxBound.y - CAMERA_SIZE, g_MaxBound.z - CAMERA_SIZE );
    FirstPersonCamera::SetClipToBoundary( true, MinBound, MaxBound );
    FirstPersonCamera::SetEnableYAxisMovement( false );
    FirstPersonCamera::SetRotateButtons( false, false, true );
    FirstPersonCamera::SetScalers( 0.001f, 4.0f );
    D3DXVECTOR3 vecEye( 0.0f, -GROUND_Y + 0.7f, 0.0f );
    D3DXVECTOR3 vecAt ( 0.0f, -GROUND_Y + 0.7f, 1.0f );
    FirstPersonCamera::SetViewParams( vecEye, vecAt );

    GAME_STATE::gameMode = GAME_RUNNING;
    GAME_STATE::nAmmoCount = 0;
    GAME_STATE::fAmmoColorLerp = 1000.0f;
    GAME_STATE::BlendFromColor = D3DXCOLOR( 0.6f, 0, 0, 1 );
    GAME_STATE::bAutoAddDroids = false;
    GAME_STATE::bDroidMove = true;

}
All of this may seem like a lot of work especially getting all the bindings with the script so many wonder, "is it worth it?" That is a valid question that all should ask themselves when they are considering adding scripting support. For such a small program such as the Direct X XACTGame sample application, it's probably not neccessary, but as your projects increase in size the value of using scripting languages will become more apparent. The binding code for AngelScript is needed because AngelScript is a general purpose scripting language and it needs to know about the application to accurately communicate with the C++ code, but AngelScript has a nice interface, and after some practice, you'll see that doing the bindings is fairly straight forward.

 So that's it for now. In the next part of this article, I'll script some of the other functions. If you download the source code, you'll have to make sure the include and library directories for the AngelScript SDK in the project properties are correct.

For the source code and Visual Studio 2010 project:
XACTGameAngelScript-Part1.zip
Download note: Because of the size, this does not include the media files needed by the project such as the audio files and graphics files. You'll need to copy the "media" folder from the XACTGame sample in the Direct X SDK. For Part 2 of this series, please click here: Programming By Example - Adding AngelScript to a Game Part 2