Creating A Class System

From Valve Developer Community
Jump to navigation Jump to search
English (en)Deutsch (de)Translate (Translate)
Broom icon.png
This article or section needs to be cleaned up to conform to a higher standard of quality because:
This has been translated from German. Layout, Third person view and maybe grammar.
For help, see the VDC Editing Help and Wikipedia cleanup process. Also, remember to check for any notes left by the tagger at this article's talk page.
Broom icon.png
This article or section should be converted to third person to conform to wiki standards.

Introduction

This coding tutorial deals with creating a class system.

Notes (/Thoughts):

  • There are 3 classes (Supporter, Medic and Assaulter)
  • When you first spawn you get a default class.
  • If you change classes you die and spawn with the new class.

Data that is needed

First, a list of what code files you need:

Header: dlls/player.h

Source code files dlls/player.cpp


The class system will be 100% server-side, but the client can change classes.

The declarations

First you have to make the declarations. What you need are some variables and some functions. First you create an enum that contains the values ​​of the classes! The enum goes into the player.h via class CBasePlayer;

First, you create the enum like this:

enum
{
	Unassigned = 0,
	Assaulter,
	Supporter,
	Medic,
};

You will need this later for the comparisons!

Now let's focus on the CBasePlayer class.

To start with, create a new public and a new private section at the end of CBasePlayer in player.h.

This is where you write all the code for the class system! // The new areas for the class system! public: private:


Here is everything you need to declare about methods and of course the variables:

/*
********************************************
**Class system:
**This is where the classes are distributed!
**It will also check for the correct classes!
********************************************
*/
public:
	// method for changing classes
	virtual void ChangeClass(int NewClass); 
	virtual int GetClass();

	// Initialization of the class system!
	void InitClassSystem();
	// Checks if we do not set an invalid value for the class variables:
	void CheckAllClassConVars();

	// Here we check whether we have changed classes:
	void OnClassChange();
	// Sets life and armor for the player!
	void SetClassStuff();

	// Sets the new value for the check variable:
	void SetCurrentClassValue();

	// Get the class:
	int GetClassValue()const;
	// Get the standard class:
	int GetDefaultClassValue()const;

	// Switching method that distributes classes:
	void SetPlayerClass();

	// Determine life for our class:
	int  GetClassHealth()const;
	// These methods set the life and armor for each class
	int GetClassMaxHealth()const;

	// Sets the armor for the class:
	int  GetClassArmor()const;
	// These methods restore the life and armor for each class
	int GetClassMaxArmor()const;

private:  
	// Important check on first spawn:
	bool m_bFirstSpawn;
	bool IsFirstSpawn();
	// What class do we have(enum):
	int m_iClass;
	// Check if we have changed classes:
	int m_iCurrentClass;
	// Here we set the standard class!
	int m_iDefaultClass;

	// Distribute weapons, life and armor of the classes:
	void SetClassDefault();
	void SetClassGroundUnit();
	void SetClassSupportUnit();
	void SetClassMedic();
	void SetHealthValue( int value );

/*
********************************************
**Player characteristics:
**Here, characteristics such as speed, condition
**and maximum classes set!
********************************************
*/
	// Integer for Armor:
	int m_iArmor;
	int m_iMaxArmor;

Now for the explanation, from top to bottom.

public

// method for changing classes
virtual void ChangeClass(int NewClass);

This method is needed so that the client (player) can change his class via menu/console.

virtual int GetClass();

You need it if you want to add things like ammo, life and armor incrementation for the supporter/medic.

// Initialization of the class system!
void InitClassSystem();

This method should start the class system from the spawn method of CBasePlayer. This calls the methods for class distribution, etc. You will then be smarter when implementing it.

// Checks if you do not set an invalid value for the class variables:
void CheckAllClassConVars();

This method checks whether you have entered a valid value. If we didn't check this, your mod would crash! If the value is incorrect, you need to set a value for the class.

// Here it is checked whether you have changed classes:
void OnClassChange();

This method is used in the Think method. If you change classes you will be killed and a point will be added, otherwise you would get points deducted.

