Entity Programming - Inheritance VS Duplication (Creating Simple Variants of Existing Entities) Last edited 2 years ago2022-09-21 10:40:14 UTC

Half-Life Programming

It is common for GoldSrc mods to feature "simple variants" of existing entities. The context of simple variants in the previous sentence being: "a copy of an existing entity with tiny changes". If we are talking about monsters for example, these tiny changes could be the model, health, damage values and so on.

Very common examples are: The goal of this page is to explain the common ways to create these simple variants and determine their pros and cons.

Keep in mind that this page is covers simple changes and may not apply to everything. If you want something like a friendly female assassin, that's out of the scope.

The "copy-paste" method

This method basically consists of selecting all the code belonging to an entity, copying to the clipboard (the famous CTRL/COMMAND+C keyboard shortcut), pasting it (CTRL/COMMAND+V to the rescue) somewhere else, and renaming everything until it compiles and works.

For example, let us suppose you want to make a poison and a fast headcrab variant like in Half-Life 2 using this method.

For the poison headcrab, you select everything that concerns monster_headcrab, CHeadCrab and so on, copy-paste it somewhere else and rename everything to monster_headcrab_poison, CHeadCrabPoison, etc.

Then you change the attack code on the poison headcrab so that it poisons the victim instead of a simple bite. Finally, you add some health regeneration logic from the poison à la HL2 to all monsters - it works, you are happy.

Same drill for the fast headcrab: copy-paste the original headcrab code, rename everything to monster_headcrab_fast, CHeadCrabFast, make some fine-tuning here and there to make it faster. It works, job's done, time to move on to something else.

Now you have your three headcrab variants, and then you receive a suggestion to add, say, a range attack to all headcrab variants, or to make them randomly strafe left and right. Assuming you agree with it and decide to implement it, that means you have to update one headcrab variant and copy-paste all the changes twice - for the poison and fast variants respectively, with potential slight modifications. You also have to be very careful not to end up breaking anything.

You might have guessed it at this point - maintenance becomes a nightmare, because you have to watch for inconsistencies between the original and your variants and likely fix them. It may be manageable if you have two or three variants, but imagine if you have to add and maintain a forth, a fifth, a sixth one...

On top of that, another problem arises. Let us assume that the poison headcrab has difficulties jumping at the same distance as the other headcrabs and that is not an intended behavior: The point is, you can easily lose track of what has and hasn't been changed.

Sure, some people might try solving this with something like version control software like git to check the history of your code, or by using software that tells the difference between two texts/files, like WinMerge.

However, not everyone, especially beginners, will bother with version control. Furthermore, if the quality of the history is not good, with large commits and meaningless messages, you will waste time and effort doing history archeology rather than fixing the problem.

As for comparing the previous code to the current one, that might work if you have some kind of guarantee that the previous code was working as intended and was intact before making the comparison. In other words, are you sure you are comparing the "good" code to the code that causes the problem?

Beginners often like to use copied code, be it from a website or a fellow programmer, without putting much thought into it.
That is another big disadvantage of the method. If you're about to do that, you'd better ask yourself the following questions if you haven't already: Also keep in mind that your compile time and the size of your binary will increase a tiny bit. You might say that the difference is negligible and everybody has fast Internet and a lot of storage space nowadays, but that sounds more like a lazy excuse not to optimize the code.

Sadly, there are situations where this method is the only way to add an entity or a feature to your mod. Several examples include weapons, skill CVARs and some other things.

The "different class name" method

As you might already know, level designers place entities using class names. The weapon_crowbar class name, for example, represents the iconic crowbar. In order to link that class name to a C++ class, the handy macro LINK_ENTITY_TO_CLASS( mapName, className ) is used.

It is possible to use the LINK_ENTITY_TO_CLASS( mapName, className ) macro multiple times to link multiple class names to the same C++ class. In fact, Valve itself did it for several entities, like the following: There is nothing restricting you from doing the same. To illustrate this, let us make a zombie Barney using this method:

