TUTORIAL 5 - ARMOR PIERCING RAILS

von SumFuka


Bist du auch immer frustriert, wenn du mit der Railgun auf einen Gegner zielst, er sich dann aber hinter einer Säule versteck? Na solchen Leuten zeigen wir mal, wo der Ziegenbock den Honig holt!

Es empfiehlt sich nun sehr, den Artikel über Vektoren durchzulesen. (Ob du es magst oder nicht, aber ein gutes Verständnis von Vektorrechnung ist grundlegend für die Quake Programmierung.)

1.WIE FUNKTIONIEREN SLUGS (Railgun)?
Zuerst suchen wir die weapon_railgun_fire in g_weapon.c:


/*
=================
weapon_railgun_fire
=================
*/

#define MAX_RAIL_HITS 4
void weapon_railgun_fire (gentity_t *ent) {

vec3_t end;
#ifdef MISSIONPACK
vec3_t impactpoint, bouncedir;
#endif
trace_t trace;
gentity_t *tent;
gentity_t *traceEnt;
int damage;
int i;
int hits;
int unlinked;
int passent;
gentity_t *unlinkedEntities[MAX_RAIL_HITS];

damage = 100 * s_quadFactor;

VectorMA (muzzle, 8192, forward, end);

// trace only against the solids, so the railgun will go through people
unlinked = 0;
hits = 0;
passent = ent->s.number;
do {


trap_Trace (&trace, muzzle, NULL, NULL, end, passent, MASK_SHOT );
if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) {


break;
}

traceEnt = &g_entities[ trace.entityNum ];
if ( traceEnt->takedamage ) {
#ifdef MISSIONPACK


if ( traceEnt->client && traceEnt->client->invulnerabilityTime > level.time ) {
if ( G_InvulnerabilityEffect( traceEnt, forward, trace.endpos, impactpoint, bouncedir ) ) {
G_BounceProjectile( muzzle, impactpoint, bouncedir, end );
// snap the endpos to integers to save net bandwidth, but nudged towards the line
SnapVectorTowards( trace.endpos, muzzle );
// send railgun beam effect
tent = G_TempEntity( trace.endpos, EV_RAILTRAIL );
// set player number for custom colors on the railtrail
tent->s.clientNum = ent->s.clientNum;
VectorCopy( muzzle, tent->s.origin2 );
// move origin a bit to come closer to the drawn gun muzzle
VectorMA( tent->s.origin2, 4, right, tent->s.origin2 );
VectorMA( tent->s.origin2, -1, up, tent->s.origin2 );
tent->s.eventParm = 255; // don't make the explosion at the end
//
VectorCopy( impactpoint, muzzle );
// the player can hit him/herself with the bounced rail
passent = ENTITYNUM_NONE;
}
}
else {
if( LogAccuracyHit( traceEnt, ent ) ) {
hits++;
}
G_Damage (traceEnt, ent, ent, forward, trace.endpos, damage, 0, MOD_RAILGUN);
}


#else
if( LogAccuracyHit( traceEnt, ent ) ) {

hits++;

}


G_Damage (traceEnt, ent, ent, forward, trace.endpos, damage, 0, MOD_RAILGUN);
#endif
}
if ( trace.contents & CONTENTS_SOLID ) {

break; // we hit something solid enough to stop the beam

}


// unlink this entity, so the next trace will go past it
trap_UnlinkEntity( traceEnt );
unlinkedEntities[unlinked] = traceEnt;
unlinked++;


} while ( unlinked < MAX_RAIL_HITS );

// link back in any entities we unlinked
for ( i = 0 ; i < unlinked ; i++ ) {
trap_LinkEntity( unlinkedEntities[i] );
}

// the final trace endpos will be the terminal point of the rail trail

// snap the endpos to integers to save net bandwidth, but nudged towards the line
SnapVectorTowards( trace.endpos, muzzle );

// send railgun beam effect
tent = G_TempEntity( trace.endpos, EV_RAILTRAIL );

// set player number for custom colors on the railtrail
tent->s.clientNum = ent->s.clientNum;

