Code3Arena

PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 29 | Next >>

menu

  • Home/News
  • ModSource
  • Compiling
  • Help!!!
  • Submission
  • Contributors
  • Staff
  • Downloads

    Tutorials
    < Index >
    1. Mod making 101
    2. Up 'n running
    3. Hello, QWorld!
    4. Infinite Haste
    5. Armor Piercing Rails
    6. Bouncing Rockets
    7. Cloaking
    8. Ladders
    9. Favourite Server
    10. Flame Thrower
    11. Vortex Grenades
    12. Grapple
    13. Lightning Discharge
    14. Locational Damage
    15. Leg Shots
    16. Weapon Switching
    17. Scoreboard frag-rate
    18. Vortex Grenades II
    19. Vulnerable Missiles
    20. Creating Classes
    21. Scrolling Credits
    22. Weapon Dropping
    23. Anti-Gravity Boots
    24. HUD scoreboard
    25. Flashlight and laser
    26. Weapon Positioning
    27. Weapon Reloading
    28. Progressive Zooming
    29. Rotating Doors
    30. Beheading (headshot!)
    31. Alt Weapon Fire
    32. Popup Menus I
    33. Popup Menus II
    34. Cluster Grenades
    35. Homing Rockets
    36. Spreadfire Powerup
    37. Instagib gameplay
    38. Accelerating rockets
    39. Server only Instagib
    40. Advanced Grapple Hook
    41. Unlagging your mod


    Articles
    < Index >
    1. Entities
    2. Vectors
    3. Good Coding
    4. Compilers I
    5. Compilers II
    6. UI Menu Primer I
    7. UI Menu Primer II
    8. UI Menu Primer III
    9. QVM Communication, Cvars, commands
    10. Metrowerks CodeWarrior
    11. 1.27g code, bugs, batch


    Links

  • Quake3 Files
  • Quake3 Forums
  • Q3A Editing Message Board
  • Quake3 Editing


    Feedback

  • SumFuka
  • Calrathan
  • HypoThermia
  • WarZone





    Site Design by:
    ICEmosis Design


  •  
    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.

    PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial 29 | Next >>