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

No comments:

Post a Comment