TUTORIAL 16 - Intelligenter Waffenwechsel

von Mark "WarZone" Smeltzer


Mit Hilfe dieses Tutorials wird es jedem Client deines Mods möglich sein, das Waffenwechseln seinem Geschmack anzupassen. Bei Quake2 waren die Prioritäten der Waffen unabänderlich aber beim guten alten Quake konnte jeder Spieler noch seine eigene Waffenwechsel-reihenfolge festlegen. In Quake2 war z.B. die BFG die "beste" Waffe, gefolgt von der Railgun, dem Hyperblaster, dem Rocketlauncher usw. Genau diese Reihenfolge kann jeder Client in Quake und in diesem Mod selbst festlegen.

1. Quake3 Waffenwechsel ?

Also: welche coolen Waffenwechsel Merkmale spendiert uns Quake3 ? Keine, na toll. In Quake3 gibt es genau 2 Arten die Waffe zu wechseln: automatisch und manuell. Automatisch heisst: sobald man eine Waffe einsammelt wird diese ausgewählt. Auch wenn man mit der BFG rumläuft und eine Shotgun einsammelt, wird sofort die Shotgun ausgewählt. Kurz gesagt: absolut unbrauchbar... manueller Waffenwechsel ist auch nicht so viel besser. Wie der Name schon vermuten lässt, wechselt der manuelle Modus nie ohne unser zutun die Waffe, man muss also immer selbst die Waffe auswählen, nachdem man sie Aufgenommen hat.

Beide Varianten sind nicht gerade clever, also werden wir da mal Hand anlegen :)

Dieses Tutorial wird also zeigen wie man Waffenwechsel à la Quake implementiert, Clientseitig völlig variabel. Jeder Client bekommt eine cvar "cg_weaponOrder" mit der die Waffenprioritäten bestimmt werden. Na dann los...

Wir werden folgende Dateien modifizieren:
cg_weapons.c
cg_main.c
cg_local.c
cg_event.c
g_items.c
g_main.c
bg_public.c

2. Definiere einige globale Variablen und Funktionsprototypen

Öffne zuerst cg_local.h und füge folgendes zu den "cg_main.c" Definitionen (Z 1190):

     
// 
// cg_main.c 
// 
const char *CG_ConfigString( int index ); 
const char *CG_Argv( int arg ); 

void QDECL CG_Printf( const char *msg, ... ); 
void QDECL CG_Error( const char *msg, ... ); 

void CG_StartMusic( void ); 

void CG_UpdateCvars( void ); 

int CG_CrosshairPlayer( void ); 
int CG_LastAttacker( void );
void CG_LoadMenus(const char *menuFile);
void CG_KeyEvent(int key, qboolean down);
void CG_MouseEvent(int x, int y);
void CG_EventHandling(int type);
void CG_RankRunFrame( void );
void CG_SetScoreSelection(void *menu);
score_t *CG_GetSelectedScore();
void CG_BuildSpectatorString(); 

//WarZone 
#define NUM_WEAPS 9 
extern  int cg_weaponsCount; 
extern  int weaponOrder[NUM_WEAPS]; 
extern  int weaponRawOrder[NUM_WEAPS]; 
int RateWeapon (int weapon); 
int NextWeapon (int curr); 
int PrevWeapon (int curr);
Speichern und schliessen. Mit cg_local.h bist du fertig.

3. Die cvar Definieren

Öffne als nächstes cg_main.c und ergänze die cvar Definitionen um folgende (Z 140):

     
vmCvar_t	cg_deferPlayers;
vmCvar_t	cg_drawTeamOverlay;
vmCvar_t	cg_teamOverlayUserinfo;
vmCvar_t	cg_weaponOrder; //WarZone 
int			cg_weaponsCount = -1; //WarZone
und in der cvar Tabelle (Z 300):
    { &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE},
	{ &cg_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE},
    { &cg_weaponOrder, "cg_weaponOrder", "1/2/3/4/5/6/7/8/9", CVAR_ARCHIVE } //WarZone
