TUTORIAL 18 - Vortex Grenades II

von TeknoDragon


In dieser Fortsetzung des Tutorials Vortex Grenades werden wir den Effekt deutlich realistischer gestalten und die Performance etwas verbessern. Einige Änderungen erfolgen auch nur aufgrund meiner Vorstellung von gutem Programmierstil. Ich gehe davon aus, dass du die Codeänderungen aus dem ersten Tutorial bereits eingefügt hast.

1. Einfache Fixes

Eine einfache Möglichkeit Performance zu sparen ist es, die think Funktion der grenade statt 25 mal pro Sekunde nur noch 10 bis 20 mal aufzurufen (im englischen Orginal wurde sie aller 20 ms, also 50 mal aufgerufen). Öffne also g_missile.c und suche folgende Zeile am Ende von G_Suck:

	self->nextthink = level.time + 40;

Ersätze die 40 durch einen höheren Wert, maximal 100 (entspricht 10 mal/sek). Das spart zwar Performance, ändert aber nichts am resultierenden Effekt: der Spieler entkommt uns auch nicht, wenn wir seine Geschwindigkeit und Richtung nur jede Zehntel-sekunde "korrigieren". Solche "magischen Konstanten" gehören eigentlich nicht direkt in den Code, sondern in #define's damit man sie einfacher ändern kann. Ersetze also den numerischen Wert durch GSUCK_TIMING und füge folgende beiden Zeilen über G_Suck ein:

		  
#define GSUCK_TIMING	80				// Aufrufperiode G_Suck
#define GSUCK_VELOCITY	900			// Stärke des Kicks, den der Spieler bekommt

Das gleiche Spiel für die Anweisung VectorScale(dir,200, target->client->ps.velocity); , ersetze den Wert 200 durch GSUCK_VELOCITY.

2. Den Effekt verbessern

Da unser Effekt bisher weder toll aussieht noch besonders authentisch wirkt, schauen wir uns den beteiligten Code etwas genauer an: die Codestelle in G_Suck, die den Spieler direkt verändert (target->client->ps.velocity ) überschreiben dessen Variablen einfach, ohne deren bisherigen Wert zu berücksichtigen, daher greift der Effekt so abrupt. Das muss sich ändern, aber wie ?
Bevor wir uns selbst den Kopf zerbrechen, schauen wir lieber, ob id etwas ähnliches bereits an anderer Stelle implementiert hat. Bei der Suche über alle Dateien nach client->ps.velocity findet sich unter anderem folgender Code aus G_Damage in g_combat.c (Z 895):

		
VectorAdd (targ->client->ps.velocity, kvel, targ->client->ps.velocity);

// set the timer so that the other client can't cancel
// out the movement immediately

if ( !targ->client->ps.pm_time ) {
	int		t;
	t = knockback * 2;
	if ( t < 50 ) {
		t = 50;
	}

	if ( t > 200 ) {
		t = 200;
	}

	targ->client->ps.pm_time = t;
	targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
}

Hier wird der Geschwindigkeitsvektor nicht einfach überschrieben, sondern ihm wird ein Vektor addiert. Im obigen Fall gibt dieser Vektor kval den Schub oder Rückschlag an, der durch einen Treffer entsteht. Nach der Korrektur der Geschwindigkeit wird die Timervariable client->ps.pm_time auf einen Wert zwischen 50 und 200 gesetzt. Wärend dieser Zeitspanne ist das Movement etwas schwammig, wodurch verhindert wird, dass der Knockback sofort ausgleichen werden kann. Etwas ähnliches werden wir für G_Suck verwenden. Definiere folgende Variable am Anfang von G_Suck:

		vec3_t start,dir,end,kvel;

Ersätze nun die Anweisung:

		VectorScale(dir,GSUCK_VELOCITY, target->client->ps.velocity);

durch folgende beiden:

		VectorScale(dir,GSUCK_VELOCITY / GSUCK_TIMING, kvel);
		VectorAdd (target->client->ps.velocity,kvel, target->client->ps.velocity);

Compiliere und teste, wie es sich anfühlt. Diese kleine Änderung bringt einen deutlich besseren Effekt. Wenn du zwischen mehreren grenades hin und her gezogen wirst, entsteht ein Zittern, da beide aller x Millisekunden auf dich einwirken. Genau dafür brauchen wir den Timer aus obigem Codebeispiel. Ergänze folgendes direkt unter der letzen Änderung:

		  
		if ( !target->client->ps.pm_time ) {
			target->client->ps.pm_time = 20 + random() * GSUCK_TIMING;
			target->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
		}

