se1:Adding New Controls (Ex: Secondary Fire)

From SeriousEngine.com

Jump to: navigation, search

Author: Michael "[SF]Entroper" Bennett
Email: entroper@seriousfortress.com
Web site: http://www.seriousfortress.com/
Last updated March 23, 2001

This tutorial is meant as a guideline to help you add another control to Serious Sam. It assumes basic familiarity with Croteam's scripting language (only to the point that you can read a .es file, and understand what a class declaration looks like, the difference between a function and a procedure, etc.). It also assumes a fair amount of proficiency with C++, which you probably have if you're working on the code for a Serious Sam mod. This tutorial will use secondary fire as an example and hopefully will explain some important things along the way regarding how the other controls work. The only files you will need to edit to make the game recognize the control will be Player.es and one of the .ctl files in the Controls folder. However, in order to make the control do something, secondary fire requires the Player class to send an event to the PlayerWeapons class (in PlayerWeapons.es). I'll walk you through how to handle the event as well, but writing the firing procedure is up to you.

The first thing you'll need to find is struct PlayerControls. It's loaded up with BOOLs and arrays of BOOLs. Go ahead and add your own BOOL bFireSecondary; after bFire or toward the end of the struct definition, depending on whether you want to keep the controls grouped by type or keep your controls separate from the preexisting ones. Now you need to declare a console variable, and bind it to the variable you just declared. This is done in CPlayer_OnInitClass(). The code will look something like this:


_pShell->DeclareSymbol("user INDEX ctl_bFireSecondary;", &pctlCurrent.bFireSecondary);


BOOLean variables don't really exist in the console, and the engine treats them all as integers anyway. What this statement does is define the console variable ctl_bFireSecondary, and sets the address of the variable to the address of pctlCurrent.bFireSecondary, which we just declared above. The .ctl files deal with console variables, and these bindings allow console commands to directly modify the variables in pctlCurrent. I do hope that makes sense.

