GameRules

From Valve Developer Community
Jump to navigation Jump to search
English (en)Deutsch (de)Translate (Translate)

The GameRules object defines the abstract rules of a game. It conventionally handles all event processing that is not innate to players, NPCs, the world, or other such tangible entities.

In a mod that supported deathmatch and capture the flag game modes, the modes would each have their own GameRules class containing the appropriate logic for when to award points, where to spawn players, and so on.

Minimum implementation

In shared\<mod>\<mod>_gamerules.cpp:

#include "cbase.h"
#include "multiplay_gamerules.h"

#ifdef CLIENT_DLL
	#define CSkeletonGameRules C_SkeletonGameRules
#endif

// Valve's base classes: CSingleplayRules, CMultiplayRules, CTeamplayRules, CTeamplayRoundBasedRules
class CSkeletonGameRules : public CMultiplayRules
{
	DECLARE_CLASS( CSkeletonGameRules, CMultiplayRules );
};

REGISTER_GAMERULES_CLASS( CSkeletonGameRules );

In server\<mod>\<mod>_client.cpp:

void InstallGameRules()
{
	CreateGameRulesObject( "CSkeletonGameRules" );
}

Basic Multiplay Game Rule Class (Beta)

Header file:src\game\shared\czgo\czgo_base_mode.h

#ifndef CZGO_BASE_MODE_H
#define CZGO_BASE_MODE_H
#pragma once
#include "multiplay_gamerules.h"

#ifdef CLIENT_DLL
#define CCZGOBaseMode C_CZGOBaseMode
#define CCZGOBaseModeProxy C_CZGOBaseModeProxy
#endif

class CCZGOBaseModeProxy : public CGameRulesProxy
{
public:
	DECLARE_CLASS(CCZGOBaseModeProxy, CGameRulesProxy);
	DECLARE_NETWORKCLASS();
};

class CCZGOBaseMode : public CMultiplayRules
{
public:
	DECLARE_CLASS(CCZGOBaseMode, CMultiplayRules);

#ifdef CLIENT_DLL
	DECLARE_CLIENTCLASS_NOBASE(); // This makes datatables able to access our private vars.
#else
	DECLARE_SERVERCLASS_NOBASE(); // This makes datatables able to access our private vars.
#endif

	CCZGOBaseMode();
	virtual ~CCZGOBaseMode();

	virtual void Precache(void);
	virtual bool ShouldCollide(int collisionGroup0, int collisionGroup1);
	virtual bool ClientCommand(CBaseEntity *pEdict, const CCommand &args);
	virtual void Think(void);
	virtual void CreateStandardEntities(void);
	virtual const char *GetGameDescription(void);
	virtual void ClientDisconnected(edict_t *pClient);

#ifndef CLIENT_DLL

	virtual bool CanHavePlayerItem(CBasePlayer *pPlayer, CBaseCombatWeapon *pItem);
	virtual bool FShouldSwitchWeapon(CBasePlayer *pPlayer, CBaseCombatWeapon *pWeapon);

	void AddLevelDesignerPlacedObject(CBaseEntity *pEntity);
	void RemoveLevelDesignerPlacedObject(CBaseEntity *pEntity);
	const char *GetChatFormat(bool bTeamOnly, CBasePlayer *pPlayer);

#endif

};

inline CCZGOBaseMode* CZGOBaseMode()
{
	return static_cast<CCZGOBaseMode*>(g_pGameRules);
}

#endif

Source Code:src\game\shared\czgo\czgo_base_mode.cpp

#include "cbase.h"
#include "czgo_base_mode.h"

#ifdef CLIENT_DLL
#include "../client/hl2mp/c_hl2mp_player.h"
#else
#include "../server/hl2mp/hl2mp_player.h"
#endif

REGISTER_GAMERULES_CLASS(CCZGOBaseMode);

BEGIN_NETWORK_TABLE_NOBASE(CCZGOBaseMode, DT_CZGOBaseMode)
END_NETWORK_TABLE()

LINK_ENTITY_TO_CLASS(czgo_basemode, CCZGOBaseModeProxy);
IMPLEMENT_NETWORKCLASS_ALIASED(CZGOBaseModeProxy, DT_CZGOBaseModeProxy)