Open dlls/zombie.cpp and search this line:
LINK_ENTITY_TO_CLASS( monster_zombie, CZombie );
We are going to represent our zombie Barney with monster_zombie_barney, so you can adapt this part of the code to something like this:
LINK_ENTITY_TO_CLASS( monster_zombie, CZombie );
LINK_ENTITY_TO_CLASS( monster_zombie_barney, CZombie );
One thing to be aware of when doing this is to check if the existing code overrides (or forces) a specific class name, as mentioned above. Look at the Spawn method of the class and check if there is a constant being assigned to pev->classname. Taking the Glock as example again, you would see something like pev->classname = MAKE_STRING( "weapon_9mmhandgun" );.

If that override exists, you will have to remove it, otherwise your copy won't work. Be careful before and after removal and make sure that it will not cause a regression when compiling the code and when testing or playing your mod.

In the zombie's case, that override does not exist, so we are safe on that side.

The next question you might ask is how can you determine what kind of zombie you've got? It is done by checking if the value of pev->classname matches our zombie Barney variant or not by using the handy inline bool FClassnameIs( edict_t *pev, const char *szClassname ) function (there's also a version that takes an entvars_t * instead of an edict_t *). inline bool becomes BOOL on "non-modern" HL SDK bases.

Assuming you only want to change the model, the code would look like this:
// Rest of the code is not shown here because no changes are needed

LINK_ENTITY_TO_CLASS( monster_zombie, CZombie );
LINK_ENTITY_TO_CLASS( monster_zombie_barney, CZombie ); // Tie "monster_zombie_barney" to the same "CZombie" class

void CZombie::Spawn()
{
    // [...]

    if ( FClassnameIs( pev, "monster_zombie_barney" ) ) // If this is the zombie Barney
        SET_MODEL( ENT( pev ), "models/zombie_barney.mdl" ); // Then use the zombie Barney model
    else // Otherwise, assume this is a zombie scientist
        SET_MODEL( ENT( pev ), "models/zombie.mdl" ); // And use the zombie scientist model instead

    // [...]
}

void CZombie::Precache()
{
    // [...]

    if ( FClassnameIs( pev, "monster_zombie_barney" ) ) // If this is the zombie barney
        PRECACHE_MODEL( "models/zombie_barney.mdl" ); // Then precache the zombie barney model
    else // Otherwise, assume this is a zombie scientist
        PRECACHE_MODEL( "models/zombie.mdl" ); // And precache the zombie scientist model instead

    // [...]
}
Assuming you have: Your zombie Barney(s) should be there.

The major advantage of this method is that you haven't duplicated the entire zombie code. Aside from the model change, everything else is identical (health, damage, behavior, etc). This way it's easier to make changes that affect all or specific zombie variants.

A flaw of this method is that the code can turn into an if / else if / else / switch / case mess if there are too many differences between the original and the variants. At that point, you should consider using the next method described below or making a separate entity.

As an exercise, you can try to add zombie soldiers to your mod using this method and making them stronger than zombie scientists or Barneys by giving more health or damage.

The "let the level designer decide" method

This is the method used by Laurie Cheers in Spirit of Half-Life. You let the level designers choose certain settings of your entity. This means that the level designers can set the model, body, skin and so on, or they can just let the game fall back to the default.

We are going to reproduce the custom model feature from Spirit of Half-Life for the headcrab to demonstrate this. Open dlls/headcrab.cpp and change the code like this:
void CHeadCrab::Spawn()
{
    // [...]

    if ( pev->model ) // If the level designer set a custom model
        SET_MODEL( ENT( pev ), STRING( pev->model ) ); // Then set it
    else // Otherwise
        SET_MODEL( ENT( pev ), "models/headcrab.mdl" ); // Use the default headcrab model

    // [...]
}

void CHeadCrab::Precache()
{
    // [...]

    if ( pev->model ) // If the level designer set a custom model
        PRECACHE_MODEL( dynamic_cast<char *>( STRING( pev->model ) ) ); // Then precache it
    else // Otherwise
        PRECACHE_MODEL( "models/headcrab.mdl" ); // Use the default headcrab model

    // [...]
}
All you need to do now is to update the FGD file by opening it with a text editor, searching for the entity (monster_headcrab in our case) and adding the model key-value definition (look at the cycler entity for example) so that level designers can use it.

