Autor: Nicolai 'Prefect' Haehnle
Ihr habt alle drauf gewartet - jetzt ist es endlich da: Ein Nachtsichtgerät mit Infrarot-Effekt! Dieses Tutorial wird ein reichlich großes Tutorial und beinhaltet so ziemlich alles von der Item-Erstellung in der Entity-DLL bis zum einer Batterie-Anzeige im HUD (Client-DLL), aber schaut's euch selber an!
Es basiert auf dem Advanced NVG Tutorial von Badger auf HLPP, also solltest du ihm Credits geben (teilweise kann man seine Arbeit noch erkennen, vor allem in CItemNVG).
Shockman gab mir einige SEHR hilfreiche Tips, die mich veranlassten, etwa 80% des 4. Abschnitts neu zu schreiben, aber es hat sich gelohnt. Danke!
Ich hab das Ganze in 4 Teile und Zusatzinfos geteilt:
Nach jedem Abschnitt solltest du lauffähige Dlls erhalten, also kannst du jeden Schritt testen.
Okay, fangen wir mit dem einfachen Teil an: dem Item-Erstellen. Wir werden nur die mp.dll verändern.
Zuallererst braucht der Spieler einige Variabeln um sich zu merken:
a) ob er das Nachtsichtgerät hat.
b) ob es gerade angeschaltet ist.
Zweitens müssen wir eine Funktion definieren, die von außerhalb aufgerufen wird, um das NVG an- und auszuschalten. Öffne also die player.h, scroll runter zu Zeile 73 und füge folgendes hinzu:
class CBasePlayer : public CBaseMonster { public: // advanced NVG BOOL m_fNVG; // do they have the NVG? BOOL m_fNVGActivated; // is it activated? void NVGToggle(BOOL activate); // advanced NVG int m_iPlayerSound; // the index of the sound list slot reserved for this player
Ich habe mich entschlossen, alles an den Anfang der Definition zu schreiben. Ist Geschmackssache…
Diese Variablen müssen irgendwo initialisiert werden. Öffne player.cpp, gehe zu Zeile 3201 (CBasePlayer::Spawn()) und füge folgendes hinzu:
m_lastx = m_lasty = 0; // advanced NVG m_fNVG = FALSE; m_fNVGActivated = FALSE; // advanced NVG g_pGameRules->PlayerSpawn( this ); }
Es ist wichtig, dass du den neuen Code vor dem Aufruf von g_pGamerules→PlayerSpawn() einfügst, weil das die Funktion ist, wo dem Spieler die Items gegeben werden. Stell dir vor, die PlayerSpawn() Funktion gibt dem Spieler das Nachtsichtgerät und wir entfernen es gleich danach mit diesem Stückchen Code - wäre doch ziemlich blöd!
Wir definieren nun die NVGToggle() Funktion. Die ist zur Zeit nur ein Skelett, aber wir fügen sie trotzdem hinzu. Ich schreibe sie so um Zeile 3730 (immer noch player.cpp) rein, aber das ist nicht so wichtig. Hier ist sie:
// advanced NVG void CBasePlayer::NVGToggle(BOOL activate) { if (!m_fNVG) return; if (activate && !m_fNVGActivated) { m_fNVGActivated = TRUE; // activate the NVG } else if (!activate && m_fNVGActivated) { m_fNVGActivated = FALSE; // deactivate the NVG } } // advanced NVG
Vielleicht möchtest du hier einen Sound ausgeben. Dafür schreibst du
EMIT_SOUND(pPlayer->edict(), CHAN_WEAPON, "your.wav", 0.8, ATTN_NORM );
in diese Funktion. Vergiss nicht, den Sound ins PreCache zu übernehmen: Öffne client.cpp und schreib in die ClientPrecache() so um Zeile 540:
// advanced NVG PRECACHE_SOUND("your.wav"); // advanced NVG
Wir sind noch nicht fertig mit dem Spieler-Code. Du willst doch nicht, dass tote Spieler das NVG haben, oder? Also schreiben wir ein paar Zeilen in die CBasePlayer::Killed() (die wird immer dann aufgerufen, wenn ein Spieler stirbt), die das Nachtsichtgerät deaktivieren und entfernen. Zeile 818:
if ( m_pTank != NULL ) { m_pTank->Use( this, this, USE_OFF, 0 ); m_pTank = NULL; } // advanced NVG // We don't want NVG while dead if (m_fNVG) { if (m_fNVGActivated) { NVGToggle(FALSE); // deactivate the NVG if necessary... } m_fNVG = FALSE; // ... and remove it } // advanced NVG // this client isn't going to be thinking for a while, so reset the sound until they respawn pSound = CSoundEnt::SoundPointerForIndex( CSoundEnt::ClientSoundIndex( edict() ) ); {
Soviel zum Spieler - nun kommen wir zum eigentlichen NVG-Entity. Es basiert auf item_longjump, mit wenigen Modifikationen. Öffne items.cpp und füge diesen Code am Ende der Datei ein:
// advanced NVG class CItemNVG : public CItem { void Spawn( void ) { Precache( ); SET_MODEL(ENT(pev), "models/w_longjump.mdl"); // you'll want to change this CItem::Spawn( ); } void Precache( void ) { PRECACHE_MODEL("models/w_longjump.mdl"); // you'll want to change this PRECACHE_SOUND("buttons/blip1.wav"); } BOOL MyTouch( CBasePlayer *pPlayer ) { if ( pPlayer->m_fNVG ) // does the player already have it { return FALSE; // if so, quit } if ( ( pPlayer->pev->weapons & (1<<WEAPON_SUIT) ) ) { pPlayer->m_fNVG = TRUE; // player now has night vision goggles // make the client.dll print out the symbol on ammo history MESSAGE_BEGIN( MSG_ONE, gmsgItemPickup, NULL, pPlayer->pev ); WRITE_STRING( STRING(pev->classname) ); MESSAGE_END(); // play a sound to tell the player he's picked up the NVG EMIT_SOUND(pPlayer->edict(), CHAN_WEAPON, "buttons/blip1.wav", 0.8, ATTN_NORM ); return TRUE; } return FALSE; } }; LINK_ENTITY_TO_CLASS( item_nvg, CItemNVG ); // advanced NVG
Nun, was macht dieser Code?
Die Spawn() Funktion ruft PreCache() auf, damit gebrauchte Models/Sounds später benutzt werden können. Dann bestimmt es das Model, das der Spieler später sehen wird und schließlich ruft es die CItem::Spawn() auf, die das ganze wichtige Zeugs erledigt - aber darum brauchen wir uns nicht zu kümmern.
Precache() stellt sicher, dass die Models für das Item und der Sound, den wir später abspielen werden, bereits im Speicher sind. Wenn man sowas nicht precached, bekommt man Abstürze wie „Bad index of edict“. Schlecht…
Die eigentliche Arbeit verrichtet die Funktion MyTouch(). Wie der Name schon sagt, wird diese Funktion aufgerufen, wenn der Spieler das Item berührt. Sie lässt den Spieler das Item aufnehmen, wenn möglich. TRUE wird zurückgegeben, wenn der Spieler das Item aufnehmen konnte, und FALSE, wenn nicht. Zuerst wird überprüft, ob der Spieler bereits das NVG hat und beendet, falls dem so ist. Außerdem wird es Spielern ohne HEV-Suit nicht erlaubt, das NVG zu bekommen. Wenn alle Bedingungen erfüllt sind, wird dem Spieler das NVG gegeben, ein Symbol am rechten Rand über der Ammo-Anzeige angezeigt und ein Sound abgespielt.
Du solltest unser neues Entity irgendwo precachen, sonst stürzt HL ab, wenn du versuchst, es dir mit give in der Konsole zu geben. Öffne also weapons.cpp und gehe zu Zeile 321(in der W_Precache() Funktion, die am Anfang jeden Levels durch das worldspawn-Entity aufgerufen wird). Dort fügst du diesen Code ein:
UTIL_PrecacheOther( "item_longjump" ); // advanced NVG UTIL_PrecacheOther( "item_nvg" ); // advanced NVG // shotgun UTIL_PrecacheOtherWeapon( "weapon_shotgun" ); UTIL_PrecacheOther( "ammo_buckshot" );
Jetzt fehlt noch ein Client-Command, das das NVG an und ausschaltet. Client-Commands werden an die Funktion ClientCommand() in client.cpp (um Zeile 330) geschickt, also schreiben wir hier ein paar Zeilen rein:
else if (FStrEq(pcmd, "lastinv" )) { GetClassPtr((CBasePlayer *)pev)->SelectLastItem(); } // advanced NVG else if (FStrEq(pcmd, "nightvision" )) { // calls the NVG routine CBasePlayer *pPlayer = GetClassPtr((CBasePlayer *)pev); if (pPlayer->m_fNVG) { if (pPlayer->m_fNVGActivated) pPlayer->NVGToggle(FALSE); else pPlayer->NVGToggle(TRUE); } } // advanced NVG else if ( g_pGameRules->ClientCommand( GetClassPtr((CBasePlayer *)pev), pcmd ) ) { // MenuSelect returns true only if the command is properly handled, so don't print a warning }
Sollte selbsterklärend sein. Wenn der give-Befehl „nightvision“ ist, dann hol dir einen ClassPointer auf den Spieler und rufe NVGToggle() mit den richtigen Parametern auf, falls der Spieler noch kein Nachtsichtgerät hat.
Soviel zum Coding-Teil. Es gibt aber noch ein paar wichtige Dinge zu tun. Das Sprite für's HUD fehlt nämlich noch!
Kopier die hud.txt aus dem valve/sprites-Verzeichnis in das sprites-Verzeichnis deines Mods. Dann füge folgendes hinzu:
125 // +2 for advanced NVG (original: 123) selection 320 320hud1 160 160 80 20
Die modifizierte Nummer gibt die Anzahl an definierten Sprites in der Datei an. Klar, dass die geändert werden muss. Nun füg das Sprite für die 320er Auflösung hinzu:
item_longjump 320 320hud2 88 52 20 20 // advanced NVG item_nvg 320 320hud2 88 52 20 20 // advanced NVG grenade 320 320hud2 36 34 18 18
Wie du siehst, benutze ich die selben Sprites wie für das Longjump-Modul. Nun brauchen wir das ganze nochmal für die 640er und höher Auflösung:
item_longjump 640 640hud2 176 96 44 44 // advanced NVG item_nvg 640 640hud2 176 96 44 44 // advanced NVG grenade 640 640hud7 48 96 24 24
Okay, soviel zum Sprite. Wir haben's gleich!
Du willst doch sicher eine Taste für dein NVG belegen, oder? Das will natürlich auch der Spieler deines Mods. Um es ihm ein bisschen einfacher zu machen, fügst du es zu den Tastenbelegungen im „Controls“-Menü hinzu. Dazu kopierst du die valve/gfx/shell/kb_act.lst ins gfx/shell-Verzeichnis deines Mods. Öffne die Datei und schreib
"nightvision" "Toggle NightVisionGoggles"
an den Anfang der Datei.
Tja, und damit hast du dich auch schon durch den ersten von 4 Abschnitten gekämpft! Mach dich für den nächsten bereit…
Nun wirst du etwas über Client.dll-Coding und Messages lernen. Wir werden den flackernden Nachtsichtgerät-Effekt erstellen (wie z.B. der in OpForce).
Hinweis: OpForce benutzt 4 Sprites, um diesen Effekt darzustellen. Ich weiß nicht, wofür die alle sind und benutze deshalb nur einen.
Hinweis 2: Wenn du noch nie client.dll-Coding gemacht hast, stell die Inline-Expansion auf aus, sonst wirst du deine Arbeit nicht genießen können.
Bevor's losgeht noch ein paar Dinge:
Um das Sprite anzuzeigen, brauchen wir ein neues HUD-Element. Es muss zuerst in der hud.h definiert werden. Öffne sie und gehe zu Zeile 190:
int MsgFunc_Train(const char *pszName, int iSize, void *pbuf); private: HSPRITE m_hSprite; int m_iPos; }; // advanced NVG // //----------------------------------------------------- // class CHudNVG: public CHudBase { public: int Init( void ); int VidInit( void ); int Draw(float flTime); int MsgFunc_NVG(const char *pszName, int iSize, void *pbuf); int MsgFunc_NVGActivate(const char *pszName, int iSize, void *pbuf); private: HSPRITE m_hFlicker; int m_iNVG; int m_iOn; float m_flBattery; }; // advanced NVG // //----------------------------------------------------- // class CHudMOTD : public CHudBase { public: int Init( void ); int VidInit( void );
Hier werden die obligatorischen Init(), VidInit() und Draw() Funktionen deklariert und die beiden Messages, die wir für die Kommunikation zwischen mp.dll und client.dll benutzen.m_flBattery wird noch nicht benutzt, aber ich hab es schon reingeschrieben, damit wir später weniger Code ändern müssen.
Immernoch in der hud.h, schreibst du die einzige Instanz von CHudNVG in die Haupt-HUD-Klasse, CHud. Zeile 608:
CHudAmmoSecondary m_AmmoSecondary; CHudTextMessage m_TextMessage; CHudStatusIcons m_StatusIcons; // advanced NVG CHudNVG m_NVG; // advanced NVG void Init( void ); void VidInit( void );
Bevor wir mit dem wirklich Interessanten anfangen können, müssen wir noch ein paar Dinge in die hud.cpp schreiben, um die NVG Klasse zu initialisieren. In Zeile 160 schreibst du dies hier:
m_SayText.Init(); m_Menu.Init(); // advanced NVG m_NVG.Init(); // advanced NVG MsgFunc_ResetHUD(0, 0, NULL ); }
Damit wird die normale Init()-Funktion aufgerufen. Aber wir müssen auch die VidInit() aufrufen. Also schreiben wir in Zeile 290:
m_TextMessage.VidInit(); m_StatusIcons.VidInit(); // advanced NVG m_NVG.VidInit(); // advanced NVG }
Soweit der langweilige, aber nötige Teil. Lass uns mal ein bisschen wirkliches Coding versuchen. Erstelle eine neue Datei namens nvg.cpp und füge sie deinem Projekt hinzu. Wir werden den Code Stück für Stück aufbauen. Fangen wir hiermit an:
#include "hud.h" #include "cl_util.h" #include <string.h> #include <stdio.h> #include <time.h> #include "parsemsg.h" DECLARE_MESSAGE(m_NVG, NVGActivate ) DECLARE_MESSAGE(m_NVG, NVG )
Wir fügen die nötigen #include-Befehle ein und deklarieren unsere eigenen Messages. Das DECLARE_MESSAGE-Macro erstellt eine globale, exportierte hook-Funktion namens__MsgFunc_XXX(), die von der Engine aufgerufen wird und die die Ausführung an die CHudNVG Memberfunktionen weitergibt.
int CHudNVG::Init(void) { HOOK_MESSAGE( NVGActivate ); HOOK_MESSAGE( NVG ); m_iNVG = 0; m_iOn = 0; m_flBattery = 0; m_iFlags = 0; gHUD.AddHudElem(this); srand( (unsigned)time( NULL ) ); return 1; }
HOOK_MESSAGE ist ein Macro, das eine Engine-Callback-Funktion aufruft, um der Engine den Namen und die Hook-Funktion für unsere neue Message mitzuteilen. Dann initialisieren wir unsere Variabeln.
m_iFlags sagt den Haupt-Hud-Funktionen, ob dieses HUD-Element dargestellt werden soll oder nicht, sowie einige andere Sachen, die uns hier nicht weiter interessieren. Weil der Spieler ohne Nachtsichtgerät startet, setzen wir sie auf Null. Wir müssen auch noch zu der Liste der HUD-Elemente hinzugefügt werden, was der nächste Funktionsaufruf erledigt.
Schließlich initialisieren wir den Zufallszahlengenerator (wir benutzen Zufallszahlen in der Draw()-Funktion) und geben 1 zurück (= wir waren erfolgreich).
Fügen wir nun VidInit() hinzu, die verantwortlich ist für das Laden der Sprites und ähnlichem.
int CHudNVG::VidInit(void) { // this is the NVG effect sprite m_hFlicker = LoadSprite("sprites/nvg.spr"); return 1; }
Nein, hierzu werde ich NICHTS sagen. Ist doch wirklich selbsterklärend!!!
Nun brauchen wir eine Zeichen-Funktion. Weil man keine Sprites größer als 256*256 machen kann (glaub ich wenigstens), ist der zeichnende Teil der Funktion in einer for()-Schleife eingebaut, mit einigen Vorbereitungen vorher.
int CHudNVG::Draw(float fTime) { if (m_iOn) { int x, y, w, h; int frame; SPR_Set(m_hFlicker, 10, 100, 10 ); // play at 15fps frame = (int)(fTime * 15) % SPR_Frames(m_hFlicker); w = SPR_Width(m_hFlicker,0); h = SPR_Height(m_hFlicker,0); for(y = -(rand() % h); y < ScreenHeight; y += h) { for(x = -(rand() % w); x < ScreenWidth; x += w) { SPR_DrawAdditive( frame, x, y, NULL ); } } } return 1; }
Zuerst überprüfen wir hier, ob das NVG aktiviert ist. Dann bereiten wir das Spritezeichnen mit SPR_Set() vor. Die letzten drei Werte der Funktion sind die Werte für rot, grün und blau des Sprites. Diese Werte müssen irgendwo zwischen 0 (=transparent) und 255 (=Vollfarbe) liegen.
Dann berechnen wir die zu zeichnende Frame-Nummer (SPR_Frames() gibt die Anzahl der Frames im Sprite zurück) und fragen die Engine nach der Sprite-Größe.
Die for()-Schleife zeichnet dann das Sprite. Sie verschiebt das erste Sprite in x und y mit einer Zufallszahl, um einen zufälligeren Effekt zu erzeugen.
Die letzte Sache auf der Client-Seite sind die Message-Funktionen. Hier ist MsgFunc_NVG():
int CHudNVG::MsgFunc_NVG(const char *pszName, int iSize, void *pbuf) { BEGIN_READ( pbuf, iSize ); // update NVG data m_iNVG = READ_BYTE(); m_flBattery = (float)READ_BYTE(); if (m_iNVG) m_iFlags |= HUD_ACTIVE; else m_iFlags &= ~HUD_ACTIVE; m_iOn = 0; return 1; }
Zuallererst ruft diese Funktion BEGIN_READ() auf, was ein Vorbereitungsfunktion ist und am Anfang jeder Message-Funktion aufgerufen werden muss.
Dann liest sie, ob der Spieler das NVG hat und wie der Batteriestatus ist (diese Funktion ist zur Zeit nur ein Dummy), und updated die m_iFlags dementsprechend.
Außerdem schaltet sie das NVG ab, was zwar nicht wirklich notwendig ist, aber sicher ist sicher.
Die MsgFunc_NVGActivate() ist ziemlich ähnlich:
int CHudNVG::MsgFunc_NVGActivate(const char *pszName, int iSize, void *pbuf) { BEGIN_READ( pbuf, iSize ); // update NVG data m_iOn = READ_BYTE(); m_flBattery = (float)READ_BYTE(); return 1; }
Sie liest den NVG-Status und den Batteriestatus.
Soweit die Client-Seite…
Nächste Sache ist das Definieren der Batteriestatusvariable, m_flNVGBattery in der Entity-DLL. Ist bloß ein Dummy bis zum dritten Kapitel, aber da es mit den Messages mitgeschickt wird, fügen wir's bereits hinzu. Öffne die player.h und füge folgendes in Zeile 74 hinzu:
// advanced NVG BOOL m_fNVG; // do they have it? BOOL m_fNVGActivated; // is it activated? float m_flNVGBattery; void NVGToggle(BOOL activate); // advanced NVG
Öffne dann die player.cpp und initialisiere sie in der CBasePlayer::Spawn() Funktion (Zeile 3202):
// advanced NVG m_fNVG = FALSE; m_fNVGActivated = FALSE; m_flNVGBattery = 100; // advanced NVG
Nun müssen wir irgendwo die Kommunikations-Messages aufrufen. Zuallererst muss die Server-Seite unsere neuen Messages registrieren (das Gegenstück zuDECLARE_MESSAGE() / HOOK_MESSAGE() in der client.dll). Füge folgendes in der player.cpp hinzu (Zeile 190):
int gmsgSetFOV = 0; int gmsgShowMenu = 0; // advanced NVG int gmsgNVG = 0; int gmsgNVGActivate = 0; // advanced NVG LINK_ENTITY_TO_CLASS( player, CBasePlayer );
Diese Variabeln werden unsere eindeutigen Message-Nummern beinhalten. Die werden in der MESSAGE_BEGIN() gebraucht, um der Engine mitzuteilen, welche Message wir senden.
Wir wollen diese Nummern irgendwo initialisieren, also gehen wir in Zeile 3280 und fügen dies hinzu:
gmsgFade = REG_USER_MSG("ScreenFade", sizeof(ScreenFade)); gmsgAmmoX = REG_USER_MSG("AmmoX", 2); // advanced NVG gmsgNVG = REG_USER_MSG("NVG", 2); gmsgNVGActivate = REG_USER_MSG("NVGActivate", 2); // advanced NVG m_iUpdateTime = 5; // won't update for 1/2 a second if ( gInitHUD ) m_fInitHUD = TRUE; }
Wir übergeben der Engine den Namen der Message (der der gleiche sein muss wie auf der Client-Seite) und die Länge der Message. Da wir den derzeitigen an/aus-Status und den Batteriestatus senden, sind sie 2 Bytes lang (wenn du nicht wissen solltest, wie lang die Message ist, zum Beispiel weil du einen String übergibst oder die Zahl der Parameter variiert, gib -1 als Länge an).
Wir wollen, dass die NVG Message gesendet wird, wenn der Spieler das NVG bekommt, also öffne items.cpp, geh zu Zeile 337 und modifiziere dort den Code:
// advanced NVG extern int gmsgNVG; // we need to send this message class CItemNVG : public CItem {
gmsgNVG ist in einer anderen Quelldatei definiert, also müssen wir ein external define einsetzen. Scroll ein bisschen runter und füge folgendes hinzu, um die Message zu schicken:
// make the client.dll print out the symbol on ammo history MESSAGE_BEGIN( MSG_ONE, gmsgItemPickup, NULL, pPlayer->pev ); WRITE_STRING( STRING(pev->classname) ); MESSAGE_END(); // ... and tell them that we now have an NVG MESSAGE_BEGIN( MSG_ONE, gmsgNVG, NULL, pPlayer->pev ); WRITE_BYTE( 1 ); WRITE_BYTE( pPlayer->m_flNVGBattery ); MESSAGE_END( ); // play a sound to tell the player he's picked up the NVG EMIT_SOUND(pPlayer->edict(), CHAN_WEAPON, "buttons/blip1.wav", 0.8, ATTN_NORM ); return TRUE; }
Die WRITE_BYTE() Aufrufe müssen in der selben Reihenfolge stehen wie die READ_BYTE() Aufrufe in der client.cll.
Natürlich müssen wir den Status auch updaten, wenn der Spieler getötet wurde. Öffne wieder die player.cpp und ändere den Code in CBasePlayer::Killed() (Zeile 818):
// advanced NVG // We don't want no NVG while dead if (m_fNVG) { if (m_fNVGActivated) { NVGToggle(FALSE); // deactivate the NVG if necessary... } m_fNVG = FALSE; // ... and remove it MESSAGE_BEGIN(MSG_ONE, gmsgNVG, NULL, pev); WRITE_BYTE( 0 ); // we don't have NVG any more WRITE_BYTE( 0 ); MESSAGE_END( ); } // advanced NVG
Fast fertig! Jetzt müssen wir nur noch NVGToggle() modifizieren, um eine Update-Message an den Client zu senden. Geh dorthin, wo du sie eingefügt hast (bei mir Zeile 3730, player.cpp) und schreib:
// advanced NVG void CBasePlayer::NVGToggle(BOOL activate) { if (!m_fNVG) return; if (activate && !m_fNVGActivated) { m_fNVGActivated = TRUE; // inform the client about the change MESSAGE_BEGIN(MSG_ONE, gmsgNVGActivate, NULL, pev); WRITE_BYTE( 1 ); WRITE_BYTE( m_flNVGBattery ); MESSAGE_END( ); } else if (!activate && m_fNVGActivated) { m_fNVGActivated = FALSE; // inform the client about the change MESSAGE_BEGIN(MSG_ONE, gmsgNVGActivate, NULL, pev); WRITE_BYTE( 0 ); WRITE_BYTE( m_flNVGBattery ); MESSAGE_END( ); } } // advanced NVG
Puh… Soviel zum zweiten Kapitel. Mach dich bereit für's nächste!!!
Immer noch dabei? Gut. Wir brauchen jetzt keine Messages mehr hinzuzufügen, die sind alle schon fertig. Aber wir müssen noch Code zur Server und zur Client-Seite dazuschreiben: Beide müssen über den Batteriestatus auf dem Laufenden sein.
Ich mach die Server-Seite zuerst. Wir brauchen eine neue Variable und Memberfunktion der CBasePlayer im Server. Die Variable wird beinhalten, wann wir zuletzt die Batterie erneuert haben, und die Funktion wird die Arbeit erledigen. Schreib folgendes in Zeile 74 der player.h:
// advanced NVG BOOL m_fNVG; // do they have it? BOOL m_fNVGActivated; // is it activated? float m_flNVGBattery; float m_flNVGUpdate; void NVGToggle(BOOL activate); void NVGUpdate(); // advanced NVG
Okay, unsere neue Variable muss noch initialisiert werden, also öffne player.cpp und geh zu Zeile 3202:
// advanced NVG m_fNVG = FALSE; m_fNVGActivated = FALSE; m_flNVGBattery = 100; m_flNVGUpdate = gpGlobals->time; // advanced NVG
Bevor wir NVGUpdate() definieren, brauchen wir eine Stelle um sie aufzurufen. Gehe zu Zeile 2180 (das ist in der CBasePlayer::PreThink(), die jedes Frame aufgerufen wird) und füge folgenden Code hinzu:
if (pev->deadflag >= DEAD_DYING) { PlayerDeathThink(); return; } // advanced NVG // update the NVG state if (m_fNVG) NVGUpdate(); // advanced NVG // So the correct flags get sent to client asap. // if ( m_afPhysicsFlags & PFLAG_ONTRAIN ) pev->flags |= FL_ONTRAIN;
Ich hab's an diese Stelle geschrieben, damit es nicht aufgerufen wird, wenn der Spieler tot ist. Ist aber letztendlich egal, weil der Spieler das NVG sowieso nicht hat, wenn er tot ist (wir entfernen es schließlich in der CBasePlayer::Killed()).
Lass uns jetzt die NVGUpdate() Funktion schreiben. Sie überprüft, ob das NVG an ist und stellt den Batteriestatus dementsprechend ein. Sie schaltet außerdem das NVG aus, wenn der Batteriestatus Null erreicht. Hier ist sie (ich hab sie unter die NVGToggle() geschrieben, also ungefähr Zeile 3730 in der player.cpp):
// inform the client about the change MESSAGE_BEGIN(MSG_ONE, gmsgNVGActivate, NULL, pev); WRITE_BYTE( 0 ); WRITE_BYTE( m_flNVGBattery ); MESSAGE_END( ); } } void CBasePlayer::NVGUpdate() { float flDelta = gpGlobals->time - m_flNVGUpdate; m_flNVGUpdate = gpGlobals->time; // update battery level if (m_fNVGActivated) { m_flNVGBattery -= flDelta * NVG_DRAIN_PER_SECOND; if (m_flNVGBattery <= 0) { m_flNVGBattery = 0; NVGToggle(FALSE); } } else if (m_flNVGBattery < 100) { m_flNVGBattery += flDelta * NVG_RECHARGE_PER_SECOND; if (m_flNVGBattery > 100) { m_flNVGBattery = 100; // inform the client of the correct state, just to be sure MESSAGE_BEGIN(MSG_ONE, gmsgNVGActivate, NULL, pev); WRITE_BYTE( 0 ); // NVG not activated WRITE_BYTE( m_flNVGBattery ); MESSAGE_END( ); } } } // advanced NVG
Beachte wie der zweite Teil gelöst ist. Er sendet die Statusinformationen, sobald die Batterie voll ist, nur um 100%ig sicher zu gehen, dass der Client auf dem Laufenden ist. Damit diese Message nicht in jedem Frame gesendet wird, wenn die Batterie voll ist, wird der zweite Teil nur aufgerufen, wenn der Batteriestatus kleiner 100% ist.
Du wirst bemerkt haben, dass ich hier zwei Konstanten benutze. Diese müssen irgendwo definiert werden, oder? Da wir sie auch in der client.cll brauchen, schreibe ich sie in diecdll_dll.h, ungefähr Zeile 46:
#define WEAPON_SUIT 31 // advanced NVG #define NVG_DRAIN_PER_SECOND 0.75 #define NVG_RECHARGE_PER_SECOND 7.5 // advanced NVG #endif
Das bedeutet, dass die Batterie genau 133 Sekunden hält und sich in 13.3 Sekunden wieder vollständig aufgeladen hat. Natürlich kannst du die Werte an deine Bedürfnisse anpassen (höhere Werte bedeuten schnelleres Entladen/Aufladen).
Okay, der Server-Teil ist fertig. Lass und mit der Client-Seite anfangen. Zuallererst brauchen wir ein paar Sprites für die Batterie (du kannst meine benutzen, auch wenn sie nicht so toll sind), die du in dein sprites-Verzeichnis kopierst. Dann musst du die hud.txt deines Mods editieren. Das muss sein, weil die Spritedateien Leer- und Vollzustand gleichzeitig beinhalten. Das bedeutet, dass wir das Bounding-Rectangle von jedem dieser Sprites in Relation zur Datei wissen müssen. Jede Zeile (außer der ersten) dieser Datei hat das folgende Layout:
spritename resolution spritefile x y width height
Du musst zwei Zeilen reinschreiben mit dem selben Spritenamen: Eine mit Resolution auf 320 und eine mit 640. Ich werde dir an meinen Sprites zeigen, wie du's machen musst. Zuerst musst du den Anfang der Datei ändern, die die Anzahl der in der Datei definierten Sprites enthält:
129 // +6 for advanced NVG (original: 123) selection 320 320hud1 160 160 80 20
Wir werden vier neue Sprites hinzufügen (zwei haben wir bereits in Kapitel 1 hinzugefügt) für den Leer- und den Vollzustand, beide mit 320er und >640er Auflösung. Du wirst sehen, wo du's hinschreiben musst:
// advanced NVG item_nvg 320 320hud2 88 52 20 20 nvg_battery_empty 320 320nvgbat 0 0 24 16 nvg_battery_full 320 320nvgbat 0 16 24 16 // advanced NVG
Nun für die >640er Auflösung:
// advanced NVG item_nvg 640 640hud2 176 96 44 44 nvg_battery_empty 640 640nvgbat 0 0 40 32 nvg_battery_full 640 640nvgbat 0 32 40 32 // advanced NVG
Bevor wir jetzt mit dem richtigen Coding anfangen, musst du wissen, dass die cdll_dll.h, die wir vorhin geändert haben, auch von der client.dll benutzt wird. Dies wird in der cl_dll.h mit einem fest einprogrammierten Verzeichnis gelöst. Dieses musst du an deine Verzeichnisse anpassen, sonst findet der Compiler nicht die von uns festgelegten Konstanten. So geht's:
#include "../engine/cdll_int.h" // change this to fit your MOD #include "../yourmod-serverdll/cdll_dll.h" extern cl_enginefunc_t gEngfuncs;
Gut, lass uns mit dem Coding beginnen. Wir müssen neue Variabeln definieren, um Spriteinformationen zu speichern, also öffne die hud.h und füge folgendes hinzu (um Zeile 210):
// advanced NVG // //----------------------------------------------------- // class CHudNVG: public CHudBase { public: int Init( void ); int VidInit( void ); int Draw(float flTime); int MsgFunc_NVG(const char *pszName, int iSize, void *pbuf); int MsgFunc_NVGActivate(const char *pszName, int iSize, void *pbuf); private: HSPRITE m_hFlicker; HSPRITE m_hBatteryFull; HSPRITE m_hBatteryEmpty; wrect_t *m_prcBatteryFull; wrect_t *m_prcBatteryEmpty; int m_iBatteryY; int m_iWidth; int m_iNVG; int m_iOn; float m_flBattery; }; // advanced NVG
Zuallererst brauchen wir die Sprite-Handles für den Batteriestatus (voll bzw. leer). Da diese in Spritedateien mit mehreren Bildern sind (schau sie dir einfach mal mit einem Spriteviewer an), speichern wir auch ihre Bounding-Rectangles. Schließlich kommen noch zwei einfache Variabeln zum Speichern des y-Offsets des Batterie-Icons und der Breite unserer Sprites hinzu.
Diese Variabeln werden in der CHudNVG::VidInit() initialisiert, also öffne nvg.cpp und füge folgendes hinzu (um Zeile 40):
int CHudNVG::VidInit(void) { // this is the NVG effect sprite m_hFlicker = LoadSprite("sprites/nvg.spr"); // get the battery sprites int HUD_battery_full = gHUD.GetSpriteIndex( "nvg_battery_full" ); int HUD_battery_empty = gHUD.GetSpriteIndex( "nvg_battery_empty" ); m_hBatteryFull = gHUD.GetSprite(HUD_battery_full); m_hBatteryEmpty = gHUD.GetSprite(HUD_battery_empty); m_prcBatteryFull = &gHUD.GetSpriteRect(HUD_battery_full); m_prcBatteryEmpty = &gHUD.GetSpriteRect(HUD_battery_empty); m_iWidth = m_prcBatteryFull->right - m_prcBatteryFull->left; // calculate the y offset (using the flashlight sprite) int HUD_flashlight = gHUD.GetSpriteIndex( "flash_empty" ); wrect_t *prcFL; prcFL = &gHUD.GetSpriteRect( HUD_flashlight ); m_iBatteryY = (prcFL->bottom - prcFL->top)*2; return 1; };
Dieser Code holt sich den Index unserer neuen Sprites (mittels einer DLL-Funktion direkt aus der hud.txt). Diesen Index benutzen wir dann um den Engine-Sprite-Handle und das Bounding-Rectangle von unserem Sprite im Spritefile zu bekommen und berechnen dann seine Breite. Danach holen wir uns die Größe des Flashlight-Icons und benutzen es, um die Batterie-y-Position zu bestimmen.
Nun müssen wir nur noch die CHudNVG::Draw() ändern, um dieses Kapitel zu beenden. Also gehe zu Zeile 60 der nvg.cpp und füge dies hinzu:
int CHudNVG::Draw(float fTime) { // draw the battery if (m_iNVG) { int r, g, b, x, y, a, offset; wrect_t rc; // updates the battery state, calculate colors if (m_iOn) { m_flBattery -= gHUD.m_flTimeDelta * NVG_DRAIN_PER_SECOND; if (m_flBattery < 0) m_flBattery = 0; a = 255; } else { m_flBattery += gHUD.m_flTimeDelta * NVG_RECHARGE_PER_SECOND; if (m_flBattery > 100) m_flBattery = 100; a = MIN_ALPHA; } if (m_flBattery < 20) UnpackRGB(r,g,b, RGB_REDISH); else UnpackRGB(r,g,b, RGB_YELLOWISH); ScaleColors(r, g, b, a); // calculate x/y offsets, empty width x = ScreenWidth - m_iWidth - m_iWidth / 2; y = m_iBatteryY; offset = (m_flBattery * m_iWidth) / 100; // draw the empty part first... if (offset < m_iWidth) { SPR_Set(m_hBatteryEmpty, r, g, b); rc = *m_prcBatteryEmpty; rc.right -= offset; SPR_DrawAdditive( 0, x, y, &rc); } // and then the full part if (offset > 0) { SPR_Set(m_hBatteryFull, r, g, b); rc = *m_prcBatteryFull; rc.left += (m_iWidth - offset); SPR_DrawAdditive( 0, x + m_iWidth - offset, y, &rc); } } if (m_iOn) { int x, y, w, h; int frame; SPR_Set(m_hFlicker, 10, 100, 10 );
Boah, das ist der größte Codeabschnitt an einem Stück in diesem Tutorial, denke ich ;-) . Lass uns den mal genauer unter die Lupe nehmen.
Ganz schönes Stückchen Arbeit hast du jetzt schon geleistet! Bin beeindruckt :-) !!! Aber noch sind wir nicht ganz fertig. Mach dich bereit für den wichtigsten Teil des Tutorials…
Ihr habt alle darauf gewartet… ihr dachtet: „Was muss das für ein fettes Stück Code sein“… Aber ich sage euch: es ist soooo einfach! Es ist sogar das kürzeste Kapitel dieses Tutorials. Man muss nur drauf kommen ;-)
Ich werde dir etwas theoretisches Zeugs erklären, bevor wir mit dem Coding beginnen. Wenn du schon mal mit temporären Entitys gearbeitet hast (man sieht sowas oft im Code, beginnend mit MESSAGE_BEGIN(MSG_PVS, SVC_TEMPENTITY);), dann hast du vielleicht schon das kleine nette TE_ELIGHT Entity bemerkt. Was macht dieses Entity? Nun, es heftet ein Entity-Licht an ein gegebenes Entity. Ein Entity-Licht ist ein Licht, das nur Enititys beeinflusst und nicht die Welt. Nun, fragst du dich, ist es wirklich sooo einfach? Nein. Das Problem ist, dass normalerweise jeder Spieler temporäre Entitys sieht. Aber wir wollen, dass nur der Spieler mit den NVG sie sieht, oder?
Der Trick ist das Modifizieren von MESSAGE_BEGIN(), so dass man sowas in der Art erhält:
MESSAGE_BEGIN(MSG_ONE, SVC_TEMPENTITY, NULL, pev);
Du wirst bemerken, dass ich nicht MSG_PVS schreibe, sondern MSG_ONE. Das macht das TempEntity nur für den Spieler sichtbar, der vom letzten Wert angegeben wurde, pev. Das ist alles! Natürlich muss noch eine Funktion drumrumgebastelt werden. Erstens bleibt das TE_ELIGHT nicht für immer, also müssen wir es nach einiger Zeit neuerstellen. Zweitens werde ich nur TE_ELIGHT bei Spielern erstellen, die gerade sichtbar sind, um den Netzwerk-Traaffiv niedrig zu halten. Drittens wird das TE_ELIGHT gelöscht, indem man ein weiteres - schwarzes - TE_ELIGHT am selben Spieler erstellt. Es kann immer nur ein Entity-Licht an ein Entity angeheftet werden (das war Shockman's Tip - Danke nochmal :-).
Wenn du dieses Tutorial ohne „Items“-Ausnahme in dein Mod einfügen willst, kannst du die alternative Version benutzen (s.u.). Die alternative Version produziert weniger Network-Traffic, beeinflusst aber alle Model-Entitys wie Waffen. Sogar RPG-Raketen, Granaten und Crossbow-Pfeile werden davon beeinflusst (Brush-Entitys nicht), und ich finde, dass das blöd aussieht.
Aber bevor ich zuviel erzähle, gebe ich dir einfach den Code. Ich werde zwei Arrays erstellen, um festzuhalten
Außerdem deklariere ich eine Funktion die die low-level TE_ELIGHT Erstellung übernimmt. Deklariere diese Dinge in der player.h, um Zeile 74:
// advanced NVG BOOL m_fNVG; // do they have it? BOOL m_fNVGActivated; // is it activated? float m_flNVGBattery; float m_flNVGUpdate; float m_flInfraredUpdate[32]; BOOL m_fInfrared[32]; void NVGCreateInfrared(edict_t *pEdict, int pIndex, BOOL fOn); void NVGToggle(BOOL activate); void NVGUpdate(); // advanced NVG
Das Array muss mit den korrekten Werten initialisiert werden. Öffne player.cpp und füge folgendes zur CBasePlayer::Spawn() (um Zeile 3201) hinzu:
// advanced NVG m_fNVG = FALSE; m_fNVGActivated = FALSE; m_flNVGBattery = 100; m_flNVGUpdate = gpGlobals->time; for(i = 0; i < 32; i++) { m_flInfraredUpdate[i] = gpGlobals->time; m_fInfrared[i] = FALSE; } // advanced NVG
Beachte, dass i bereits in einer vorhergehenden for()-Schleife definiert wurde. Wenn dein Compiler sich beschwert, i sei nicht definiert, dann ändere den Code in:
for(int i = 0; i < 32; i++) { m_flInfraredUpdate[i] = gpGlobals->time; m_fInfrared[i] = FALSE; }
Bloß andere C++-Standards…
Hier ist die NVGCreateInfrared Funktion, die das TE_ELIGHT sendet. Füge sie dort ein, wo die anderen NVG-Funktionen sind (player.cpp, um Zeile 3745):
// this will create/destroy the infrared effect on a player void CBasePlayer::NVGCreateInfrared(edict_t *pPlayer, int pIndex, BOOL fOn) { int r, g, b, life; // just some sanity checks if (!pPlayer || pPlayer->free) return; // set up some variables if (fOn) { r = 128; g = 255; b = 128; life = 255; m_flInfraredUpdate[pIndex-1] = gpGlobals->time + 24.5; m_fInfrared[pIndex-1] = TRUE; } else { r = g = b = 0; life = 0; m_fInfrared[pIndex-1] = FALSE; } // ... this is it!! MESSAGE_BEGIN( MSG_ONE, SVC_TEMPENTITY, pPlayer->v.origin, pev ); WRITE_BYTE(TE_ELIGHT); WRITE_SHORT(pIndex); // entity to follow WRITE_COORD(pPlayer->v.origin.x); // original origin WRITE_COORD(pPlayer->v.origin.y); WRITE_COORD(pPlayer->v.origin.z); WRITE_COORD( 2 ); // radius WRITE_BYTE( r ); // color WRITE_BYTE( g ); WRITE_BYTE( b ); WRITE_BYTE( life ); // life WRITE_COORD( 0 ); // decay MESSAGE_END(); } void CBasePlayer::NVGUpdate() {
Diese Funktion übernimmt den Player-Edict, seinen Index und ob der Infrarot-Effekt an- oder ausgeschaltet werden soll. Zuerst macht sie ein paar Sicherheitsabfragen und entscheidet dann, was für eine Farbe das naue Entity-Licht haben soll, und wie lange es anbleiben soll. Außerdem erneuert es die vorhin erstellten Arrays. Ich könnte das auch während des Message-Aufrufs machen, aber so ist es viel sauberer und übersichtlicher.
Natürlich kannst du auch diese Farben wieder an deine Bedürfnisse anpassen. Ach, und vielleicht bemerkst du, dass die Farbwerte im Softwaremodus ignoriert werden. Da kann ich aber nix gegen tun, das ist ein Engine-Problem. Die Lebensdauer des Entitys ist 25.5 Sekunden, es wird aber alle 24.5 Sekunden neu erstellt. Dies verhindert ein Geschwindigkeitsproblem, bei dem der Infrarot-Effekt verschwindet für die Zeit, die das TempEntity-Packet braucht um zum Client zu gelangen. Dieses Problem taucht wieder auf, wenn der Ping über 1000 steigt, wenn ich richtig vermute, aber es wird sowieso niemand mit so einem Ping spielen wollen. Vielleicht willst du wissen, warum ich die Lebensdauer noch höher gesetzt habe. Schließlich könnte der Netzwetk-Traffic dadurch noch geringer gehalten werden. Das stimmt zwar, aber die Lebensdauer wird als ein Byte gesendet. In ein Byte passen nun aber nur Werte von 0 bis 255, also ist das bereits das obere Limit. Die Daten einmal in 24.5 Sekunden zu verschicken sollte sowieso kein Problem darstellen.
Es gibt zwei Stellen, von denen wir NVGCreateInfrared() aufrufen wollen. Der erste Platz ist (logisch) NVGUpdate(). Hier ist der neue Code (um Zeile 3815):
// inform the client of the correct state, just to be sure MESSAGE_BEGIN(MSG_ONE, gmsgNVGActivate, NULL, pev); WRITE_BYTE( 0 ); // NVG not activated WRITE_BYTE( m_flNVGBattery ); MESSAGE_END( ); } } // now for the infrared effect: if (m_fNVGActivated) { edict_t *pEdict; pEdict = UTIL_EntitiesInPVS(edict()); while(!FNullEnt(pEdict)) { int index = ENTINDEX(pEdict); CBaseEntity *pPlayer = Instance(pEdict); // is it an existing, valid player ? if (pPlayer && pPlayer->IsPlayer()) { // is he dead or alive? if (!pPlayer->IsAlive()) { // if he's dead, shut the effect off (unless this is already done) if (m_fInfrared[index-1]) NVGCreateInfrared(pEdict, index, FALSE); } else { // if he's alive, check if it's time to update him if (!m_fInfrared[index-1] || m_flInfraredUpdate[index-1] < gpGlobals->time) NVGCreateInfrared(pEdict, index, TRUE); } } pEdict = pEdict->v.chain; } } } // advanced NVG
Dieser Teil mag dir ein wenig unklar erscheinen. Gut, gehen wir's langsam durch. Die Funktion UTIL_EntitiesInPVS() gibt uns Zugriff auf die Liste aller Entitys im „potentiell sichtbaren Set“ (Potentially Visible Set, Shockman hat mir erklärt, dass das die Langform von PVS ist) des Spielers. Dies sind alle Entitys, die laut VIS-Check sichtbar sind. Das bedeutet, dass dort mehr Entitys aufgeführt sind, als tatsächlich sichtbar sind, weil die Engine ja um Ecken, durch Türen und ähnliches sehen kann. Wenn der Map-Ersteller vergessen hat, Vis auszuführen, enthält dies alle Entitys. Für den Fall, dass du dich mit Kartenerstellen ein bisschen auskennst, kennst du sicher den Befehl r_draworder 1, der im Softwaremodus alle Polygone von hinten nach vorne zeichnet. Dadurch kannst du alles sehen, was im PVS ist. Sehr wichtig, um den Polycount (die r_speeds) zu reduzieren.
UTIL_EntitiesInPVS() gibt das erste Entity dieser Liste zurück. Um alle zu bekommen, folgen wir der verknüpften Liste mit pEdict→v.chain. Das geht aber nur, wenn wirUTIL_EnitiesInPVS() aufgerufen haben und könnte zerstört werden, wenn wie eine andere Engine-Funktion aufrufen. Wenn wir ein gültiges Entity erwischt haben, wir überprüft haben, dass es vollständig gespawnt hat und es sich um einen Spieler handelt, rufen wir NVGCreateInfrared() auf, je nachdem, ob der Spieler lebt oder tot ist.
Die zweite Stelle, von der wir NVGCreateInfrared() aufrufen ist dort, wo wir das NVG ausschalten, also füge folgendes zur NVGToggle() hinzu (Zeile 3735):
// inform the client about the change MESSAGE_BEGIN(MSG_ONE, gmsgNVGActivate, NULL, pev); WRITE_BYTE( 0 ); WRITE_BYTE( m_flNVGBattery ); MESSAGE_END( ); for(int i = 1; i < 32; i++) { if (m_fInfrared[i-1]) { edict_t *pPlayer = INDEXENT(i); if (pPlayer) NVGCreateInfrared(pPlayer, i, FALSE); } } } } // this will create/destroy the infrared effect on a player void CBasePlayer::NVGCreateInfrared(edict_t *pPlayer, int pIndex, BOOL fOn)
Sollte klar sein. Schaltet einfach durch alle Spieler und deaktiviert deren Infrarot-Effekt, falls er an ist.
Dies ist die versprochene alternative Version. Wenn du nicht verstehst, was hier passiert, dann lies einfach die erste Version - die hat einige Kommentare.
Diesmal brauchst du kein Array zu erstellen, nur eine einfache float, die die Update-Zeit enthalten wird:
// advanced NVG BOOL m_fNVG; // do they have it? BOOL m_fNVGActivated; // is it activated? float m_flNVGBattery; float m_flNVGUpdate; float m_flInfraredUpdate; void NVGCreateInfrared(BOOL fOn); void NVGToggle(BOOL activate); void NVGUpdate(); // advanced NVG Logischerweise wird Spawn() dadurch einfacher: // advanced NVG m_fNVG = FALSE; m_fNVGActivated = FALSE; m_flNVGBattery = 100; m_flNVGUpdate = gpGlobals->time; m_flInfraredUpdate = gpGlobals->time; // advanced NVG
NVGCreateInfrared() ist fast gleichgeblieben
// this will create/destroy the infrared effect on the player void CBasePlayer::NVGCreateInfrared(BOOL fOn) { int r, g, b, life; // set up some variables if (fOn) { r = 128; g = 255; b = 128; life = 255; m_flInfraredUpdate = gpGlobals->time + 24.5; } else { r = g = b = 0; life = 0; m_flInfraredUpdate = gpGlobals->time; } // ... this is it!! MESSAGE_BEGIN( MSG_ONE, SVC_TEMPENTITY, pev->origin, pev ); WRITE_BYTE(TE_ELIGHT); WRITE_SHORT(ENTINDEX(edict())); // entity to follow WRITE_COORD(pev->origin.x); // original origin WRITE_COORD(pev->origin.y); WRITE_COORD(pev->origin.z); WRITE_COORD( 2048 ); // radius WRITE_BYTE( r ); // color WRITE_BYTE( g ); WRITE_BYTE( b ); WRITE_BYTE( life ); // life WRITE_COORD( 0 ); // decay MESSAGE_END(); } void CBasePlayer::NVGUpdate()
Der einzige wirkliche Unterschied in TE_ELIGHT ist der Radius. Du solltest wissen, dass du den Wert mit 10 (oder 8, irgendwie sowas) multiplizieren musst, um den tatsächlichen Radius zu bekommen, also haben wir einen Radius von 20480. Da die maximalen Ausmaße einer Map 8192 * 8192 * 8192 betragen, ist die maximal sichtbare Distanz d = sqrt( a2 + b2 + c2 ) = 14200…
NVGUpdate() wird viel einfacher:
// inform the client of the correct state, just to be sure MESSAGE_BEGIN(MSG_ONE, gmsgNVGActivate, NULL, pev); WRITE_BYTE( 0 ); // NVG not activated WRITE_BYTE( m_flNVGBattery ); MESSAGE_END( ); } } // now for the infrared effect: if (m_fNVGActivated && m_flInfraredUpdate < gpGlobals->time) { NVGCreateInfrared(TRUE); } } // advanced NVG
Und schließlich NVGToggle():
// inform the client about the change MESSAGE_BEGIN(MSG_ONE, gmsgNVGActivate, NULL, pev); WRITE_BYTE( 0 ); WRITE_BYTE( m_flNVGBattery ); MESSAGE_END( ); NVGCreateInfrared(FALSE); } } // this will create/destroy the infrared effect on a player void CBasePlayer::NVGCreateInfrared(edict_t *pPlayer, int pIndex, BOOL fOn)
Das ist alles!
Hey, letztendlich hast du es geschafft! Gratuliere! Es gibt aber noch ein paar „Dinge, die du wissen solltest“…
Einige Fragen, die du stellen könntest:
F: Ich hab bemerkt, dass da ein DLIGHT in Badger's Advanced NVG Tutorial enthalten war, um den Level aufzuhellen.
A: Erstens wurde das DLIGHT nicht so schnell erneuert wie die Spielerpostition, deshalb sah's bescheiden aus. Zweitens ist die ein INFRAROT-NVG, nicht wahr!?! Und drittens und am wichtigsten war es ein Performancemonster. Ich hab bemerkt, dass die Framerate um ungefähr 50% sank, wenn ich es benutzte (kein Problem mit meinem Athlon 550 und TNT2, aber vor noch nicht allzulanger Zeit war ich noch einer von denen, die jedes einzelne Polygon zählten, also will ich nett zu denen sein :-). Wenn du's trotzdem haben willst, dann nimm einfach den entsprechenden Code von Badger's Tutorial und kopier ihn in NVGUpdate().
F: In Badger's Tutorial gab's aber eine Zoom-Funktion!!!
A: Okay, erstens ist dieses Tutorial bereits so sehr lang. Zweitens war die Zoomoption meiner Meinung nach schwer zu kontrollieren (man musste mehrmals drücken, um zu zoomen). Ich hab ein gutes Tutorial über Client-Side Smooth Zooming bei HLPP gesehen, das solltest du vielleicht verwenden.
F: Ich habe bemerkt, dass der Infrarot-Effekt manchmal für eine kurze Zeit verschwindet, wenn ich im Internet spiele. Was ist da los?
A: Das Problem ist, dass die TempEntity-Informationen etwas Zeit brauchen, um zum Client zu gelangen. Wenn sie zu spät ankommen, verschwindet der Effekt für kurze Zeit. Wenn das der Fall ist, hast du vermutlich eine SEHR schlechte Internet-Verbindung und wirst vermutlich sowieso nicht spielen wollen. Wenn du trotzdem meinst, es stört, dann kannst du versuchen, die Update-Häufigkeit des TempEntitys in der NVGCreateInfrared() zu erhöhen. Das ist zwar keine wirklich vernünftige Lösung (ich glaub, es gibt keine), aber es wird besser funktionieren.
F: Ich habe den Infrarot-Effekt ausprobiert, aber er funktioniert nicht oder ist nicht farbig.
A: Das ELIGHT scheint im Software-Modus nicht farbig zu sein, wie ich in einem Testlauf festgestellt habe. Wenn du Hardware-Beschleunigung verwendest und eines dieser Probleme bekommst, dann ist es sehr wahrscheinlich, dass deine Grafikkarte dies nicht unterstützt. Tut mir leid, dass ich es nur mit Riva-Chips von NVidia testen konnte (ELSA Victory Erazor, ELSA Erazor II/III, Diamond Viper 330/550/770 benutzen beispielsweise diese Chips) und damit funktionierte es. Probier vielleicht noch, ob ein Wechsel zwischen OpenGL und Direct3D etwas bringt, aber wenn das nicht funktionert, dann kann ich auch nichts machen - tut mir leid!
Wenn du willst, kannst du meine Sprites verwenden. Ich garantiere, dass ich sie selbstgemacht habe, also kein Copyright außer meinem. Das Batterie-Icon sieht recht bescheiden aus, also wirst du wohl ein eigenes erstellen wollen. Das NVG-Sprite habe ich mit einem kleinen Bitmap-Generator und ein bisschen Nachbearbeitung erstellt. Hier sind die Sprites, die ich benutze:
Du kannst dir alle Sprites hier downloaden: NVG-Sprites by Prefect
Dieses Tutorial stammt aus der ehemaligen Sammlung des resourcecode.de und konnte dank der freundlichen Zustimmung des Autors in das thewall-Wiki übertragen werden.