#ifdef CLIENT_DLL
void RecvProxy_CZGOBaseMode(const RecvProp *pProp, void **pOut, void *pData, int objectID)
{
	CCZGOBaseMode *pRules = CZGOBaseMode();
	Assert(pRules);
	*pOut = pRules;
}

BEGIN_RECV_TABLE(CCZGOBaseModeProxy, DT_CZGOBaseModeProxy)
RecvPropDataTable("czgo_basemode_data", 0, 0, &REFERENCE_RECV_TABLE(DT_CZGOBaseMode), RecvProxy_CZGOBaseMode)
END_RECV_TABLE()
#else
void* SendProxy_CZGOBaseMode(const SendProp *pProp, const void *pStructBase, const void *pData, CSendProxyRecipients *pRecipients, int objectID)
{
	CCZGOBaseMode *pRules = CZGOBaseMode();
	Assert(pRules);
	return pRules;
}

BEGIN_SEND_TABLE(CCZGOBaseModeProxy, DT_CZGOBaseModeProxy)
SendPropDataTable("czgo_basemode_data", 0, &REFERENCE_SEND_TABLE(DT_CZGOBaseMode), SendProxy_CZGOBaseMode)
END_SEND_TABLE()
#endif

CCZGOBaseMode::CCZGOBaseMode()
{
}

CCZGOBaseMode::~CCZGOBaseMode()
{
}

bool CCZGOBaseMode::ClientCommand(CBaseEntity *pEdict, const CCommand &args)
{
#ifndef CLIENT_DLL
	if (BaseClass::ClientCommand(pEdict, args))
		return true;


	CHL2MP_Player *pPlayer = (CHL2MP_Player *)pEdict;

	if (pPlayer->ClientCommand(args))
		return true;
#endif

	return false;
}

void CCZGOBaseMode::CreateStandardEntities(void)
{

#ifndef CLIENT_DLL
	// Create the entity that will send our data to the client.
	BaseClass::CreateStandardEntities();

#ifdef DBGFLAG_ASSERT
	CBaseEntity *pEnt =
#endif

	CBaseEntity::Create("czgo_basemode", vec3_origin, vec3_angle);
	Assert(pEnt);
#endif

}

void CCZGOBaseMode::Think(void)
{

#ifndef CLIENT_DLL
	CGameRules::Think();
#endif

}

#ifndef CLIENT_DLL

//=========================================================
// CanHaveWeapon - returns false if the player is not allowed
// to pick up this weapon
//=========================================================
bool CCZGOBaseMode::CanHavePlayerItem(CBasePlayer *pPlayer, CBaseCombatWeapon *pItem)
{
	return true;
}

#endif

void CCZGOBaseMode::ClientDisconnected(edict_t *pClient)
{
}

const char *CCZGOBaseMode::GetGameDescription(void)
{
	return "Condition Zero: Global Operation";
}

//-----------------------------------------------------------------------------
// Purpose: 
//-----------------------------------------------------------------------------
void CCZGOBaseMode::Precache(void)
{
}

bool CCZGOBaseMode::ShouldCollide(int collisionGroup0, int collisionGroup1)
{
	if (collisionGroup0 > collisionGroup1)
	{
		// swap so that lowest is always first
		V_swap(collisionGroup0, collisionGroup1);
	}

	if ((collisionGroup0 == COLLISION_GROUP_PLAYER || collisionGroup0 == COLLISION_GROUP_PLAYER_MOVEMENT) &&
		collisionGroup1 == COLLISION_GROUP_WEAPON)
	{
		return false;
	}

	return BaseClass::ShouldCollide(collisionGroup0, collisionGroup1);

}


#ifdef CLIENT_DLL

#else

bool CCZGOBaseMode::FShouldSwitchWeapon(CBasePlayer *pPlayer, CBaseCombatWeapon *pWeapon)
{
	if (pPlayer->GetActiveWeapon() && pPlayer->IsNetClient())
	{
		// Player has an active item, so let's check cl_autowepswitch.
		const char *cl_autowepswitch = engine->GetClientConVarValue(engine->IndexOfEdict(pPlayer->edict()), "cl_autowepswitch");
		if (cl_autowepswitch && atoi(cl_autowepswitch) <= 0)
		{
			return false;
		}
	}

	return BaseClass::FShouldSwitchWeapon(pPlayer, pWeapon);
}