//	{ &cg_pmove_fixed, "cg_pmove_fixed", "0", CVAR_USERINFO | CVAR_ARCHIVE }
vergiss das Komma am Ende der cg_trueLightning-Zeile nicht!

4. Die Hilfsfunktionen definieren

Jetzt folgt der Grossteil des neuen Codes. Wir platzieren ihn direkt unter die cvar Tabelle (Z 305).

      
	{ &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE},
	{ &cg_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE},
	{ &cg_weaponOrder, "cg_weaponOrder", "1/2/3/4/5/6/7/8/9", CVAR_ARCHIVE } //WarZone
//	{ &cg_pmove_fixed, "cg_pmove_fixed", "0", CVAR_USERINFO | CVAR_ARCHIVE }
};

static int  cvarTableSize = sizeof( cvarTable ) / sizeof( cvarTable[0] );

     //<WarZone> 
     int weaponOrder[NUM_WEAPS]; 
     int weaponRawOrder[NUM_WEAPS]; 

     int NextWeapon (int curr) 
     { 
       int i; 
       int w = -1; 

       for (i = 0; i < NUM_WEAPS; i++) 
       { 
         if (weaponRawOrder[i] == curr) 
         { 
           w = i; 
           break; 
         } 
       } 

       if (w == -1) 
         return curr; //shouldn't happen 

       return weaponRawOrder[(w + 1) % NUM_WEAPS]; 
     } 

     int PrevWeapon (int curr) 
     { 
       int i; 
       int w = -1; 

       for (i = 0; i < NUM_WEAPS; i++) 
       { 
         if (weaponRawOrder[i] == curr) 
         { 
           w = i; 
           break; 
         } 
       } 

       if (w == -1) 
         return curr; //shouldn't happen 

       return weaponRawOrder[w - 1 >= 0 ? w - 1 : NUM_WEAPS - 1]; 
     } 

     int RateWeapon (int weapon) 
     { 
       weapon--; 

       if (weapon > 8 || weapon < 0) 
         return 0; //bad weapon 

       return weaponOrder[weapon]; 
     } 

     int contains(int *list, int size, int number) 
     { 
       int i; 

       for (i = 0; i < size; i++) 
         if (list[i] == number) return 1; 

       return 0; 
     } 

     void UpdateWeaponOrder (void) 
     { 
       char *order = cg_weaponOrder.string; 
       char weapon[3]; 
       int i, start; 
       int tempOrder[NUM_WEAPS]; 
       int weapUsed[NUM_WEAPS]; 
       int temp; 

       weapon[1] = '\0'; 
       memset(tempOrder, 0, sizeof(tempOrder)); 
       memset(weapUsed, 0, sizeof(weapUsed)); 

       i = 0; 
       while (order != NULL && *order != '\0' && i < NUM_WEAPS) 
       { 
         weapon[0] = *order; 
         order++; 

         if (*order != '\\' && *order != '/') 
         { 
           weapon[1] = *order; 
           weapon[2] = '\0'; 
           order++; 
         } else { 
           weapon[1] = '\0'; 
         } 

         if (*order != '\0') 
           order++; 

         temp = atoi( weapon ); 
         if (temp < 1 || temp > NUM_WEAPS) 
         { 
           CG_Printf( "Error: %i is out of range. Ignoring..\n", temp ); 
         } 
         else if ( contains( tempOrder, sizeof(tempOrder)/sizeof(tempOrder[0]), temp ) )

         { 
           CG_Printf( "Error: %s (%i) already in list. Ignoring..\n",
             (BG_FindItemForWeapon( temp ))->pickup_name, temp );

         } else { 
           tempOrder[i] = temp; 
           weapUsed[temp - 1] = 1; 
           i++; 
         } 
       } 

       //error checking.. 
       start = 0; 
       for (i = 0; i < NUM_WEAPS; i++) 
       { 
         if (weapUsed[i]) 
           continue; 
         CG_Printf( "Error: %s (%i) not in list. Adding it to front of the list..\n",
           (BG_FindItemForWeapon( i + 1 ))->pickup_name, i + 1 );

         weaponRawOrder[start++] = i + 1; 
       } 
       //build the raw order list 
       for (i = start; i < NUM_WEAPS; i++) 
         weaponRawOrder[i] = tempOrder[i - start]; 

       //built the remaping table 
       for (i = 0; i < NUM_WEAPS; i++) 
         weaponOrder[weaponRawOrder[i] - 1] = i + 1; 

     } 
     //</WarZone>

