Adding an experience system:zh-cn

From Valve Developer Community
Jump to: navigation, search
English 简体中文


Abstract Coding series Discuss your thoughts - Help us develop the articles or ideas you want

Levels & XP | Optimization | Procedural Textures | Sights & Sniperrifles | Special effects | Vehicles | Threads | Save Game Files | Night Vision | Non-offensive Weapons | Dynamic Weapon Spawns | Dynamic Weapon Spawns (Advanced)


介绍

这个教程将会介绍如何在你的Mod里添加一个基本的经验(XP)系统。这将会假定你正在给HL2MP制作Mod,但这个mod能够通过更改一些所玩家代码,从而很容易地使这个"面向初学者"的教程或单人模式SDK运作起来。尽管这会出奇的长且涉及编辑一大堆文件,但每一步将会非常简易而且尽可能的清晰明了。这篇教程面向刚起步或是具有中等技能的Modders。

基本设置

我们将会通过添加一些变量与定义一些函数来开始实现等级与经验系统。

服务器这边的变量

给玩家类添加两个变量以起步:一个是当前的经验,另一个是当前的等级。

打开 'hl2mp_player.h,然后在 CHL2MPPlayerprivate: 的结尾部分添加上这两行(第163行附近):

CNetworkVar( int, m_iExp );
CNetworkVar( int, m_iLevel );

这俩变量需要些函数去控制与使用它们。在 CHL2MPPlayerpublic: 部分(第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(); // 调用 LevelUp()  将会重设最高生命值和其他的东西。
}

我们需要去设定它们的默认值,所以打开 hl2mp_player.cpp,然后转到此构造函数:

CHL2MP_Player::CHL2MP_Player() : m_PlayerAnimState( this )

在这个函数里,加上:

	m_iExp = 0;
	m_iLevel = 1;
	LevelUp(); // 设置默认值

我们将为 CheckLevel()LevelUp() 编写简短的实现。经验值或将影响与其相关的事情,比如玩家速度之类的,而且我们也想把它显示在我们的屏幕上,所以我们得能够在客户端侧访问玩家的等级。

客户端方变量

打开 c_hl2mp_player.h,此后我们将在这里放置所需的变量与相应的访问器。 于 private: 部分:

int m_iExp, m_iLevel;

public: 部分添加上:

int GetXP() { return m_iExp; }
int GetLevel() { return m_iLevel; }

我们不需要从客户端方去更改这些变量,而是由服务器方来进行更改。

将客户端与服务器连接起来

为了让客户端方和服务器方的 m_iExpm_iLevel 保持一致,我们需要在玩家的网络表(Network Table,登记着服务器与客户端来往的变量)中添加这些:接近 hl2mp_player.cpp 的顶部的地方,你会发现这张 发送表(客户端登记所发送的变量的表):

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?

升级

上面的部分不会完整地完成我们的工作,因为我们还需要为 CheckLevel()LevelUp() 编写实现代码。为了完成我们的工程,我们得整个让玩家在该升级的时候就升级的机会。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(); // 得到玩家的等级

	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.png 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.


Note.png 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 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.

Note.png 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:

	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:

Note.png 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 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).


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 to this tutorial!