Entity Programming - Overview Last edited 3 years ago2021-11-06 14:39:52 UTC

You are viewing an older revision of this wiki page. The current revision may be more detailed and up-to-date. Click here to see the current revision of this page.

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;
}

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 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 treats booleans as integers. This is due to 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: crash the game.

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 "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.
Note: The next page isn't written yet.

Comments

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