Entity Programming - Overview Last edited 2 months ago2020-05-25 13:57:13 UTC

Half-Life Programming

  • Entity Programming - Overview
In the previous chapter, we covered weapons. Weapons are a special type of entity, but an entity nonetheless.
In this chapter, we will look at entities more generally, and see how they work. Later, we'll create two point entities (trigger_timer and trigger_conprint - prints to the console) and a brush entity (a "counting impulse" button - the more the player presses E, the more it'll progress).

entvars_t vs. edict_t vs. entity classes

Let's make this clear. The engine doesn't know the difference between a CBaseEntity and a CFuncWall. Instead, the engine only knows one form of entity. And that is edict_t.
It's basically an entity dictionary, containing data such as whether the entity's memory is free, its "serial number", its link to a BSP leaf, if it's a brush entity how many leaves it has, private entity data (to store member variables specific to the C++ classes of entities), and an entvars_t variable.
Generally, you won't use edict_t often.

In CBaseEntity, there's an entvars_t as well.
entvars_t *pev;
"pev" stands for "Pointer to Entity Variables", and it is essentially a set of variables that is common to all entities. entvars_t is defined in progdefs.h, and constants that can be used with variables from entvars_t are in const.h.
Some of the most important variables from entvars_t are: Certain utility functions will accept edict_t as a parameter, so you'll need to convert your CBaseEntity or CFuncWall or any entity class you're using into edict_t. While there's no direct conversion, you can use the ENT utility function to 'convert' an entvars_t into an edict_t. For example:
SET_MODEL(ENT(pev), STRING(pev->model));
We'll discuss utility functions later on.

string_t

You may have noticed several variables of the string_t type. This is not a C-string.

Beginners get very confused by string_t, since it's essentially an unsigned int, so I'll briefly explain it here.
The engine has a giant internal string. string_t is in fact just an offset into that giant string.

You can imagine it this way:
H  e  l  l  o     w  o  r  l  d \0 f  u  n  c  _  w  a  l  l  \0
0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21
If we wanted to allocate a new string there via ALLOC_STRING, like so:
string_t something = ALLOC_STRING( "something" );
...it'd return 22. If we had a string_t variable with a value of 11, STRING would return 11. That is essentially what string_t is. Just an offset into the internal string.

CBaseEntity

The mother of all entities.
As mentioned previously, every entity in the HL SDK inherits from CBaseEntity. We won't cover all of its methods and members, but we will go through the most important ones.

Spawn

Called for every entity in the map by the engine.

Precache

Entities override this so they can load model, sound and other files that they may use.
Files can be precached with the following macros:
PRECACHE_MODEL - precaches studio models
PRECACHE_SOUND - precaches audio files
PRECACHE_GENERIC - precaches any file, you can even precache a whole .bsp if you wanted to

KeyValue

Handles map keyvalues and assigns values from them to member variables. The engine calls this multiple times until it covers all keyvalues of every entity.

Parameters: This is how keyvalues are typically handled:
if (FStrEq(pkvd->szKeyName, "myKeyvalue"))
{
    m_myVariable = atoi(pkvd->szValue);
    pkvd->fHandled = TRUE;
}

else
    CBaseEntity::KeyValue(pkvd);
We compare szKeyName to all possible keyvalues this entity may have, and then convert the szValue string into a value of the type we need.
If no match is found, then go back up to the KeyValue method of our superclass.

Save & Restore

A note about multiplayer
For multiplayer-only projects, save/restore is needless because you can't save and load games in multiplayer. However, it's mandatory for singleplayer-only and singleplayer + multiplayer projects.
Save and Restore are the key methods for transferring custom data fields across savefiles. If an entity has its own members, but doesn't save/restore, then it is most likely to break while loading a savefile, or across level transitions.

A save/restore is fully set up when an entity class has defined and implemented a save-restore table, as well as the save-restore methods.

Inside the entity class, you override the Save and Restore methods as well as declaring the save-restore table (TYPEDESCRIPTION) as m_SaveData like this:
virtual int        Save( CSave &save );
virtual int        Restore( CRestore &restore );
static    TYPEDESCRIPTION m_SaveData[];
Outside of the entity class, you define the save-restore table like this:
TYPEDESCRIPTION    CPendulum::m_SaveData[] =
{
    DEFINE_FIELD( CPendulum, m_accel, FIELD_FLOAT ),
    DEFINE_FIELD( CPendulum, m_distance, FIELD_FLOAT ),
    DEFINE_FIELD( CPendulum, m_time, FIELD_TIME ),
    DEFINE_FIELD( CPendulum, m_damp, FIELD_FLOAT ),
    DEFINE_FIELD( CPendulum, m_maxSpeed, FIELD_FLOAT ),
    DEFINE_FIELD( CPendulum, m_dampSpeed, FIELD_FLOAT ),
    DEFINE_FIELD( CPendulum, m_center, FIELD_VECTOR ),
    DEFINE_FIELD( CPendulum, m_start, FIELD_VECTOR ),
};

IMPLEMENT_SAVERESTORE( CPendulum, CBaseEntity );
Note: Valve generally placed this near either the class declaration or the LINK_ENTITY_TO_CLASS line, but it can be placed anywhere.