// Sets life and armor for the player!
void SetClassStuff();

This method sets the life, maximum life and armor values ​​for each class. Unfortunately, the armor is determined by the recharger code, which you have to change yourself.

// Sets the new value for the check variable:
void SetCurrentClassValue();

This method changes the value of the CheckVariable for the classes, otherwise you would die again and again!

// Get the class:
int GetClassValue()const;

This method gets the value of the class you have.

// Get the standard class:
int GetDefaultClassValue()const;

This method gets the default class from the server so that in case of an invalid class you get it.

// Switching method that distributes classes:
void SetPlayerClass();

This method is the control center of our class system. Everything is controlled there with the value of the class variable. From weapons and life distribution to setting the player model (unfortunately you still have to do that!).

// Sets the life for the class:
int  GetClassHealth()const;

This method brings out the life you have in the moment.

// These methods set the life and armor for each class
int GetClassMaxHealth()const;

The method gets the maximum life the class can have.

// Sets the armor for the class:
int  GetClassArmor()const;

This method retrieves the armor you currently have.

// These methods bring the life and armor for each class
int GetClassMaxArmor()const;

This method gets the maximum armor of the class (unfortunately it is still unusable because of the recharger code!)

private

// Important check on first spawn:
bool m_bFirstSpawn;

This variable is needed for the query for the first spawn.

bool IsFirstSpawn();

This method tells you if you are spawning for the first time.

// What class does one have(enum):
int m_iClass;

This variable stores the value of the class:

// Check if you have changed classes:
int m_iCurrentClass;

This is the check variable that contains the value of the current class. The value is then changed when the class is changed.

// This is where you set the standard class!
int m_iDefaultClass;

This variable gets the class of the server and is also the first class you get when you first spawn.

// Distribute weapons, life and armor of the classes:
void SetClassDefault();
void SetClassGroundUnit();
void SetClassSupportUnit();
void SetClassMedic();

These are the methods for distributing all the important things of a class. (Life, maximum life, armor, maximum armor, weapons, etc.)

// Integer for Armor:
int m_iArmor;
int m_iMaxArmor;

These are the variables for Armor and maximum Armor.

Now you know what you need and why.

Now you come to the part where you install everything.

The implementation

Now that you have everything you need, all you have to do is install everything! Everything goes into the player.cpp at the very end.

Here is the code with explanation:

Added in the constructor:

	// First spawn?
	m_bFirstSpawn = true;

	// Set starting class:
	m_iClass = default_class.GetInt();

	m_iCurrentClass = m_iClass;
	// Set the default class!
	m_iDefaultClass = default_class.GetInt();

	// Default values ​​100 for Life/Armor:
	m_iHealth = 100;
	m_iArmor = 100;

	// Maximum Life/Armor for each class:
	m_iMaxHealth = 100;
	m_iMaxArmor = 100;

void CBasePlayer::InitClassSystem()
{
	DevMsg("Class system is initialized!\n");
	CheckAllClassConVars();
	SetPlayerClass();
	SetClassStuff();
}

int CBasePlayer::GetClassValue()const
{
	return m_iClass;
}

int CBasePlayer::GetDefaultClassValue()const
{
	return m_iDefaultClass;
}

bool CBasePlayer::IsFirstSpawn()
{
	return m_bFirstSpawn;
}

// Should distribute the weapons:
// There's a bug here somewhere!
void CBasePlayer::SetPlayerClass()
{
	if(IsFirstSpawn())
	{
		m_iClass = m_iDefaultClass;
	}
	// Now the classes are distributed:
	switch(m_iClass)
	{
	case Assaulter:
		SetClassGroundUnit();
		break;
	case Supporter:
		SetClassSupportUnit();
		break;
	case Medic:
		SetClassMedic();
		break;
	//case Invalid:
		// Still needs to be installed!
		// Here you only get the standard weapons!
	case Unassigned:
		// This is where you set the standard class!
	default:
		SetClassDefault();
		break;
	}
}

