Adding an experience system: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(Cleanup, code formatting (+ syntax highlighting) and QoL changes)
m (This page had a translation but no multipage, OR lang? So someone just created it without adding it to this page at all??)
Tag: Replaced
Line 1: Line 1:
{{Abstract Coding}}
{{Abstract Coding}}
 
{{Multipage}}
 
== 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 <code>private:</code> section of the <code>CHL2MPPlayer</code> class, around line 163:
 
<source lang=cpp>
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)
{
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.
}
</source>
 
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>
 
In this function, add:
<source lang=cpp>
m_iExp = 0;
m_iLevel = 1;
LevelUp(); // Sets default values
</source>
 
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 ===
Open up '''c_hl2mp_player.h''', as we're going to put the variables and accessors in there also.
In a <code>private:</code> section:
<source lang=cpp>
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.
 
=== Linking client and server ===
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 <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.
 
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:
<source lang=cpp>
RecvPropInt( RECVINFO( m_iExp ) ),
RecvPropInt( RECVINFO( m_iLevel ) ),
</source>
 
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 <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 ===
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), <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 ===
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 ==
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:
<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:
<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>
void CHudLevel::OnThink( void )
{
C_HL2MP_Player *pPlayer = C_HL2MP_Player::GetLocalHL2MPPlayer();
if ( pPlayer )
SetDisplayValue( pPlayer->GetLevel() );
}
</source>
 
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>
"Hud_Level" "Level"
</pre>
 
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.
 
=== 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:
<pre>
"Playerid_sameteam" "Friend: %s1 Health: %s2"
"Playerid_diffteam" "Enemy: %s1"
"Playerid_noteam" "%s1 Health:%s2"
</pre>
 
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:
<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
<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 {{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
<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 <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 ==
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 <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 <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.
 
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:
<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 )
{
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 );
}
</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 ==
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]]

Revision as of 08:50, 12 January 2024