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++
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 )
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.zipDownload 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