Entities Programming - Inheritance VS Duplication (Creating Simple Variants of Existing Entities) Last edited 2 years ago2021-06-19 16:50:17 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

[cat: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 from the original". If we are talking about monsters for example, "tiny changes" could be the model, health, damage values and so on.

Very common examples are making "simple variants of existing monsters" like: The goal of this page is to explain the common ways to create these simple variants and share their pros and cons.

Keep in mind that this page is for "simple variants" and may not apply to everything, if you want something like a friendly Female Assassin, that is another story.

The "copy-paste" method

This method basically consist of selecting all the code that concerns an entity, copy in the clipboard (the famous CTRL/COMMAND+C keyboard shortcut), paste it (CTRL/COMMAND+V to the rescue) somewhere else, and rename everything differently from the original until it compiles and works.

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

For the poison Headcrab, you select everything that concerns monster_headcrab, CHeadCrab and so on, copy all of that, paste that somewhere else and rename everything by monster_headcrab_poison, CHeadCrabPoison and so on.

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

Same drill for the fast Headcrab, copy/paste the original Headcrab, rename everything to monster_headcrab_fast, CHeadCrabFast, make some fine tuning there and there to make him faster, it works, job done, time to move on to something else.

You have your three Headcrab variants, and then you receive a suggestion to add "a range attack to all Headcrab variants" and/or "make them randomly strafe left and right to make aiming at them harder". Assuming you agree with those and decide to implement them, that means you have to update one Headcrab variant and copy/paste the changes two times for the poison and fast variants (with potential slight modifications). And you have to be very careful not breaking your code.

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. Some people will say "that is fine if you have two or three variants" but imagine if you have to add and maintain a forth one, a fifth one, a sixth one.

Putting the "number of copies to maintain" argument aside, 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: Basically, you can easily lose track of what changed and did not changed, what is right or wrong.

Several people might counter this argument by saying "use a version control software like git to check the history of your code and/or use software that tells the differences between two texts/files like WinMerge".

For starters, not everybody especially beginners will bother with version control. Furthermore, if the quality of the history is not good (talking about "commits with huge amounts of differences", "commits with meaningless messages like 'fix headcrab' or 'updated this file'"), you will waste time and effort doing "history archeology" rather than fixing the problem.

About comparing the previous code with the current one: that might works if you have some kind of guarantee that the previous code was working as intended and was intact before making the comparison (are you sure you are comparing a "good" code with the one that causes the problem? Because comparing a "bad" code with another one will make things worse).

Another big disadvantage is "education for beginners", programming is often "meme-ed" for "copy/paste code from some website or someone". In some situations, that "meme" is true, but before you copy/paste that piece of code, are you asking yourself the following questions? Another argument is that your compile time and the size of your binary will be increased a very tiny bit. Some people will say "that argument is 'obsolete' in 2021 because the difference is 'meh' and everybody has fast Internet and a lot of storage space for GoldSrc games and mods" but that is more like a lazy excuse to not optimize the code.

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

The "different class name" method

As you might already know, level designers places entities using class names (weapon_crowbar 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.

You have the right to use the LINK_ENTITY_TO_CLASS( mapName, className ) macro multiple times to link different class names to the same C++ class. In fact, Valve has done this for several entities like the following examples: There is nothing restricting you to do 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 similar to this one:
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 override (or force) a specific class name (like mentioned above). Look at the concerned class's Spawn method 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 need to remove it otherwise your "copy" would not work. Be careful before and after removing it to check that it will not cause a regression when compiling the code and when testing/playing your mod.

In the case of the Zombie, that override does not exist so we are safe on that side.

The next question you might ask yourself is how can I make the difference between the Scientist and Barney Zombies? By checking if the value of pev->classname matches our Barney variant or not by using the handy BOOL FClassnameIs( edict_t *pev, const char *szClassname ) function (there is also a version where you can pass an entvars_t * instead of the edict_t *).

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 and work.

The major advantage of this method is that you did not duplicated the entire Zombie code to make a clone. Aside from the model change, the rest is identical (health, damage, behavior...)

It is easier to make changes that affect all or specific Zombie variants.

One problem with this method would be that the code could turn into an "`if`/`else if`/`else`/`switch`/`case` soup" if there would be too many differences between the original and the copies. At this point, you might consider using another method described below or make two separate distinct entities.

As an exercise, you can try to add Zombie Soldiers to your mod using the same method and making him stronger compared to Zombie Scientist/Barney by giving him more health and/or extra damage when attacking.

The "let the level designer decide" method

This is the method used by Laurie Cheers when programming Spirit of Half-Life. You let the level designers choose certain settings of your entity. Taking monsters and weapons as examples: this means that the level designers set the models, bodies, skins and so on or they can let the game code set the default one.

We are going to reproduce the "choose my 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, search for the concerned entity (monster_headcrab in our case) and add the model definition (look at the cycler entity for example) so that level designers can choose the model of one or more Headcrabs.

One potential problem with this method is testing with custom settings. Spawning the concerned entity through give (remember that the entity has to be precached by the game or the map first before spawning. Otherwise, it is a game/engine crash or return to main menu error) will spawn the entity with its "default" settings. The same applies for weapons with the impulse 101 cheat command (if said weapons were added to the cheat in the first place).

Basically, you need to place one or more of the concerned entity in a (test) map, setup the desired custom settings in Hammer (or J.A.C.K.), compile the (test) map, start the mod, start the map, test and repeat until everything works. You will also likely need to update the settings in your entity in your (test) map before repeating the test and that implies compiling the map again.

You can get around that problem with temporarily changing the "default" settings (careful to put back the original values after the tests are done) or making a temporary server command that would create the entity, set the custom settings and spawn it (look at the RPG rockets and crossbow bolts for examples).
Special cases for this method
Some variables that are part of the entity variables (pev) and classes attributes requires additional work for this method to work. We were lucky with pev->model because it seems to be already handled by the engine itself.

To demonstrate this, look in the dlls/func_tank.cpp file and more precisely the KeyValue( KeyValueData *pkvd ) method override of the CFuncTank class. Look how the firing rate represented by the m_fireRate attribute is assigned using the firerate key/value pair that level designers set through Hammer (or J.A.C.K.).

This also explains how the FGD and the game code works together. Additionally, look at the previous page of this book for a more in-depth explanation of the KeyValue( KeyValueData *pkvd ) method from the CBaseEntity class.
This method share the same problem as the "different class name" method which is that your code can turn into a "`if`/`else if`/`else`/`switch`/`case` soup" if there is too many customization. Perhaps another method would work or it would be best to make two separate entities.

As an exercise, you could try editing existing entities to allow level designers to change more settings and/or you could make an entity that would print a message in 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 in C++, some will say that it is "C with C++ classes" but it is still C++ in the end.

You should already know that C++ is an "oriented object programming language" like Java, C# and more. This means that there are concepts of "inheritance" and "abstraction".

This book will not teach you these concepts by telling you how to create classes, making children of these classes, the difference between public`/`protected`/`private, when to use virtual`/`override and so on. If you did not understood anything in the previous sentence, you should probably pause 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 the maps, level designers uses entities like trigger_once and trigger_multiple. Both are brush entities, do the same thing, the only difference is that trigger_once works only one time and trigger_multiple works multiple times (unless it gets "killed" by another entity or the "wait before trigger again" value is negative but for education purposes, we are going to assume these situations don't happen).

If you look at the CTriggerOnce class, you can see it is a child of CTriggerMultiple meaning it inherits everything from it. Only one difference is the method Spawn being overridden to force the "wait before trigger again" delay (m_flWait) to a negative value before calling the Spawn method of its parent. That is what gives trigger_once its "once nature".

CTriggerMultiple which represent the trigger_multiple entity is itself a child of CBaseTrigger because there are other methods than "touching invisible zones" to trigger entities.

CBaseTrigger itself is a child of CBaseToggle which itself because triggers can have a "master" (see multisource entity) and you can finish the chain yourself if you want.

Do you remember the normal, poison and fast Headcrabs situation described in the "copy/paste" section? Inheritance would work better here.

Do you want a fast Headcrab that turn faster? 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 Headcran...
    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/use the model (while calling the parent to precache the rest like sounds).

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 we can't do this.
Exported methods and inheritance don't like each other
One thing to be aware with using inheritence in Half-Life is that you cannot override exported methods (methods declared with the EXPORT macro).

For all entities, these are Blocked( CBaseEntity *pOther ), Think(), Touch( CBaseEntity *pOther ) and Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ).
The LeapTouch being exported, we have no other choice but to copy/paste/adapt our own version called PoisonLeapTouch and override everything that set the touch method to LeapTouch so we can replace it by our PoisonLeapTouch version instead. Luckily for us, there is only one which is 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 copied-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 best/should I use?

Whenever possible, avoid the "copy/paste" method. Treat it like a "last report option" if the other ones are not possible.

Besides that, there is no "silver bullet". Each method has its pros and cons and depending on what you are working on, a method might work best than another. Sometimes, you will be required to make another entity and potentially duplicate the code of another one. The hardest thing is to pick the right one.

Comments

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