CTRL/COMMAND+C
keyboard shortcut), pasting it (CTRL/COMMAND+V
to the rescue) somewhere else, and renaming everything until it compiles and works.CHeadCrab
and so on, copy-paste it somewhere else and rename everything to monster_headcrab_poison
, CHeadCrabPoison
, etc.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.LINK_ENTITY_TO_CLASS( mapName, className )
is used.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:
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.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.CPointEntity
class.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" );
.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.// 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:
zombie_barney.mdl
) in your mod's models
folder,monster_zombie_barney
so that zombie barneys can be used in the editor,monster_zombie_barney
in it,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.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.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.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.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.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.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.LeapTouch
method where the "bite" actually happens but unfortunately this is impossible.
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 )
.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 );
}
You must log in to post a comment. You can login or register a new account.