TUTORIAL 29 - Rotating Doors!
by Valkyrie
Anyone who's played a realism mod knows what it's like to have swinging doors in the maps instead of
your average style sliding door, right? Would it be right if you are trying to invade a large
mansion with big double doors and brass doorknobs that SLIDE open? While at first it might trigger
some humour in the players, it certainly wouldn't look right. Since Quake 3 did not originally have
swinging doors to begin with, we're gonna have to put it in ourselves. Unfortunately, many players
often take rotating doors for granted, but oh, (going opera here!) just how they would feel if they
know the dreaded truth of how the hard-working coders manage to get this
not-so-difficult-but-long-and-tedious task done! =D
To be honest, however, all we have to do is to duplicate each and every little bit of code that
contributes to the sliding door code, with the exception of the function that triggers the door
open, and to also make some new definitions that go along with it. Then we change this new
duplicate so that it manipulates the door's angle instead of the position. This tutorial shows you
the most basic of rotating doors, and they will open the same way as you would a sliding door.
Anyways, enough of my ranting. On with the doors!
This modification is all server side, so that's another good thing to know.
You will be modifying the following files:
g_local.h
g_combat.c
g_spawn.c
g_mover.c - major beyond belief
There's also a sample map (including the un-compiled map source) so you can test
that the code changes work, and see a functional map in Q3Radiant. Download link at the end
of the tutorial.
1. SETTING UP
I'll try to make this as easy as possible. We'll start around line 41 in file
g_local.h. Add the following code in.
Be sure to include the comma that is now present after the MOVER_2TO1 enum.
// movers are things like doors, plats, buttons, etc
typedef enum {
MOVER_POS1,
MOVER_POS2,
MOVER_1TO2,
MOVER_2TO1,
// VALKYRIE: angle movements
ROTATOR_POS1,
ROTATOR_POS2,
ROTATOR_1TO2,
ROTATOR_2TO1
} moverState_t;
What I did here is make 4 new additions to the moverState_t enumeration so that the game will
recognize this as the new rotating door code, and will treat it accordingly.
Let's go to around line 155, inside the gentity_s structure. Add the following line in.
// timing variables
float wait;
float random;
gitem_t *item; // for bonus items
qboolean botDelayBegin;
float distance; // VALKYRIE: for rotating door
};
What we've done here is setup a new variable for the gentity_s structure that will tell the
game how many degrees the door will open before it is considered fully open.
This finishes g_local.h, and we are now ready to move into the
other parts of this tutorial.
2. PREPARING THE SPAWN FUNCTION
Starting at around line 96 in g_spawn.c, add the following line of code
to the list.
field_t fields[] = {
{"classname", FOFS(classname), F_LSTRING},
{"origin", FOFS(s.origin), F_VECTOR},
{"model", FOFS(model), F_LSTRING},
{"model2", FOFS(model2), F_LSTRING},
{"spawnflags", FOFS(spawnflags), F_INT},
{"speed", FOFS(speed), F_FLOAT},
{"target", FOFS(target), F_LSTRING},
{"targetname", FOFS(targetname), F_LSTRING},
{"message", FOFS(message), F_LSTRING},
{"team", FOFS(team), F_LSTRING},
{"wait", FOFS(wait), F_FLOAT},
{"random", FOFS(random), F_FLOAT},
{"count", FOFS(count), F_INT},
{"health", FOFS(health), F_INT},
{"light", 0, F_IGNORE},
{"dmg", FOFS(damage), F_INT},
{"angles", FOFS(s.angles), F_VECTOR},
{"angle", FOFS(s.angles), F_ANGLEHACK},
{"distance", FOFS(distance), F_FLOAT}, // VALKYRIE: for rotating doors
{NULL}
};
What we did here is add the previously declared variable distance to the list of fields that will
be initialized by any object that exists in a map. When the mapper creates the door, he will give the
keyword, "distance" a numerical value that will be passed on to distance (I hope you're
still with me). If this is missing, then distance will always be zero, because there's nothing
telling the spawn function (which we will add later) what the value of distance should be. Later, we
will also check that if distance is NULL or zero, we will force a default value of 90 degrees.
Go further down, around line 169, and add the following line in.
void SP_team_CTF_redplayer( gentity_t *ent );
void SP_team_CTF_blueplayer( gentity_t *ent );
void SP_team_CTF_redspawn( gentity_t *ent );
void SP_team_CTF_bluespawn( gentity_t *ent );
void SP_func_door_rotating( gentity_t *ent ); // VALKYRIE: for rotating doors
This will allow the function to be called anywhere in the file after it, so that later it can call
the real function that is located in another file. This is the similar to adding a function definition
into g_local.h, but it is limited only to the file which defined it.
Go further down once more, to around line 242, and add the following line.
{"team_CTF_redplayer", SP_team_CTF_redplayer},
{"team_CTF_blueplayer", SP_team_CTF_blueplayer},
{"team_CTF_redspawn", SP_team_CTF_redspawn},
{"team_CTF_bluespawn", SP_team_CTF_bluespawn},
{"func_door_rotating", SP_func_door_rotating}, // VALKYRIE: for rotating doors
{0, 0}
};
The above line is what will actually call our rotating door function should the current map contain
func_door_rotating entities. Without this line, our rotating doors would not have a spawn function to call.
Now, at around line 469 in g_combat.c, inside the G_Damage function
add the following modifications. This will allow us to shoot open rotating doors as well as normal doors.
Notice the extra pair of parentheses I've placed into the condition. This tells the game that in order for
the entire condition to pass, targ->use must be valid, and targ->moverState has to be
either MOVER_POS1, or ROTATOR_POS1.
// shootable doors / buttons don't actually have any health
if ( targ->s.eType == ET_MOVER ) {
if ( targ->use && (targ->moverState == MOVER_POS1
|| targ->moverState == ROTATOR_POS1) ) {
targ->use( targ, inflictor, attacker );
}
return;
}
This finishes g_spawn.c and g_combat.c,
and we are now ready to move into the more difficult parts of this tutorial.
3. ADDING THE SPAWN FUNCTION
At around line 869 of g_mover.c, after the SP_func_door function, add this function in. This is the
function that spawns the door on the map and initializes everything else that pertains to the
door.The commented QUAKED definition is what the mappers will use to create the door in their
maps. More on that at the end.
/*QUAKED func_door_rotating (0 .5 .8) START_OPEN CRUSHER REVERSE TOGGLE X_AXIS Y_AXIS
This is the rotating door... just as the name suggests it's a door that rotates
START_OPEN the door to moves to its destination when spawned, and operate in reverse.
REVERSE if you want the door to open in the other direction, use this switch.
TOGGLE wait in both the start and end states for a trigger event.
X_AXIS open on the X-axis instead of the Z-axis
Y_AXIS open on the Y-axis instead of the Z-axis
You need to have an origin brush as part of this entity. The center of that brush will be
the point around which it is rotated. It will rotate around the Z axis by default. You can
check either the X_AXIS or Y_AXIS box to change that.
"model2" .md3 model to also draw
"distance" how many degrees the door will open
"speed" how fast the door will open (degrees/second)
"color" constantLight color
"light" constantLight radius
*/
void SP_func_door_rotating ( gentity_t *ent ) {
ent->sound1to2 = ent->sound2to1 = G_SoundIndex("sound/movers/doors/dr1_strt.wav");
ent->soundPos1 = ent->soundPos2 = G_SoundIndex("sound/movers/doors/dr1_end.wav");
ent->blocked = Blocked_Door;
// default speed of 120
if (!ent->speed)
ent->speed = 120;
// if speed is negative, positize it and add reverse flag
if ( ent->speed < 0 ) {
ent->speed *= -1;
ent->spawnflags |= 8;
}
// default of 2 seconds
if (!ent->wait)
ent->wait = 2;
ent->wait *= 1000;
// set the axis of rotation
VectorClear( ent->movedir );
VectorClear( ent->s.angles );
if ( ent->spawnflags & 32 ) {
ent->movedir[2] = 1.0;
} else if ( ent->spawnflags & 64 ) {
ent->movedir[0] = 1.0;
} else {
ent->movedir[1] = 1.0;
}
// reverse direction if necessary
if ( ent->spawnflags & 8 )
VectorNegate ( ent->movedir, ent->movedir );
// default distance of 90 degrees. This is something the mapper should not
// leave out, so we'll tell him if he does.
if ( !ent->distance ) {
G_Printf("%s at %s with no distance set.\n",
ent->classname, vtos(ent->s.origin));
ent->distance = 90.0;
}
VectorCopy( ent->s.angles, ent->pos1 );
trap_SetBrushModel( ent, ent->model );
VectorMA ( ent->pos1, ent->distance, ent->movedir, ent->pos2 );
// if "start_open", reverse position 1 and 2
if ( ent->spawnflags & 1 ) {
vec3_t temp;
VectorCopy( ent->pos2, temp );
VectorCopy( ent->s.angles, ent->pos2 );
VectorCopy( temp, ent->pos1 );
VectorNegate ( ent->movedir, ent->movedir );
}
// set origin
VectorCopy( ent->s.origin, ent->s.pos.trBase );
VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin );
InitRotator( ent );
ent->nextthink = level.time + FRAMETIME;
if ( ! (ent->flags & FL_TEAMSLAVE ) ) {
int health;
G_SpawnInt( "health", "0", &health );
if ( health ) {
ent->takedamage = qtrue;
}
if ( ent->targetname || health ) {
// non touch/shoot doors
ent->think = Think_MatchTeam;
} else {
ent->think = Think_SpawnNewDoorTrigger;
}
}
}
So... What exactly does this function do? Starting from the top, we have what makes the door sounds.
Followed by that is the function call for a door that is blocked (by another entity, like a foolish player).
Next, we have a condition that sets a default speed of 120 deg/s should the mapper leave "speed" NULL
or zero. Mappers who do not use our QUAKED statement and enter a negative value for speed to state
opposite direction will be in for a surprise. (If you do not understand the following, don't worry about
it.) Since the duration of rotation for the door is the delta*1000/speed, if your
speed is negative, then your duration will be negative. To keep as many values as possible to be positive to avoid
troubles, we'll correct that now by positizing it and also adding our REVERSE spawnflag.
After that we have our usual 2 second delay on the door after it is opened before closing.
Those who think a negative speed will suffice, you are thinking velocity. Don't get them mixed up!
Here, we have our initializations for our new vectors that will be used for our rotating doors.
movedir is the variable which determines the direction in which our door will turn. It will
consist of one of the 3 axes, and will either be moving forward or backward. After that we have our
condition which checks that if the REVERSE flag was true, then it will negate the vector, movedir,
so that the door will move in the opposite direction.
VectorNegate and VectorInverse are entirely different calculations in the real world and
will produce different results! Do not get these two mixed up! Whoever coded VectorInverse for Q3A
never got around to finishing it, and you do not find it called anywhere in the source, so you should not use
it for the rotating doors either. Always use VectorNegate to completely reverse a vector.
Now we have distance check to make sure that the mapper gave "distance" a value and that the
value is not NULL or zero. If the mapper really doesn't want to use the REVERSE flag, you can specify a
negative distance here, although I've not tried the results of doing so, but I assume that it would
work fine anyway. The remaining parts of the function should be self-explainatory, with the exception of
InitRotator, which is our version of InitMover, the former being designed for rotating doors.
Phew... That finishes the function, but you're not done yet! Grab a glass of water and some music to
listen to. You're only halfway. =D
4. MAKING IT HAPPEN
From here on, it should be fairly simple. All we're doing now is a full duplicate of everything else that
pertains to the func_door code, except that it will be made to work for rotating doors by manipulating
the angle instead of the position (for sliding doors).
Experienced programmers may find the following excessive and not the most optimal, but I did it this way
so that inexperienced programmers do not get confused. Feel free to optimize the code in anyway you
want if you understand what you are doing.
Around line 638, right after the InitMover function, place our function, the InitRotator in.
This function initializes the entity and sets it up for operation.
/*
================
InitRotator
"pos1", "pos2", and "speed" should be set before calling,
so the movement delta can be calculated
================
*/
void InitRotator( gentity_t *ent ) {
vec3_t move;
float angle;
float light;
vec3_t color;
qboolean lightSet, colorSet;
char *sound;
// if the "model2" key is set, use a seperate model
// for drawing, but clip against the brushes
if ( ent->model2 ) {
ent->s.modelindex2 = G_ModelIndex( ent->model2 );
}
// if the "loopsound" key is set, use a constant looping sound when moving
if ( G_SpawnString( "noise", "100", &sound ) ) {
ent->s.loopSound = G_SoundIndex( sound );
}
// if the "color" or "light" keys are set, setup constantLight
lightSet = G_SpawnFloat( "light", "100", &light );
colorSet = G_SpawnVector( "color", "1 1 1", color );
if ( lightSet || colorSet ) {
int r, g, b, i;
r = color[0] * 255;
if ( r > 255 ) {
r = 255;
}
g = color[1] * 255;
if ( g > 255 ) {
g = 255;
}
b = color[2] * 255;
if ( b > 255 ) {
b = 255;
}
i = light / 4;
if ( i > 255 ) {
i = 255;
}
ent->s.constantLight = r | ( g << 8 ) | ( b << 16 ) | ( i << 24 );
}
ent->use = Use_BinaryMover;
ent->reached = Reached_BinaryMover;
ent->moverState = ROTATOR_POS1;
ent->r.svFlags = SVF_USE_CURRENT_ORIGIN;
ent->s.eType = ET_MOVER;
VectorCopy( ent->pos1, ent->r.currentAngles );
trap_LinkEntity (ent);
ent->s.apos.trType = TR_STATIONARY;
VectorCopy( ent->pos1, ent->s.apos.trBase );
// calculate time to reach second position from speed
VectorSubtract( ent->pos2, ent->pos1, move );
angle = VectorLength( move );
if ( ! ent->speed ) {
ent->speed = 120;
}
VectorScale( move, ent->speed, ent->s.apos.trDelta );
ent->s.apos.trDuration = angle * 1000 / ent->speed;
if ( ent->s.apos.trDuration <= 0 ) {
ent->s.apos.trDuration = 1;
}
}
At around line 557, inside the Use_BinaryMover function, add the following.
Depending on the current moverState, this function will set the corresponding
action for our door. It will either start the movement of the door if it is already closed,
or it will delay the door from closing if already opened.
// only partway up before reversing
if ( ent->moverState == MOVER_1TO2 ) {
total = ent->s.pos.trDuration;
partial = level.time - ent->s.time;
if ( partial > total ) {
partial = total;
}
MatchTeam( ent, MOVER_2TO1, level.time - ( total - partial ) );
if ( ent->sound2to1 ) {
G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 );
}
return;
}
if ( ent->moverState == ROTATOR_POS1 ) {
// start moving 50 msec later, becase if this was player
// triggered, level.time hasn't been advanced yet
MatchTeam( ent, ROTATOR_1TO2, level.time + 50 );
// starting sound
if ( ent->sound1to2 ) {
G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 );
}
// looping sound
ent->s.loopSound = ent->soundLoop;
// open areaportal
if ( ent->teammaster == ent || !ent->teammaster ) {
trap_AdjustAreaPortalState( ent, qtrue );
}
return;
}
// if all the way up, just delay before coming down
if ( ent->moverState == ROTATOR_POS2 ) {
ent->nextthink = level.time + ent->wait;
return;
}
// only partway down before reversing
if ( ent->moverState == ROTATOR_2TO1 ) {
total = ent->s.apos.trDuration;
partial = level.time - ent->s.time;
if ( partial > total ) {
partial = total;
}
MatchTeam( ent, ROTATOR_1TO2, level.time - ( total - partial ) );
if ( ent->sound1to2 ) {
G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 );
}
return;
}
// only partway up before reversing
if ( ent->moverState == ROTATOR_1TO2 ) {
total = ent->s.apos.trDuration;
partial = level.time - ent->s.time;
if ( partial > total ) {
partial = total;
}
MatchTeam( ent, ROTATOR_2TO1, level.time - ( total - partial ) );
if ( ent->sound2to1 ) {
G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 );
}
return;
}
}
At around line 477, inside the function Reached_BinaryMover, add the following.
This function tells the door to stop moving after it has reached its point, and depending on the
current moverState, it will either set the door to fully open, or fully closed.
} else if ( ent->moverState == MOVER_2TO1 ) {
// reached pos1
SetMoverState( ent, MOVER_POS1, level.time );
// play sound
if ( ent->soundPos1 ) {
G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos1 );
}
// close areaportals
if ( ent->teammaster == ent || !ent->teammaster ) {
trap_AdjustAreaPortalState( ent, qfalse );
}
} else if ( ent->moverState == ROTATOR_1TO2 ) {
// reached pos2
SetMoverState( ent, ROTATOR_POS2, level.time );
// play sound
if ( ent->soundPos2 ) {
G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos2 );
}
// return to apos1 after a delay
ent->think = ReturnToApos1;
ent->nextthink = level.time + ent->wait;
// fire targets
if ( !ent->activator ) {
ent->activator = ent;
}
G_UseTargets( ent, ent->activator );
} else if ( ent->moverState == ROTATOR_2TO1 ) {
// reached pos1
SetMoverState( ent, ROTATOR_POS1, level.time );
// play sound
if ( ent->soundPos1 ) {
G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos1 );
}
// close areaportals
if ( ent->teammaster == ent || !ent->teammaster ) {
trap_AdjustAreaPortalState( ent, qfalse );
}
} else {
G_Error( "Reached_BinaryMover: bad moverState" );
}
}
At line 435, under the ReturnToPos1 function, we'll add our version of the that function.
Since the Q3 version will trigger the position of the door instead of the angle, we need to
make one that will trigger our rotating door instead.
/*
================
ReturnToApos1
================
*/
void ReturnToApos1( gentity_t *ent ) {
MatchTeam( ent, ROTATOR_2TO1, level.time );
// looping sound
ent->s.loopSound = ent->soundLoop;
// starting sound
if ( ent->sound2to1 ) {
G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 );
}
}
At around line 370, add this piece of code inside the SetMoverState function.
This function handles setting the state of any door in Q3, meaning this is the function
that tells the doors what to actually do. Our version of the code is made for setting the
angle of the door instead of the position.
vec3_t delta;
float f;
ent->moverState = moverState;
ent->s.pos.trTime = time;
ent->s.apos.trTime = time;
switch( moverState ) {
case MOVER_POS1:
VectorCopy( ent->pos1, ent->s.pos.trBase );
ent->s.pos.trType = TR_STATIONARY;
break;
At around line 395, add this piece of code in.
case MOVER_2TO1:
VectorCopy( ent->pos2, ent->s.pos.trBase );
VectorSubtract( ent->pos1, ent->pos2, delta );
f = 1000.0 / ent->s.pos.trDuration;
VectorScale( delta, f, ent->s.pos.trDelta );
ent->s.pos.trType = TR_LINEAR_STOP;
break;
case ROTATOR_POS1:
VectorCopy( ent->pos1, ent->s.apos.trBase );
ent->s.apos.trType = TR_STATIONARY;
break;
case ROTATOR_POS2:
VectorCopy( ent->pos2, ent->s.apos.trBase );
ent->s.apos.trType = TR_STATIONARY;
break;
case ROTATOR_1TO2:
VectorCopy( ent->pos1, ent->s.apos.trBase );
VectorSubtract( ent->pos2, ent->pos1, delta );
f = 1000.0 / ent->s.apos.trDuration;
VectorScale( delta, f, ent->s.apos.trDelta );
ent->s.apos.trType = TR_LINEAR_STOP;
break;
case ROTATOR_2TO1:
VectorCopy( ent->pos2, ent->s.apos.trBase );
VectorSubtract( ent->pos1, ent->pos2, delta );
f = 1000.0 / ent->s.apos.trDuration;
VectorScale( delta, f, ent->s.apos.trDelta );
ent->s.apos.trType = TR_LINEAR_STOP;
break;
}
BG_EvaluateTrajectory( &ent->s.pos, level.time, ent->r.currentOrigin );
BG_EvaluateTrajectory( &ent->s.apos, level.time, ent->r.currentAngles );
trap_LinkEntity( ent );
}
At around line 325, inside the G_MoverTeam function, add the following condition.
The original condition considers only the position of the door, so we have to make
our own condition that considers only the angle of the door to make it even.
// the move succeeded
for ( part = ent ; part ; part = part->teamchain ) {
// call the reached function if time is at or past end point
if ( part->s.pos.trType == TR_LINEAR_STOP ) {
if ( level.time >= part->s.pos.trTime + part->s.pos.trDuration ) {
if ( part->reached ) {
part->reached( part );
}
}
}
if ( part->s.apos.trType == TR_LINEAR_STOP ) {
if ( level.time >= part->s.apos.trTime + part->s.apos.trDuration ) {
if ( part->reached ) {
part->reached( part );
}
}
}
}
}
And finally, at around line 928, inside the Touch_DoorTrigger function, add the following lines in.
This function is the actual function that check whether or not a client is standing within range of the door,
so that it will open. We will simply modify that condition so that it will check for our rotating doors as well
as standard Q3 doors.
/*
================
Touch_DoorTrigger
================
*/
void Touch_DoorTrigger( gentity_t *ent, gentity_t *other, trace_t *trace ) {
if ( other->client && other->client->sess.sessionTeam == TEAM_SPECTATOR ) {
// if the door is not open and not opening
if ( ent->parent->moverState != MOVER_1TO2 &&
ent->parent->moverState != MOVER_POS2 &&
ent->parent->moverState != ROTATOR_1TO2 &&
ent->parent->moverState != ROTATOR_POS2 ) {
Touch_DoorTriggerSpectator( ent, other, trace );
}
}
else if ( ent->parent->moverState != MOVER_1TO2 &&
ent->parent->moverState != ROTATOR_1TO2 ) {
Use_BinaryMover( ent->parent, ent, other );
}
}
*Sigh...* Code wise, we're finally done. Just one thing left to explain, and that is the QUAKED statement
for the mappers to use.
/*QUAKED func_door_rotating (0 .5 .8) START_OPEN CRUSHER REVERSE TOGGLE X_AXIS Y_AXIS
This is the rotating door... just as the name suggests it's a door that rotates
START_OPEN the door to moves to its destination when spawned, and operate in reverse.
REVERSE if you want the door to open in the other direction, use this switch.
TOGGLE wait in both the start and end states for a trigger event.
X_AXIS open on the X-axis instead of the Z-axis
Y_AXIS open on the Y-axis instead of the Z-axis
You need to have an origin brush as part of this entity. The center of that brush will be
the point around which it is rotated. It will rotate around the Z axis by default. You can
check either the X_AXIS or Y_AXIS box to change that.
"model2" .md3 model to also draw
"distance" how many degrees the door will open
"speed" how fast the door will open (degrees/second)
"color" constantLight color
"light" constantLight radius
*/
Your mapper *should* know how to deal with this, but the programmer should also have a little insight on
this as well. This is an entity definition for Q3Radiant. When added into the file, entities.def (via a text editor)
, this will appear as an option available to the mapper when adding objects into the map, such as spawnpoints,
weapons, and in this case, doors. The first line is very important as that tells the function in our
actual code what spawnflags will be used for this particular object. The order of objects in the first
line must not be altered in anyway, as that will cause instructional problems between the map's options
and the code's functions.
The remaining lines in the statement gives the mapper extra information on what to add to the object
to actually make it work. Should the mapper forget some required values, such as the speed, we have
placed a condition in our code so that a default value is used if this happens.
Well, I guess that's it. Compile this code and use it on a map that has rotating doors!
Thanks to Ro4dDogG, we have a package containing both
compiled and
pre-compiled forms of the test map available for download. Follow the instructions
included in the archive for installing and running the map.
Addendum: The one error I've found is that the rotating doors do not light properly if the mapper does not
use "-light -extra" switches. Please keep that in mind when you are testing your maps.
Doors, when blocked during closing/opening, will start again from the opposite end of its opening direction.
This also applies to sliding doors, and can be noticed if the door is moving slow enough. This is due to miscoding in
original Q3 source. Maybe someday I will work up a solution to this.
|