VectorCopy( muzzle, tent->s.origin2 );
// move origin a bit to come closer to the drawn gun muzzle
VectorMA( tent->s.origin2, 4, right, tent->s.origin2 );
VectorMA( tent->s.origin2, -1, up, tent->s.origin2 );

// no explosion at end if SURF_NOIMPACT, but still make the trail
if ( trace.surfaceFlags & SURF_NOIMPACT ) {

tent->s.eventParm = 255; // don't make the explosion at the end

} else {

tent->s.eventParm = DirToByte( trace.plane.normal );

}
tent->s.clientNum = ent->s.clientNum;

// give the shooter a reward sound if they have made two railgun hits in a row
if ( hits == 0 ) {
// complete miss
ent->client->accurateCount = 0;
} else {
// check for "impressive" reward sound
ent->client->accurateCount += hits;
if ( ent->client->accurateCount >= 2 ) {
ent->client->accurateCount -= 2;
ent->client->ps.persistant[PERS_IMPRESSIVE_COUNT]++;
// add the sprite over the player's head
ent->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP );
ent->client->ps.eFlags |= EF_AWARD_IMPRESSIVE;
ent->client->rewardTime = level.time + REWARD_SPRITE_TIME;
}
ent->client->accuracy_hits++;
}

}

Lest das ruhig 2 bis 3 mal in euerm Quellcode im Visual Studio (bessere Einrückung als hier) durch. Die Abschnitte mit #ifdef MISSIONPACK sind nur für Quake3 Team Arena relevant sind, bis ein #else, #elif oder #endif folgt.

Laß uns annehmen, dass "muzzle" ein Ortsvektor direkt vor dem Abfeuernden ist, und "forward" ist ein Richtungvektor, der genau in die Richtung zeigt, in die der Client gerade schaut. Ein großer Teil des Codes ist in einer Schleife verschachtelt, die im Grunde bedeuted:

do { verfolge den Strahl des Railgun Schusses bis du etwas triffst; wenn das Slug eine Wand getroffen hat, beende die Schleife;}. Das do { } heißt einfach, dass alles was in den geschweiften Klammern so lange ausgeführt wird, bis ein break kommt oder die Bedingung (unlinked < MAX_RAIL_HITS) nicht mehr erfüllt ist. Am Anfang der Schleife wird die Funktion trap_Trace(bla bla bla) aufgerufen - diese verfolgt einen Strahl durch den Raum vom Ursprung (muzzle) aus in die Richtung der Railgun (forward) mit einer Länge von 8192 Einheiten. Wenn wir etwas treffen, so bekommen wir über diese Funktion Informationen darüber, was wir getroffen haben.

Haben wir nichts getroffen, dann läuft die Schleife einfach weiter. Wenn wir etwas getroffen haben, das Schaden nehmen kann [if ( traceEnt->takedamage )], z.B. einen Player oder ein Button, so wird dieses Ziel beschädigt durch den Aufruf der Funktion G_Damage(). Wenn unser Slug eine Wand trifft, oder "das Ende der Welt [Map]" trifft [also if ( trace.contents & CONTENTS_SOLID )], so bricht die Schleife ab. Genau das wollen wir aber ändern!!

2. DURCH WÄNDE FEUERN
Als erstes benötigen wir eine neue Vektorvariable (direkt unter den anderen, oben in der Funktion):


vec3_t tracefrom; // SUM

Einige Zeilen tiefer gibt es einen Aufruf der VectorMA() Funktion. Diese erzeugt einen "end" Vektor, der 8192 Einheiten nach vorne (forward) von der Position des Schießenden (muzzle) ist. Wir benötigen eine Kopie von "muzzle", wir wollen diese in unserer neuen Variablen "traceform" speichern. Daher schreiben wir direkt unter den VectorMA() Aufruf:

VectorMA (muzzle, 8192, forward, end);
VectorCopy (muzzle, tracefrom);

Als nächstes ändern wir den trap_Trace Aufruf, damit er als Position der Railgun "tracefrom" annimmt, und nicht mehr "muzzle"... es gibt dafür einen guten Grund! Lese weiter... !