This is a good time to go ahead and open up your Controls.ctl, and add the secondary fire button. I chose Mouse Button 2, and made Use/Computer the 'E' key (I'm used to Half-Life). Add the following lines to the file:

Button
 Name: TTRS Secondary Fire
 Key 1: Mouse Button 2
 Key 2: None
 Pressed: ctl_bFireSecondary = 1;
 Released: ctl_bFireSecondary = 0;

The first line gives the name of the control, and the next two lines are the two buttons that will activate it. The following line is the console command to execute when the button is pressed, and the last line is the console command to execute when the button is released. Back to the code.

Next, you'll need to find ctl_ComposeActionPacket(). This reads from pctlCurrent, and as you might have guessed, composes a packet of actions for the player to take. These packets are sent to the clients, who interpret them and perform the proper actions on the player entities. We don't need to worry about the network stuff though; we just need to get the packet composed, and later tell the player what to do with it. So let's get it composed then. Study that function for a moment, and just try to figure out what it's doing. The action packets consist of three vectors, representing the player's movement, and a bitfield containing flags for all the buttons he's pressed. If you were adding some sort of movement control, you'd simply code the movement behavior here to modify the translation and rotation vectors, and the engine would take care of it when it reads the packet. </span>We have to use the button flags for firing though. You'll need to #define a new button flag. Do a search for "#define PLACT_FIRE", and you'll see where all the button flags are #defined. These are sorted in order of most often pressed, so that the bits that are turned on stay closer to the vectors in the packet. This way, you usually get a bunch of zeroes in a row, and the packet compresses better. If you have no idea what I'm talking about, that's ok. Just make sure, if you add new button flags, to put them in in the order of how often they're pressed. I placed PLACT_FIRE_SECONDARY right after PLACT_FIRE. The code looks like this:


#define PLACT_FIRE                (1L<<0)
#define PLACT_FIRE_SECONDARY      (1L<<1) //Mike Bennett, Feb 25 2001
#define PLACT_RELOAD              (1L<<2)
#define PLACT_WEAPON_NEXT         (1L<<3) 
//etc.
#define PLACT_SELECT_WEAPON_SHIFT (10)


If you understand bit shifting, then all of these #defines should make sense. Now let's get back to composing the action packet. Find the lines that read similar to this:


if(pctlCurrent.bFire)  paAction.pa_ulButtons |= PLACT_FIRE;


That turns on the bit for firing the weapon if the fire button is being pressed. Add a line for secondary fire, like so:


if(pctlCurrent.bFireSecondary)  paAction.pa_ulButtons |= PLACT_FIRE_SECONDARY;


With that added, we're almost done with Player.es. The last thing we need to do is make the Player class read the secondary fire button from the action packet and send an event to the PlayerWeapons class.

The ApplyAction() function reads from an action packet and performs all the actions on a Player. It saves the previous button presses, so it knows when a button is first pressed, and when it's first released. It stores these bits in ulNewButtons and ulReleasedButtons, and ulButtonsNow stores which buttons are being pressed at the current moment. We don't need to actually edit ApplyAction(). It calls AliveActions(), which calls ButtonsActions(), which is the function we want. Find ButtonsActions(), and find the code that looks like this:


// if fire is pressed
if (ulNewButtons&PLACT_FIRE) {
  ((CPlayerWeapons&)*m_penWeapons).SendEvent(EFireWeapon());
}


That mess of boolean operators, typecasting, dereferencing, and so forth accomplishes sending the EFireWeapon event to the PlayerWeapons class when the fire button is first pressed. Add your own mess of boolean operators, typecasting, dereferencing, and so forth, replacing PLACT_FIRE with PLACT_FIRE_SECONDARY and EFireWeapon() with EFireSecondary(). (It's really not that messy. I'm just picking on C++, not Croteam's programmers, whose code is exceptionally readable.  : ) You also need to send the EReleaseSecondary event when the button is released. The code is a few lines down and looks very similar, except with ulReleasedButtons and EReleaseWeapon. Copy that block of code and replace the flag and the event. We're now all done with Player.es, and can move onto handling (and in fact, defining) the two new events in PlayerWeapons.es.

First let's define the event types. You'll find (in PlayerWeapons.es) the event types EFireWeapon and EReleaseWeapon already defined. They're empty events because they don't need to transmit any data, just a notification that the event is taking place. The secondary fire and release events don't need to transmit data either, so just declare them similarly below the existing events, like so:


// fire weapon
event EFireWeapon {};
// release weapon
event EReleaseWeapon {};
// reload weapon
event EReloadWeapon {};
//secondary fire weapon
event EFireSecondary {};
//release weapon secondary fire
event EReleaseSecondary {};


When the PlayerWeapons class is initialized, it calls the Main() procedure, which is at the very end of the file. Main() initializes some stuff, then calls Idle() from its wait loop. Any events that aren't caught by Idle() (or the procedures Idle() calls or jumps to) will fall down the stack and be caught by Main(), if Main() has an event handler for it. It's sort of like throwing an exception, except that the game doesn't abort if there is no handler, it just ignores the event. </span>Anyway, you'll notice that the wait loop in Main() handles EFireWeapon and EReleaseWeapon by turning on and off the m_bFireWeapon flag. Why doesn't it just call Fire()? </span>Well, if another procedure is doing something important, like reloading your colts, that procedure would be interrupted if Main() called another one below it. So when Reload() is finished, it returns to the wait loop in Idle(), and Idle() notices that m_bFireWeapon is set and jumps to Fire(). This is exactly what needs to happen for handling EFireSecondary and EReleaseSecondary events. You can pretty much mimic the code, replacing Fire with FireSecondary, and then you'll need to write a FireSecondary procedure. This is where I leave you to make your secondary fire do whatever you want. A few notes though, about the procedure. It'll probably look like a switch, with cases for each weapon you will implement a secondary fire for. You'll have to check secondary ammo for the weapons, and exit the procedure if there's no ammo left. It'll look different from the Fire() procedure, since you don't want to switch weapons if you're out of secondary ammo. Maybe you also want Fire() not to switch your weapon if you've run out of primary ammo, and have secondary ammo left. It's up to you.

Also, here's a problem I ran into: I implemented a select fire for one of my weapons, which changed how many shots were fired at a time. All FireSecondary() had to do was set a variable and jump back to Idle(). So it did that, and m_bFireSecondary was still set, so it jumped back to FireSecondary(). Which jumped back to Idle(). Which jumped back.... you get the idea. When I pressed the right mouse button, I got a stack overflow immediately. So why doesn't this happen when you fire other weapons, since it's implemented in the same way? The answer is that every other weapon, when it is fired, has a wait loop somewhere. The procedure will execute away until it hits the wait loop, and then it is able to receive events and pass them down the stack if it doesn't know what to do with them. Main() never received my EReleaseSecondary event because the procedures simply kept executing away, jumping back and forth, never hitting a wait loop. I fixed the problem by turning off the m_bFireSecondary flag at the end of the FireSecondary() procedure. When the procedure returns, you won't be firing anymore, so it's safe to just turn the flag off.

I hope you found this tutorial helpful. If you have any questions or helpful comments, please contact me at entroper@seriousfortress.com. You may freely distribute this tutorial in its entirety.

Personal tools