TUTORIAL 29 - Schwingtüren

von valkyrie


Von Realismus-Mods, Total Conversations oder anderen Spielen ist euch sicher bekannt, wie sich schwingende Türen in Maps "anfühlen". Man stelle sich ein grosses Herrenhaus mit ausladenden Türflügeln vor, die bei betreten auseinanderGLEITEN. Für einen Scherz vielleicht ganz nett, aber im Sinne der authentischen Athmosphäre sicher unpassend.
Quake3 unterstützt schwingende Türen bisher nicht, also müssen wir selbst Hand anlegen. Dieser kleine Effekt kostet den Coder allerdings reichlich viel Aufwand.

Konkret werden wir jeden noch so kleinen Schnipsel duplizieren, der zum bisherigen (gleit-)Türencode beträgt, mit Ausnahme der Funktion, die das Öffnen der Tür triggert. Diese Duplikate werden wir anschliessend so ändern, dass der Winkel anstatt der Position verändert wird. Klingt fast zu einfach, also lasst uns einfach anfangen.

Alle Modifikationen werden im game-Modul vorgenommen, folgende Dateien werden editiert:

g_local.h
g_combat.c
g_spawn.c
g_mover.c

Es steht auch eine Beispielmap inklusive Map-source für Tests zur Verfügung, den Link gibts zum Schluss.


1. Die Basis

Fangen wir ganz einfach an. Um den Zustand der öffnenden Tür verwalten zu können, muss er erstmal einen Namen bekommen. Öffne g_local.h und füge in moverState_t folgende Zeilen ein (Z 40):


typedef enum {
	MOVER_POS1,
	MOVER_POS2,
	MOVER_1TO2,
	MOVER_2TO1,
	
	// VALKYRIE: angle movements
	ROTATOR_POS1,
	ROTATOR_POS2,
	ROTATOR_1TO2,
	ROTATOR_2TO1
} moverState_t;

Die Tür (der Mover) soll also vier neue Zustände einnehmen können: geschlossen (POS1), offen(POS2), öffnend(1TO2) und schliessend(2TO1). Nun spendieren wir gentity_s folgende Zeile (Z 165):

		  
	// timing variables
	float		wait;
	float		random;

	gitem_t		*item;			// for bonus items

	float		distance;		// VALKYRIE: for rotating door
};

Diese Variable verwaltet den Fortschritt der Öffnung/Schliessung, d.h. wie weit dieser Vorgang abgeschlossen ist.
Das wäre alles für g_local.h .


2. Die spawn-Funktion vorbereiten

Folgende Zeile f�gen wir in g_spawn.c ein(Z 100):


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},
	{"targetShaderName", FOFS(targetShaderName), F_LSTRING},
	{"targetShaderNewName", FOFS(targetShaderNewName), F_LSTRING},
	{"distance", FOFS(distance), F_FLOAT},	// VALKYRIE: for rotating doors

	{NULL}
};

Hier ergänzen wir die Liste derjenigen Variablen, die für jedes Mapobject() initialisiert werden, um die gerade deklarierte distance. Der Mapper der die Tür erstellt kann nun im Radiant dem Keyword "distance" einen numerischen Wert zuweisen, welcher dann beim Laden der Map an die Tür-entity übergeben wird.

Nun brauchen wir den Prototyp der spawn-funktion der T�r, die wir erst später definieren können, aber vorher schon mit unserem Mapobject verbinden müssen (Z 168):


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

Diese spawn-funktion verbinden wir nun mit dem Entitytyp func_door_rotating, also unserem Mapobject-typ. Dazu folgende Zeile (Z 245):


	{"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

#ifdef MISSIONPACK
	{"team_redobelisk", SP_team_redobelisk},
	{"team_blueobelisk", SP_team_blueobelisk},
	{"team_neutralobelisk", SP_team_neutralobelisk},
#endif

	{0, 0}
};

Jetzt braucht es noch einen kleinen fix in g_combat.c um die Türen öffen zu können, sobald ein Projektil sie trifft. Folgendes kommt in G_Damage (Z 845):


// 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;
	}

Diese Ergänzung wird nötig, da die Schwingtüren nur noch ROTATOR-Zustände einnehmen werden, im Gegensatz zu den MOVER Zuständen der gewöhnlichen Türen.
Damit bleiben nur noch die Modifikationen in g_mover.c zu tun.


3. Die spawn-Funktion

Öffne g_mover.c und füge folgenden Code nach dem Fuktionsrumpf von SP_func_door ein. Diese Funktion kümmert sich um das spawnen der Tür beim Laden der Map und alle betreffenden Initialisierungen. Die auskommentierten Definitionen werden von Mappern benutzt um Türen zu erzeugen. Mehr dazu nach dem Code.