Also gut, was macht dieser Code ? Wir haben die Funktionen NextWeapon(), PrevWeapon(), RateWeapon(), contains() und UpdateWeaponOrder() definiert. UpdateWeaponOrder() wird immer dann aufgerufen, wenn der Client die Waffenproritäten ändert. NextWeapon() und PrevWeapon() dienen, um vorwärts und rückwärts durch die Waffen zu wechseln. RateWeapon() liefert die Priorität ( die "Güte" entsprechend den Vorlieben des Spielers) einer Waffe, dies wird z.B. gebraucht um festzustellen, ob eine aufgenommene Waffe besser als die bisher getragene ist. contains() ist eine einfache Hilfsfunktion, die überprüft, ob das übergebene Feld den übergebenen Wert enthält.

5. Auf die Änderung der cvar reagieren

Immernoch in cg_main.c, ändern wir die Funktion CG_UpdateCvars() (Z 490):
void CG_UpdateCvars( void ) {
	int			i;
	cvarTable_t	*cv;

	for ( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ ) {
		trap_Cvar_Update( cv->vmCvar );
	}

	// check for modications here

	// If team overlay is on, ask for updates from the server.  If its off,
	// let the server know so we don't receive it
	if ( drawTeamOverlayModificationCount != cg_drawTeamOverlay.modificationCount ) {
		drawTeamOverlayModificationCount = cg_drawTeamOverlay.modificationCount;

		if ( cg_drawTeamOverlay.integer > 0 ) {
			trap_Cvar_Set( "teamoverlay", "1" );
		} else {
			trap_Cvar_Set( "teamoverlay", "0" );
		}
		// FIXME E3 HACK
		trap_Cvar_Set( "teamoverlay", "1" );
	}

	// if force model changed
	if ( forceModelModificationCount != cg_forceModel.modificationCount ) {
		forceModelModificationCount = cg_forceModel.modificationCount;
		CG_ForceModelChange();
	}
    //WarZone 
    if ( cg_weaponsCount != cg_weaponOrder.modificationCount ) 
    { 
         UpdateWeaponOrder(); 
         cg_weaponsCount = cg_weaponOrder.modificationCount; 
    } 

Dieser Code ruft UpdateWeaponOrder() sobald die cvar "cg_weaponOrder" verändert wird.

Damit sind wir fertig mit cg_main.c. Öffne nun cg_weapons.c und suche die Funktion CG_DrawWeaponSelect. Diese Funktion werden wir komplett ersetzen, du kannst also die alte Version löschen oder auskommentieren. Hier die neue Version (Z 1440):

     void CG_DrawWeaponSelect( void ) { 
      int  i; 
      int  bits; 
      int  count; 
      int  weap; 
      int  x, y, w; 
      char *name; 
      float *color; 

      // don't display if dead 
      if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) { 
        return; 
      } 

      color = CG_FadeColor( cg.weaponSelectTime, WEAPON_SELECT_TIME ); 
      if ( !color ) { 
        return; 
      } 
      trap_R_SetColor( color ); 

      // showing weapon select clears pickup item display, but not the blend blob 
      cg.itemPickupTime = 0; 

      // count the number of weapons owned 
      bits = cg.snap->ps.stats[ STAT_WEAPONS ]; 
      count = 0; 
      for ( i = 1 ; i < NUM_WEAPS ; i++ ) { //WarZone 
        if ( bits & ( 1 << i ) ) { 
          count++; 
        } 
      } 

      x = 320 - count * 20; 
      y = 380; 

      weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- select last weapon 
      for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone 
        weap = NextWeapon( weap ); 

        if ( !( bits & ( 1 << weap ) ) ) { 
          continue; 
        } 

        CG_RegisterWeapon( weap ); 

        // draw weapon icon 
        CG_DrawPic( x, y, 32, 32, cg_weapons[weap].weaponIcon ); 

        // draw selection marker 
        if ( weap == cg.weaponSelect ) { 
          CG_DrawPic( x-4, y-4, 40, 40, cgs.media.selectShader ); 
        } 

        // no ammo cross on top 
        if ( !cg.snap->ps.ammo[ weap ] ) { 
          CG_DrawPic( x, y, 32, 32, cgs.media.noammoShader ); 
        } 

        x += 40; 
      } 

      // draw the selected name 
      if ( cg_weapons[ cg.weaponSelect ].item ) { 
        name = cg_weapons[ cg.weaponSelect ].item->pickup_name; 
        if ( name ) { 
          w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH; 
          x = ( SCREEN_WIDTH - w ) / 2; 
          CG_DrawBigStringColor(x, y - 22, name, color); 
        } 
      } 

      trap_R_SetColor( NULL ); 
     }