One potential problem with this method is testing. Spawning the entity with give (the entity must be precached by the game or the map, otherwise it can lead to a crash) will cause the entity to appear with its default settings. The same applies to weapons given by the impulse 101 cheat command, assuming the weapons you need have been added to the command in the first place.

Basically, you need to place one or more instances of the entity in the map, set up the desired custom settings in your editor, compile the map, load it, then test and repeat until everything works. You will also likely need to update the settings of the entities in the map before repeating the test and that implies compiling the map again.

You can get around that problem by temporarily changing the defaults (make sure to roll these changes back once done with the testing) or making a temporary server command that will spawn the entity with the desired custom settings (look at the RPG rocket and crossbow bolt code for example).
Special cases for this method
Some variables that are part of the entity variables (pev) or are class attributes may require additional work for this method to work. We were lucky with pev->model because it seems to be already handled by the engine itself.

For an example, take a look at the dlls/func_tank.cpp file and, more precisely, the KeyValue( KeyValueData *pkvd ) method override of the CFuncTank class. Notice how the firing rate represented by the m_fireRate attribute is assigned to using the firerate key-value pair that is set through the map editor.

This also explains how the FGD and the game code work together. Refer to the previous page of this book for a more in-depth explanation of the KeyValue( KeyValueData *pkvd ) method of the CBaseEntity class.
However, this method shares its problem with the "different class name" method: your code can turn into a if`/`else`/`switch`/`case mess if there is way too much customization going on. In this case, perhaps you should try another method or, again, make a separate entity.

As an exercise, you could try editing existing entities to allow level designers to change more settings, or you could make an entity that prints a message to the console and let the level designer decide what message should be printed.

The "inheritance" method

You know this already: the game code of Half-Life is written in C++. Although some might say that it really is "C with C++ classes", it's still C++ in the end.

You should already know that C++ is an object-oriented programming language, like Java or C#. This implies the concepts of inheritance and abstraction.

This book will not teach you these concepts by telling you how to create classes or making children of these classes. Neither will this book explain the difference between public / protected / private, when to use virtual / override, and so on. If you did not understood a single thing in that last sentence, you should probably stop reading this book and read a book about C++ instead.

The purpose of this section is to demonstrate the application of these concepts within the Half-Life 1 game code.

To trigger entities in maps, level designers use such entities as trigger_once and trigger_multiple. Both of those brush entities do the same thing, with the only difference that trigger_once fires only, well, once, and trigger_multiple can fire multiple times.

If you look at the CTriggerOnce class, you can see it is a child of CTriggerMultiple, meaning it inherits everything from it. The only difference is the method Spawn being overridden to enforce a negative value to be assigned to "Delay before reset" (m_flWait) before calling the Spawn method of its parent. That is what gives trigger_once its "oncey" nature.

CTriggerMultiple, which represents the trigger_multiple entity, is itself a child of CBaseTrigger, because there are more methods of triggering entities aside from touching invisible zones. CBaseTrigger is a child of CBaseToggle so that triggers can have a master (see the multisource entity). You can finish the chain yourself if you want.

Now, do you remember the normal, poison and fast headcrabs situation described in the "copy/paste" section? Inheritance would work better there. Let us make a headcrab variant that turns faster. For that, make a new child of CHeadCrab and override SetYawSpeed like this:
class CHeadCrabFast : public CHeadCrab
{
public:
    void SetYawSpeed() override;
};
LINK_ENTITY_TO_CLASS( monster_headcrab_fast, CHeadCrabFast );

void CHeadCrabFast::SetYawSpeed()
{
    // Use the same values as the original headcrab
    CHeadCrab::SetYawSpeed();

    // ...then multiply it by two to make it turn faster
    pev->yaw_speed *= 2;
}
Assuming you want a different headcrab model (to mimic Half-Life 2), you would need to override Spawn and Precache as well to precache and use the model, while calling the parent's precache method for the rest of the resources.

