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