Mit Hilfe dieser Funktion werden die Icons der Waffen entsprechend der Prioritäten des Spielers angezeigt... verdammt cool :)

6. Ersetze CG_NextWeapon_f und CG_PrevWeapon_f

Etwas weiter unter findest du CG_NextWeapon_f. Ersetze sie durch folgende Funktion (Z 1540):

     void CG_NextWeapon_f( void ) { 
      int  i; 
      int  original; 

      if ( !cg.snap ) { 
       return; 
      } 
      if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) { 
       return; 
      } 

      cg.weaponSelectTime = cg.time; 
      original = cg.weaponSelect; 

       for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone 
       cg.weaponSelect = NextWeapon( cg.weaponSelect ); //WarZone 

       if ( cg.weaponSelect == WP_GAUNTLET ) { 
        continue;  // never cycle to gauntlet 
       } 
       if ( CG_WeaponSelectable( cg.weaponSelect ) ) { 
        break; 
       } 
      } 
     }

Diese Funktion ruft unsere NextWeapon() Funktion aus cg_main.c auf, um die nächste Waffe zu wählen.

Das gleiche Spiel noch einmal mit CG_PrevWeapon_f. Ersetze sie durch folgende Funktion (Z 1570):

     void CG_PrevWeapon_f( void ) { 
      int  i; 
      int  original; 

      if ( !cg.snap ) { 
       return; 
      } 
      if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) { 
       return; 
      } 

      cg.weaponSelectTime = cg.time; 
      original = cg.weaponSelect; 

      for ( i = 0 ; i < NUM_WEAPS ; i++ ) { //WarZone 
       cg.weaponSelect = PrevWeapon( cg.weaponSelect ); //WarZone 

       if ( cg.weaponSelect == WP_GAUNTLET ) { 
        continue;  // never cycle to gauntlet 
       } 
       if ( CG_WeaponSelectable( cg.weaponSelect ) ) { 
        break; 
       } 
      } 
     }

Hier wird PrevWeapon() benutzt, um die nächstschlechtere Waffe entsprechend der Reihenfolge des Spielers zu wählen.

7. Ersetze CG_OutOfAmmoChange()

Scrolle etwas weiter nach unten und ersetze CG_OutOfAmmoChange durch folgende Funktion (Z 1630):

     void CG_OutOfAmmoChange( void ) { 
       int  i; 
       int  weap; 

       cg.weaponSelectTime = cg.time; 
       weap = weaponRawOrder[NUM_WEAPS - 1]; //WarZone -- pick the best weapon they have 

       for ( i = 0 ; i < NUM_WEAPS ; i++, weap = PrevWeapon( weap )) { 
         if ( CG_WeaponSelectable( weap ) ) { 
           if (weap != WP_GAUNTLET) 
           { 
             cg.weaponSelect = weap; 
             return; 
           } 
         } 
       } 

       cg.weaponSelect = WP_GAUNTLET; 
     } 