Here is a more complete example with the poison ability of the poison headcrab. We could have overriden the LeapTouch method where the "bite" actually happens but unfortunately this is impossible.
Exported methods and inheritance don't get along.
One thing to be aware of when using inheritance in Half-Life 1's game code is that you cannot override exported methods - methods declared with the EXPORT macro.

The following exported methods are present in all entities: Blocked( CBaseEntity *pOther ), Think(), Touch( CBaseEntity *pOther ) and Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ).
With LeapTouch being exported, we have no other choice but to copy-paste and adapt our own version called PoisonLeapTouch. Then we'll have to override everything that sets the touch method to LeapTouch to use the PoisonLeapTouch version instead. Luckily, there is only one place where we'll have to do it - StartTask( Task_t *pTask ).

With all that said, we could imagine the result being something like this:
class CHeadCrabPoison : public CHeadCrab
{
public:
    void StartTask( Task_t *pTask ) override; // Needed because we can't override exported (EXPORT) methods like "CHeadcrab::LeapTouch"

    void PoisonLeapTouch( CBaseEntity *pOther );
};
LINK_ENTITY_TO_CLASS( monster_headcrab_poison, CHeadCrabPoison );

// This method is a copy/paste from the original headcrab code with the exception of the "SetTouch" call pointing to our poison version of "LeapTouch"
// This is required because overriding exported (EXPORT) methods like "CHeadcrab::LeapTouch" causes conflicts and crashes
void CHeadCrabPoison::StartTask( Task_t *pTask )
{
    m_iTaskStatus = TASKSTATUS_RUNNING;

    switch ( pTask->iTask )
    {
    case TASK_RANGE_ATTACK1:
        {
            EMIT_SOUND_DYN( edict(), CHAN_WEAPON, pAttackSounds[0], GetSoundVolue(), ATTN_IDLE, 0, GetVoicePitch() );
            m_IdealActivity = ACT_RANGE_ATTACK1;
            SetTouch( &CHeadCrabPoison::PoisonLeapTouch );
            break;
        }
    default:
        {
            CHeadCrab::StartTask( pTask ); // Another difference here is that we call "StartTask" from "CHeadCrab" instead of "CBaseMonster" to make sure we are not skipping a parent in the chain
        }
    }
}

void CHeadCrabPoison::PoisonLeapTouch( CBaseEntity *pOther )
{
    // This code is copy-pasted from the original Headcrab
    if ( !pOther->pev->takedamage )
        return;

    if ( pOther->Classify() == Classify() )
        return;

    // Don't hit if back on ground
    if ( !FBitSet( pev->flags, FL_ONGROUND ) )
    {
        EMIT_SOUND_DYN( edict(), CHAN_WEAPON, RANDOM_SOUND_ARRAY( pBiteSounds ), GetSoundVolue(), ATTN_IDLE, 0, GetVoicePitch() );

        // Starting from this point, this is specific to the poison headcrab
        // Check if victim is another monster that is not a machine
        if ( pOther->MyMonsterPointer() && pOther->MyMonsterPointer()->Classify() != CLASS_NONE && pOther->MyMonsterPointer()->Classify() != CLASS_MACHINE )
        {
            // Deal enough poison damage to make the victim have one single HP
            const float flDamage = pOther->pev->health - 1;
            pOther->TakeDamage( pev, pev, flDamage, DMG_POISON );

            // Run some custom regenerate health logic thing à la HL2 on the victim
            pOther->HasBeenPoisonHeadcrabbed();
        }
        else // Victim is not human, do the same thing as normal headcrabs to prevent issues
        {
            pOther->TakeDamage( pev, pev, GetDamageAmount(), DMG_SLASH );
        }
    }

    SetTouch( nullptr );
}

Which method is the best?

Whenever possible, avoid the "copy-paste" method. Treat it as a last resort if the other ones are not applicable.

Besides that, there is no "silver bullet". Each method has its pros and cons and depending on what you are working on, one method might work better than another. And yes, sometimes you will have to make a separate entity, potentially duplicating the code of another one. Picking the right approach is the hardest thing.

Comments

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