#endif

#ifndef CLIENT_DLL
const char *CCZGOBaseMode::GetChatFormat(bool bTeamOnly, CBasePlayer *pPlayer)
{
	if (!pPlayer)  // dedicated server output
	{
		return NULL;
	}

	const char *pszFormat = NULL;

	// team only
	if (bTeamOnly == TRUE)
	{
		if (pPlayer->GetTeamNumber() == TEAM_SPECTATOR)
		{
			pszFormat = "HL2MP_Chat_Spec";
		}
		else
		{
			const char *chatLocation = GetChatLocation(bTeamOnly, pPlayer);
			if (chatLocation && *chatLocation)
			{
				pszFormat = "HL2MP_Chat_Team_Loc";
			}
			else
			{
				pszFormat = "HL2MP_Chat_Team";
			}
		}
	}
	// everyone
	else
	{
		if (pPlayer->GetTeamNumber() != TEAM_SPECTATOR)
		{
			pszFormat = "HL2MP_Chat_All";
		}
		else
		{
			pszFormat = "HL2MP_Chat_AllSpec";
		}
	}

	return pszFormat;
}

#endif

IMPORTANT: One last step, navigate to line 1257 in hl2mp_player.cpp, find and comment out:

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

This is because class "CMultiplay" doesn't define "Team".

Todo: Need to figure out how to avoid this...

Managing shared code

GameRules are mostly written in shared code so that the client can predict the effects of its actions. However, there are some functions that are only present on the client or on the server.

Todo: Best practices for recognising and managing this. #ifdefs confuse Visual Studio...

Networking

GameRules are not themselves networked. Instead, a proxy object is created by inheriting from the CGameRulesProxy class.

Notable functions

Shared

int Damage_GetNoPhysicsForce()
int Damage_GetShouldGibCorpse()
etc.
Various functions that return a integer bitstring containing the damage types that fall into the given category.
bool ShouldCollide()
Whether entities in the given collision groups should collide with each other. Since the arguments are collision groups and not entities, this function isn't as useful as it might at first seem.
bool SwitchToNextBestWeapon()
CBaseCombatWeapon* GetNextBestWeapon()
Two functions that can be used to choose a weapon for the player if, for instance, their current one runs out of ammo. SwitchToNextBestWeapon() returns true if the switch was successful.
float GetAmmoDamage()
Decides how much damage to apply when an entity (any entity!) fires bullets. Normally forwards calls to GetAmmoDef().
unsigned char* GetEncryptionKey()
Your mod's ICE key for script encryption.
CViewVectors* GetViewVectors()
Accessor for g_DefaultViewVectors.

Server

void LevelShutdown()
Called when the GameRules are destroyed by the world.
void FrameUpdatePostEntityThink()
Called each frame. By default forwards the call to Think().
Todo: What is the difference between this "PostEntityThink" func and EndGameFrame()?
void EndGameFrame()
Called at the end of CServerGameDLL::GameFrame() (i.e. after all game logic has run).
CreateStandardEntities()
Creates the external entities needed for the GameRules to function.
Warning.pngWarning:An untraceable crash will occur if you override this function without calling BaseClass::CreateStandardEntities().

There are too many other important functions to list here individually. See game\shared\gamerules.h line 179 onwards for the complete list, which covers:

  • Difficulty levels
  • The single/multiplayer status of a game
  • Client connection & disconnection
  • Client damage rules
  • Client kills and scoring
  • Spawning and respawning control
  • Weapon Damage
  • Weapon/item/ammo retrieval
  • Weapon/item spawn and respawn control
  • AI Definitions
  • Healthcharger respawn control
  • What happens to a dead player's weapons and ammo
  • Teamplay rules
  • Footstep sounds
  • NPCs
  • TraceLines
  • Chat team/location prefixes
  • Achievements