Last Man Standing Gametype

From Valve Developer Community
Jump to: navigation, search


The purpose of this tutorial is to setup game rules to allow for a "last man standing" gametype.

IMPORTANT NOTICE:

Upon testing, this tutorial does not work on Source 2013. A complete reboot of this tutorial may be made fixing it up for Source 2013. Please see the Talk page for more information.

Solution

Player life counter

We are, again, trying to make a last man standing gametype. One of the characteristics (about the only one in fact) is players having a set number of lives. I'm going to make them server controlled, (and ill even show you how to add them to the create server dialog box) with maximum and minimum values so we can have some fun and be able to blast some stuff while we are at it.

Open up c_hl2mp_player.cpp, c_hl2mp_player.h, hl2mp_player.cpp, hl2mp_player.h, hl2mp_gamerules.cpp and hl2mp_gamerules.h so we don't have to keep opening stuff up and what not. These are the only files you need to edit for the gametype to work.

First things first, we need our lives counter. I made it simple and, looking forward... we need to make this into a cvar on the server section and a normal variable on the client. i wont get into detail on why but suffice to say that this is how it works, so its how i done it.

Server-side variables

switch to hl2mp_player.h and find :

int m_iModelType;

below it we are going to add our variables.

   int m_iRewarded;
   CNetworkVar( int, m_iPlayerLives );

the finished product is:

...
private:

   CNetworkQAngle( m_angEyeAngles );
   CPlayerAnimState   m_PlayerAnimState;

   int m_iLastWeaponFireUsercmd;
   int m_iModelType;
   int m_iRewarded;
   CNetworkVar( int, m_iPlayerLives );
   CNetworkVar( int, m_iSpawnInterpCounter );
   CNetworkVar( int, m_iPlayerSoundType );

   float m_flNextModelChangeTime;
   float m_flNextTeamChangeTime;
...

Client-side variable

That's all for the variables to be created on the server. but... we want to allow the client to be able to see our variable on the other end, so lets jump over to c_hl2mp_player.h and add the player lives counter there too.

In the other file we had a variable, immediately below our player counter doing something that we can follow very closely to, it is also passing an integer value to the client.

CNetworkVar( int, m_iSpawnInterpCounter );

on the client side we have a variable that is simply defined

int     m_iSpawnInterpCounter;

it doesn't make it very hard to assume what we are going to have to do with ours. Right below m_iSpawnInterpCounter we add this

int     m_iPlayerLives;

finished product is:

   int     m_iSpawnInterpCounter;
   int     m_iPlayerLives;
   int     m_iSpawnInterpCounterCache;


Server-Client communication

To get these variables to communicate in a manner that's becoming of an officer, we have to do some sending and receiving that may boggle your mind atm. switch over to c_hl2mp_player.cpp and go down till you see:

RecvPropInt( RECVINFO( m_iSpawnInterpCounter ) ),

and right below it we will add our own...

RecvPropInt( RECVINFO( m_iPlayerLives ) ),

now we just go over to the other end of the game, server, and add a very similar statement. Switch over to hl2mp_player.cpp and go down till you see:

SendPropInt( SENDINFO( m_iSpawnInterpCounter), 4 ),

Before the questions arise, I'm gonna let you know that the four is the most important part of the whole thing, and I assure you, lots of errors will arise because of it. Four is the number of bits that the game will be using for passing the integer. If you are unfamiliar with binary, this is the time for you to pause, and go look it up on Google or something. For the quick and dirty, a binary number is just a bunch of flags that are either off or on to count to the next number. The largest value that m_iSpawnInterpCounter can hold is 15. With 4 flags all on, 15 is the number. If you don't believe me, open up microshafts calculator and switch the view to scientific, click on the bin radio box, and type in \"1111\" then click on the dec radio box. 15. You're damn right. Okay so now we have to make a decision. what is the maximum amount of lives a player can have. I chose 30, just to have some leeway in the display and for servers to be able to be as customizable as possible. add this below the statement we just talked about.

SendPropInt( SENDINFO( m_iPlayerLives), 5 ),

there are also a handful of useful send flags that you can use here... hint hint. open up dt_common.h

Initialization of variables:

Just like all other variables, you have to initialize the ones we just created, and we can even use this as a point to make our job easier.

