Regenerating Health

From Valve Developer Community
Jump to: navigation, search

In this article, we will walk through the steps to add a Call of Duty/Portal style regenerating health system for singleplayer, and with a little modification multiplayer too. The code used was discovered in the Alien Swarm SDK.

Overview

Here is how the logic will work out:

  • Check to see if the player's health is below the max amount
  • Check the regeneration delay timer and the last time since the player was hurt
  • Add the regeneration amount to a float based on the frametime
  • Check if the float is greater than one, and if it is, add it to the player's health

Adding the ConVars

For this tutorial, we will be modifying the base player class. Open up server/player.cpp and add the following convars:

ConVar sv_regeneration ("sv_regeneration", "1", FCVAR_REPLICATED );
ConVar sv_regeneration_wait_time ("sv_regeneration_wait_time", "1.0", FCVAR_REPLICATED );
ConVar sv_regeneration_rate ("sv_regeneration_rate", "0.5", FCVAR_REPLICATED );

This will allow us to balance the regeneration rate while in-game. However, we will also need a value to hold the current regeneration amount. Place:

float     m_fRegenRemander;

under

int	   gEvilImpulse101;

To get the constructor to work go to this code in player.cpp: CBasePlayer::CBasePlayer( ) below m_iHealth = 0; around line 552 add this:

m_fRegenRemander = 0;

Getting Last Hurt Time

Note: If using the HL2-Singleplayer codebase, simply use m_flLastDamageTime rather than adding m_fTimeLastHurt.

Now we need to figure out the time since the player was last hurt. We need this because we want to delay the time before regeneration starts so that the player does not start regenerating when he is taking damage.

To do this, open up player.h and add the following around line 860

float			m_fTimeLastHurt;

Back in player.cpp place:

DEFINE_FIELD( m_fTimeLastHurt, FIELD_TIME ),

under

DEFINE_FIELD( m_tbdPrev, FIELD_TIME ),

at about line 306.

Now that we have a number to keep track of when the player was last hurt, we will need to keep that value up to date. To do this add:

if ( GetHealth() < 100 )
{
    m_fTimeLastHurt = gpGlobals->curtime;
}

at the end of the CBasePlayer::OnTakeDamage method.

Regenerating

Now that everything is in place, it is time to add in the regeneration logic. At the end of CBasePlayer::PostThink add the following:

// Regenerate heath
if ( IsAlive() && GetHealth() < GetMaxHealth() && (sv_regeneration.GetInt() == 1) )
{
	// Color to overlay on the screen while the player is taking damage
	color32 hurtScreenOverlay = {80,0,0,64};

	if ( gpGlobals->curtime > m_fTimeLastHurt + sv_regeneration_wait_time.GetFloat() )
	{
                //Regenerate based on rate, and scale it by the frametime
		m_fRegenRemander += sv_regeneration_rate.GetFloat() * gpGlobals->frametime;
		
		if(m_fRegenRemander >= 1)
		{
			TakeHealth( m_fRegenRemander, DMG_GENERIC );
			m_fRegenRemander = 0;
		}
	}
	else
	{
		UTIL_ScreenFade( this, hurtScreenOverlay, 1.0f, 0.1f, FFADE_IN|FFADE_PURGE );
	}	
}

And that is it.

Advanced Regeneration

If you want to only regenerate part of your health, there are two ways to do it:

  1. Restrict regeneration to only occur up to a certain health value (i.e. 50)
  2. Restrict regeneration to only occur up to a certain interval of health values (i.e. 20, 40, 60, 80, and 100 or etc.)

To do that, we need more ConVars.

ConVar sv_regen_interval("sv_regen_interval", "20", FCVAR_REPLICATED, "Set what interval of health to regen to.\n    i.e. if this is set to the default value (20), if you are damaged to 75 health, you'll regenerate to 80 health.\n    Set this to 0 to disable this mechanic.");
ConVar sv_regen_limit("sv_regen_limit", "0", FCVAR_REPLICATED, "Set the limit as to how much health you can regen to.\n    i.e. if this is set at 50, you can only regen to 50 health. If you are hurt and you are above 50 health, you will not regen.\n    Set this to 0 to disable this mechanic.");

Now, we need to implement these in our regen code. Remember this?

if(m_fRegenRemander >= 1)
{
	TakeHealth( m_fRegenRemander, DMG_GENERIC );
	m_fRegenRemander = 0;
}

Time to make this more complicated.

if(m_fRegenRemander >= 1)
{
	//If the regen interval is set, and the health is evenly divisible by that interval, don't regen.
	if (sv_regen_interval.GetFloat() > 0 && floor(m_iHealth / sv_regen_interval.GetFloat()) == m_iHealth / sv_regen_interval.GetFloat()){
		m_fRegenRemander = 0;
	}
	//If the regen limit is set, and the health is equal to or above the limit, don't regen.
	else if (sv_regen_limit.GetFloat() > 0 && m_iHealth >= sv_regen_limit.GetFloat()){
		m_fRegenRemander = 0;
	}
	else {
		TakeHealth(m_fRegenRemander, DMG_GENERIC);
		m_fRegenRemander = 0;
	}
}

The edit to this code means that when a call to regenerate health is made, it first checks to see if the current health meets a restriction that we set up, and if it does, then it doesn't add any health.

Let's go through this code a little slower:

if (sv_regen_interval.GetFloat() > 0 && floor(m_iHealth / sv_regen_interval.GetFloat()) == m_iHealth / sv_regen_interval.GetFloat()){
	m_fRegenRemander = 0;
}

This, as explained in the comment, checks if sv_regen_interval is set to something greater than 0, then checks if the current health divided by the interval value gives an integer or a whole number. If both are true, health is not added.

else if (sv_regen_limit.GetFloat() > 0 && m_iHealth >= sv_regen_limit.GetFloat()){
	m_fRegenRemander = 0;
}

This, as also explained in the comment, checks if sv_regen_limit is set to something greater than 0, then checks if the current health meets or exceeds that limit. If both are true, health is not added.