Hier wird, sobald die aktuelle Waffe ohne Ammo ist, die nächstbeste Waffe ausgewählt.

Das war alles in cg_weapons.c

8. Ändere CG_ItemPickUp

Öffne nun cg_event.c und suche die Funktion CG_ItemPickUp, ersetze sie durch folgende (Z 380):

     static void CG_ItemPickup( int itemNum, int isnewitem ) { //WarZone 
       cg.itemPickup = itemNum; 
       cg.itemPickupTime = cg.time; 
       cg.itemPickupBlendTime = cg.time; 
       // see if it should be the grabbed weapon 
       if ( bg_itemlist[itemNum].giType == IT_WEAPON && isnewitem ) //WarZone 
       { 
         // select it immediately 
         if ( cg_autoswitch.integer && bg_itemlist[itemNum].giTag != WP_MACHINEGUN ) { 
           if (RateWeapon( bg_itemlist[itemNum].giTag) > RateWeapon( cg.weaponSelect )) //WarZone 
           { 
             cg.weaponSelectTime = cg.time; 
             cg.weaponSelect = bg_itemlist[itemNum].giTag; 
           } 
         } 
       } 
     }

Wenn du dir den Code genau anschaust, wirst du feststellen, dass wir dem Funktionsheader von CG_ItemPickUp den Parameter "isnewitem" hinzugefügt haben. An dieser Stelle mutiert die Modifikation von "nur Client" zu "sowohl Client als auch Server". Das liegt vor allem daran, dass der Client keine einfache Möglichkeit hat, festzustellen, ob ein Item gerade aufgenommen wurde, oder schon länger im Inventar liegt ( man könnte z.B. eine "oldweapons" Variable anlegen, und dann immer die aktuelle Waffenmenge mit der alten Vergleichen, aber das scheitert spätestens wenn der Spieler stirbt und somit alle Waffen verliert).

9. Ein neues Event "Item Aufnahme"

Suche nun die Funktion CG_EntityEvent und füge darin folgendes um das EV_ITEM_PICKUP Event (Z 655):

         case EV_ITEM_PICKUP2: 
         DEBUGNAME("EV_ITEM_PICKUP2"); 
           es->number = es->otherEntityNum; //this is a bit of a hack.. but it works GRREAT! 
         case EV_ITEM_PICKUP2: 
       DEBUGNAME("EV_ITEM_PICKUP2"); 
         es->number = es->otherEntityNum; //this is a bit of a hack.. but it works GRREAT!
	
	case EV_ITEM_PICKUP:
		DEBUGNAME("EV_ITEM_PICKUP");
		{
			gitem_t	*item;
			int		index;
			int   isnewitem; //WarZone 

			index = es->eventParm;		// player predicted
			isnewitem = es->otherEntityNum2; //WarZone 

			if ( index < 1 || index >= bg_numItems ) {
				break;
			}
			item = &bg_itemlist[ index ];

			// powerups and team items will have a separate global sound, this one
			// will be played at prediction time
			if ( item->giType == IT_POWERUP || item->giType == IT_TEAM) {
				trap_S_StartSound (NULL, es->number, CHAN_AUTO,	cgs.media.n_healthSound );
			} else if (item->giType == IT_PERSISTANT_POWERUP) {
#ifdef MISSIONPACK
			switch (item->giTag ) {
				case PW_SCOUT:
					trap_S_StartSound (NULL, es->number, CHAN_AUTO,	cgs.media.scoutSound );
				break;
				case PW_GUARD:
					trap_S_StartSound (NULL, es->number, CHAN_AUTO,	cgs.media.guardSound );
				break;
				case PW_DOUBLER:
					trap_S_StartSound (NULL, es->number, CHAN_AUTO,	cgs.media.doublerSound );
				break;
				case PW_AMMOREGEN:
					trap_S_StartSound (NULL, es->number, CHAN_AUTO,	cgs.media.ammoregenSound );
				break;
			}
#endif
		} else {
			trap_S_StartSound (NULL, es->number, 
			CHAN_AUTO,	
			trap_S_RegisterSound( item->pickup_sound, 
			qfalse ) );
		}

		// show icon and name on status bar
		if ( es->number == cg.snap->ps.clientNum ) {
			CG_ItemPickup( index, isnewitem ); //WarZone 
		}
		}
		break;