if you are familiar with hl2mp_player you will recognize that we have 2 spawn functions as well as a constructor to work with. in the constructor, we should initialize our two variables. if we just initialize m_iPlayerLives to 0 than it will come back to bite us a little later, so lets set m_iPlayerLives to -1. This allows us to use it as a flag, which will be explained later. while we are here, set m_iRewarded to 0.

	m_iPlayerLives = -1;
	m_iRewarded = 0;

Now is when things get fun. As I'm opening up my code, I'll let you in on a big secret.

I love console variables (ConVars). They make our job so much easier. Who would have thought that a simple console variable could make or break a game as well as allow a server administrator to personalize their use of your mod, thereby increasing its portability.

And with that being said, I introduce you to:

ConVar   sv_playerlives( "sv_playerlives", "5", FCVAR_CHEAT | FCVAR_NOTIFY | FCVAR_REPLICATED, "Number of lives players have by default" );
ConVar   sv_reward_num( "sv_reward_num", "3", FCVAR_CHEAT | FCVAR_NOTIFY | FCVAR_REPLICATED, "Number of kills till the player receives a life" );

These are our two convars. One of which is very necessary in our initial spawn function.

Finished product:

CHL2MP_Player::~CHL2MP_Player(void)
{
	m_iPlayerLives = -1;
	m_iRewarded = 0;


	InitialSpawn();
}
	ConVar   sv_playerlives("sv_playerlives", "5", FCVAR_CHEAT | FCVAR_NOTIFY | FCVAR_REPLICATED, "Number of lives players have by default");
	ConVar   sv_reward_num("sv_reward_num", "3", FCVAR_CHEAT | FCVAR_NOTIFY | FCVAR_REPLICATED, "Number of kills till the player receives a life");

As you have probably guessed, our initial spawn function is the function that is called when we spawn for the first time(initially). And in that matter, it is never called again, so it makes very good sense to make this where we change our variables into the convars because we would like rounds to begin where the players are given lives and then later out of them.

This is going to blow you away.

void CHL2MP_Player::InitialSpawn( void )
{
   m_iPlayerLives = sv_playerlives.GetInt();

   BaseClass::InitialSpawn();
}

On to the next part.

I decided to add a little message to the players via a devmsg in order to assure testing was allowing the mod to roll correctly. I dropped it into the spawn function.

void CHL2MP_Player::Spawn(void)
{
...
   DevMsg("You have %d lives left before you are a spec\\n", m_iPlayerLives);
...
}

Functionality

We have a series of functions to add in order for our code to do what we want. first things first, lets start with the easy. Client side.

We need a function to get the players lives for the output to the screen. i added this right below GetPlayerModelSoundPrefix() as such: inside c_hl2mp_player.h

   const char   *GetPlayerModelSoundPrefix( void );
   int      GetPlayerLives(void) { return m_iPlayerLives; }

That's it folks.

Moving to the server end of things, we are going to make 3 functions: get, set and add. Now, I'm going to let you know they are not 100% necessary, but I have been taught to make variables private or protected and then make get and set functions for them to keep your data secure so here goes.

Add these to your hl2mp_player.h in a public section

   int GetPlayerLives(void) { return m_iPlayerLives; }
   void AddPlayerLives(int i) { m_iPlayerLives += i; }

   void SetPlayerLives(int i) { m_iPlayerLives = i; }

Notes:

  • You can alternatively add a SubtractPlayerLives() function, but it's not required.

What's left?

We have to make our variables work correctly.

In order to make this as smooth as one would like it to be, lets map out some conditions and see where they are located...

player gets the default amount of lives - When they spawn initially or the round begins anew player gains lives - When they hit the rewarded amount player loses lives - When they die.

We already have the first part taken care of with the initialization earlier. We have to add a couple conditions for the later two.

First let's add the condition for the loss of lives... This is an event triggered by the players life being < 1 and the event itself resides in our very close, and loved, hl2mp_player.cpp. One line is all it takes...

   m_iPlayerLives--;

It can go anywhere within the Event_Killed function as long as its not inside any {} but i found that it does a dandy job right after the DetonateTripmines() function.

   void CHL2MP_Player::Event_Killed( const CTakeDamageInfo &info )
{
   ...
   DetonateTripmines();

   m_iPlayerLives--;
   ...
}

This next portion is even better. LETS REWARD OURSELVES! a little below that statement, in the same function is a test that is very important to us...