void CBasePlayer::OnClassChange()
{
	if( m_iClass != m_iCurrentClass )
	{    
		// Kill player and increase points by 1 (due to suicide)!
		CommitSuicide();
		IncrementFragCount(1);

		// Sit down so that you don't have the same class!
		m_iCurrentClass = m_iClass;
	}
}

void CBasePlayer::CheckAllClassConVars()
{
	// Do you have a class that lies between Spec-Ops and the last class?
	if( m_iClass < Assaulter || m_iClass > Medic )
	{
		m_iClass = Assaulter;
	}

	if( m_iDefaultClass < Assaulter || m_iDefaultClass > Medic )
	{
		m_iDefaultClass = Assaulter;
	}
}

void CBasePlayer::SetClassDefault()
{
	Msg("You have the class Default!\n");

	CheckAllClassConVars();
	SetPlayerClass();
}

// Assault:
void CBasePlayer::SetClassGroundUnit()
{
	// Standard values ​​for life/armor:
	m_iHealth = 125;
	m_iArmor = 125;

	// Maximum Life/Armor for each class:
	m_iMaxHealth = 125;
	m_iMaxArmor = 125;

	// Distribute weapons:
	Msg("You are now a Ground Unit!\n");

	CBasePlayer::GiveNamedItem( "weapon_357" );	
	CBasePlayer::GiveNamedItem( "weapon_smg1" );
	CBasePlayer::GiveNamedItem( "weapon_frag" );
	
	CBasePlayer::GiveAmmo( Magazin_357*3 ,	"357" );
	CBasePlayer::GiveAmmo( Magazin_SMG1*3         ,	"SMG1");
	CBasePlayer::GiveAmmo( Magazin_SMG1_Granates*1,	"smg1_grenade");
	CBasePlayer::GiveAmmo( Magazin_Frag*5         ,	"grenade" );
}

// Supporter
void CBasePlayer::SetClassSupportUnit()
{
	// Default values ​​for life/armor:
	m_iHealth = 100;
	m_iArmor = 300;

	// Maximum Health/Armor for Class:
	m_iMaxHealth = 100;
	m_iMaxArmor = 300;

	// Distribute weapons:
	Msg("You are now a Support Unit!\n");

	CBasePlayer::GiveNamedItem( "weapon_ar2" );
	CBasePlayer::GiveNamedItem( "weapon_frag" );
	CBasePlayer::GiveNamedItem( "weapon_357" );	
	
	CBasePlayer::GiveAmmo( Magazin_AR2*3,	"AR2" );
	CBasePlayer::GiveAmmo( Magazin_AR2AltFire*2,	"AR2AltFire" );
	CBasePlayer::GiveAmmo( Magazin_Frag*3,	"grenade" );
	CBasePlayer::GiveAmmo( Magazin_357*3 ,	"357" );
	
	// Call the supporter's property:
	// GetAmmo();
}

// Medic:
void CBasePlayer::SetClassMedic()
{
	// Standard values ​​for life/armor:
	m_iHealth = 100;
	m_iArmor = 75;

	// Maximum Health/Armor for Class:
	m_iMaxHealth = 100;
	m_iMaxArmor = 75;

	// Distribute weapons:
	Msg("You are now a Medic!\n");

	CBasePlayer::GiveNamedItem( "weapon_357" );
	CBasePlayer::GiveNamedItem( "weapon_smg1" );
	
	CBasePlayer::GiveAmmo( Magazin_357*2,	"357" );
	CBasePlayer::GiveAmmo( Magazin_SMG1*2 ,	"SMG1");
	CBasePlayer::GiveAmmo( Magazin_SMG1_Granates*3,	"smg1_grenade");
}

// Sets the life for the class:
int  CBasePlayer::GetClassHealth()const
{
	return m_iHealth;
}

// Sets the armor for the class:
int  CBasePlayer::GetClassArmor()const
{
	return m_iArmor;
}

// These methods set the life and armor for each class
int CBasePlayer::GetClassMaxHealth()const
{
	return m_iMaxHealth;
}

// These methods restore the life and armor for each class
int CBasePlayer::GetClassMaxArmor()const
{
	return m_iMaxArmor;
} 