DEFINE_FIELD requires 3 parameters: the class being saved/restored, the attribute being saved/restored and its type. The following table contains all available types of variables you can save and restore. The types with an asterisk (*) are the ones you will see and likely use very often in the HL SDK :
TypeDescription
FIELD_FLOAT*A floating point value (0.2, 75.5) that is not related to time (use FIELD_TIME for that purpose)
FIELD_STRING*A string ID (string_t - often the result of ALLOC_STRING)
FIELD_ENTITYAn entity offset (EOFFSET)
FIELD_CLASSPTR*An entity class pointer (like CBaseEntity *)
FIELD_EHANDLE*An entity handle (EHANDLE)
FIELD_EVARSAn entity variables pointer (EVARS *)
FIELD_EDICTAn entity dictionary pointer (edict_t *)
FIELD_VECTOR*A vector (Vector, vec3_t, array of 3 float)
FIELD_POSITION_VECTORA world coordinate (fixed up across level transitions "automagically")
FIELD_POINTERArbitrary data pointer (scheduled to be removed by Valve but seems that isn't the case)
FIELD_INTEGER*An integer (5, 3) or enum value.
FIELD_FUNCTIONA class function pointer (like Think, Touch, Use...)
FIELD_BOOLEAN*A "boolean" value (see the warning below as to why boolean is between quotes)
FIELD_SHORTA 2 byte integer value.
FIELD_CHARACTERA single byte value.
FIELD_TIME*Same as FIELD_FLOAT but for variables related to time (like weapons reload time), usually a floating point value that has a relation to the game time (gpGlobals->time)
FIELD_MODELNAMEAn engine string that is a model's name (requires precaching).
FIELD_SOUNDNAMESame as previous one but for sounds.
The last line, IMPLEMENT_SAVERESTORE requires 2 parameters: the class being saved and its parent. It tells the save/restore code to take care of your entity variables as well as the parent's ones. This is handy because it avoids having to redefine every variable from the parent(s).
An important note about FIELD_BOOLEAN
The save/restore code treat booleans as integers. This is because of GoldSrc's Quake engine legacy, which was C only and the language didn't have a proper built-in boolean type. That is why you will see a lot of qboolean and BOOL around the HL SDK code. So make sure you use BOOL on variables you wish to save rather than the built-in C++ bool type. If you don't do that, there is a huge risk of offsets being produced during the save/restore process and entities will not be saved/restored properly and even cause save corruption and/or even worse: game crash.

This does not apply if you update the core save-restore code and update all entities that have a save-restore table with one or more boolean variables. This will not be detailed here since this isn't the scope of this page.
Saves, save-restore tables and testing
If you update the save-restore table of any entity, it's good practice to not use (even better, to delete) the previous saves. This is because the saved data is now out of sync with your new save-restore table.

ObjectCaps

A way of storing read-only flags for entities. Entities can override this to return certain constants that will make the entity usable by the player, or held down continuously, transferable between levels etc.

The constants can be:
FCAP_CUSTOMSAVE - unknown and unused
FCAP_ACROSS_TRANSITION - the entity will transfer across level transitions
FCAP_MUST_SPAWN - calls Spawn right after Restore, i.e. after loading a savefile
FCAP_DONT_SAVE - don't write into the savefile
FCAP_IMPULSE_USE - can be used by the player
FCAP_CONTINUOUS_USE - can be held by the player (such as levers and valves)
FCAP_ONOFF_USE - can be toggled by the player
FCAP_DIRECTIONAL_USE - receives +/- from the player, only used by func_tracktrain
FCAP_MASTER - entity can be used as a master (multisource has this cap)
FCAP_FORCE_TRANSITION - entity always goes across transitions

Active

Called on every entity after the server is activated. It is used, for example, by CFuncTrain/func_train to teleport itself to the first path_corner as soon as the server starts. It is rarely used.

Modular methods

These methods are actually callbacks.
void (CBaseEntity ::*m_pfnThink)(void);
void (CBaseEntity ::*m_pfnTouch)( CBaseEntity *pOther );
void (CBaseEntity ::*m_pfnUse)( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value );
void (CBaseEntity ::*m_pfnBlocked)( CBaseEntity *pOther );
This means that each can be changed at any time.

Think

Called every time period, or every tick, this method is often used by entities to perform real-time or otherwise periodic logic, e.g. check if a certain entity is within radius every 0.5 seconds.

To set a think callback, use the SetThink macro.

To set the next time an entity will "think", set the pev->nextthink variable.
Example:
pev->nextthink = gpGlobals->time + 0.5; // think every 0.5 seconds

Touch

Called every time the entity is touched by another entity. Entities such as trigger_once absolutely rely on this to work.

To set a touch callback, use the SetTouch macro.

Parameters:

Use

Called either by players when USE-ing an entity, or by other entities that have the ability to trigger other entities. For example, when a func_button triggers a func_door, it is calling the func_door's Use method.

To set the use callback, use the SetUse macro.

Parameters:

Blocked

Called every time an entity blocks this entity.
This is essentially how doors and trains damage and gib things. They apply damage to any entity that blocked them.

Parameters:

Static methods

These methods are useful in certain situations.

Create

Allocates a new entity of any classname and spawns it at given coordinates. Returns a CBaseEntity pointer to the newly spawned entity.

Parameters:

Instance

Retrieves private data from an existing entity. It is used to 'convert' edict_t*, entvars_t* or entity indices into CBaseEntity*.

Parameters:

Dispatch functions

If you wonder how the engine actually calls these functions, it is done through the Dispatch functions:
DispatchSpawn
DispatchKeyValue
DispatchTouch
DispatchUse
DispatchThink
DispatchBlocked
DispatchSave
DispatchRestore
DispatchObjectCollsionBox
It locates their addresses when it loads hl.dll, and then calls them for each edict_t in its internal array of entities.

Other base classes and important entity classes

CWorld - worldspawn entity
CItem - base item class
CBaseDelay - generic delay entity, can be spawned when an entity tries to trigger another entity with a delay On the next page, you'll learn how to create a very basic entity that will print a mapper-defined message to the console.
Note: The next page isn't actually written yet.

Comments

You must log in to post a comment. You can login or register a new account.