Adding an experience system: Difference between revisions
No edit summary |
|||
(6 intermediate revisions by 5 users not shown) | |||
Line 1: | Line 1: | ||
{{Abstract Coding | {{LanguageBar}} | ||
[[Category:Abstract Coding]] | |||
This tutorial will cover adding a basic experience (XP) system to your mod. It will assume you are modding | == Introduction == | ||
This tutorial will cover adding a basic experience (XP) system to your mod. It will assume you are modding HL2MP but can be easily modified to work with the "from scratch" or singleplayer SDK, by simply changing the player files used. Although it's quite long and involves editing many different files, each step is relatively simple and tries to be as clear as possible. This tutorial is aimed at beginner to moderately skilled modders. | |||
== Basic setup == | == Basic setup == | ||
Line 7: | Line 9: | ||
=== Server-side variables === | === Server-side variables === | ||
Begin by adding two variables to the player class: one for current XP, and one for current level. | Begin by adding two variables to the player class: one for current XP, and one for the current level. | ||
Open hl2mp_player.h, and add these two lines to the end of private section of the CHL2MPPlayer class, around line 163: | Open '''hl2mp_player.h''', and add these two lines to the end of <code>private:</code> section of the <code>CHL2MPPlayer</code> class, around line 163: | ||
< | <source lang=cpp> | ||
CNetworkVar(int, m_iLevel);</ | CNetworkVar( int, m_iExp ); | ||
CNetworkVar( int, m_iLevel ); | |||
</source> | |||
These variables will need functions to control and use them. Define these in the <code>public:</code> section of the same class (<code>CHL2MPPlayer</code>), around line 138. | |||
<source lang=cpp> | |||
int GetXP() { return m_iExp; } | |||
< | void AddXP( int add = 1) | ||
void AddXP(int add=1) { m_iExp += add; CheckLevel(); } | { | ||
m_iExp += add; | |||
CheckLevel(); | |||
} | |||
int GetLevel() { return m_iLevel; } | int GetLevel() { return m_iLevel; } | ||
Line 24: | Line 32: | ||
void LevelUp(); | void LevelUp(); | ||
void ResetXP() { m_iExp = 0; m_iLevel = 1; LevelUp(); | void ResetXP() | ||
{ | |||
m_iExp = 0; | |||
m_iLevel = 1; | |||
LevelUp(); // Calling LevelUp() will reset max health, etc. | |||
} | |||
</source> | |||
We will need to set default values, so open hl2mp_player.cpp, and look for this constructor: | We will need to set default values, so open '''hl2mp_player.cpp''', and look for this constructor: | ||
<pre>CHL2MP_Player::CHL2MP_Player() : m_PlayerAnimState( this )</pre> | <pre>CHL2MP_Player::CHL2MP_Player() : m_PlayerAnimState( this )</pre> | ||
In this function, add: | In this function, add: | ||
< | <source lang=cpp> | ||
m_iLevel = 1; | m_iExp = 0; | ||
LevelUp(); // | m_iLevel = 1; | ||
LevelUp(); // Sets default values | |||
</source> | |||
We will create bodies for CheckLevel and LevelUp shortly. It seems likely that XP may affect things related to prediction, such as player movement speed, and we will also want to be able to display it on their screen, so we will need to be able to access the player's level on the client. | We will create bodies for <code>CheckLevel()</code> and <code>LevelUp()</code> shortly. It seems likely that XP may affect things related to prediction, such as player movement speed, and we will also want to be able to display it on their screen, so we will need to be able to access the player's level on the client. | ||
=== Client-side variables === | === Client-side variables === | ||
Open up c_hl2mp_player.h, as we're going to put the variables | Open up '''c_hl2mp_player.h''', as we're going to put the variables and accessors in there also. | ||
In a private section: | In a <code>private:</code> section: | ||
<source lang=cpp> | |||
And in a public section: | int m_iExp, m_iLevel; | ||
</source> | |||
And in a <code>public:</code> section: | |||
<source lang=cpp> | |||
int GetXP() { return m_iExp; } | |||
int GetLevel() { return m_iLevel; } | |||
</source> | |||
We don't need to be able to change these variables from the client, as they will be updated by the server. | We don't need to be able to change these variables from the client, as they will be updated by the server. | ||
=== Linking client | === Linking client and server === | ||
In order to have the client m_iExp and m_iLevel keep the same values as they have on the server, we will need to add them to the player's network table. Near the top of hl2mp_player.cpp, you should find the '''send table''': | In order to have the client <code>m_iExp</code> and <code>m_iLevel</code> keep the same values as they have on the server, we will need to add them to the player's network table. Near the top of '''hl2mp_player.cpp''', you should find the '''send table''': | ||
<source lang=cpp> | |||
IMPLEMENT_SERVERCLASS_ST( CHL2MP_Player, DT_HL2MP_Player ) | |||
SendPropBlahBlah( ... ) | |||
SendPropYaddaYadda( ... ) | |||
END_SEND_TABLE() | |||
</source> | |||
At the ''top'' of this send table (the line immediately after IMPLEMENT_SERVERCLASS_ST), add our own variables like so: | At the ''top'' of this send table (the line immediately after <code>IMPLEMENT_SERVERCLASS_ST</code>), add our own variables like so: | ||
<source lang=cpp> | |||
SendPropInt( SENDINFO( m_iExp ) ), | |||
SendPropInt( SENDINFO( m_iLevel ) ), | |||
</source> | |||
Now we need to modify the client's receive table to read these. | Now we need to modify the client's receive table to read these. | ||
Near the top of c_hl2mp_player.cpp, you'll find the matching '''receive table''': | Near the top of '''c_hl2mp_player.cpp''', you'll find the matching '''receive table''': | ||
<source lang=cpp> | |||
IMPLEMENT_CLIENTCLASS_DT( C_HL2MP_Player, DT_HL2MP_Player, CHL2MP_Player ) | |||
</source> | |||
At the top of this, receive our variables like so: | At the top of this, receive our variables like so: | ||
<source lang=cpp> | |||
RecvPropInt( RECVINFO( m_iExp ) ), | |||
RecvPropInt( RECVINFO( m_iLevel ) ), | |||
</source> | |||
And | And that's them fully networked! Straightforward enough, right? | ||
== Leveling up == | == Leveling up == | ||
That won't yet fully compile, because we still have to add function bodies for CheckLevel and LevelUp. In order to do so, we will need a way to decide when a player is ready to level up. We're going to assume only 5 levels for now, you can of course add many more, and adjust the | That won't yet fully compile, because we still have to add function bodies for <code>CheckLevel()</code> and <code>LevelUp()</code>. In order to do so, we will need a way to decide when a player is ready to level up. We're going to assume only 5 levels, for now, you can of course add many more, and adjust the XP requirements as you see fit. We'll also assume that the player starts out as level 1, and not 0! | ||
=== Level XP limits === | === Level XP limits === | ||
Somewhere in hl2mp_player.cpp, add the CheckLevel function, and be sure to add all these defines in front of it | Somewhere in '''hl2mp_player.cpp''', add the <code>CheckLevel()</code> function, and be sure to add all these defines in front of it: | ||
<source lang=cpp> | |||
const int XP_FOR_LEVELS[] = { 5, 12, 22, 35 }; | |||
// We have gained XP; decide if we should level up, and do it if needed | |||
// Note: the XP limits here could be in an array, but that would be less clear | |||
void CHL2MP_Player::CheckLevel() | |||
{ | |||
int CurrentLevel = GetLevel(); // Find the player's level | |||
if ( GetXP() >= XP_FOR_LEVELS[CurrentLevel - 1] ) // Check if the player's XP exceeds that which he needs to level up | |||
{ | |||
m_iLevel ++; // Actually increment player's level | |||
LevelUp(); // and then adjust their settings (speed, health, damage) to reflect the change | |||
ClientPrint( this, HUD_PRINTTALK, UTIL_VarArgs( "You have reached level %i\n", GetLevel() ) ); // Write it on their screen | |||
UTIL_ClientPrintAll( HUD_PRINTCONSOLE, UTIL_VarArgs( "%s has reached level %i\n", GetPlayerName(), GetLevel() ) ); // Write it in everyone's console | |||
} | |||
} | |||
</source> | |||
{{Note|The #defines and if ... else if ... else if system is used here for clarity, and could certainly be made more efficient. For examples of how this could be improved, visit [https://web.archive.org/web/20170521072109/http://forums.steampowered.com/forums/showthread.php?t=667167 this thread], in particular the 6th post.}} | |||
As we currently have no level-related features (different health, speed, etc), LevelUp will be empty for now. Add it underneath CheckLevel: | As we currently have no level-related features (different health, speed, etc), <code>LevelUp()</code> will be empty for now. Add it underneath <code>CheckLevel()</code>: | ||
<source lang=cpp> | |||
void CHL2MP_Player::LevelUp() | |||
{ | |||
} | |||
</source> | |||
=== Giving XP for kills === | === Giving XP for kills === | ||
This should be quite straightforward. | This should be quite straightforward. Go to '''hl2mp_gamerules.cpp''', and find the <code>PlayerKilled( ... )</code> function. Add a little bit onto the end: | ||
<source lang=cpp highlight=3-7> | |||
BaseClass::PlayerKilled( pVictim, info ); | |||
CBaseEntity *pInflictor = info.GetInflictor(); | |||
CBaseEntity *pKiller = info.GetAttacker(); | |||
CHL2MP_Player *pScorer = ToHL2MPPlayer( GetDeathScorer( pKiller, pInflictor ) ); | |||
if ( pScorer && pKiller == pInflictor && pKiller != pVictim && PlayerRelationship( pKiller, pVictim ) == GR_NOTTEAMMATE ) | |||
pScorer->AddXP( 1 ); | |||
#endif | |||
} | |||
</source> | |||
== Displaying the player's level == | == Displaying the player's level == | ||
A player should be able to see at a glance what level they are. A | A player should be able to see at a glance what level they are. A VGUI HUD panel would be good for this! Create a new .cpp file in the client.dll project, called '''hud_level.cpp'''. Ideally (for neatness) it should go in the '''.../HL2MP/UI''' folder, but as long as it's in the client project, it should work just the same. | ||
What we want here is just a panel that displays a number; the game includes several of these already, so we'll just copy | What we want here is just a panel that displays a number; the game includes several of these already, so we'll just copy and paste one, and change it where necessary. Open up '''hud_battery.cpp''', select and copy the entire file contents. Paste it into your new '''hud_level.cpp''', and then close '''hud_battery.cpp''' just so you don't change the wrong file. | ||
In hud_level.cpp, have | In '''hud_level.cpp''', have Visual Studio do a Find and Replace, changing all instances of '''CHudBattery''' to '''CHudLevel'''. | ||
Now there | Now there are a few lines that need to be trimmed: | ||
'''Remove''' lines 41 - 46: | '''Remove''' lines 41 - 46: | ||
<source lang=cpp> | |||
void MsgFunc_Battery( bf_read &msg ); | |||
bool ShouldDraw(); | |||
private: | |||
int m_iBat; | |||
int m_iNewBat; | |||
</source> | |||
And now remove the new line 44: | And now remove the new line 44: | ||
<source lang=cpp> | |||
DECLARE_HUD_MESSAGE( CHudLevel, Battery ); | |||
</source> | |||
On the line 48 constructor, change <code>HudSuit</code> to <code>HudLevel</code>, like so: | |||
<source lang=cpp> | |||
CHudLevel::CHudLevel( const char *pElementName ) : BaseClass( NULL, "HudLevel" ), CHudElement( pElementName ) | |||
</source> | |||
Remove lines 58, 60 and 61 from <code>Init()</code>, leaving only the call to <code>Reset()</code>: | |||
<source lang=cpp highlight=1,3-4> | |||
HOOK_HUD_MESSAGE( CHudLevel, Battery); | |||
Reset(); | |||
m_iBat = INIT_BAT; | |||
m_iNewBat = 0; | |||
</source> | |||
Replace the <code>Reset()</code> function itself with this: | |||
<source lang=cpp> | |||
void CHudLevel::Reset( void ) | |||
{ | |||
wchar_t *tempString = vgui::localize()->Find( "#Hud_Level" ); | |||
if ( tempString ) | |||
{ | |||
SetLabelText( tempString ); | |||
} | |||
else | |||
{ | |||
SetLabelText( L"LEVEL" ); | |||
} | |||
} | |||
</source> | |||
Checking the string exists before displaying prevents a crash that previously had occurred when loading the main menu. | |||
{{Note|<code>vgui::localize()</code> may not be in your namespace. This is the case if you are using Orange Box code. To fix this problem use <code>g_pVGuiLocalize</code> instead.}} | |||
Next, delete the entire <code>ShouldDraw()</code> function. | |||
We're not going to bother with animating this panel, as that's beyond the scope of this tutorial, so delete all the contents of <code>OnThink()</code> and replace it with just: | |||
<source lang=cpp> | |||
We're not going to bother with animating this panel, as | void CHudLevel::OnThink( void ) | ||
{ | |||
C_HL2MP_Player *pPlayer = C_HL2MP_Player::GetLocalHL2MPPlayer(); | |||
if ( pPlayer ) | |||
SetDisplayValue( pPlayer->GetLevel() ); | |||
} | |||
</source> | |||
Next, delete the MsgFunc_Battery function, and as a last change, add one line to the list of includes at the top of the file: | Next, delete the <code>MsgFunc_Battery</code> function, and as a last change, add one line to the list of includes at the top of the file: | ||
<source lang=cpp> | |||
#include "c_hl2mp_player.h" | |||
</source> | |||
That's all the code done for our panel, but we need to define in the script files where and how it's displayed on the HUD. | |||
Firstly, add the following [[keyvalue]] to '''resource/<YourModName>_english.txt''': | |||
<pre> | |||
Firstly, add the following [[keyvalue]] to < | "Hud_Level" "Level" | ||
</pre> | |||
That gives an easy way to set the panel text, based upon your mod user's language. | That gives an easy way to set the panel text, based upon your mod user's language. | ||
{{ | {{Note|'''<YourModName>_english.txt''' does not come automatically with the mod source code. Either create this file yourself or alter a copy from HL2DM's VPK files.}} | ||
Now open '''HudLayout.res''', in the scripts folder. Add onto the end: | |||
<pre> | |||
HudLevel | |||
{ | |||
"fieldName" "HudLevel" | |||
"xpos" "300" | |||
"ypos" "432" | |||
"wide" "96" | |||
"tall" "36" | |||
"visible" "1" | |||
"enabled" "1" | |||
"PaintBackgroundType" "2" | |||
"text_xpos" "8" | |||
"text_ypos" "20" | |||
"digit_xpos" "50" | |||
"digit_ypos" "2" | |||
} | |||
</pre> | |||
You'll want to adjust those values to suit your own taste in HUD layout, trial and error is the way to go. And realize that all positions are based on a 640x480 screen resolution, even if that isn't the resolution you're playing at! See [[VGUI Documentation#Schemes]] for more information about this step. | You'll want to adjust those values to suit your own taste in HUD layout, trial and error is the way to go. And realize that all positions are based on a 640x480 screen resolution, even if that isn't the resolution you're playing at! See [[VGUI Documentation#Schemes]] for more information about this step. | ||
Line 216: | Line 260: | ||
It would be a nice feature to show a player's level when you move the mouse over them, so that instead of saying "Enemy: Winston" it will say "Enemy: Winston (level 2)" | It would be a nice feature to show a player's level when you move the mouse over them, so that instead of saying "Enemy: Winston" it will say "Enemy: Winston (level 2)" | ||
Open | Open '''<YourModName>_english.txt''' from your mod's resource folder, and replace the following three lines: | ||
<pre> | |||
"Playerid_sameteam" "Friend: %s1 Health: %s2" | |||
"Playerid_diffteam" "Enemy: %s1" | |||
"Playerid_noteam" "%s1 Health:%s2" | |||
</pre> | |||
Now go to hl2mp_hud_target_id.cpp, which is the file that controls drawing other player's names when you look at them. | With: | ||
<pre> | |||
"Playerid_sameteam" "Friend: %s1 Health: %s2 (level %s3)" | |||
"Playerid_diffteam" "Enemy: %s1 (level %s2)" | |||
"Playerid_noteam" "%s1 Health:%s2 (level %s3)" | |||
</pre> | |||
Now go to '''hl2mp_hud_target_id.cpp''', which is the file that controls drawing other player's names when you look at them. | |||
At around line 142 you'll see these: | At around line 142 you'll see these: | ||
<source lang=cpp> | |||
C_BasePlayer *pPlayer = static_cast<C_BasePlayer *>( cl_entitylist->GetEnt( iEntIndex ) ); | |||
C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer(); | |||
</source> | |||
Replace them with these | Replace them with these | ||
<source lang=cpp> | |||
C_HL2MP_Player *pPlayer = ToHL2MPPlayer( cl_entitylist->GetEnt( iEntIndex ) ); | |||
C_HL2MP_Player *pLocalPlayer = C_HL2MP_Player::GetLocalHL2MPPlayer(); | |||
</source> | |||
Add at line 148: (new line | Add at line 148: (new line {{Highlight|'''highlighted'''}}) | ||
<source lang=cpp highlight=3> | |||
wchar_t wszPlayerName[ MAX_PLAYER_NAME_LENGTH ]; | |||
wchar_t wszHealthText[ 10 ]; | |||
wchar_t wszLevelText[ 10 ]; | |||
bool bShowHealth = false; | |||
bool bShowPlayerName = false; | |||
</source> | |||
And then at line 177 | And then at line 177 | ||
<source lang=cpp highlight=6-7> | |||
if ( bShowHealth ) | |||
{ | |||
_snwprintf( wszHealthText, ARRAYSIZE(wszHealthText) - 1, L"%.0f%%", ((float)pPlayer->GetHealth() / (float)pPlayer->GetMaxHealth() ) ); | |||
wszHealthText[ ARRAYSIZE(wszHealthText)-1 ] = '\0'; | |||
} | |||
_snwprintf( wszLevelText, ARRAYSIZE(wszLevelText) - 1, L"%i", pPlayer->GetLevel() ); | |||
wszLevelText[ ARRAYSIZE(wszLevelText) - 1 ] = '\0'; | |||
} | |||
</source> | |||
Now, from line 182, replace the entire if ( printFormatString ) statement with: | Now, from line 182, replace the entire <code>if ( printFormatString )</code> statement with: | ||
{{ | {{Note| If you are using Orange Box code make sure to replace <code>vgui::localize()</code> with <code>g_pVGuiLocalize</code> just as before.}} | ||
<source lang=cpp> | |||
if ( printFormatString ) | |||
{ | |||
if ( bShowPlayerName && bShowHealth ) | |||
{ | |||
vgui::localize()->ConstructString( sIDString, sizeof(sIDString), vgui::localize()->Find(printFormatString), 3, wszPlayerName, wszHealthText, wszLevelText ); | |||
} | |||
else if ( bShowPlayerName ) | |||
{ | |||
vgui::localize()->ConstructString( sIDString, sizeof(sIDString), vgui::localize()->Find(printFormatString), 2, wszPlayerName, wszLevelText ); | |||
} | |||
else if ( bShowHealth ) | |||
{ | |||
vgui::localize()->ConstructString( sIDString, sizeof(sIDString), vgui::localize()->Find(printFormatString), 2, wszHealthText, wszLevelText ); | |||
} | |||
else | |||
{ | |||
vgui::localize()->ConstructString( sIDString, sizeof(sIDString), vgui::localize()->Find(printFormatString), 1, wszLevelText ); | |||
} | |||
} | |||
</source> | |||
== Experience effects == | == Experience effects == | ||
Line 281: | Line 337: | ||
=== Health increase === | === Health increase === | ||
Put the following into your LevelUp function (in hl2mp_player.cpp): | Put the following into your <code>LevelUp()</code> function (in '''hl2mp_player.cpp'''): | ||
<source lang=cpp> | |||
int currentLevel = GetLevel(); | |||
SetHealthMax( 100 + ( ( currentLevel - 1 ) * 10) ); | |||
m_iHealth = m_iMaxHealth = m_iHealthMax; | |||
</source> | |||
We will need to declare the SetHealthMax function (somewhere public in hl2mp_player.h) | We will need to declare the <code>SetHealthMax</code> function (somewhere <code>public:</code> in '''hl2mp_player.h''') | ||
<source lang=cpp> | |||
void SetHealthMax( int h ) { m_iHealthMax = h; } | |||
</source> | |||
This variable, <code>m_iHealthMax</code>, should be defined in a <code>private:</code> section in this file (still '''hl2mp_player.h'''), like so: | |||
<source lang=cpp> | |||
int m_iHealthMax; | |||
</source> | |||
You needn't worry about setting a default value, as the constructor's call to <code>LevelUp()</code> will take care of that. | |||
In | Now to apply this health max each time they spawn, too. In '''hl2mp_player.cpp''', find the <code>CHL2MP_Player::Spawn</code> function, and add onto the ''end'': | ||
<source lang=cpp> | |||
m_iHealth = m_iMaxHealth = m_iHealthMax; | |||
</source> | |||
Yes, there's already a variable called <code>m_iMaxHealth</code>, and we're making one called <code>m_iHealthMax</code> rather than using that. Yes, that's quite poor practice, but it's by far the simplest way! | |||
=== Speed increase === | |||
We'll do this one a bit differently. The player's maximum allowed speed is changed every time they press or release the sprint button, so adding a new variable would be fiddly. Instead, we'll just adjust the <code>SetMaxSpeed( ... )</code> function. | |||
In both '''hl2mp_player.h''' and '''c_hl2mp_player.h''' (this is needed for prediction), declare: | |||
<source lang=cpp> | |||
virtual void SetMaxSpeed( float flMaxSpeed ); | |||
</source> | |||
This lets us override the default function. | |||
Now open up '''hl2mp_player_shared.cpp''', and put the function there. By putting it in this file, the same code will be executed on the client and the server, ensuring that we don't have any prediction problems. | |||
<source lang=cpp> | |||
void CHL2MP_Player::SetMaxSpeed( float flMaxSpeed ) | |||
{ | |||
BaseClass::SetMaxSpeed( flMaxSpeed + 10.0f * ( GetLevel() - 1.0f ) ); | |||
} | |||
</source> | |||
That should add on 10 units per second onto the player's movement speed, for each level they have (beyond level 1). <code>-1.0f</code> is there so that level 1 players get no speed increase; their increase is 0. | |||
=== Max Armor increase === | |||
In '''hl2mp_player.h''' in the <code>HL2MP_Player</code> class, somewhere in the <code>public:</code> section add: | |||
SetMaxArmorValue( | <source lang=cpp> | ||
int m_iMaxArmor; | |||
int m_iOldMaxArmor; | |||
void SetMaxArmorValue( int iMaxArmorValue ) { m_iMaxArmor = iMaxArmorValue; }; | |||
void SetOldMaxArmorValue( int iOldMaxArmorValue ) { m_iOldMaxArmor = iOldMaxArmorValue; }; | |||
</source> | |||
In the constructor of the player or the Spawn function add: | |||
<source lang=cpp> | |||
SetOldMaxArmorValue( 100 + GetLevel() * 4 ); | |||
</source> | |||
This will initialize <code>m_iOldMaxArmor</code> to the player's starting armor value for their level. | |||
In '''hl2mp_player.cpp''', inside the <code>LevelUp()</code> function, add: | |||
<source lang=cpp> | |||
SetMaxArmorValue( 100 + GetLevel() * 4 ); | |||
if ( m_iMaxArmor > m_iOldMaxArmor ) | |||
if ( m_iMaxArmor > | |||
{ | { | ||
IncrementArmorValue | IncrementArmorValue( m_iMaxArmor - m_iOldMaxArmor, m_iMaxArmor ); | ||
} | } | ||
if ( m_iArmor > m_iMaxArmor ) | if ( m_iArmor > m_iMaxArmor ) | ||
{ | { | ||
if ( | if ( m_iOldMaxArmor != m_iMaxArmor ) | ||
{ | { | ||
m_iArmor = m_iMaxArmor; | m_iArmor = m_iMaxArmor; | ||
SetOldMaxArmorValue( m_iMaxArmor ); | |||
} | |||
} | } | ||
if ( | if ( m_iOldMaxArmor != m_iMaxArmor ) | ||
{ | { | ||
SetOldMaxArmorValue( m_iMaxArmor ); | |||
} | } | ||
</source> | |||
This will implement an armor upgrade that won't set the armor back to the max value on every upgrade. Instead, when the player earns an armor upgrade, they will have the value of the upgrade added to their armor value (i.e. if they have 50 armor out of 100, and get an upgrade to 150 max armor, they are now at 100 armor, instead of 150). | |||
{{Todo|Implement overrides to allow armor pickups to reach the new max armor value.}} | |||
== Conclusion == | == Conclusion == | ||
This tutorial has covered the implementation of a very basic experience system. The user is encouraged to develop their own extensions, to affect things like maximum stamina, weapon damage, and armor. Feel free to add any such extensions | This tutorial has covered the implementation of a very basic experience system. The user is encouraged to develop their own extensions, to affect things like maximum stamina, weapon damage, and armor. Feel free to add any such extensions to this tutorial! | ||
[[Category:Programming]] | [[Category:Programming]] | ||
[[Category:Tutorials]] | [[Category:Tutorials]] |
Latest revision as of 04:04, 12 July 2024
Introduction
This tutorial will cover adding a basic experience (XP) system to your mod. It will assume you are modding HL2MP but can be easily modified to work with the "from scratch" or singleplayer SDK, by simply changing the player files used. Although it's quite long and involves editing many different files, each step is relatively simple and tries to be as clear as possible. This tutorial is aimed at beginner to moderately skilled modders.
Basic setup
We will start by adding some variables and defining the key functions for level and experience.
Server-side variables
Begin by adding two variables to the player class: one for current XP, and one for the current level.
Open hl2mp_player.h, and add these two lines to the end of private:
section of the CHL2MPPlayer
class, around line 163:
CNetworkVar( int, m_iExp );
CNetworkVar( int, m_iLevel );
These variables will need functions to control and use them. Define these in the public:
section of the same class (CHL2MPPlayer
), around line 138.
int GetXP() { return m_iExp; }
void AddXP( int add = 1)
{
m_iExp += add;
CheckLevel();
}
int GetLevel() { return m_iLevel; }
void CheckLevel();
void LevelUp();
void ResetXP()
{
m_iExp = 0;
m_iLevel = 1;
LevelUp(); // Calling LevelUp() will reset max health, etc.
}
We will need to set default values, so open hl2mp_player.cpp, and look for this constructor:
CHL2MP_Player::CHL2MP_Player() : m_PlayerAnimState( this )
In this function, add:
m_iExp = 0;
m_iLevel = 1;
LevelUp(); // Sets default values
We will create bodies for CheckLevel()
and LevelUp()
shortly. It seems likely that XP may affect things related to prediction, such as player movement speed, and we will also want to be able to display it on their screen, so we will need to be able to access the player's level on the client.
Client-side variables
Open up c_hl2mp_player.h, as we're going to put the variables and accessors in there also.
In a private:
section:
int m_iExp, m_iLevel;
And in a public:
section:
int GetXP() { return m_iExp; }
int GetLevel() { return m_iLevel; }
We don't need to be able to change these variables from the client, as they will be updated by the server.
Linking client and server
In order to have the client m_iExp
and m_iLevel
keep the same values as they have on the server, we will need to add them to the player's network table. Near the top of hl2mp_player.cpp, you should find the send table:
IMPLEMENT_SERVERCLASS_ST( CHL2MP_Player, DT_HL2MP_Player )
SendPropBlahBlah( ... )
SendPropYaddaYadda( ... )
END_SEND_TABLE()
At the top of this send table (the line immediately after IMPLEMENT_SERVERCLASS_ST
), add our own variables like so:
SendPropInt( SENDINFO( m_iExp ) ),
SendPropInt( SENDINFO( m_iLevel ) ),
Now we need to modify the client's receive table to read these.
Near the top of c_hl2mp_player.cpp, you'll find the matching receive table:
IMPLEMENT_CLIENTCLASS_DT( C_HL2MP_Player, DT_HL2MP_Player, CHL2MP_Player )
At the top of this, receive our variables like so:
RecvPropInt( RECVINFO( m_iExp ) ),
RecvPropInt( RECVINFO( m_iLevel ) ),
And that's them fully networked! Straightforward enough, right?
Leveling up
That won't yet fully compile, because we still have to add function bodies for CheckLevel()
and LevelUp()
. In order to do so, we will need a way to decide when a player is ready to level up. We're going to assume only 5 levels, for now, you can of course add many more, and adjust the XP requirements as you see fit. We'll also assume that the player starts out as level 1, and not 0!
Level XP limits
Somewhere in hl2mp_player.cpp, add the CheckLevel()
function, and be sure to add all these defines in front of it:
const int XP_FOR_LEVELS[] = { 5, 12, 22, 35 };
// We have gained XP; decide if we should level up, and do it if needed
// Note: the XP limits here could be in an array, but that would be less clear
void CHL2MP_Player::CheckLevel()
{
int CurrentLevel = GetLevel(); // Find the player's level
if ( GetXP() >= XP_FOR_LEVELS[CurrentLevel - 1] ) // Check if the player's XP exceeds that which he needs to level up
{
m_iLevel ++; // Actually increment player's level
LevelUp(); // and then adjust their settings (speed, health, damage) to reflect the change
ClientPrint( this, HUD_PRINTTALK, UTIL_VarArgs( "You have reached level %i\n", GetLevel() ) ); // Write it on their screen
UTIL_ClientPrintAll( HUD_PRINTCONSOLE, UTIL_VarArgs( "%s has reached level %i\n", GetPlayerName(), GetLevel() ) ); // Write it in everyone's console
}
}
Note:
As we currently have no level-related features (different health, speed, etc), LevelUp()
will be empty for now. Add it underneath CheckLevel()
:
void CHL2MP_Player::LevelUp()
{
}
Giving XP for kills
This should be quite straightforward. Go to hl2mp_gamerules.cpp, and find the PlayerKilled( ... )
function. Add a little bit onto the end:
BaseClass::PlayerKilled( pVictim, info );
CBaseEntity *pInflictor = info.GetInflictor();
CBaseEntity *pKiller = info.GetAttacker();
CHL2MP_Player *pScorer = ToHL2MPPlayer( GetDeathScorer( pKiller, pInflictor ) );
if ( pScorer && pKiller == pInflictor && pKiller != pVictim && PlayerRelationship( pKiller, pVictim ) == GR_NOTTEAMMATE )
pScorer->AddXP( 1 );
#endif
}
Displaying the player's level
A player should be able to see at a glance what level they are. A VGUI HUD panel would be good for this! Create a new .cpp file in the client.dll project, called hud_level.cpp. Ideally (for neatness) it should go in the .../HL2MP/UI folder, but as long as it's in the client project, it should work just the same.
What we want here is just a panel that displays a number; the game includes several of these already, so we'll just copy and paste one, and change it where necessary. Open up hud_battery.cpp, select and copy the entire file contents. Paste it into your new hud_level.cpp, and then close hud_battery.cpp just so you don't change the wrong file.
In hud_level.cpp, have Visual Studio do a Find and Replace, changing all instances of CHudBattery to CHudLevel.
Now there are a few lines that need to be trimmed:
Remove lines 41 - 46:
void MsgFunc_Battery( bf_read &msg );
bool ShouldDraw();
private:
int m_iBat;
int m_iNewBat;
And now remove the new line 44:
DECLARE_HUD_MESSAGE( CHudLevel, Battery );
On the line 48 constructor, change HudSuit
to HudLevel
, like so:
CHudLevel::CHudLevel( const char *pElementName ) : BaseClass( NULL, "HudLevel" ), CHudElement( pElementName )
Remove lines 58, 60 and 61 from Init()
, leaving only the call to Reset()
:
HOOK_HUD_MESSAGE( CHudLevel, Battery);
Reset();
m_iBat = INIT_BAT;
m_iNewBat = 0;
Replace the Reset()
function itself with this:
void CHudLevel::Reset( void )
{
wchar_t *tempString = vgui::localize()->Find( "#Hud_Level" );
if ( tempString )
{
SetLabelText( tempString );
}
else
{
SetLabelText( L"LEVEL" );
}
}
Checking the string exists before displaying prevents a crash that previously had occurred when loading the main menu.

vgui::localize()
may not be in your namespace. This is the case if you are using Orange Box code. To fix this problem use g_pVGuiLocalize
instead.
Next, delete the entire ShouldDraw()
function.
We're not going to bother with animating this panel, as that's beyond the scope of this tutorial, so delete all the contents of OnThink()
and replace it with just:
void CHudLevel::OnThink( void )
{
C_HL2MP_Player *pPlayer = C_HL2MP_Player::GetLocalHL2MPPlayer();
if ( pPlayer )
SetDisplayValue( pPlayer->GetLevel() );
}
Next, delete the MsgFunc_Battery
function, and as a last change, add one line to the list of includes at the top of the file:
#include "c_hl2mp_player.h"
That's all the code done for our panel, but we need to define in the script files where and how it's displayed on the HUD.
Firstly, add the following keyvalue to resource/<YourModName>_english.txt:
"Hud_Level" "Level"
That gives an easy way to set the panel text, based upon your mod user's language.

Now open HudLayout.res, in the scripts folder. Add onto the end:
HudLevel { "fieldName" "HudLevel" "xpos" "300" "ypos" "432" "wide" "96" "tall" "36" "visible" "1" "enabled" "1" "PaintBackgroundType" "2" "text_xpos" "8" "text_ypos" "20" "digit_xpos" "50" "digit_ypos" "2" }
You'll want to adjust those values to suit your own taste in HUD layout, trial and error is the way to go. And realize that all positions are based on a 640x480 screen resolution, even if that isn't the resolution you're playing at! See VGUI Documentation#Schemes for more information about this step.
Showing other players' level
It would be a nice feature to show a player's level when you move the mouse over them, so that instead of saying "Enemy: Winston" it will say "Enemy: Winston (level 2)"
Open <YourModName>_english.txt from your mod's resource folder, and replace the following three lines:
"Playerid_sameteam" "Friend: %s1 Health: %s2" "Playerid_diffteam" "Enemy: %s1" "Playerid_noteam" "%s1 Health:%s2"
With:
"Playerid_sameteam" "Friend: %s1 Health: %s2 (level %s3)" "Playerid_diffteam" "Enemy: %s1 (level %s2)" "Playerid_noteam" "%s1 Health:%s2 (level %s3)"
Now go to hl2mp_hud_target_id.cpp, which is the file that controls drawing other player's names when you look at them. At around line 142 you'll see these:
C_BasePlayer *pPlayer = static_cast<C_BasePlayer *>( cl_entitylist->GetEnt( iEntIndex ) );
C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer();
Replace them with these
C_HL2MP_Player *pPlayer = ToHL2MPPlayer( cl_entitylist->GetEnt( iEntIndex ) );
C_HL2MP_Player *pLocalPlayer = C_HL2MP_Player::GetLocalHL2MPPlayer();
Add at line 148: (new line highlighted)
wchar_t wszPlayerName[ MAX_PLAYER_NAME_LENGTH ];
wchar_t wszHealthText[ 10 ];
wchar_t wszLevelText[ 10 ];
bool bShowHealth = false;
bool bShowPlayerName = false;
And then at line 177
if ( bShowHealth )
{
_snwprintf( wszHealthText, ARRAYSIZE(wszHealthText) - 1, L"%.0f%%", ((float)pPlayer->GetHealth() / (float)pPlayer->GetMaxHealth() ) );
wszHealthText[ ARRAYSIZE(wszHealthText)-1 ] = '\0';
}
_snwprintf( wszLevelText, ARRAYSIZE(wszLevelText) - 1, L"%i", pPlayer->GetLevel() );
wszLevelText[ ARRAYSIZE(wszLevelText) - 1 ] = '\0';
}
Now, from line 182, replace the entire if ( printFormatString )
statement with:

vgui::localize()
with g_pVGuiLocalize
just as before. if ( printFormatString )
{
if ( bShowPlayerName && bShowHealth )
{
vgui::localize()->ConstructString( sIDString, sizeof(sIDString), vgui::localize()->Find(printFormatString), 3, wszPlayerName, wszHealthText, wszLevelText );
}
else if ( bShowPlayerName )
{
vgui::localize()->ConstructString( sIDString, sizeof(sIDString), vgui::localize()->Find(printFormatString), 2, wszPlayerName, wszLevelText );
}
else if ( bShowHealth )
{
vgui::localize()->ConstructString( sIDString, sizeof(sIDString), vgui::localize()->Find(printFormatString), 2, wszHealthText, wszLevelText );
}
else
{
vgui::localize()->ConstructString( sIDString, sizeof(sIDString), vgui::localize()->Find(printFormatString), 1, wszLevelText );
}
}
Experience effects
Now, at last, the interesting part. Different things for different levels! Use these effects as a basis for developing your own, unique, level differences for your mod.
Health increase
Put the following into your LevelUp()
function (in hl2mp_player.cpp):
int currentLevel = GetLevel();
SetHealthMax( 100 + ( ( currentLevel - 1 ) * 10) );
m_iHealth = m_iMaxHealth = m_iHealthMax;
We will need to declare the SetHealthMax
function (somewhere public:
in hl2mp_player.h)
void SetHealthMax( int h ) { m_iHealthMax = h; }
This variable, m_iHealthMax
, should be defined in a private:
section in this file (still hl2mp_player.h), like so:
int m_iHealthMax;
You needn't worry about setting a default value, as the constructor's call to LevelUp()
will take care of that.
Now to apply this health max each time they spawn, too. In hl2mp_player.cpp, find the CHL2MP_Player::Spawn
function, and add onto the end:
m_iHealth = m_iMaxHealth = m_iHealthMax;
Yes, there's already a variable called m_iMaxHealth
, and we're making one called m_iHealthMax
rather than using that. Yes, that's quite poor practice, but it's by far the simplest way!
Speed increase
We'll do this one a bit differently. The player's maximum allowed speed is changed every time they press or release the sprint button, so adding a new variable would be fiddly. Instead, we'll just adjust the SetMaxSpeed( ... )
function.
In both hl2mp_player.h and c_hl2mp_player.h (this is needed for prediction), declare:
virtual void SetMaxSpeed( float flMaxSpeed );
This lets us override the default function.
Now open up hl2mp_player_shared.cpp, and put the function there. By putting it in this file, the same code will be executed on the client and the server, ensuring that we don't have any prediction problems.
void CHL2MP_Player::SetMaxSpeed( float flMaxSpeed )
{
BaseClass::SetMaxSpeed( flMaxSpeed + 10.0f * ( GetLevel() - 1.0f ) );
}
That should add on 10 units per second onto the player's movement speed, for each level they have (beyond level 1). -1.0f
is there so that level 1 players get no speed increase; their increase is 0.
Max Armor increase
In hl2mp_player.h in the HL2MP_Player
class, somewhere in the public:
section add:
int m_iMaxArmor;
int m_iOldMaxArmor;
void SetMaxArmorValue( int iMaxArmorValue ) { m_iMaxArmor = iMaxArmorValue; };
void SetOldMaxArmorValue( int iOldMaxArmorValue ) { m_iOldMaxArmor = iOldMaxArmorValue; };
In the constructor of the player or the Spawn function add:
SetOldMaxArmorValue( 100 + GetLevel() * 4 );
This will initialize m_iOldMaxArmor
to the player's starting armor value for their level.
In hl2mp_player.cpp, inside the LevelUp()
function, add:
SetMaxArmorValue( 100 + GetLevel() * 4 );
if ( m_iMaxArmor > m_iOldMaxArmor )
{
IncrementArmorValue( m_iMaxArmor - m_iOldMaxArmor, m_iMaxArmor );
}
if ( m_iArmor > m_iMaxArmor )
{
if ( m_iOldMaxArmor != m_iMaxArmor )
{
m_iArmor = m_iMaxArmor;
SetOldMaxArmorValue( m_iMaxArmor );
}
}
if ( m_iOldMaxArmor != m_iMaxArmor )
{
SetOldMaxArmorValue( m_iMaxArmor );
}
This will implement an armor upgrade that won't set the armor back to the max value on every upgrade. Instead, when the player earns an armor upgrade, they will have the value of the upgrade added to their armor value (i.e. if they have 50 armor out of 100, and get an upgrade to 150 max armor, they are now at 100 armor, instead of 150).
Conclusion
This tutorial has covered the implementation of a very basic experience system. The user is encouraged to develop their own extensions, to affect things like maximum stamina, weapon damage, and armor. Feel free to add any such extensions to this tutorial!