// Sets life and armor for the player!
void CBasePlayer::SetClassStuff()
{
	// Hier Leben setzen:    
	SetHealthValue(GetClassHealth());
	SetMaxHealthValue(GetClassMaxHealth());

	// Variable does not belong to CBasePlayer!!!
	// TODO: This is a private variable that I need to change!
	SetArmorValue(GetClassArmor());
	// SetMaxArmorValue(GetClassMaxArmor());
}

// Class via ConVar change for client:
void CBasePlayer::ChangeClass(int NewClass)
{
	m_iClass = NewClass;
}

Since you already know what all the methods are for, you just have to see how everything works :)

But now to install the ConVars that you need:

// The server has the same default class for everyone!
// The admin can change the default class!
ConVar default_class("default_class", "3", FCVAR_ARCHIVE, "Variable for standard class!");

Now the server has its default class, which every player gets when they spawn. You can then change this via the console :)

You can also make more settings available to the admin:

// Allow maximum classes (admins!)
// TODO: Still needs to be activated in the code!
ConVar max_assaulter("max_assaulter", "3", FCVAR_ARCHIVE, "Variable for maximum Assaulter!");
ConVar max_supporter("max_supporter", "3", FCVAR_ARCHIVE, "Variable for maximum Supporters!");
ConVar max_medic("max_medic", "3", FCVAR_ARCHIVE, "Variable for maximum Medics!");

// Allow classes (admins):
// TODO: Still needs to be activated in the code!
ConVar allow_assaulter("allow_assaulter", "1", FCVAR_ARCHIVE, "Allowed class Ground Unit!");
ConVar allow_supporter("allow_supporter", "1", FCVAR_ARCHIVE, "Allowed class Support Unit!");
ConVar allow_medic("allow_medic", "1", FCVAR_ARCHIVE, "Allowed class Medic!");

But you have to implement this yourself!

Now you just have to give the player the option to change the class! And then you have to add the class checks!

To do this, you simply have to add the following to the end of the OnClientCommand method:

	else if ( !stricmp( cmd, "class" ) )
	{
		if ( engine->Cmd_Argc() < 2 )
			return true;

		int iClass = atoi( engine->Cmd_Argv(1) );
		ChangeClass(iClass);

		return true;
	}

This simulates a ConVar. But you can change the class by entering Class and then the class number, i.e. Class 1! Now you have to call the test method and then you have it!

You do this in the PostThink() method;

At the end you simply insert the following:

OnClassChange();

That's it :) if everything is correct then the class system should work ;)

Important information about the code

To ensure that the code runs without problems, a few things still need to be fixed!

First, you need to add the following methods in the public area:

// Declarations:
	void SetHealthValue( int value );
	int  GetHealthValue();
	void SetMaxHealthValue(int MaxValue);
	int  GetMaxHealthValue();
	void IncrementHealthValue( int nCount );

	int     GetArmorValue();

// Implementation:

void CBasePlayer::SetHealthValue( int value )
{
	m_iHealth = value;
}

void CBasePlayer::SetMaxHealthValue( int MaxValue )
{
	m_iMaxHealth = MaxValue;
}

int CBasePlayer::GetHealthValue()
{
	return m_iHealth;
}

int CBasePlayer::GetMaxHealthValue()
{
	return m_iMaxHealth;
}

void CBasePlayer::IncrementHealthValue( int nCount )
{ 
	m_iHealth += nCount;
	if (m_iMaxHealth > 0 && m_iHealth > m_iMaxHealth)
		m_iHealth = m_iMaxHealth;
}

int CBasePlayer::GetArmorValue()
{
	return m_ArmorValue;
}

This allows you to set the armor and health (depending on how much the classes should have). But Medics and Supporters can also regenerate their life/armor this way!

Now you have to change the ammunition distribution. You can either enter the ammunition numbers yourself or you can create a header as follows: Start header definition:

#ifndef _Magazin_H
#define _Magazin_H

The magazine definitions:

