For Part 2, please click here:
Programming By Example - Adding AngelScript to a Game Part 2
For Part 3, please click here:
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 using the XACTGame example program given in the Direct X SDK.
AngelScript Concepts Covered
- Loading a script with CScriptBuilder add-on
- Binding a C++ Interface to the script
- Registering a value type
- Registering an enum
- Registering global variables
- Registering global functions
- Registering a namespace
- Executing Scripts
Adding AngelScript to a Game
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 and API will be needed to understand all of it. See
this page, "AngelScript: Your first script", in the AngelScript documentation for details. The main focus of this article is not to teach AngelScript or its API, but to show how to apply it in an actual game. 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. 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 scope 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 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 "angelscript.h"
#include "scriptbuilder/scriptbuilder.h"
#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.
Registering an Enum
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.
Registering a Value Type
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. Registering a type as a value type means that it will be located on the stack or locally as a member of another object. Value types don't support handles and can be passed by value or reference to application registered functions. The files are a little long so please check the included source files for details.
The following will register our
D3DXVECTOR with AngelScript:
r = engine->RegisterObjectType("D3DXVECTOR3", sizeof(D3DXVECTOR3), asOBJ_VALUE | asOBJ_APP_CLASS |
asOBJ_APP_CLASS_CONSTRUCTOR | asOBJ_APP_CLASS_COPY_CONSTRUCTOR |
asOBJ_APP_CLASS_DESTRUCTOR);
With value types, we should give the name of the type and the size so AngelScript will know how much memory on the stack it needs to reserve for the variable. The flags at the end tell what behaviors we will define for our class. We should register the constructors and destructor as behaviors, but the other methods are registered as object methods. Here's the function that registers the entire
D3DXVECTOR3 object type.
int RegisterD3DXVECTOR3(asIScriptEngine *engine)
{
int r;
// Register the string type
r = engine->RegisterObjectType("D3DXVECTOR3", sizeof(D3DXVECTOR3), asOBJ_VALUE |
asOBJ_APP_CLASS | asOBJ_APP_CLASS_CONSTRUCTOR | asOBJ_APP_CLASS_COPY_CONSTRUCTOR | asOBJ_APP_CLASS_DESTRUCTOR);
if(r < 0) return r;
// Register the object operator overloads
r = engine->RegisterObjectBehaviour("D3DXVECTOR3", asBEHAVE_CONSTRUCT, "void f()", asFUNCTION(ConstructVector3), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectBehaviour("D3DXVECTOR3", asBEHAVE_CONSTRUCT, "void f(const D3DXVECTOR3 ∈)", asFUNCTION(CopyConstructVector3), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectBehaviour("D3DXVECTOR3", asBEHAVE_CONSTRUCT, "void f(float, float, float)", asFUNCTION(ConstructVector3FromFloats), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectBehaviour("D3DXVECTOR3", asBEHAVE_DESTRUCT, "void f()", asFUNCTION(DestructVector3), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 &opAssign(const D3DXVECTOR3 ∈)", asMETHODPR(D3DXVECTOR3, operator =, (const D3DXVECTOR3&), D3DXVECTOR3&), asCALL_THISCALL);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 &opAddAssign(const D3DXVECTOR3 ∈)", asFUNCTION(AddAssignTwoVectors), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 &opSubAssign(const D3DXVECTOR3 ∈)", asFUNCTION(MinusAssignTwoVectors), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 &opMulAssign(float)", asFUNCTION(MulAssignWithFloat), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 &opDivAssign(float)", asFUNCTION(DivAssignWithFloat), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 opAdd(const D3DXVECTOR3 ∈)", asFUNCTION(AddTwoVectors), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 opSub(const D3DXVECTOR3 ∈)", asFUNCTION(MinusTwoVectors), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 opMul(float)", asFUNCTION(MulVectorScalar), asCALL_CDECL_OBJFIRST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 opMul_r(float)", asFUNCTION(MulVectorScalar), asCALL_CDECL_OBJLAST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "D3DXVECTOR3 opDiv(float)", asFUNCTION(DivVectorScalar), asCALL_CDECL_OBJFIRST);
if(r < 0) return r;
r = engine->RegisterObjectMethod("D3DXVECTOR3", "bool opEquals(const D3DXVECTOR3 ∈) const", asFUNCTION(VectorsEqual), asCALL_CDECL_OBJFIRST);
if(r < 0) return r;
r = engine->RegisterObjectProperty("D3DXVECTOR3", "float x", asOFFSET(D3DXVECTOR3,x));
if(r < 0) return r;
r = engine->RegisterObjectProperty("D3DXVECTOR3", "float y", asOFFSET(D3DXVECTOR3,y));
if(r < 0) return r;
r = engine->RegisterObjectProperty("D3DXVECTOR3", "float z", asOFFSET(D3DXVECTOR3,z));
return r;
}
The type is registered using the native calling convention. To registers a method use
RegisterObjectMethod(). The
RegisterObjectMethod() takes four parameters. The first parameter is the name of the type as it'll be used in AngelScript. The second is the AngelScript function declaration. The third is the C++ function pointer to the function to bind, and the last parameter is the calling convention. AngelScript also supports
asCALL_THISCALL, which will allow you to register C++ class members directly. However, since I wrapped all of the functions, I used either
asCALL_CDECL_OBJLAST or
asCALL_CDECL_OBJFIRST. This allows me to use non-member functions with a parameter that mimics the "this" pointer.
asCALL_CDECL_OBJLAST means the last parameter should get the "this" pointer, and
asCALL_CDECL_OBJFIRST means the first parameter should recieve the "this" pointer. Here's one of the functions that I'm registering:
static D3DXVECTOR3& DivAssignWithFloat/* operator /= */ ( FLOAT f, D3DXVECTOR3 &dest/*acts like the this pointer*/ )
{
dest /= f;
return dest;
}
RegisterObjectBehaviour() is similar. This will allow us to register constructors and destructors. The first parameter is the type. The second is the behavior that you're defining. The third one is the AngelScript function declaration. The fourth is the C++ function pointer, and the final one is the calling convention. Use
RegisterObjectProperty() to register member variables.
There's one last thing I want to say about this type. Notice that when registering operators, special names have to be used. For a list of the operators, you can check the
AngelScript manual.
Registering Global Functions and Namespaces
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 ∈, 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 ∈, D3DXVECTOR3 ∈ )", 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;
}
Registering Global Properties(variables)
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;
}
Loading and Executing a Script
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;
}
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.
// THIS IS NOT C++. This is AngelScript
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;
}
When all of these changes have been made, the program will run exactly the same with the exception that it's now using C++ and AngelScript. Changes can now be made by altering the AngelScript file.
Conclusion
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.
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:
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. The project is a Visual Studio 2010 solution.
NOTE: This article was originally posted on the Squared'D Programming Blog with some modifications.
http://squaredprogramming.blogspot.com/2013/12/programming-by-example-adding.html
Article Update Log
27 Dec 2013: Initial release
Why would one want to write platform-dependent(Windows-only) code?