/*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;
		}
	}
}

Also.. was genau tut diese Funktion ? Direkt zu Anfang haben wir die Zuweisung der Sounds. Danach wird der Pointer für die blocked Funktion gesetzt. Dann wird speed auf 120 gesetzt, falls es nicht oder auf 0 gesetzt ist. Falls speed einen negativen Wert hat, wird das Reverse-flag gesetzt und speed bekommt ein positives Vorzeichen. Dann wird wait auf 2 sec. gesetzt, dieser Wert reguliert, wie lange die Tür geöffnet bleibt.

Nun werden diverse Vektoren initialisiert, movedir z.B. legt die Öffnungsrichtung fest, entlang der x,y oder z Achse und natürlich vorwärts oder rückwärts. Das wird durch das oben genannte Reverse flag gesteuert. Danach wird der distance Wert geprüft und, falls 0, auf den Defaultwert 90 gesetzt. Aus movedir und distance wird der Wert der Endposition bestimmt, sozusagen der Öffnungswinkel. Wenn der Mapper das REVERSE-flag nicht setzten will, kann er distance auch einen negativen geben um die Öffnungsrichtung umzukehren. Zu InitRotator kommen wir gleich noch, die Zeilen darunter dienen der Unterscheidung zwischen Türen, die sich nur per Schalter öffnen lassen (Think_MatchTeam) und gewöhnlichen Türen (Think_SpawnNewDoorTrigger).


4. Jetzt aber richtig

Der Rest wird recht simpel sein, wir werden allen Code f�r func_door Objecte wie oben gesagt duplizieren und die Winkel anstatt der Positionen manipulieren. Fortgeschrittene Programmierer werden bei folgendem Code vielleicht die Augen verdrehen, aber ich denke in dieser Form ist er für Einsteiger eher verständlich.

Fangen wir mit InitRotator an, diese ist im Wesentlichen eine Kopie von InitMover und wird direkt unter dieser eingefügt.(Z 765):

/*
================
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;
	}
}

Hier wird die Initialisierung der Tür abgeschlossen. Weiter zu Use_BinaryMover, hier fügen wir nachfolgendes ein. Diese Funktion wird aufgerufen, sobald die Tür benutzt wird, sei es per Schalter, per Treffer oder weil Ein Client in ihre Nähe kommt. Abhängig vom gegebenen moverstate bestimmt diese Funktion die Aktion der Tür: wenn sie geschlossen ist wird die Rotation gestartet, wenn sie geöffnet ist wird sie die Schliessung um (weitere) 2 sec. verzögert.(Z 685):


	// only partway up before reversing
	if ( ent->moverState == MOVER_1TO2 ) {
		total = ent->s.pos.trDuration;
		partial = level.time - ent->s.pos.trTime;
		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;
	}
}

Der nächste Kandidat ist Reached_BinaryMover. Diese Funktion wird aufgerufen sobald die Rotation ihr Ziel erreicht hat und abhängig von moverstate entscheidet sich, ob die Tür als geöffnet oder geschlossen behandelt wird. (Z 600):


	} 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" );
	}
}

Es folgt unsere Version von ReturnToPos1, der Funktion die prüft, ob es Zeit ist, die Tür wieder zu schliessen. Wir nennen sie ReturnToApos1 (Z 560):

/*
================
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 );
	}
}

Die Funktion SetMoverState besorgt die konkrete Bewegung und Änderung des Zustandes der Tür, sozusagen die physics. Wir müssen ihr nur noch beibringen mit Winkeln umzugehen.(Z 490):

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;
	case MOVER_POS2:
		VectorCopy( ent->pos2, ent->s.pos.trBase );
		ent->s.pos.trType = TR_STATIONARY;
		break;
	case MOVER_1TO2:
		VectorCopy( ent->pos1, ent->s.pos.trBase );
		VectorSubtract( ent->pos2, ent->pos1, delta );
		f = 1000.0 / ent->s.pos.trDuration;
		VectorScale( delta, f, ent->s.pos.trDelta );
		ent->s.pos.trType = TR_LINEAR_STOP;
		break;
	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 );
}

Wir arbeiten uns in g_mover.c weiter nach oben. Nun folgt G_MoverTeam, mit deren Hilfe einmal pro Serverframe festgestellt wird, ob der Mover sein Ziel erreicht hat. Wenn das zutrifft wird die reached-Funktion des Movers aufgerufen, für unsere Tür ist das die oben genannte Reached_BinaryMover. (Z 450):


	// 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 );
				}
			}
		}
	}
}

Die letzte Änderung erfolgt in Touch_DoorTrigger. Diese Funktion prüft ob ein Client nahe an der Tür steht (bzw. die trigger Entity berührt) und löst in diesem Fall das Öffnen aus. Wir ergänzen nur Bedingungen unter denen das geschehen soll. (Z 1060):


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 );
	}
}

Puh.. Codemässig hätten wir es damit, bleibt nur noch der QUAKED Kommentar für die Mapper zu erklären:


/*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
*/

Ein Mapper sollte damit eigentlich etwas anfangen können aber auch für den Coder kann ein grober Einblick nicht schaden. Es handelt sich hierbei um eine Entity-definition für den Q3Radiant. Wenn man diesen Text in die Datein entities.def einfügt, erscheint eins neues Objekt zur Auswahl, so das man neben Spawnpunkten, Waffen oder Powerups eben auch Schwingtüren in die Map einbauen kann. Die erste Zeile ist entscheidend, sie teilt der spawn-Funktion mit, wie die spawnflags für dieses Objekt zu interpretieren sind. Daher darf auch die Reihenfolge der Objekte nicht geändert werden. Die restlichen Zeilen geben dem Mapper nur noch Detailinformation zu den einzelnen Objekten. Falls er vergisst, einen der Werte zu setzen, wirken sich die im Code gesetzten defaultwerte aus.

So, das wäre alles. Compiliere den Code und teste ihn....

Nachtrag: Es gibt Probleme mit dem Licht, wenn der Mapper ohne "-lights -extra" arbeitet.

<< Tutorial 20 | | Themenübersicht >>