trap_Trace (&trace, tracefrom, NULL, NULL, end, ent->s.number, MASK_SHOT );

Der nächste Schritt ist es, das Verhalten vom if ( trace.contents & CONTENTS_SOLID ) { ... } Block zu ändern. Dieser Bereich behandelt den Fall, dass wir etwas Solides (also eine Wand oder den Himmel) getroffen haben. Was wollen wir daran ändern? Nun, anstatt einfach aus der Schleife auszubrechen (und den Endpunkt für unser Slug zu markieren), wollen wir, dass unser Slug durch die Wand hindurchgeht. Trotzdem wollen wir natürlich, dass er beim Himmel aufhört, dahinter wäre ja ohnehin nichts mehr. Also sieht die if-Bedingung folgendendermaßen aus:


if ( trace.contents & CONTENTS_SOLID ) {

// SUM abbrechen, wenn wir den Himmel berührt haben
if (trace.surfaceFlags & SURF_SKY)

break;
// Hypo: abbrechen, wenn wir die Länge des Vektors überschritten haben
if (trace.fraction == 1.0)
break;
// anderenfalls einfach durch die Wand weiter durchgehen
VectorMA (trace.endpos,1,forward,tracefrom);
continue;

}

In anderen Worten: Wenn wir den Himmel getroffen haben (dies zeigen uns die surfaceFlags an), so wollen wir aufhören. Wenn wir bis zur vollen Länge des Vektors gewandert sind müssen wir aufhören (es gibt keine Garantie, dass wir jemals einen Sky berühren würden). Wenn nichts davon passiert ist haben wir eine solide Wand getroffen. Wir setzen nun unseren Vektor "tracefrom" um genau 1 Einheit weiter weg vom Einschlagpunkt in der Wand. Dadurch "tunnelt" sich unser Schuß hinein / hindurch. Die Schleife wiederholt sich, der continue Befehl bringt uns zurück an den Schleifenanfang.

Einen Haken gibts noch. Die im Code bereits enthaltene Abfrage:

if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) {
break;

}

Diese trifft leider ein, wenn wir duch die Wand schießen, daher habe ich diesen Bereich einfach komplett auskommentiert (mit /* und */). Dadurch könnte es zu Fehlern bei der Ausführung kommen, aber momentan habe ich noch keine bessere Lösung gefunden.

Noch eine Anmerkung zu Kommentaren wie // SUM oder // Hypo. Es empfiehlt sich den selbst modifizierten Code in irgendeiner Weise sinnvoll zu kennzeichnen, z.B. mit einem für euch eindeutigen Kürzel, das aber am Besten zum Wiederfinden kein C-typisches Wort ist, und mindestens drei Zeichen lang sein sollte. Gerade wenn mehrere Leute an einer Modifikation arbeiten ist es bei der Fehlersuche sehr wichtig sofort zu erkennen, wer was wo geändert hat.

3. KANN DAS AUCH JEDER SEHEN??
So wie alles momentan eingestellt ist, kann es passieren, dass man den Strahl der Railgun nicht von überall aus sieht. Die nun folgende kleine Änderung stellt sicher, dass die Information zum Zeichnen des Strahls an alle Mitspieler geschickt wird (Broadcasting). Das ist auch durchaus sinnvoll, denn es wäre ungeschickt, eine durch Wände feuernde Railgun zu haben, aber das Opfer würde selbst den Strahl davon nicht sehen.


// keine Explosion am Aufschlagspunkt wenn SURF_NOIMPACT gesetzt ist, aber trotzdem Railstrahl machen
if ( trace.surfaceFlags & SURF_NOIMPACT ) {


tent->s.eventParm = 255; // keine Explosion am Ende


} else {


tent->s.eventParm = DirToByte( trace.plane.normal );


}
tent->s.clientNum = ent->s.clientNum;

// Schicke den Effekt an alle Clients
tent->r.svFlags |= SVF_BROADCAST;

Die letzte Zeile stellt sicher, dass jeder Spieler die Information zugeschickt bekommt, wie dieser Strahl der Railgun zu zeichnen ist.

Nun lade q3tourney6 und heize Xaero mal so richtig den Hintern ein!

>