if ( pAttacker )

   {
      int iScoreToAdd = 1;

      if ( pAttacker == this )
      {
         iScoreToAdd = -1;
      }

      GetGlobalTeam( pAttacker->GetTeamNumber() )->AddScore( iScoreToAdd );
   }

This is where we should be adding lives, mainly because it does everything for us already. This is the finished function:

if ( pAttacker )
   {
      int iScoreToAdd = 1;

      if ( pAttacker == this )
      {
         iScoreToAdd = -1;
      }

      if (pAttacker->IsPlayer())
         if (m_iRewarded==sv_reward_num.GetInt())   {
            m_iRewarded = 0;
            ToHL2MPPlayer(pAttacker)->AddPlayerLives(1);
         } else {
            m_iRewarded++;
         }

      GetGlobalTeam( pAttacker->GetTeamNumber() )->AddScore( iScoreToAdd );
   }

And that does it.

Restarting

//EDITOR'S NOTE: THE REFERENCED TUTORIAL MAY BE OUTDATED. I'M NOT SURE IF YOU'RE SUPPOSED TO DO THIS, SO HANG ON TIGHT. Lonkfania (talk) 03:29, 6 December 2016 (UTC)

In order to get this to work we need to follow another tutorial...

Resetting the Map

This basically makes it so restarting the round resets all props and entities.

Finish that one and then come back.

Alright, so you have the functions ready for us...

Here is the code and some explanations for it

I added another intermission function...

void CHL2MPRules::GoToIntermission2( void )
{
#ifndef CLIENT_DLL
   if ( g_fGameOver )
      return;

   m_bGameRestart = true;

   m_flIntermissionEndTime = gpGlobals->curtime + mp_chattime.GetInt();

   for ( int i = 0; i < MAX_PLAYERS; i++ )
   {
      CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );

      if ( !pPlayer )
         continue;

      pPlayer->ShowViewPortPanel( PANEL_SCOREBOARD );
      pPlayer->AddFlag( FL_FROZEN );
   }
#endif
}

and then updated the think function

void CHL2MPRules::Think( void )
{

#ifndef CLIENT_DLL

   CGameRules::Think();

   if ( g_fGameOver )   // someone else quit the game already
   {
      // check to see if we should change levels now
      if ( m_flIntermissionEndTime < gpGlobals->curtime )
         ChangeLevel();

      return;
   }
   if ( m_bGameRestart )
   {
      if ( m_flIntermissionEndTime < gpGlobals->curtime )
         RestartRound();
      return;
   }

   float flTimeLimit = mp_timelimit.GetFloat() * 60;
   // float flFragLimit = fraglimit.GetFloat();
// this is commented out because the player will
// not be able to achieve a max frags for an end to the round.
   
   if ( flTimeLimit != 0 && gpGlobals->curtime >= flTimeLimit )
   {
      GoToIntermission();
      return;
   }
   int playercountertotal = 0;
   int playercounter = 0;
   // check if any player is over the frag limit
   for ( int i = 1; i <= gpGlobals->maxClients; i++ )
   {
      CBasePlayer *pPlayer = UTIL_PlayerByIndex( i );
      
      if ( pPlayer )
         playercountertotal++;
      if( pPlayer && ToHL2MPPlayer(pPlayer)->GetPlayerLives()>0)
         playercounter++;
   }
   if (playercountertotal>0 && playercounter<=1)
   {
      GoToIntermission2();
      return;
   }
   ManageObjectRelocation();

   if (GetRoundtimerRemain()<=0&&m_bTimerStarted)
      GoToIntermission2();

#endif
}

Note: You are going to want to make sure your addkeeps include all of the following unless you like to see your game end with really bad messages.

   filter.AddKeep("worldspawn");
   filter.AddKeep("soundent");
   filter.AddKeep("hl2mp_gamerules");
   filter.AddKeep("scene_manager");
   filter.AddKeep("predicted_viewmodel");
   filter.AddKeep("team_manager");
   filter.AddKeep("event_queue_saveload_proxy");
   filter.AddKeep("player_manager");
   filter.AddKeep("player");
   filter.AddKeep("info_player_deathmatch");
   filter.AddKeep("info_player_rebel");
   filter.AddKeep("info_player_combine");
   filter.AddKeep("info_player_start");
   filter.AddKeep("ai_network");

