Adding an experience system

From Valve Developer Community
Jump to: navigation, search
Abstract Coding series

Levels & XPOptimizationProcedural TexturesSights & SniperriflesSpecial effectsVehiclesThreadsSave Game FilesNight VisionNon-offensive WeaponsDynamic Weapon SpawnsDynamic Weapon Spawns (Advanced)

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 its 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 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 & 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 & 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 thats 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: 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 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:

void CHL2MP_Player::LevelUp()
{
	
}

Giving XP for kills

This should be quite straightforward. Goto 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 & 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 & replace, changing all CHudBattery to CHudLevel.

Now there's a few lines that need 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"

CHudLevel::CHudLevel( const char *pElementName ) : BaseClass(NULL, "HudLevel"), CHudElement( pElementName )

Remove lines 58, 60 & 61 from Init(), leaving only the call to Reset();

	HOOK_HUD_MESSAGE( CHudLevel, Battery);
	Reset(); // leave this one
	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 occured when loading the main menu.


Note: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 thats 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"

Thats all the code done for our panel, but we need to define in the script files where & how its displayed on the HUD.

Firstly, add the following keyvalue to resource\yourmod_english.txt:

"Hud_Level"		"Level"

That gives an easy way to set the panel text, based upon your mod user's language.

Note:yourmod_english.txt does not come automatically with the mod source code. Either create this file yourself or alter a copy from HL2DM's gfc files.


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 yourmod_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 in bold)

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:

Note: If you are using Orange box code make sure to replace 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 players 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 public add:

int	m_iMaxArmor;
int	m_iOldMaxArmor;
void	SetMaxArmorValue( int MaxArmorValue ) { m_iMaxArmor = MaxArmorValue; };
void	SetOldMaxArmorValue( int OldMaxArmorValue ) { m_iOldMaxArmor = OldMaxArmorValue; };

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 (in 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 a 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).

To do: 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 onto this tutorial!