Tutorial 2 for SDK 1.0

Tutorial 2 for SDK 1.0
Buttons, Impulses, and Commands
by Trevor Hogan

User Input

There are four types of user inputs in Doom 3: core commands (this is a general catch all term), buttons, impulses, and custom commands. The engine handles core commands internally and gives the SDK a secondary interface. For example, when you walk forward the engine handles the relevant user input and passes along an intensity value for each local direction (local to the player). This sounds pretty strange since walking doesn't really have an intensity value but stop and consider mousemove for a second; the slower you move the mouse the slower you should walk. The important thing to note is that you cannot poll the engine for the status of the walk forward key on your keyboard. You can only determine the intensity value which makes it difficult to tell if the player is using the keyboard or the mouse to move without using hacks like reading the state of the mousemove button.

Alright, let's get started so take a look at the user input structure in framework/UsercmdGen.h. This is the guts of the user input system.

class usercmd_t {
public:
      int         gameFrame;                                // frame number
      int         gameTime;                                 // game time
      int         duplicateCount;                           // duplication count for networking
      byte        buttons;                                  // buttons
      signed char forwardmove;                              // forward/backward movement
      signed char rightmove;                                // left/right movement
      signed char upmove;                                   // up/down movement
      short       angles[3];                                // view angles
      short       mx;                                       // mouse delta x
      short       my;                                       // mouse delta y
      signed char impulse;                                  // impulse command
      byte        flags;                                    // additional flags
      int         sequence;                                 // just for debugging

Note that the movement intensities are stored in forwardmove, rightmove, and upmove. There's also a variable for button state and another for impulse state but nothing for custom commands. This is because custom commands are handled elsewhere.

Before I move on I should mention that buttons and impulses are also (mis)handled by the engine before being passed along to your mod's code. At least there's an upside - unlike movement commands, buttons and impulses are left in a much more usable state. However, you should still be aware of the engine's penchant for meddling (more on this later).

Buttons

You may have noticed that the buttons variable in usercmd_t (shown above) is stored in a single byte. This means that Doom 3 supports eight buttons in total and unfortunately five are already in use so you'll have to design your mod with a three button limitation in mind. Each button is represented as one bit in the buttons variable so each button can be either on or off and that's it. Since a new usercmd_t is generated each frame buttons are well suited for continuous user input. Open framework/UsercmdGen.h and take a look at the button definitions.

// usercmd_t->button bits
const int BUTTON_ATTACK             = BIT(0); // "_button0" or "_attack"
const int BUTTON_RUN                = BIT(1); // "_button1" or "_speed"
const int BUTTON_ZOOM               = BIT(2); // "_button2" or "_zoom"
const int BUTTON_SCORES             = BIT(3); // "_button3" or "_impulse19"
const int BUTTON_MLOOK              = BIT(4); // "_mlook"
const int BUTTON_5                  = BIT(5); // "_button5"
const int BUTTON_6                  = BIT(6); // "_button6"
const int BUTTON_7                  = BIT(7); // "_button7"

The comments are mine and show the pairing between each button and its console command(s). It looks like core commands, buttons, and impulses all begin with an underscore (e.g. "_forward"). Note that some buttons have multiple names that look like core commands and that one button even overlaps with an impulse (and that "_button4" doesn't seem to trigger BUTTON_MLOOK). Sigh.

Okay, if you've played through Doom 3 before you'll know that these actions are continuous actions. You can hold down the attack button and your gun will fire continously rather than firing just one bullet. Similarly running (sprinting), zooming, showing the scoreboard, and mouselook are also continuous actions. If you want to use a button in your mod, just bind a key to "_button5", "_button6", or "_button7" and check the player's buttons variable for BUTTON_5, BUTTON_6, or BUTTON_7 somewhere in your code. As an example, I bound a key to "_button5" and added this code to the end of idPlayer :: Think in game/Player.cpp.

if( usercmd.buttons & BUTTON_5 )
      common->Printf( "button 5 pressed!\n" );

image1

I only tapped the key once very quickly but the message showed up five times! This is because the player's Think function was executed multiple times while the key was down.

Why can't you just hijack one of the other buttons for your own purposes if you run out of buttons? Well, as I mentioned earlier the engine likes to meddle with things. For example, when the mouselook button is pressed the engine will adjust the forwardmove intensity according to your mouse's movement before passing along the button's state. If you search the SDK for BUTTON_MLOOK you'll discover that the mod code only centers the player's view when the mouselook button is pressed. Everything else is handled by the engine and cannot be disabled so the mouselook button will always be a mouselook button no matter what you do. I haven't researched the other buttons but I wouldn't be surprised if the engine gets involved with them as well.

Important! At the time of this writing button 7 is unusable due to a bug in the engine!

Impulses

An impulse is executed only once when the relevant key is pressed and is not updated continuously like a button. Weapon switch commands and weapon reloading are examples of impulses. Check framework/UsercmdGen.h for an incomplete list - even though impulses 0-29 and 40 are the only ones defined here, I've verified that impulses 30-39 and 41-61 also work. Impulses 62 and 63 don't generate an unknown command but don't seem to make it through the engine so they're probably off limits. As you can see there's a lot of room for growth! To add your own impulse, open game/Player.cpp and look for the PerformImpulse function.

case IMPULSE_40: {
      UseVehicle();
      break;
}
case 60: {
      common->Printf( "impulse 60!\n" );
      break;
}

image2

In this example I held the key down for several seconds but the message showed up only once. If you're making a multiplayer mod you should know that impulses are automatically replicated over the network so the above example will actually print "impulse 60!" on the server as well!

Custom Commands

Custom commands are like impulses except that they're not automatically replicated over the network in a multiplayer game and they can carry arguments. If you've worked with Half-Life before you'll know that console commands in Half-Life are automatically sent to the server for processing; this is not the case in Doom 3. You can use custom commands for client only functions or you can send a custom network message to the server (the "clientDropWeapon" custom command does this). I don't know why id didn't use an impulse for this action but hey, it leaves another impulse available for us to use! To implement your own custom command, open game/gamesys/SysCmds.cpp and scroll to the bottom of InitConsoleCommands.

// this is your new custom command
// the first argument is the name of the custom command (typed in the console)
// the second argument is a custom command function
// the third argument is a flag (see framework/CmdSystem.h)
// the fourth argument is a short description of the custom command

cmdSystem->AddCommand( "sayhello", myfunction, CMD_FL_GAME, "says hello." );

This tells the engine that you want myfunction to be called whenever the user types "sayhello" in the console (or presses a key bound to "sayhello"). Now you need to actually write myfunction so scroll up a few pages to the top of InitConsoleCommands and paste this code above the function body.

void myfunction( const idCmdArgs &args )
{
      common->Printf( "hello user!\n" );
}

Every custom command function takes the same argument, an idCmdArgs object which wraps the typed arguments in an easy to use format. Take a look at the "give" command (Cmd_Give_f) for an example of using arguments in your custom command function. See if you can write a custom command named "addone" that adds one to any number, so "addone 5" would print "6" to the console.

image3

As I said earlier custom commands are not sent over the network so this example will only print "hello user!" in the console of the user who typed the command.

October 30, 2004