Letting players see their lives

this is a very simple thing. All I did was dupe the health section and move it around a little bit rename some stuff and plop. Here is my hud_lives.cpp:

#include "cbase.h"
#include "hud.h"
#include "hud_macros.h"
#include "view.h"

#include "iclientmode.h"

#include <KeyValues.h>
#include "c_hl2mp_player.h"
#include <vgui/ISurface.h>
#include <vgui/ISystem.h>
#include <vgui_controls/AnimationController.h>

#include <vgui/ILocalize.h>

using namespace vgui;

#include "hudelement.h"
#include "hud_numericdisplay.h"

#include "ConVar.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

#define INIT_LIVES -1

//-----------------------------------------------------------------------------
// Purpose: Health panel
//-----------------------------------------------------------------------------
class CHudLives : public CHudElement, public CHudNumericDisplay
{
   DECLARE_CLASS_SIMPLE( CHudLives, CHudNumericDisplay );

public:
   CHudLives( const char *pElementName );
   virtual void Init( void );
   virtual void VidInit( void );
   virtual void Reset( void );
   virtual void OnThink();
         void MsgFunc_Damage( bf_read &msg );

private:
   // old variables
   int      m_iLives;
   
   int      m_bitsDamage;
};   

DECLARE_HUDELEMENT( CHudLives );
DECLARE_HUD_MESSAGE( CHudLives, Damage );

