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:
- 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).
- Barney and soldier variants of zombies (Opposing Force).
- H.E.V. suit variants of anything mentioned above.
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:
- Did you change something in the poison headcrab code and forgot to do it as well for the normal and fast variants?
- What if it's the other way around, and you've changed something in the normal and fast variants but forgot to update the poison one?
- Maybe it's something else that you have not found yet?
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:
- What does this code do?
- Why do I need to copy-paste it?
- Is there a potential alternative that would work better for what I'm trying to achieve?
- Is simply copying it enough?
- Will I be able to maintain the code in case it causes any issues, directly or not?
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:
- The Glock is tied to the weapon_9mmhandgun and
weapon_glock
class names and both are linked to the same CGlock
class. weapon_glock
has been there for historical reasons - to preserve compatibility with early Half-Life 1 maps. It is replaced automatically with weapon_9mmhandgun on spawn to prevent certain HUD-related issues. - Same for the 9mm AR with weapon_9mmAR and
weapon_mp5
class names, both tied to the CMP5
class. weapon_mp5
is the one being replaced with the other for the same reasons as the Glock. - info_landmark (used for level transitions in the singleplayer mode) and info_player_start (default player starting point) are both tied to the
CPointEntity
class.
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:
- Compiled the code and have put the updated server DLL in the appropriate folder of your mod's directory,
- Placed the zombie Barney model (
zombie_barney.mdl
) in your mod's models
folder, - Updated the FGD file by copy-pasting the monster_zombie definition and renaming everything to
monster_zombie_barney
so that zombie barneys can be used in the editor, - Made a map with
monster_zombie_barney
in it,
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).
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.
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.