Auch hier beziehen wir uns wieder auf neuen Servercode. es->otherEntityNum2 muss Serverseitig gesetzt werden, um dem Clientcode mitzuteilen, ob das aufgenommene Item ein neues ist oder nicht. Das neue Event EV_ITEM_PICKUP2 werde ich weiter unten erklären.

10. Eine weitere Modifikation zur Item-aufnahme

Srolle nun bis EV_GLOBAL_ITEM_PICKUP nach unten und ändere folgendes:

      case EV_GLOBAL_ITEM_PICKUP: 
       DEBUGNAME("EV_GLOBAL_ITEM_PICKUP"); 
       { 
        gitem_t *item; 
        int  index; 

        index = es->eventParm;  // player predicted 

        if ( index < 1 || index >= bg_numItems ) { 
          break; 
        } 
        item = &bg_itemlist[ index ]; 
        // powerup pickups are global 
        trap_S_StartSound (NULL, cg.snap->ps.clientNum, 
        CHAN_AUTO, 
        trap_S_RegisterSound( item->pickup_sound ) ); 

        // show icon and name on status bar 
        if ( es->number == cg.snap->ps.clientNum ) { 
          CG_ItemPickup( index, 1 ); //WarZone 
        } 
       } 
       break;

Das war alles in cg_event.c

11. Ändere das Verhalten von Touch_Item()

Öffne nun g_items.c im game Projekt und suche die Funktion Touch_Item. Hier werden wir nur einige kleine Änderungen vornehmen (Z 400):

     void Touch_Item (gentity_t *ent, gentity_t *other, trace_t *trace) { 
      int   respawn;
	  qboolean	predict; 
      int   had = 1; //WarZone 
...
      case IT_WEAPON: 
         //WarZone 
         if ( other->client->ps.stats[STAT_WEAPONS] & (1 << ent->item->giTag) ) 
           had = 1; 
         else 
           had = 0; 
       respawn = Pickup_Weapon(ent, other);
//	   predict = qfalse; 
       break; 

Entferne nun das Stück if (predict) so das es wie folgt aussieht (Z 460):

      if ( 0 ) { //WarZone 
        //do nothing... 
      } else { 
         //WarZone 
         gentity_t *event; 

         event = G_TempEntity(ent->s.origin, EV_ITEM_PICKUP2); //WarZone
         event->s.eventParm = ent->s.modelindex; 
         event->s.otherEntityNum = other->s.number; 
         event->s.otherEntityNum2 = !had; //WarZone -- used to tell cgame if its a new weapon 
         event->r.svFlags |= SVF_BROADCAST; //broadcast it to everyone

//         G_AddEvent( other, EV_ITEM_PICKUP, ent->s.modelindex ); //kill this line 
      } 
     ... 
     }

Das "..." bedeutet "irgendwelcher Code dazwischen", also schreib das ja nicht in deinen Code.

Wenn du aufmerksam warst, hast du festgestellt, dass wir die Item-aufnahme Logik drastisch verändert haben. Der (erste und wichtigste) Grund dafür ist, dass vorhergesagte Events nur ein Argument haben dürfen. Der zweite Grund ist die Unzuverlässigkeit der Event-vorhersage Struktur. Bist du schon einmal in Quake3 über ein Item gelaufen, aber der Waffenaufnahme-sound kam nicht ? Das liegt daran, das die Event-vorhersage Struktur überflutet wird, und dein Event verloren geht.

