Monsters Programming - Standard and Squad Monsters Last edited 2 years ago2022-07-03 18:15:38 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

Now that we covered the theory of monsters programming, let us see how to actually program one.

For the context of this chapter, we are going to re-create one of Half-Rats: Parasomnia's monsters: the Armored Man.

Assets

Download the test mod that contains the Armored Man assets (models and sounds) and a test map here. The twhl_monsters_tutorial folder goes in your Half-Life installation directory. Restart Steam if you do not see the test mod in your library.

An alternative for those who have Half-Rats: Parasomnia already installed is to borrow the assets directly from the mod but that involves figuring out the MDL and sounds to copy/paste.
Important legal/copyright stuff
Heath Games, the developers of Half-Rats: Parasomnia allow usage of its assets and derivatives in other projects as long as:

1) You do not use the assets and any derivatives for commercial purposes.

2) You give the proper credits to Heath Games; do not claim you made the assets.

Introducing the Armored Man

For those who have not played Half-Rats: Parasomnia or need to refresh their memory, here's a description of the Armored Man.

He is a tall "human-like" enemy carrying a helmet and body armor that protects him from bullets similar to Barney's helmet in Half-Life.

He does not have a melee attack, but he has a shield that he can use whenever his health is low to protect him from bullets (with a "delay before use it again" to prevent him from using it every 2 seconds).

He uses a double barrel shotgun as range attack, he will fire one barrel if the enemy is far or both if he is close.

If the enemy is too far, he will try to get closer to him.

Finally, he has the ability to lead a squad or be part of one similar to Half-Life's alien grunts.

"Technical analysis"

Open the armorman.mdl file with any Half-Life Model Viewer. Solokiller's Half-Life Assets Manager works best since it will give you more details without having to decompile the model itself. If you are using another Half-Life Model Viewer, you might need to decompile the model and read the QC file to gather the information explained here.

The reason we are doing this "technical analysis" is to gather "technical information" about the monster itself that is going to be used in the code. When working on a mod, it is very common to have the programmers and the artists (modeling, animating, compiling...) working together to define the needed sequences, activities, animation events and so on to bring the newly created monster to life.

Look at sequences first: we can see that we have several idle animations tied to the ACT_IDLE activity. Likewise, we also have animations for walking, running, turning left and right and various flinching and deaths tied to their matching activities. Those are the very basic sequences/activities used by all monsters so we do not need to program them again.

On walking and running sequences, animation event "1004" (SCRIPT_EVENT_SOUND) is used to play footstep sounds. One thing worth mentioning is that we have custom footsteps sounds instead of the default ones from Half-Life (common/npc_stepX.wav). Likewise, on the death animations, you will see animation event "2001" (MONSTER_EVENT_BODYDROP_LIGHT) being used to play the body drop sound (those are default Half-Life sounds unlike the footsteps ones). Those are common to all monsters and therefore do not require extra programming.

The assasinated and standing_idle sequences are not tied to activities. They were supposed to be used in specific contexts (scripted sequences) in Half-Rats: Parasomnia but they ended up being unused. We are not going to use them.

We have two identical "shoot" sequences (only one should have been enough) tied to ACT_RANGE_ATTACK1. They have two animation events: "5001" for the muzzle flash and "3" for the actual shoot. The muzzle flash animation event there in the default/common code but we need to program the shoot one since it is specific to the Armored Man.