Auch dies solltest du compilieren und austesten.

Jetzt ist es Zeit, die beiden #define's zu manipulieren. Verringere beispielsweise GSUCK_TIMING damit der Spieler nicht so weit an der grenade vorbeischnipsen kann. Natürlich musst du immer auch die andere Konstante anpassen, wenn du eine änderst.

3. Performance sparen

Vor allem in low level Code trifft man des öfteren auf Funktionen aus g_syscalls.c, insbesondere trap_trace. Diese Funktion dient der Kollisionserkennung. Eine dieser Funktionen, die uns im Moment nützlich ist, heisst trap_EntitiesInBox. Der Aufrufer übergibt ihr zwei Ortsvektoren und einen Zeiger auf ein leeres int-Feld, sie füllt dieses Feld mit den Indizes der Entitys, die sich in dem Würfel befinden, der sich zwischen beiden Punkten aufspannt, und gibt die Anzahl der Entitys zurück. Fast perfekt für unsere Zwecke, wir hätten lieber eine Kugel als einen Würfel, darum kümmern wir uns noch.

Ergänze folgendes am Anfang von G_Suck:

	vec3_t start,dir,end,kvel,mins,maxs;
	int targNum[128];
	int num;

Zur Liste der #define's kommt folgende Zeile:

		#define GSUCK_RADIUS 500

Nun ersetze die Zeile:

		while ((target = findradius(target, self->r.currentOrigin, 500)) != NULL) {

durch folgendes:


		mins[0] = -GSUCK_RADIUS * 1.42;
		mins[1] = -GSUCK_RADIUS * 1.42;
		mins[2] = -GSUCK_RADIUS * 1.42;
		maxs[0] = GSUCK_RADIUS * 1.42;
		maxs[1] = GSUCK_RADIUS * 1.42;
		maxs[2] = GSUCK_RADIUS * 1.42;

		VectorAdd( self->r.currentOrigin, mins, mins );
		VectorAdd( self->r.currentOrigin, maxs, maxs );

		num = trap_EntitiesInBox(mins,maxs,targNum,MAX_GENTITIES);
		for(num--; num > 0; num--) {    // count from num-1 down to 0
			target = &g_entities[targNum[num]];

abschliessend ergänze folgende drei Zeilen:

	// Entity muss Schaden nehmen können
	if (!target->takedamage) 
		continue;

	// Entity muss sich innerhalb von GSUCK_RADIUS befinden
	if ( Distance(self->r.currentOrigin,targ->r.currentOrigin) > GSUCK_RADIUS )
		continue;

Wieso 1.42 ? Die Koordinaten der grenade sollen den Mittelpunkt des Würfels bilden, der Abstand zu den Aussenseiten soll GSUCK_RADIUS betragen. Wir benötigen aber die Koordinaten der Eckpunkte des Würfels, also errechnen wir zuerst den Verbindungsvektor zwischen Mittelpunkt und Eckpunkt und addieren diesen dann zum Mittelpunkt. Alle drei Komponenten des Verbindungsvektors sind Hypotenusen in gleichschenkligen Dreiecken deren Katheten jeweils die Länge GSUCK_RADIUS besitzen. Nach Pythagoras ist das Verhältnis von Kathete zu Hypotenuse in gleichschenkligen Dreiecken 1 zu wurzel(2) was wiederum annährend 1.42 beträgt.
Die for Schleife ist ein wenig tricky, aber wenn man weiss, dass num die Anzahl der gefunden Entities angibt, und dass in targNum deren Indizes für das g_entities Feld stehen, ist sie zu verstehen. Mit Hilfe des Distance Checks erzeugen wir innerhalb des Würfels noch eine Kugel und sieben alle Entities aus, die sich ausserhalb dieser befinden.

Toll, haufenweise neuer Code und keine neuen Features :(
Die gesparte Performance erlaubt uns aber ein paar Extras: du kannst z.B. am Ende von G_SUCK ein weiteres trap_EntitiesInBox verwenden um einen engen Bereich um die grenade zu prüfen, und sie sofort explodieren lassen, wenn sich ein Spieler darin befindet. Der Code dazu ist im Wesentlichen eine Kopie des gerade besprochenen.
Hausaufgabe :P

<< Tutorial 17 | | Tutorial 19 >>