Wir schicken die EV_ITEM_PICKUP Events über eine externe temporäre entity um sicherzustellen, dass diese Nachrichten nicht in der Hitze des Gefechts untergehen.
g_items.c ist damit eredigt.

12. Den Server anzeigen lassen, dass er individuelle Waffenordnung unterstützt

Das wird unsere letzte Ergänzung. Öffne g_main.c und füge folgendes an das Ende der cvar Tabelle (Z 160):

    { &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse},
	{ &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse},
	{ &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse},

	{ &g_rankings, "g_rankings", "0", 0, 0, qfalse},
    { NULL, "g_supportsWeaponOrder", "1", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse } //WarZone
achte auch hier wieder auf das Komma am Ende der g_rankings Zeile.

Diese cvar erleichtert es Spielern, diejenigen Server zu finden, welche die cg_weaponOrder variable unterstützen. Das ist wichtig, da der neue Clientcode nicht mehr zu Standard Q3A Servern kompatibel ist.

Fertig mit g_main.c, und fast fertig mit der mod...

Wir haben vergessen, EV_ITEM_PICKUP2 zu definieren ! Upss.. also fix noch bg_public.h geöffnet und folgendes angefügt:

    EV_TAUNT_NO,
	EV_TAUNT_FOLLOWME,
	EV_TAUNT_GETFLAG,
	EV_TAUNT_GUARDBASE,
	EV_TAUNT_PATROL,
    EV_ITEM_PICKUP2   // WarZone -- used to create a temp entity to send the pickup message 

} entity_event_t;
das Komma, du weisst...

So, das wars endgültig !

13. Schlussbemerkung

Du musst sowohl den game als auch den cgame code kompilieren, damit alles läuft. Und wie funktioniert die neue cg_weaponOrder variable ? Folgendermassen: der Anfangswert ist "1/2/3/4/5/6/7/8/8", dieser legt die Reihenfolge fest, in der die Waffen des Spielers gewechselt werden sollen. Der Anfangswert wird ein Wechselverhalten vergleichbar mit dem in Quake2 erzeugen (wenn neueWaffe > alteWaffe dann waffeWechseln): wenn du eine Shotgun(3) hast und einen Rocketlauncher(5) aufnimmst, wird zum Rocketlauncher gewechselt. Wenn du einen Rocketlauncher hast, und einen Granadelauncher aufnimmst, bleibt der Rocketlauncher ausgewählt.

Jede Waffe hat einen spezifischen Index:

1 = Gauntlet
2 = Machine Gun
3 = Shot Gun
4 = Grenade Launcher
5 = Rocket Launcher
6 = Lightning Gun
7 = Rail Gun
8 = Plasma Gun
9 = BFG 10K

Ich persönlich bevorzuge "1/2/3/4/6/8/5/7/9", was in etwa der Reihenfolge von Quake2 entspricht.

Wenn dein Mod mehr als 9 Waffen enthält, musst du die #define NUM_WEAPS Zeile in cg_local.c entsprechend ändern. Der cg_weaponOrder String muss wie folgt formattiert werden:

     xx\xx\xx 
     xx/xx/xx 
     x/xx\xx 
     xx\x/xx 
     x/x\x 
     x/x/x 
     x\x\x 

D.h. zwischen den Seperatoren ('/' oder '\') dürfen bis zu zwie Ziffern stehen. Doppelte oder ungültige Einträge werden ignoriert und fehlende Einträge werden am Anfang der Liste eingefügt. Der String "2/2/3/3/4/5/6/7/8/8/8/9" erzeugt das selbe resultat wie "1/2/3/4/5/6/7/8/9", da die fehlende 1 am Anfang eingefügt wird und die doppelten Werte ignoriert werden.

Dieser Code unterliegt dem Copyright (c) 2000 von Mark "WarZone" Smeltzer. Du darfst den Code frei verwenden, so lange du auf den Autor verweist.

<< Tutorial 15 | | Themenübersicht >>