//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CHudLives::CHudLives( const char *pElementName ) : CHudElement( pElementName ), CHudNumericDisplay(NULL, "HudLives")
{
   SetHiddenBits( HIDEHUD_HEALTH | HIDEHUD_PLAYERDEAD | HIDEHUD_NEEDSUIT );
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CHudLives::Init()
{
   HOOK_HUD_MESSAGE( CHudLives, Damage );
   Reset();
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CHudLives::Reset()
{
   m_iLives      = INIT_LIVES;
   m_bitsDamage   = 0;

   wchar_t *tempString = g_pVGuiLocalize->Find("#LDM_Lives");

   if (tempString)
   {
      SetLabelText(tempString);
   }
   else
   {
      SetLabelText(L"Lives");
   }
   SetDisplayValue(m_iLives);
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CHudLives::VidInit()
{
   Reset();
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CHudLives::OnThink()
{
   int newLives = 0;
   C_BasePlayer *local = C_BasePlayer::GetLocalPlayer();
   CHL2MP_Player *pPlayer = ToHL2MPPlayer( local );
   if ( local )
   {
      // Never below zero
      newLives = max( pPlayer->GetPlayerLives(), 0 );
   }

   // Only update the fade if we've changed health
   if ( newLives == m_iLives )
   {
      return;
   }

   m_iLives = newLives;

   if ( m_iLives >= 3 )
   {
      g_pClientMode->GetViewportAnimationController()->StartAnimationSequence("LivesIncreasedAbove4");
   }
   else if ( m_iLives > 0 )
   {
      g_pClientMode->GetViewportAnimationController()->StartAnimationSequence("LivesIncreasedBelow2");
      g_pClientMode->GetViewportAnimationController()->StartAnimationSequence("LivesLow");
   }

   SetDisplayValue(m_iLives);
}

//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CHudLives::MsgFunc_Damage( bf_read &msg )
{

   int armor = msg.ReadByte();   // armor
   int damageTaken = msg.ReadByte();   // health
   long bitsDamage = msg.ReadLong(); // damage bits
   bitsDamage; // variable still sent but not used

   Vector vecFrom;

   vecFrom.x = msg.ReadBitCoord();
   vecFrom.y = msg.ReadBitCoord();
   vecFrom.z = msg.ReadBitCoord();

   // Actually took damage?
   if ( damageTaken > 0 || armor > 0 )
   {
      if ( damageTaken > 0 )
      {
         // start the animation
         g_pClientMode->GetViewportAnimationController()->StartAnimationSequence("HealthDamageTaken");
      }
   }
}

open up HudLayout.res and add the lives section to the listing...

   HudLives
   {
      "fieldName"      "HudLives"
      "xpos"   "r150"
      "ypos"   "392"
      "wide"   "78"
      "tall"  "36"
      "visible" "1"
      "enabled" "1"

      "PaintBackgroundType"   "2"
      
      "text_xpos" "8"
      "text_ypos" "20"
      "digit_xpos" "42"
      "digit_ypos" "2"
   }

All Better.

You might have noticed that there are a couple new hud animations... here they are scripted...

event LivesIncreasedAbove4
{
   Animate   HudLives   BgColor   "BgColor"   Linear   0.0   0.0
   
   Animate   HudLives   TextColor "FgColor" Linear 0.0 0.04
   Animate   HudLives   FgColor   "FgColor" Linear 0.0 0.03
   
   Animate   HudLives      Blur      "3"         Linear   0.0      0.1
   Animate   HudLives      Blur      "0"         Deaccel   0.1      2.0
}

event LivesIncreasedBelow2
{
   Animate HudLives   FgColor      "BrightFg"   Linear   0.0      0.25
   Animate HudLives   FgColor      "FgColor"      Linear   0.3      0.75
   
   Animate HudLives      Blur      "3"         Linear   0.0      0.1
   Animate HudLives      Blur      "0"         Deaccel   0.1      2.0
}

event LivesLow
{
   Animate HudLives   BgColor      "DamagedBg"      Linear   0.0      0.1
   Animate HudLives   BgColor      "BgColor"      Deaccel   0.1      1.75
   
   Animate HudLives   FgColor      "BrightFg"   Linear   0.0      0.2
   Animate HudLives   FgColor      "DamagedFg"      Linear   0.2      1.2
   
   Animate HudLives TextColor      "BrightFg"   Linear   0.0      0.1
   Animate HudLives   TextColor      "DamagedFg"      Linear   0.1      1.2
   
   Animate HudLives      Blur      "5"         Linear   0.0      0.1
   Animate HudLives      Blur      "3"         Deaccel   0.1      0.9
}

Creating a server...

Adding this section is completely optional, but makes your mod look a whole lot more complete, in my opinion.

We are going to add our two CVars to the listing of things we are able to change when creating a server.

The cvars are already ready already. We just have to remind ourselves of their names and what nots.

ConVar   sv_playerlives( "sv_playerlives", "5", FCVAR_CHEAT | FCVAR_NOTIFY | FCVAR_REPLICATED, "Number of lives players have by default" );
ConVar   sv_reward_num( "sv_reward_num", "3", FCVAR_CHEAT | FCVAR_NOTIFY | FCVAR_REPLICATED, "Number of kills till the player receives a life" );

We are going to add two variables to our listing, both numbers, and for our own sakes, the values are going to be kept simple.

Open up cfg/settings.scr and add this.

   "sv_playerlives"
   {
      "Number Of Lives"
      { NUMBER 0 -1 }
      { "5" }
   }

   "sv_reward_num"
   {
      "Number Of Kills Before Rewarded"
      { NUMBER 0 -1 }
      { "3" }
   }

This right here will allow the admin of the server to choose the number for either cvar at will. Looks kinda neat. You can play around with its order with the others if you want but i found that it looked best like this:

DESCRIPTION SERVER_OPTIONS
{
   "hostname"
   {
      "Server name"
      { STRING }
      { "Lazy Dev - Testing #01" }
   }

   "sv_password"
   {
      "Password"
      { STRING }
      { "DevMsg" }
   }

   "maxplayers"
   {
      "Max Players"
      { NUMBER 0 32 }
      { "16" }
   }

   "mp_fraglimit"
   {
      "Frag Limit"
      { NUMBER 0 -1 }
      { "10" }
   }

   "mp_timelimit"
   {
      "Time Limit (Min.)"
      { NUMBER 0 -1 }
      { "0" }
   }

   "sv_playerlives"
   {
      "Number Of Lives"
      { NUMBER 0 -1 }
      { "1" }
   }

   "sv_reward_num"
   {
      "Number Of Kills Before Rewarded"
      { NUMBER 0 -1 }
      { "3" }
   }

   "mp_forcerespawn"
   {
      "Force Respawn"
      { BOOL }
      { "1" }
   }

   "mp_weaponstay"
   {
      "Weapons Stay"
      { BOOL }
      { "0" }
   }

   "mp_footsteps"
   {
      "Footsteps"
      { BOOL }
      { "1" }
   }

   "mp_flashlight"
   {
      "Flashlight"
      { BOOL }
      { "1" }
   }
}

Credits

This Tutorial was originally from gneu.org and moved here to combine all the tutorials into one location.