// Only change if you change the ammunition of the weapon magazines in the script/code!
// Sets the magazines (magazine and then size in bullets)!
#define Magazin_None           0
#define Magazin_Pistole       20
#define Magazin_357            6
#define Magazin_SMG1          50
#define Magazin_SMG1_Granates  1
#define Magazin_AR2          100
#define Magazin_AR2AltFire     1
#define Magazin_RPG            1
#define Magazin_Slam           1
#define Magazin_Frag           1
#define Magazin_Shotgun        8
#define Magazin_Crossbow       1

The maximum definition of a magazine:

// Sets the maximum ammo/magazines:
#define Max_Magazin_None           Magazin_None*0
#define Max_Magazin_Pistole        Magazin_Pistole*16
#define Max_Magazin_357            Magazin_357*6
#define Max_Magazin_SMG1           Magazin_SMG1*8
#define Max_Magazin_SMG1_Granates  Magazin_SMG1_Granates*7
#define Max_Magazin_AR2            Magazin_AR2*3
#define Max_Magazin_AR2AltFire     Magazin_AR2AltFire*2
#define Max_Magazin_RPG            Magazin_RPG*25
#define Max_Magazin_Slam           Magazin_Slam*10
#define Max_Magazin_Frag           Magazin_Frag*10
#define Max_Magazin_Shotgun        Magazin_Shotgun*13
#define Max_Magazin_Crossbow       Magazin_Crossbow*50

//Finish the magazine definition:
#endif //_Magazin_H

I would recommend the second method, as you can also change the ammunition definitions in the gamerules.cpp and include the maximum ammunition!

And you should set the default class using CooVars or remove the default class from the code completely!

Open class menu

In order to be able to call the class system later, you should enter this in the baseviewport.cpp:

// sub dialogs
#include "clientscoreboarddialog.h"
#include "spectatorgui.h"
#include "teammenu.h"
#include "classmenu.h"
#include "vguitextwindow.h"
#include "IGameUIFuncs.h"
#include "mapoverview.h"
#include "hud.h"
#include "NavProgress.h"

Now the command to call:

CON_COMMAND( chooseclass, "Opens a menu for class choose" )
{
	if ( !gViewPortInterface )
		return;

	gViewPortInterface->ShowPanel( "class", true );
}

A little further down this:

void CBaseViewport::CreateDefaultPanels( void )
{
#ifndef _XBOX
	AddNewPanel( CreatePanelByName( PANEL_SCOREBOARD ) );
	AddNewPanel( CreatePanelByName( PANEL_INFO ) );
	AddNewPanel( CreatePanelByName( PANEL_SPECGUI ) );
	AddNewPanel( CreatePanelByName( PANEL_SPECMENU ) );
	AddNewPanel( CreatePanelByName( PANEL_NAV_PROGRESS ) );

	AddNewPanel( CreatePanelByName( PANEL_TEAM ) );
	AddNewPanel( CreatePanelByName( PANEL_CLASS ) );

	// AddNewPanel( CreatePanelByName( PANEL_BUY ) );
#endif
}

And finally this:

IViewPortPanel* CBaseViewport::CreatePanelByName(const char *szPanelName)
{
	...
	else if ( Q_strcmp(PANEL_TEAM, szPanelName) == 0 )
	{
		newpanel = new CTeamMenu( this );
	}
	else if ( Q_strcmp(PANEL_CLASS, szPanelName) == 0 )
	{
		newpanel = new CTeamMenu( this );
	}
	...
}

Bonus

Now you can have some fun and assign a specific speed to each class.

To do this you need the hl2_player.cpp and the player.h/.cpp

First you have to find out where the player sets his running speed!

You do this here:

#ifdef HL2MP
#define	HL2_WALK_SPEED 150
#define	HL2_NORM_SPEED 190
#define	HL2_SPRINT_SPEED 320
#else
#define	HL2_WALK_SPEED hl2_walkspeed.GetFloat()
#define	HL2_NORM_SPEED hl2_normspeed.GetFloat()
#define	HL2_SPRINT_SPEED hl2_sprintspeed.GetFloat()
#endif

The explanation is simple. If HL2MP is defined, then default values ​​are set:

#ifdef HL2MP
#define	HL2_WALK_SPEED 150
#define	HL2_NORM_SPEED 190
#define	HL2_SPRINT_SPEED 320

Otherwise you can do everything via the ConVars, but in HL2DM this is only possible via the default values!

#else
#define	HL2_WALK_SPEED hl2_walkspeed.GetFloat()
#define	HL2_NORM_SPEED hl2_normspeed.GetFloat()
#define	HL2_SPRINT_SPEED hl2_sprintspeed.GetFloat()
#endif

But now you have to build a controller for the speed!

To do this you just have to add a little something to the base player. Open player.h and add the following in the last public area:

// Here the player speed is set:
void SetWalkSpeed(int WalkSpeed);
void SetNormSpeed(int NormSpeed);
void SetSprintSpeed(int SprintSpeed);

This allows you to set the values, but now you need the class variables:

// Player speed:
int m_iWalkSpeed; 
int m_iNormSpeed;
int m_iSprintSpeed;

Now the methods for getting the values:

int GetWalkSpeed(); 
int GetNormSpeed();
int GetSprintSpeed();

Now to the implementation:

void CBasePlayer::SetWalkSpeed(int WalkSpeed)
{
	m_iWalkSpeed=WalkSpeed;
}

void CBasePlayer::SetNormSpeed(int NormSpeed)
{
	m_iNormSpeed=NormSpeed;
}

void CBasePlayer::SetSprintSpeed(int SprintSpeed)
{
	m_iSprintSpeed=SprintSpeed;
}

int CBasePlayer::GetWalkSpeed() 
{
	return m_iWalkSpeed;
}

int CBasePlayer::GetNormSpeed()
{
	return m_iNormSpeed;
}

int CBasePlayer::GetSprintSpeed()
{
	return m_iSprintSpeed;
}

Now you only have to do 2 things!

First you have to say that the Get methods control the speed.

You do this as follows:

#define	HL2_WALK_SPEED CBasePlayer::GetWalkSpeed()
#define	HL2_NORM_SPEED CBasePlayer::GetNormSpeed()
#define	HL2_SPRINT_SPEED CBasePlayer::GetSprintSpeed()

That's almost it.

Now you just have to define what speed your class should have.

To do this, you just have to add the new SetSpeed ​​methods to the SetClass methods.

Then you can quickly add a class.

Here is an example of my support unit class:

// Supporter
void CBasePlayer::SetClassSupportUnit()
{
	// Enter code here!
	// m_szClassName = ClassNames[CLASS_SUPPORT];

	// Default values ​​for life/armor:
	m_iHealth = 100;
	m_iArmor = 300;

	// Maximum Life/Armor for Each Class:
	m_iMaxHealth = 100;
	m_iMaxArmor = 300;

	// Distribute weapons:
	Msg("You are now a Support Unit!\n");


	switch(GetTeamNumber())
	{
	case TEAM_REBELS:
		// Distribute weapons here: 
		// Rebels:
		CBasePlayer::GiveNamedItem( "weapon_reb_hmg" );
		CBasePlayer::GiveNamedItem( "weapon_reb_frag" );
		break;

	case TEAM_COMBINE:
		// Distribute weapons here:
		// Combine:
		CBasePlayer::GiveNamedItem( "weapon_com_hmg" );
		CBasePlayer::GiveNamedItem( "weapon_com_frag" );	
		break;

	default:
		CBasePlayer::GiveNamedItem( "weapon_ar2" );
		CBasePlayer::GiveNamedItem( "weapon_frag" );
		break;
	}
	CBasePlayer::GiveNamedItem( "weapon_ammo_spawner" );
	CBasePlayer::GiveAmmo( Magazin_AR2*3,	"AR2" );
	CBasePlayer::GiveAmmo( Magazin_Frag*3,	"grenade" );
	SetWalkSpeed(90);
	SetNormSpeed(130);
	SetSprintSpeed(200);
}

I have already set the weapon distribution to the teams but I can explain that too ;) As soon as I have improved that (e.g. with a team variable) ;)

So have fun and write to me if you have any problems or criticism ;)