Entity Programming - Overview Last edited 5 months ago2022-06-27 11:20:04 UTC

Half-Life Programming

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 on, we'll dive into writing some useful entities for mappers.

*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.
struct edict_s
{
    qboolean free;
    int serialnumber;
    link_t area; // linked to a division node or leaf

    int headnode;    // -1 to use normal leaf check
    int num_leafs; // How many leaves the entity occupies, shouldn't be more than MAX_ENT_LEAFS
    short leafnums[MAX_ENT_LEAFS];

    float freetime;    // sv.time when the object was freed

    void* pvPrivateData; // Alloced and freed by engine, used by DLLs; pointer to a HL SDK entity
    entvars_t v; // Common entity variables
};
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, a pointer to the game entity (an instance of an HL SDK entity class), and an entvars_t variable to hold common entity variables.
Generally, you won't use edict_t often.

In CBaseEntity, there's a reference to this entvars_t.
entvars_t *pev;
"pev" stands for "Pointer to Entity Variables", and it is essentially a group 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: Uninitialised values will be 0, as this structure is memset'ed by the engine while spawning the entity.

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 string allocator, which is essentially a very large 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 allocate a string and return some number. If we had a string_t variable with a value of 11, STRING would return "func_wall" in the example above. In reality, you'd get garbage at low string_t values, and usable strings usually show up around 250 million.
User posted image

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; // This line is NOT needed if using Solokiller's Half-Life: Updated SDK!
}

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 single+multiplayer projects.
Save and Restore are the key methods for transferring custom data fields across save files. If an entity has its own members, but doesn't save/restore, then it is most likely to break while loading a save file, 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 bool Save( CSave &save );
virtual bool 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.
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 booleans and the save/restore code depending on your HL SDK base
If you are not using Solokiller's Half-Life Updated SDK (or any SDK that did not made an "save/restore integer to boolean migration"), you will likely notice that bool in this section are either BOOL or int in the SDK you are using.

GoldSrc is a heavily modified version of the Quake engine and many of it's code has been kept. It's important to note that Quake was programmed with the C programming language which didn't have a proper built-in boolean type (no bool with a size of one). A common trick that was applied back then in and outside of Half-Life is to typedef the int variable type to improve the code's readability and thus the types qboolean (Quake) and BOOL (Half-Life, Windows API and many others) were born and used.

To conclude: Solokiller's Half-Life Updated SDK saves boolean values with a size of one bit, so make sure your saved/restored variables are typed as bool. For other SDKs, type them as BOOL because it likely saves boolean values with a size of an integer. If in doubt, check how an existing boolean variable from an existing entity (m_fLongJump in CBasePlayer for example) is declared.

Failure to take this into account will cause a huge risk of offsets being produced during the save/restore process and entities will not be saved/restored properly caused by a corruption during the save process and/or even worse: crash the game/mod.
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 "capability 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

You can add your own cap flags as well.

*Activate*

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.

Modular methods

CBaseEntity has a lot of overrideable methods, but some of these are modular - they can be changed dynamically at runtime.
One of those is Think:
virtual void Think( void )
{
    if ( m_pfnThink )
        (this->*m_pfnThink)();
}
These methods actually call 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 );
Keep in mind, however, that you don't really have to use these, and for simple entities, you can just override these methods without consequences.
Note
Editor's note: this section needs verification.

If the entity uses MOVETYPE_PUSH, it will call Think differently. From a few experiments, it seems that Think is called every 0.1s or 0.05s with this movetype, despite pev->nextthink being far smaller.
According to the Quake source code, a simplified version of the thinking logic would be the following:
oldltime = pev->ltime;

// We can assume that the smallest movetime will be the host's frametime
if ( pev->nextthink < pev->ltime + host_frametime )
   movetime = max( 0, pev->nextthink - pev->ltime );
else
   movetime = host_frametime;

// pev->ltime will always be increased by movetime
if ( movetime )
{
   SV_PushMove( pev, movetime )
   {
      pev->ltime += movetime;
   }
}

// The only way all these conditions can be satisfied is:
// - pev->nextthink was smaller than pev->ltime + host_frametime
// - pev->ltime was between pev->nextthink and (pev->nextthink - host_frametime)
if ( pev->nextthink > oldltime && pev->nextthink <= pev->ltime )
{
   Think();
}
So, in order to work around that, you will have to set nextthink this way:
pev->nextthink = pev->ltime + SOME_CONSTANT;
Where SOME_CONSTANT may be between 0.0 and 0.1.

*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 pressing the use button aiming at an entity (if the respective object cap is enabled), 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 a class pointer 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 various Dispatch functions:
DispatchSpawn
DispatchKeyValue
DispatchTouch
DispatchUse
DispatchThink
DispatchBlocked
DispatchSave
DispatchRestore
DispatchObjectCollsionBox
The game exports these to the engine when hl.dll is loaded, after which the engine 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.

Comments

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