Here's some handy information on weapon scripts which you should read first.
Weapons in Doom 3 are almost entirely controlled by weapon scripts so you'll need to understand how the SDK interfaces with scripts before you make any major changes. Weapon scripts have a lot of control over weapon attributes and behaviour so SDK intervention is usually not required but some changes like alternate firing modes do require editing the SDK.
It looks like scripting in the game code is handled by the idProgram object in idGameLocal but you won't usually access this object yourself. Instead, every entity in the game, including weapons, has an associated idScriptObject called scriptObject (see game/Entity.h). Weapon scripts are actually objects (classes) so this makes sense. The scriptObject is your entrypoint to the weapon script - you can access script functions and link variables to the script from here. Note that each player owns a single weapon object which is reused when the player switches weapons. This is unlike other games where each weapon tends to have a seperate object.
Other than the scriptObject weapons also have an idThread object which executes the currently running function in the weapon script. The path of execution is pretty convoluted so I'll summarize it here.
The engine calls idGameLocal :: RunFrame
which calls idPlayer :: Think
which calls idPlayer :: UpdateWeapon
which calls idWeapon :: PresentWeapon
which calls idWeapon :: UpdateScript
which calls thread->Execute( )
which executes the currently running script function
|
If you're confused you should know that Doom 3 is singly threaded so scripts are not run in parallel beside the game code. Whenever an idThread's Execute function is called it executes until the script exits by terminating or calling "waitFrame" or something else halts the thread. For example, the "weaponState" event calls thread->DoneProcessing( ) in the SDK which halts the thread. I'll explain this in more detail later.
There's a whole lot of stuff going on behind the scenes so I'll cover script variables first then weapon states and finally script events.
Script variables are extremely cool. You can associate an idScriptVariable with a string defining a variable name to be accessed through the script and then the variable can be accessed through both the SDK and the script itself. If you give the variable the same name in both the SDK and the script it's almost like the variable transcends into another dimension. Riiight. Anyway, there are five types of script variables: booleans, floats, ints, vectors, and strings. Take a look at the idWeapon class in game/Weapon.h.
// script control idScriptBool WEAPON_ATTACK; idScriptBool WEAPON_RELOAD; idScriptBool WEAPON_NETRELOAD; idScriptBool WEAPON_NETENDRELOAD; idScriptBool WEAPON_RAISEWEAPON; idScriptBool WEAPON_LOWERWEAPON; |
These are script variables. Let's take a look at how they're linked to a script. Open game/Weapon.cpp and find idWeapon :: GetWeaponDef. GetWeaponDef is called whenever the player switches weapons so it's kind of like a weapon constructor. It handles clearing out the previous weapon and loading the new weapon def and everything inside it (models, scripts, sounds, and more).
WEAPON_ATTACK.LinkTo( scriptObject, "WEAPON_ATTACK" ); WEAPON_RELOAD.LinkTo( scriptObject, "WEAPON_RELOAD" ); WEAPON_NETRELOAD.LinkTo( scriptObject, "WEAPON_NETRELOAD" ); WEAPON_NETENDRELOAD.LinkTo( scriptObject, "WEAPON_NETENDRELOAD" ); WEAPON_RAISEWEAPON.LinkTo( scriptObject, "WEAPON_RAISEWEAPON" ); WEAPON_LOWERWEAPON.LinkTo( scriptObject, "WEAPON_LOWERWEAPON" ); |
These variables have to be relinked every time the player switches weapons because the script object is reloaded (they're unlinked in idWeapon :: Clear in case you're worried). The first argument is the scriptObject to link the variable to and the second argument is the name the variable will be accessible through inside the script. Since these variables have the same name in both the SDK and the script, you can set WEAPON_ATTACK to true in the SDK and it automatically becomes true in the script as well. This is why weapon firing can be confusing; the idWeapon :: BeginAttack function doesn't actually fire the weapon, instead it sets WEAPON_ATTACK to true and lets the script handle the rest. The script detects the change in weapon_pistol :: Idle and changes the weapon's state to "Fire". Note that the pistol still hasn't actually been fired yet but weapon states are pretty confusing so I'll talk about them before continuing.
Every weapon has a state associated with it. Weapon states are actually script functions so a weapon in state "Fire" is currently executing the weapon script's Fire function. The ideal state (this is the state we want to move to) is stored in idWeapon :: idealState and is set through the "weaponState" event (more on events later). Just remember that the idealState variable stores the name of the function to be executed next or nothing if the currently executing function should be resumed.
Back to the weapon firing example, at this point the pistol's state has been changed to "Fire". The game code unloads the old thread and recreates it, this time starting in the weapon_pistol :: Fire function. This is important. When the weapon script calls "weaponState" it's actually terminating the current execution path and starting over again in a different function. So the weapon_pistol :: Fire function is now executed which calls launchProjectiles (an event) to actually fire the weapon. About time, eh?
There's a lot of stuff to remember here so I'll summarize a bit. Thread execution is performed in idWeapon :: UpdateScript. This function executes the current script function, checks to see if "weaponState" was called, and if it was it calls SetState which destroys the old thread and recreates it in the new state (i.e. the new script function). It does this until the script terminates without calling "weaponState" or until 10 iterations pass, whichever comes first. I don't know what would happen if you allowed your script function to terminate - after all, every script function in script/weapon_pistol.script ends with a call to "weaponState". Let's take a quick look at idWeapon :: SetState in game/Weapon.cpp.
func = scriptObject.GetFunction( statename );
if ( !func ) {
assert( 0 );
gameLocal.Error( "Can't find function '%s' in object '%s'", statename, scriptObject.GetTypeName() );
}
thread->CallFunction( this, func, true );
state = statename;
|
As you can see it searches the scriptObject for the function with the same name as the state. Then it calls thread->CallFunction which tells the thread to switch execution to the new function. It does NOT execute anything at this point, it just makes some changes to the thread's stack and performs some other boring interpreter crap. Can you believe that some people actually spend time studying these things (lexers, parsers, and interpreters)? Just kidding... not! Note that the third parameter is true which means the thread will clear its stack (check the function definition in game/script/Script_Thread.cpp). This removes the thread's history of function calls which makes it as if the thread was starting execution for the first time.
Another thing to remember is that Doom 3 isn't threaded so even though script execution is controlled with an idThread object it's not actually running as a thread.
You've seen how variables can be shared between the SDK and the weapon script. You've seen how the SDK can call and execute a script function. Now we'll look at how threads can call functions in the SDK; these are called script events. Open game/Weapon.cpp and take a look at the event definitions near the top of the file.
const idEventDef EV_Weapon_Clear( "<clear>" );
const idEventDef EV_Weapon_GetOwner( "getOwner", NULL, 'e' );
const idEventDef EV_Weapon_Next( "nextWeapon" );
const idEventDef EV_Weapon_State( "weaponState", "sd" );
// ... snip ...
CLASS_DECLARATION( idAnimatedEntity, idWeapon )
EVENT( EV_Weapon_Clear, idWeapon::Event_Clear )
EVENT( EV_Weapon_GetOwner, idWeapon::Event_GetOwner )
EVENT( EV_Weapon_State, idWeapon::Event_WeaponState )
|
Events are linked to the script through some extremely cryptic macros which I don't understand so I won't pretend to. Rest assured that at some point the script interpreter translates event calls in the script to function calls in the SDK.
The idEventDef constructor takes three arguments. The first is the event name as seen by the script. The second is a string representation of the function arguments. The third is a character representation of the function return value. Take a look at game/gamesys/Event.h for the character codes.
#define D_EVENT_VOID ( ( char )0 ) #define D_EVENT_INTEGER 'd' #define D_EVENT_FLOAT 'f' #define D_EVENT_VECTOR 'v' #define D_EVENT_STRING 's' #define D_EVENT_ENTITY 'e' #define D_EVENT_ENTITY_NULL 'E' // event can handle NULL entity pointers #define D_EVENT_TRACE 't' |
So the "getOwner" event takes no arguments and returns an entity. The "weaponState" event takes a string and an integer and returns nothing.
The CLASS_DECLARATION and EVENT macros seem to do the actual linking. In this case the EV_Weapon_State event is linked to the idWeapon :: Event_WeaponState function. When a script calls "weaponState" the idWeapon :: Event_WeaponState function is called. Let's take a look (it's in game/Weapon.cpp).
void idWeapon::Event_WeaponState( const char *statename, int blendFrames ) {
const function_t *func;
func = scriptObject.GetFunction( statename );
if ( !func ) {
assert( 0 );
gameLocal.Error( "Can't find function '%s' in object '%s'", statename, scriptObject.GetTypeName() );
}
idealState = statename;
animBlendFrames = blendFrames;
thread->DoneProcessing();
}
|
As I mentioned a long time ago the "weaponState" event actually terminates thread execution. You can see this here with the call to thread->DoneProcessing( ). The only other thing this function does is set idealState to a new state - it doesn't even switch execution to the new state. This is because thread execution is handled elsewhere, so let's tie this all together.
Scripts are executed by the UpdateScript function. If a script calls "weaponState" then the game sets idealState to the desired function name and terminates execution. Now the UpdateScript function loops and discovers that idealState is not empty so it calls SetState which restarts the thread at the beginning of the new function (the SetState function also clears idealState). It executes the new thread and if the script once again calls "weaponState" the process will repeat itself. Otherwise the thread will terminate normally (say by calling "waitFrame") and idealState will be clear so UpdateScript will return.
There are a few more subtleties with weapons but this tutorial has gone on long enough. At this point you should have a general idea of how weapons work even though I haven't talked about specific examples yet.
November 4, 2004