Entity Programming - Overview Last edited 5 months ago2023-12-15 06:22:10 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.


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


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.


Called for every entity in the map by the engine when the map is loaded for the first time. When loading a save game, this is not called unless the entity has the FCAP_MUST_SPAWN flag.

If your entity has a custom Precache method, it should be called from Spawn. The engine will not automatically call Precache except when loading a save game.


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


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!

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

Save and Restore are the key methods for saving and loading custom data fields across save files (the ones in the mod's SAVE folder). In order for those to know what to save/load and what not to save/load, they need a "save/restore table". Needless to say that this is a requirement for singleplayer modifications (and not needed for multiplayer only ones).

Everything you need to know about the save/restore system is available on it's dedicated page, the choice to read now or later is up to you.


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 instead of Precache 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.


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 )
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.
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 );
   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 )
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.


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.
pev->nextthink = gpGlobals->time + 0.5; // think every 0.5 seconds


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.



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.



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.


Static methods

These methods are useful in certain situations.


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



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


Dispatch functions

If you wonder how the engine actually calls these functions, it is done through the various Dispatch functions:
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.

Entity lifecycle

When the game is loaded, the dispatch functions are called by the engine, which then call the matching functions on every entity. This is the order of when those functions are called:

Loading a new map

  1. KeyValue(kvpd) for each keyvalue for the entity
  2. Spawn()
  3. Activate()
Notice that Precache() is not called. If your entity has a custom Precache function, you should call it from the Spawn method.

Saving a save game

  1. ObjectCaps()
    • This is called by DispatchSave to see if the entity has the FCAP_DONT_SAVE flag, in which case Save will not be called.
  2. Save()

Loading a save game

  1. ObjectCaps()
    • This is called by DispatchRestore to see if the entity has the FCAP_MUST_SPAWN flag, in which case Spawn will be called instead of Precache below.
  2. Restore()
  3. Precache()
  4. Activate()

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.


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