[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:
- Female variants of Barney (Kate and Barniel from Azure Sheep).
- Female Scientists (Decay has them as "players" but it is possible to rig their models to the Scientist skeleton and have dialogue recorded for them to behave like male Scientists).
- Otis (Blue-Shift and Opposing Force).
- Rosenberg (Blue-Shift).
- Zombie and Soldiers variants of Zombies (Opposing Force).
- H.E.V. suits variants of anything mentioned above.
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:
- Did you changed something in the poison Headcrab and forgot to do it as well for the normal and fast variants?
- Or the other way around (changed something in the Normal and fast variants but forgot to update the poison one)?
- Or because it is something else which you have not found yet?
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?
- What this code does?
- Why I need to copy-paste it?
- Is there any potential alternative that could be better for what I am trying to achieve?
- Is the copy/paste itself enough?
- Am I able to maintain the code in case it is causing an issue (directly or indirectly)?
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:
- The Glock is tied to the
weapon_9mmhandgun
and weapon_glock
class names and both are linked to the same CGlock
class. weapon_glock
is there for "historical reasons" (preserve compatibility with early Half-Life 1 maps if you know the history of its difficult development) and is being replaced automatically by weapon_9mmhandgun
when spawning to prevent issues (like HUD related problems where it seek Glock data using the weapon_9mmhandgun
class name). - Same for the 9mm AR with
weapon_9mmAR
and weapon_mp5
both tied to the CMP5
class, weapon_mp5
is the one being replaced by the other for the same reason as the Glock. info_landmark
(used for level transitions in single player) and info_player_start
(default player starting point) are both tied to the CPointEntity
class.
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:
- Compiled the code and have the updated server DLL placed in the appropriate place of your mod folder.
- Placed the Zombie Barney model (
zombie_barney.mdl
) in your mod's models
folder. - Updated the FGD (File Game Definition) file by making a copy/paste of the
monster_zombie
to monster_zombie_barney
so that level designers can place Zombie Barneys in the maps through Hammer (or J.A.C.K.). - Made a test map (or used a map that will be part of your mod) with
monster_zombie_barney
in it.
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).
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.
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.