Next we have the "draw" and "holster" sequences tied respectively to the ACT_ARM and ACT_DISARM activities respectively and have the events "2" and "4" respectively. When you have finished reading this page and programming the Armored Man, an exercise for you would to be program that arm weapon behavior (look at Half-Life's Barney for inspiration) and probably finish Valve’s unfinished work by programming the holster one.

We have a reload sequence with several animation events "1004" (SCRIPT_EVENT_SOUND) for the shotgun sounds. One important animation event to note is "5" because it is the one responsible for completing the actual reload. We will need to program that reload logic.

Finally, the last sequence is the one for the shield tied to the ACT_EXCITED activity. If you read the important "disclaimer" about modifying activities, you know why its name does not match the action. Since the shield is specific to the Armored Man, we will have to program it.

Now that we have finished analyzing the sequences, time to look at hitboxes.

Some of them have a hitgroup value of "0" while the rest has "1". Ideally, hitgroups values are different since they represent each part of the human body (head, left arm, right arm, stomach...) This is how headshots are detected in order to multiply the damage inflicted.

The Armored Man is a special case; he wears a helmet and an armored vest so headshots and body shots will not deal damage. "0" means "non-protected area" and "1" means "protected area". In other words, we will have to override some stuff when it comes to tracing attacks.

For a more concrete example on a real human, you can open the Human Grunt (hgrunt.mdl) and/or player (player.mdl) models and compare the hitgroup values.

Here is a table of hitgroups values and what they correspond to (monsters.h):
ConstantValueBody part
HITGROUP_GENERIC0Generic.
HITGROUP_HEAD1Head.
HITGROUP_CHEST2Chest.
HITGROUP_STOMACH3Stomach.
HITGROUP_LEFTARM4Left arm.
HITGROUP_RIGHTARM5Right arm.
HITGROUP_LEFTLEG6Left leg.
HITGROUP_RIGHTLEG7Right leg.
Another thing to note is the presence of a bone controller that allow the Armored Man to turn his head. Head movement is already handled by the existing code so no extra programming required there.

We also have the presence of a gun bodygroup to control the visibility of the shotgun, we will have to handle this too when the Armored Man dies and drop it's shotgun.

Finally, there is only one attachment on the Armored Man's double barrel shotgun, more specifically, the end of the barrels to produce the muzzle flash sprite. It's easy to tell the engine to render that muzzle flash and you'll see when the time comes.

Basics

Now that we know more about this fellow, it is time to program it!
Several assumptions when following this chapter
This chapter assumes you are using the official Half-Life source code on GitHub or Solokiller's fork (the one that has updates for Visual Studio 2017/2019) or anything derived from them. If you are using a different source code base, it is your responsibility to adapt the code in this chapter to fit the changes.

The code in this chapter uses "modern C++" standards rather than the "legacy" C++-98 used during the development of Half-Life. Do not be surprised if you see #pragma once instead of #ifndef ARMORMAN_H #define ARMORMAN_H #endif for header safe-guards and keywords such as override, constexpr and so on.

Do not blindly copy/paste the code blocks. Most of them are not complete on purpose and you will miss many things if you skip reading the text.

There are many different ways to program things and it is not a big deal if the code you make is not 100% identical this chapter's content. Here is one example: at some point, you will declare the monster's class in a header file (.h). Nothing prevents you to do it in a source file (.cpp) like most existing Half-Life monsters. Each way has its advantages and disadvantages. This is more like a "coding style" debate that goes beyond the scope of this chapter.
First things first, create a new empty source file called armorman.cpp in the dlls folder of your mod's source code. Do not forget to add it to the server project (hldll for Windows/Visual Studio, linux/Makefile.hldll for Linux Makefiles) so that it gets compiled when building the binaries.

We are going to add the very basic "server sided" includes common to all entities and monsters.h common to all monsters:
#include "extdll.h"
#include "util.h"
#include "cbase.h"

#include "monsters.h"

#include "armorman.h"
Notice we also have armorman.h which will contain the class declaration of the Armored Man. Right now, this file does not exists and this will cause an error, so fix it by creating it in the same place as our source file. If you are using an IDE like Visual Studio, do not forget to add it in the project so you can access it more easily.

Now, declare the class for the Armored Man, we will also override some core methods. Do not forget the header safe-guards as well:
#pragma once

class CArmorMan : public CBaseMonster
{
public:
    void Spawn() override;
    void Precache() override;

    int Classify() override;
};
Are we supposed to use CSquadMonster instead of CBaseMonster?
Yes, you are right, it should inherit from CSquadMonster instead.

But for education purposes, we will make him "basic" first and "upgrade" him later with squad functionality in a later page.
Go back to the source file. Do not forget to link the class to an entity by adding the LINK_ENTITY_TO_CLASS( monster_armorman, CArmorMan ); line below the includes.

First method to implement is the Spawn() one, here is the code:
void CArmorMan::Spawn()
{
    Precache();
    SET_MODEL( ENT( pev ), "models/armorman.mdl" );

    UTIL_SetSize( pev, Vector( -24.0f, -24.0f, 0.0f ), Vector( 24.0f, 24.0f, 108.0f ) );
    pev->solid = SOLID_SLIDEBOX;
    pev->movetype = MOVETYPE_STEP;
    m_bloodColor = BLOOD_COLOR_RED;
    pev->health = gSkillData.armormanHealth;

    pev->view_ofs = Vector( 0.0f, -10.0f, 110.0f );
    m_flFieldOfView = VIEW_FIELD_WIDE;
    m_MonsterState = MONSTERSTATE_NONE;
    m_afCapability = bits_CAP_HEAR | bits_CAP_DOORS_GROUP | bits_CAP_TURN_HEAD | bits_CAP_RANGE_ATTACK1;

    MonsterInit();
}
Now, the explanations as to why you put all these lines inside of it.

First, we precache the assets (models and later on sounds) because no one likes crashes. Once the precache is complete, we can set and use the model without problems.

The next section set the size of the Armored Man, the vectors in the arguments of the UTIL_SetSize( entvars_t *pev, const Vector &vecMin, const Vector &vecMax ) function are offsets from the model's origin. The latter being at the center of its legs.

The first vector is the "minimum" and the second is the "maximum". If you picture the Armored Man in the box, "minimum" would be the lower left corner and "maximum" would be the upper right of that box.

For "normal humans" like the player, Barney and scientists, the handy constants VEC_HUMAN_HULL_MIN and VEC_HUMAN_HULL_MAX exists.

We then define that the Armored Man's collision is a "moving/sliding box" and that it moves like a human. We also setup a red blood color and setup his initial health to the value of the skill CVAR (Console VARiable) gSkillData.armormanHealth.
Skill what?
If you are not familiar with the concept of "skill CVAR" in the Half-Life SDK, please take a break from reading this book and read this guide. Come back to this book once you have finished.

For those who are tempted to skip the "add the skill CVAR" part and rely on hardcoded values: do not do that. We are going to use that skill CVAR when programming the shield behavior and the "magic constants/numbers everywhere" programming style makes maintenance harder and painful.
Program that gSkillData.armormanHealth skill CVAR and do not forget to copy/paste and/or update the skill.cfg file with proper values. If you get a compile error that gSkillData is not defined, that means you forgot to include the skill.h header file in the Armored Man source file.

In the other section, pev->view_ofs defines the offset from the origin to the position of the monster's eyes. The Armored Man being taller than "normal humans" are, we need to use a custom offset. For "normal humans", you can use the handy constant VEC_VIEW.

The m_flFieldOfView defines the field of view of the Armored Man's eyes, VIEW_FIELD_WIDE is a handy constant for 0.5f meaning "180° radius".

After that, the monster's state is initialized to MONSTERSTATE_NONE. We also tell that the Armored Man is capable of hearing sounds in the world, use doors, turn his head since it has a bone controller for that purpose and perform one kind of range attack.

We finalize the spawning process by calling the MonsterInit() method from CBaseMonster.

Next method to implement is the Precache() one. We are only using the model for now; we will add the rest later. If you followed the book's previous chapters (entity and weapons programming), you should be able to program this one easily on your own and that will be your first exercise on this chapter's page.

Need the answer or validation to check if yours is correct? Here it is:
void CArmorMan::Precache()
{
    PRECACHE_MODEL( "models/armorman.mdl" );
}
The third method to override and implement is the int Classify() one. The Armored Man will be on the same "faction" as Half-Life's human grunts. Here is your second exercise, implement this method by yourself. If you are stuck, read the "Classification" page again and give another go at the exercise.

Stuck or need to check your answer? Here it goes:
int CArmorMan::Classify()
{
    return CLASS_HUMAN_MILITARY;
}
Time to see if the basics of the Armored Man works properly, compile the binaries and install them in the test mod. You can use the test map called test_armorman provided with the assets assuming you did not changed the name of the entity when linking it with the class.

Alternatively, you can make your own test map with J.A.C.K. or Hammer if you know how to. Here is the FGD (File Game Definition) entry for the Armored Man if you need to update it and have J.A.C.K. and Hammer recognize the monster:
@PointClass base(Monster, Sequence) size(-24 -24 0, 24 24 108) studio("models/armorman.mdl") = monster_armorman : "Armored Man" []
If you see the Armored Man, it means that you have done things correctly for now.

You will notice that he does not turn towards you neither to any sound you make (footsteps and weapons). He is looking to his left rather than forward and he does not move.

That is because we forgot to override the SetYawSpeed() method, do it and set his pev->yaw_speed to 200.0f for now.

Compile and install the binaries again and make another test. He should now be able to turn around, walk and run.
He behaves in a weird way, he tries to shoot the ground and he run over me when I stick myself to him!
This is "normal"; we did not programmed the range attack (shotgun), shield behavior and so on yet.

What you see is the default/common code to all monsters running.

Attacking

To make the Armored Man more interesting, we will need to do some extra work, starting with the range attack or shotgun shooting logic.

In the class declaration, we are going to override the bool CheckRangeAttack1( float flDot, float flDist ) and HandleAnimEvent( MonsterEvent_t *pEvent ) methods.

If the first method returns true, this will notify the default/common code that the Armored Man is capable of doing the first kind of range attack (play sequences tied to the ACT_RANGE_ATTACK1 activity). By default, it returns false for "he can't do it".

The second method responsible to process animation events will contain the actual shooting code. Later on, there will be other code for other animation events for the other behaviors (reloading for example).

We will also add some extra variables that we will use later to delay the "range attack check" giving them a sense of "delay before acting". Barney and human grunt from Half-Life also have that.

The class declaration should now be something similar to this:
#pragma once

constexpr int ARMORMAN_AE_SHOOT = 3;

#define ARMORMAN_SHOTGUN_OFFSET Vector( 0.0f, 0.0f, 55.0f )

class CArmorMan : public CBaseMonster
{
public:
    void Spawn() override;
    void Precache() override;

    int Classify() override;

    void SetYawSpeed() override;

    bool CheckRangeAttack1( float flDot, float flDist ) override;

    void HandleAnimEvent( MonsterEvent_t *pEvent ) override;

    bool Save( CSave &save ) override;
    bool Restore( CRestore &restore ) override;
    static TYPEDESCRIPTION m_SaveData[];

private:
    bool m_bLastCheckAttackResult;
    float m_flNextCheckAttackTime;
};
Why you did those changes? Here are the answers.

The first constant is ARMORMAN_AE_SHOOT, AE is a shortcut for "animation event". Why "3"? Because during our "technical analysis", we noticed that the shoot sequences calls animation events with the ID "3" for the actual shooting.

The ARMORMAN_SHOTGUN_OFFSET is another constant which is the offset between the model's origin and the end of the shotgun's barrels. In other words, it is the starting position of shotgun shots.

Do notice the presence of overrides for the int Save( CSave &save ) and int Restore( CRestore &restore ) methods as well as the declaration of the save/restore table (TYPEDESCRIPTION m_SaveData[]). We will save/restore the two extra variables m_bLastCheckAttackResult and m_flNextCheckAttackTime regarding that "range attack check delay" to prevent issues when loading and saving games.

Once you have finished updating the class declaration, you can go back to the source file and define the save/restore table. You should be able to do this on your own since save/restore is not limited to monsters.

Do you have trouble doing it by yourself? Here is the answer:
TYPEDESCRIPTION CArmorMan::m_SaveData[] =
{
    DEFINE_FIELD( CArmorMan, m_bLastCheckAttackResult, FIELD_BOOLEAN ),
    DEFINE_FIELD( CArmorMan, m_flNextCheckAttackTime, FIELD_TIME )
};
IMPLEMENT_SAVERESTORE( CArmorMan, CBaseMonster );
Time to implement our overridden version of the bool CheckRangeAttack1( float flDot, float flDist ) method, this code is similar to Barney and human grunts from Half-Life 1 with minor modifications:
bool CArmorMan::CheckRangeAttack1( float flDot, float flDist )
{
    if ( flDist > 1024.0f || flDot < 0.5f )
        return false;

    if ( gpGlobals->time <= m_flNextCheckAttackTime )
        return m_bLastCheckAttackResult;

    const Vector vecShootOrigin = pev->origin + ARMORMAN_SHOTGUN_OFFSET;
    CBaseEntity *pEnemy = m_hEnemy;
    const Vector vecShootTarget = pEnemy->BodyTarget( vecShootOrigin ) - pEnemy->pev->origin + m_vecEnemyLKP;

    TraceResult tr;
    UTIL_TraceLine( vecShootOrigin, vecShootTarget, dont_ignore_monsters, ENT( pev ), &tr );
    if ( tr.flFraction == 1.0f || tr.pHit && Instance( tr.pHit ) == pEnemy )
        m_bLastCheckAttackResult = true;
    else
        m_bLastCheckAttackResult = false;

    m_flNextCheckAttackTime = gpGlobals->time + 1.5f;
    return m_bLastCheckAttackResult;
}
Again, the explanations of why you are doing all of this.

First, if our Armored Man is too far (flDist) or not facing (flDot) the enemy, we are denying the attack. Remember that he has a shotgun, not a sniper rifle.

Second, we check if it is too soon to perform another check, if that is the case, we return the result from the previous one.

Otherwise, we perform some aiming calculations and we trace an invisible line between the Armored Man's shotgun and the target. If the hit is valid, we return true to "validate" the attack. Otherwise, we return false.

The Armored Man has now the ability to perform his range attack (use his shotgun if you prefer) if the criterias are met. One last thing to do: implement his overridden HandleAnimEvent( HandleAnimEvent *pEvent ) to perform the actual shooting.

Like before, this code is inspired from Barney and the human grunt from Half-Life:
void CArmorMan::HandleAnimEvent( MonsterEvent_t *pEvent )
{
    if ( pEvent->event != ARMORMAN_AE_SHOOT )
    {
        CBaseMonster::HandleAnimEvent( pEvent );
        return;
    }

    UTIL_MakeVectors( pev->angles );
    const Vector vecShootOrigin = pev->origin + ARMORMAN_SHOTGUN_OFFSET;
    const Vector vecShootDir = ShootAtEnemy( vecShootOrigin );

    const Vector vecAngDir = UTIL_VecToAngles( vecShootDir );
    SetBlending( 0, vecAngDir.x );
    pev->effects = EF_MUZZLEFLASH;

    if ( m_hEnemy && (m_hEnemy->pev->origin - pev->origin).Length() <= 256.0f )
    {
        FireBullets( 12, vecShootOrigin, vecShootDir, VECTOR_CONE_15DEGREES, 1024.0f, BULLET_PLAYER_BUCKSHOT );
        EMIT_SOUND_DYN( ENT( pev ), CHAN_WEAPON, "weapons/dbarrel1.wav", VOL_NORM, ATTN_NORM, 0, PITCH_NORM );
    }
    else
    {
        FireBullets( 6, vecShootOrigin, vecShootDir, VECTOR_CONE_10DEGREES, 1024.0f, BULLET_PLAYER_BUCKSHOT );
        EMIT_SOUND_DYN( ENT( pev ), CHAN_WEAPON, "weapons/sbarrel1.wav", VOL_NORM, ATTN_NORM, 0, PITCH_NORM );
    }
    CSoundEnt::InsertSound( bits_SOUND_COMBAT, pev->origin, 384, 0.3f );
}
Your IDE will likely yell about CSoundEnt, VECTOR_CONE_15DEGREES and BULLET_PLAYER_BUCKSHOT being undefined. First can be fixed by including soundent.h and the two others with weapons.h.

As usual, the explanations are coming.

If the animation event is not the shooting one, we let the default/common monsters code handle it and do not proceed any further.

The first section handles aiming, some code is similar to what has been done in bool CheckRangeAttack1( float flDist, float flDot ).

The second one take care of blending the model's angle on the X-axis and tell the engine to spawn the muzzle flash effect.

The next section is specific to the Armored Man, we check if we have an enemy and if he is very close (256 Hammer units), the "one barrel shot" becomes a "double barrel shot". Pay attention to the different number of shots, accuracy and firing sound.

Do not forget to precache the weapons/dbarrel1.wav (PRECACHE_SOUND( "weapons/dbarrel1.wav" );) and weapons/sbarrel1.wav (PRECACHE_SOUND( "weapons/sbarrel1.wav" );) sounds in the Precache() method or you will not be able to hear them. Those are Half-Life's shotgun sounds ("d" for double, "s" for single).
Why the Armored Man uses "player buckshot"?
Simply because there is no "monster" equivalent. The human grunt in Half-Life does the same thing.

Do you want to practice yourself with the Half-Life SDK outside of monsters programming a bit? Try to add the "monster buckshot" and make the Armored Man and/or human grunt using it.
The last line is the most interesting one: it places a "combat sound" with the size of 384 units sphere radius at the Armored Man's location for a duration of 0,3 seconds. In other words, all other monsters within that sphere will "hear" the shot and might react to it depending on their AI behavior.

Compile the binaries and test again. If you have done everything correctly, the Armored Man will be able to shoot and kill you. You will likely notice that he is kinda hard to kill because he does not reload and have infinite ammo. Do not worry, we will fix this by implementing the reload logic.

Reloading

For the reload logic, we are going to use a custom schedule, a custom task and another animation event. This is the same approach used for the human grunt in Half-Life.

The ID of the animation event is "5" (see "technical analysis" if you forgot why). We will also need to override some methods, these are: CheckAmmo(), Schedule_t *GetSchedule(), Schedule_t *GetScheduleOfType( int Type ) and RunTask( Task_t *pTask ).

We are also going to add CUSTOM_SCHEDULES in the class declaration to declare and define our custom schedule and custom task in a similar fashion as the save/restore feature.

Your header file should be similar to this one:
#pragma once

constexpr int ARMORMAN_AE_SHOOT = 3;
constexpr int ARMORMAN_AE_RELOAD = 5;

#define ARMORMAN_SHOTGUN_OFFSET Vector( 0.0f, 0.0f, 55.0f )

class CArmorMan : public CBaseMonster
{
public:
    void Spawn() override;
    void Precache() override;

    int Classify() override;

    void SetYawSpeed() override;

    bool CheckRangeAttack1( float flDot, float flDist ) override;

    void CheckAmmo() override;
    void HandleAnimEvent( MonsterEvent_t *pEvent ) override;

    Schedule_t *GetSchedule() override;
    Schedule_t *GetScheduleOfType( int Type ) override;
    void RunTask( Task_t *pTask ) override;

    bool Save( CSave &save ) override;
    bool Restore( CRestore &restore ) override;
    static TYPEDESCRIPTION m_SaveData[];

    CUSTOM_SCHEDULES;

private:
    bool m_bLastCheckAttackResult;
    float m_flNextCheckAttackTime;
};
Why you do not add another variable to store how many buckshots are loaded?
Because CBaseMonster already has a variable for that purpose called m_cAmmoLoaded and it is already saved/restored by the same class. There is no need to add another variable for the same purpose and save/restore it ourselves.
We are going to take care of the infinite ammo problem first. In the Spawn() method, you can initialize m_cAmmoLoaded to 2 just before calling MonsterInit(). The Armored Man will have a loaded shotgun instead of an empty one upon spawning.

Then we need to make some changes in the shooting animation event code in HandleAnimEvent( MonsterEvent_t *pEvent ) to subtract ammo during shots.

For the "double barrel shot" part, add an extra check if m_cAmmoLoaded is equals or higher than two. This prevents the Armored Man from performing a "double barrel shot" if he only has one buckshot loaded or his shotgun is empty.

In the case we are performing a "double barrel shot", we subtract two from m_cAmmoLoaded. Otherwise, we just subtract one.

The shooting logic should be similar to this one after applying the changes:
if ( m_cAmmoLoaded >= 2 && m_hEnemy && (m_hEnemy->pev->origin - pev->origin).Length() <= 256.0f )
{
    FireBullets( 12, vecShootOrigin, vecShootDir, VECTOR_CONE_15DEGREES, 1024.0f, BULLET_PLAYER_BUCKSHOT );
    EMIT_SOUND_DYN( ENT( pev ), CHAN_WEAPON, "weapons/dbarrel1.wav", VOL_NORM, ATTN_NORM, 0, PITCH_NORM );
    m_cAmmoLoaded -= 2;
}
else
{
    FireBullets( 6, vecShootOrigin, vecShootDir, VECTOR_CONE_10DEGREES, 1024.0f, BULLET_PLAYER_BUCKSHOT );
    EMIT_SOUND_DYN( ENT( pev ), CHAN_WEAPON, "weapons/sbarrel1.wav", VOL_NORM, ATTN_NORM, 0, PITCH_NORM );
    m_cAmmoLoaded--;
}
Now that the infinite ammo problem is fixed, we need to take care of the reloading part.

We are going to create the reload task first. The Armored Man need to stop moving, face the enemy (to seek visual information and receive some protection from his shield) and play the reload sequence.

Luckily, these three tasks are already part of the common/default monsters code, so we just have to use it. Those are TASK_STOP_MOVING, TASK_FACE_ENEMY and TASK_PLAY_SEQUENCE.

Try to program this task on your own, do not hesitate to go back to the "Schedules & Tasks" page and/or look at the human grunt's reload task for inspiration.

The task should look like this:
Task_t tlArmorManReload[] =
{
    { TASK_STOP_MOVING, 0.0f },
    { TASK_FACE_ENEMY, 0.0f },
    { TASK_PLAY_SEQUENCE, (float)ACT_RELOAD }
};
Do note that you can improve this task by adding more "steps" such as "taking cover" and so on. Once you have finished implementing the reload logic, feel free to experiment a bit and discover by yourself.

Once the task is setup, we need to setup the appropriate schedule. To make things interesting, we are going to say that receiving heavy damage (bits_COND_HEAVY_DAMAGE) and/or hearing dangerous sounds (bits_COND_HEAD_SOUND and bits_SOUND_DANGER) can interrupt this schedule.

Here is your second exercise for this part: implement that schedule.

Here is the answer if you need it:
Schedule_t slArmorManReload[] =
{
    {
        tlArmorManReload,
        ARRAYSIZE( tlArmorManReload ),
        bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND,
        bits_SOUND_DANGER,
        "ArmorMan Reload"
    }
};
Once the schedule is setup, you can "register" it in a similar way as save/restore tables like this:
DEFINE_CUSTOM_SCHEDULES( CArmorMan )
{
    slArmorManReload
};
IMPLEMENT_CUSTOM_SCHEDULES( CArmorMan, CBaseMonster );
Now that the task and schedules are setup properly, we need to tell the Armored Man to use them when he has to reload.

We are going to implement our overridden version of CheckAmmo() by checking if m_cAmmoLoaded is less or equals than zero and if that is the case, we turn on the condition flag about "no ammo is loaded right now".

You can try to do this on your own or if you need the code, here it is:
void CArmorMan::CheckAmmo()
{
    if ( m_cAmmoLoaded <= 0 )
        SetConditions( bits_COND_NO_AMMO_LOADED );
}
Next method to implement the overridden version is Schedule_t *GetSchedule(). We need to tell the Armored Man that if he is in combat (MONSTERSTATE_COMBAT) and if he has to reload (bits_COND_NO_AMMO_LOADED is set), then he has to reload his shotgun. For other cases, the default/common monsters code can handle them.

Here is the code to achieve this:
Schedule_t *CArmorMan::GetSchedule()
{
    if ( m_MonsterState == MONSTERSTATE_COMBAT && HasConditions( bits_COND_NO_AMMO_LOADED ) )
        return GetScheduleOfType( SCHED_RELOAD );

    return CBaseMonster::GetSchedule();
}
However, it is not going to be enough. When the Armored Man's current schedule is to perform the reload (SCHED_RELOAD), he need to use our custom schedule (slArmorManReload) and this is where the overridden implementation of Schedule_t *CArmorMan::GetScheduleOfType( int Type ) intervenes.

The answer to that problem is here:
Schedule_t *CArmorMan::GetScheduleOfType( int Type )
{
    if ( Type == SCHED_RELOAD )
        return slArmorManReload;

    return CBaseMonster::GetScheduleOfType( Type );
}
Another thing that we need to be sure is that when the Armored Man is performing the reload (TASK_RELOAD), he needs to actually play the reload activity (ACT_RELOAD) in order to have the reload itself completed and prevent loops. That is why RunTask( Task_t *pTask ) has to be overridden and implemented.

You know the drill by now:
void CArmorMan::RunTask( Task_t *pTask )
{
    if ( pTask->iTask == TASK_RELOAD )
    {
        m_IdealActivity = ACT_RELOAD;
        return;
    }

    CBaseMonster::RunTask( pTask );
}
There is one more thing to do: write the code of the animation event responsible for completing the reload. We need to clear the "no ammo is loaded" condition flag (bits_COND_NO_AMMO_LOADED) and update the value of m_cAmmoLoaded to two.

Before doing that, you might want to refactor the existing HandleAnimEvent( MonsterEvent_t *pEvent ) method to use switch and case instead of the if`/`return structure.

The method should look something like this:
void CArmorMan::HandleAnimEvent( MonsterEvent_t *pEvent )
{
    switch ( pEvent->event )
    {
    case ARMORMAN_AE_SHOOT:
        {
            // Shooting code goes here
        }
        break;
    case ARMORMAN_AE_RELOAD:
        m_cAmmoLoaded = 2;
        ClearConditions( bits_COND_NO_AMMO_LOADED );
        break;
    default:
        CBaseMonster::HandleAnimEvent( pEvent );
    }
}
Before compiling the binaries, do not forget to precache the shotgun reload sounds included in the weapons folders otherwise you will not hear the reloading sounds. After that, you can compile the binaries and test them. The Armored Man should now reload after performing a "double barrel shot" or after two "single barrel shots" before being able to shoot again.

Using the shield

Before adding the "use shield" behavior, we are going to add some code to tell which hitboxes should "deny" receiving damage and those who should not.

The method responsible for that is TraceAttack( entvars_t *pevAttacker, float flDamage, Vector vecDir, TraceResult *ptr, int bitsDamageType ), it takes several arguments which most of them are self-explanatory.

The TraceResult structure has an iHitgroup variable which we are going to use to detect if the damage is inflicted to a non-protected area (0) or a protected one (1).

To make things more interesting, the shield and protected areas should only protect against bullet, slash, blast and club damage, that's stored in bitsDamageType which you can check for these types of damage using their DMG_ constants.

We will also assume that shots made to the shield hit the left arm, this means change the value ptr->iHitgroup to HITGROUP_LEFTARM.

Oh and do not forget to "nullify" the damage by changing the value of flDamage.

All of that need to happen before calling the default code.

You should be able to program this method on your own. If you need help, look at the code for Barney.

Here is the answer if you need it (do not forget to declare the method in the class declaration):
void CArmorMan::TraceAttack( entvars_t *pevAttacker, float flDamage, Vector vecDir, TraceResult *ptr, int bitsDamageType )
{
    if ( ptr->iHitgroup == 1 )
    {
        if ( bitsDamageType & (DMG_BULLET | DMG_SLASH | DMG_BLAST | DMG_CLUB) )
        {
            UTIL_Ricochet( ptr->vecEndPos, 1.0f );
            flDamage = 0.01f;
        }

        ptr->iHitgroup = HITGROUP_LEFTARM;
    }

    CBaseMonster::TraceAttack( pevAttacker, flDamage, vecDir, ptr, bitsDamageType );
}
The UTIL_Ricochet( const Vector &position, float scale ) add a little spark effect and the ptr->iHitgroup = HITGROUP_LEFTARM is there to attempt to force a left flinch animation to be played.

You will notice that we use a value of 0.01f for flDamage instead of 0. This is a "hack" to have the monster recognize he took damage (setting the condition flag bits_COND_LIGHT_DAMAGE bit to true).

Now, for the "use shield" behavior itself, we are going to use another custom schedule/task for this one.

For the schedule's interruption conditions, there will be none. There will be two tasks needed: a "stop moving" and a "play sequence while facing enemy" one. Remember that the latter requires an activity as parameter: it's the one tied to our "use shield" animation.

Again, you should be able to program this one on your own. If you need the answer, here it is:
Task_t tlArmorManShield[] =
{
    { TASK_STOP_MOVING, 0.0f },
    { TASK_PLAY_SEQUENCE_FACE_ENEMY, (float)ACT_EXCITED },
};

Schedule_t slArmorManShield[] =
{
    {
        tlArmorManShield,
        ARRAYSIZE( tlArmorManShield ),
        0,
        0,
        "ArmorMan Shield"
    }
};
Do not forget to register the custom schedule alongside the existing reload one.

We are going to need an extra class variable called m_flNextShieldTime so that the Armored Man does not use his shield every 5 seconds, add it and do not forget to add it to the save/restore table for saved games.

Time for another exercise, you are going to program the logic itself. There are two parts: the first one where you setup the "scenario" to use the shield and the second one to use the new custom schedule.

For the first part: if our Armored Man is in combat, has an enemy, has less than half of health and hasn't used his shield recently, then we are going to update the "next shield time" to "now + 30 seconds" and play the "cower" schedule. All of this will happen in the Schedule_t *GetSchedule() method which you might need to refactor a bit to make the code cleaner.

For the second one which happens in Schedule_t *GetScheduleOfType( int Type ): if the schedule is "cower" then we use our custom schedule. You might need to refactor this one too a bit.

Have you finished or you have trouble figuring out? Here is the code:
Schedule_t *CArmorMan::GetSchedule()
{
    if ( m_MonsterState != MONSTERSTATE_COMBAT )
        return CBaseMonster::GetSchedule();

    if ( HasConditions( bits_COND_NO_AMMO_LOADED ) )
        return GetScheduleOfType( SCHED_RELOAD );

    if ( m_hEnemy && pev->health <= (gSkillData.armormanHealth / 2.0f) && gpGlobals->time > m_flNextShieldTime )
    {
        m_flNextShieldTime = gpGlobals->time + 30.0f;
        return GetScheduleOfType( SCHED_COWER );
    }

    return CBaseMonster::GetSchedule();
}

Schedule_t *CArmorMan::GetScheduleOfType( int Type )
{
    switch ( Type )
    {
    case SCHED_RELOAD:
        return slArmorManReload;
    case SCHED_COWER:
        return slArmorManShield;
    }

    return CBaseMonster::GetScheduleOfType( Type );
}
You can now compile the binaries, put them in your mod folder, start the test map and try to shoot the shield, smash it with the crowbar and use explosives to see if the Armored Man does not take damage.

If you wound the Armored Man enough without killing him, he should be using his shield while facing you and attack you when he is done.

Sounds

This is the easiest part of this page. You need to precache the sounds and override the corresponding methods.

About precaching sounds, you could precache the "sets" (alert, idle, death...) individually with PRECACHE_SOUND( const char *s ) but that would be tedious. The prefered way is to use sound arrays like existing Half-Life monsters.

Try with the "alert set". In the class declaration, you would add this:
static const char *pAlertSounds[];
Then you define a set like this somewhere in the source file:
const char *CArmorMan::pAlertSounds[] =
{
    "armorman/am_alert1.wav",
    "armorman/am_alert2.wav",
    "armorman/am_alert3.wav"
};
Then you use the handy PRECACHE_SOUND_ARRAY( a ) macro to precache the set.

Now that the alert sounds are precached, we need to play them. Just override the AlertSound() method and use the EMIT_SOUND_DYN( edict_t *entity, int channel, const char *sample, float volume, float attenuation, int flags, int pitch ) method to play a random sound.

It would look like this:
void CArmorMan::AlertSound()
{
    EMIT_SOUND_DYN( ENT( pev ), CHAN_VOICE, pAlertSounds[RANDOM_LONG( 0, ARRAYSIZE( pAlertSounds ) - 1)], VOL_NORM, ATTN_NORM, 0, PITCH_NORM );
}
That is it, your exercise now is to do the same for the death, idle and pain sets.
About the footsteps sounds
If your custom monster uses the default footsteps sounds (common/npc_stepX.wav), you do not need to precache them neither play them by yourself because the default code already handle that for you.

Since the Armored Man uses custom footsteps sounds. You will need to precache them otherwise you will not hear them. Remember, precaching them is enough, you do not need to override any method.
Another exercise to practice Half-Life programming in general is to change the normal sound pitch by random ones. You can look at weapons and other monsters for inspirations.

Drop an item after death

There are many ways to handle this: one of them is doing the same thing as Half-Life's human grunt and have an animation event drop the item, same when the monster gets gibbed (GibMonster()).

Since our Armored Man does not have a dedicated animation event for that, we need to do an alternative. We are going to override Killed( entvars_t *pevAttacker, int iGib ), check if the model body is less than 1 (0 = shotgun in hand, 1 = no shotgun) and if that's the case: change the body to 1 and drop an ammo_buckshot entity using the CBaseEntity *DropItem( char *pszItemName, const Vector &vecPos, const Vector &vecAng ) method from CBaseMonster. Oh and do not forget to call the default code.

The overriden method would look like this:
void CArmorMan::Killed( entvars_t *pevAttacker, int iGib )
{
    if ( pev->body < 1 ) // Shotgun in hand
    {
        pev->body = 0; // No shotgun

        // This part retrieves the position and angles of the attachment that is the shotgun
        // You could just use "pev->origin" and "pev->angles" for "DropItem" but the dropped
        // item would look like dropped from the stomach which would be a bit weird
        Vector vecGunPos;
        Vector vecGunAng;
        GetAttachment( 0, vecGunPos, vecGunAng );
        vecGunAng.x = 0.0f;
        vecGunAng.z = 0.0f;

        DropItem( "ammo_buckshot", vecGunPos, vecGunAng );
    }

    CBaseMonster::Killed( pevAttacker, iGib );
}

Upgrading to "squad monster"

If you want to upgrade the Armored Man to have the same squad behavior like Half-Life's human grunt. You need to change everything CBaseMonster to CSquadMonster, add the bits_CAP_SQUAD capability flag to m_afCapability in the Spawn() method. You will need to include squadmonster.h as well.

You will also need to override PrescheduleThink() to implement some interesting squad logic borrowed from Half-Life's human grunt:
void CArmorMan::PrescheduleThink()
{
    if ( !InSquad() || !m_hEnemy )
        return;

    if ( HasConditions( bits_COND_SEE_ENEMY ) )
    {
        // update the squad's last enemy sighting time.
        MySquadLeader()->m_flLastEnemySightTime = gpGlobals->time;
    }
    else
    {
        if ( gpGlobals->time - MySquadLeader()->m_flLastEnemySightTime > 5.0f )
        {
            // been a while since we've seen the enemy
            MySquadLeader()->m_fEnemyEluded = true;
        }
    }
}
Do not forget to update the FGD so that level designers can assign an Armored Man to a squad, look at monster_hgrunt for an exemple.

Do not hesitate to practice monsters programming a bit by changing the behavior, variables, trying new stuff and so on. The full code of this page can be found here.

1 Comment

Commented 3 months ago2024-08-29 00:54:00 UTC Comment #106343
if anyone wants to compare notes i got this npc working in game. you can reference things i did if anyone is stuck anywhere

link to solution:
https://twhl.info/thread/view/20840

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