Tutorial: Coding NPCs in GoldSrc Last edited 3 years ago2021-01-20 09:47:31 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.
✔️ Download example

Since the dawn of Half Life, people have been making maps for it. Occasionally, models, or "reskins" were made. But in only a fraction of the mods were whole new NPCs made. The process is fairly easy, but not done too often. This tutorial will tell you how to do it, getting further into detail the farther you go.

IMPORTANT: This tutorial requires you to have downloaded and installed the Half-Life SDK and Visual Studio. If you haven't, you should, it's pretty fun to mess around with. You also need to know some C++ basics.
The world of coding starts here!The world of coding starts here!
So, first let's have an introduction to two things - what are NPCs and how they work, and what is the state/schedule/task system.

NPCs stand for Non-Player Characters. Without them, SP mods would be fairly bland (except if you're going for an Amnesia feel). They can be Marines, Scientists, aliens or even the cockroaches you see. Artificial Intelligence is the code behind it. Now, these NPCs usually use models (the visual representation of the NPCs) and send signals back and forth from the code to the models. For example, the code first tells the NPC to play a shooting animation, and then uses events to trigger certain...events. More about that later.

Now, on to the topic of Monster States, Schedules and Tasks. This is a system that Half Life uses to make NPC development easier and cleaner, as well as more efficient and better.

A monster state is how a monster feels. In the default HLSDK, these were MONSTERSTATE_NONE, MONSTERSTATE_IDLE, MONSTERSTATE_COMBAT, MONSTERSTATE_ALERT, MONSTERSTATE_HUNT, MONSTERSTATE_PRONE, MONSTERSTATE_SCRIPT, MONSTERSTATE_PLAYDEAD, and MONSTERSTATE_DEAD. These are all pretty self-explanatory except for Script and Prone. Prone is done when barnacles are eating their victims. Script is for Scripted Sequences and stuff.

Schedules are what the NPC wants to do, depending on his mood and certain set conditions. Tasks are the steps in each schedule for him to complete his goal. There are too many of either of these to mention, but the following two paragraphs contains a few notable ones. You can skip it, if you want. There's a list in the "schedules.h" file in the HLSDK's dlls folder. More of these can be defined in each monster, or types of monsters.

There are three types of attacks for each NPC, as well as two subtypes. There is Melee, Ranged, and Special. The subtypes are differentiated between by 1 and 2 after the names (IE: SCHED_MELEE_ATTACK1 and SCHED_MELEE_ATTACK2). SCHED_COMBAT_FACE tells the NPC to look at something. SCHED_COWER is something that the Scientist does a lot (after this tutorial, you can change that, though), and SCHED_TAKE_COVER_FROM_ENEMY, SCHED_TAKE_COVER_FROM_BEST_SOUND and SCHED_TAKE_COVER_FROM_ORIGIN are things all NPCs do (all self explanatory.) There's also SCHED_RELOAD for Marines.

There are tasks for each type of attack, as well as two NOTURN under each subtype. NPCs use a GET_PATH system to understand where to go, while they use TASK_RUN_PATH or TASK_WALK_PATH to get there. Use TASK_WAIT to tell an NPC to do the next task AFTER the RUN or WALK_PATH tasks.
Half-Life's A.I. was extraordinary for it's age. Can YOU master it?Half-Life's A.I. was extraordinary for it's age. Can YOU master it?
Okay, the introduction is over. Up next, I'll tell you how to add a basic NPC.

Adding a simple NPC

So, now let's make an NPC. First, choose a base class. You could start with the CScientist (or some other cheap base, you lazy readers) if you really wanted to, but we'll go with CBaseMonster. For the lolz. Also, decide whether you want to start a new .cpp file (best choice for cleanliness, but requires a bit more space byte-wise (not too much), or to use the baseclass's .cpp file. You also should choose whether or not you want a seperate .h file (Seperating makes it harder to edit, but you can't include a .cpp file - just .h-es). Forgive me if it's a little blocky, but they ARE calling it blocks of code.

For the tutorial's sake, we'll go with a Pit Drone, from Opposing Force (really a great game), and Sven Coop (really a great mod). Be sure to insert all the sounds and models into their proper folders.
The models we'll work with - the main body, the launched spike, and gibs.The models we'll work with - the main body, the launched spike, and gibs.
First, we have to add the includes. This will tell use what code we used from other .h-es. Type this:
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "monsters.h"
#include "schedule.h"
#include "decals.h"
The top 5 are necessary. The last is just for this guy - it allows you, obviously, to make decals.

Oh, and I forgot to mention. Above that, you can create a block of comments saying how awesome you are, though nobody will ever read it unless you release the code or show it to someone else. Use * * to create a block of comments (with all the contents in between) and use // to create a line of code (with the comments AFTER the //)

Now, you can go on and create some #defines to say values that you'll use a lot. Mainly numbers.
//=========================
//--Monster's Anim Events--
//=========================
#define PDRONE_FIRE_SPIKE  ( 1 )
#define PDRONE_MELEE_LEFT  ( 3 )
#define PDRONE_MELEE_RIGHT ( 8 )
#define PDRONE_MELEE_BOTH  ( 6 )

//=========================
//--Monster's Extra Defs---
//=========================

//-Body Groups
#define BODYGROUP_BODY              1
#define BODYGROUP_SPIKES            2

//-Spike Groups
#define BODY_NO_SPIKES              0
#define BODY_SIX_SPIKES             1
#define BODY_FIVE_SPIKES            2
#define BODY_FOUR_SPIKES            3
#define BODY_THREE_SPIKES           4
#define BODY_TWO_SPIKES             5
#define BODY_ONE_SPIKES             6
The first 4 lines are animation events. In animation, there are certain frames that trigger code-side eventes. The numbers are the "names" of these events, while the PDRONE_ stuff is the thing we reference the numbers by, to make it easier for us to edit.

Afterwards, we have to body groups. if you've ever messed around in a HL Model Viewer (Which you should have by now - I suggest Jed's Half Life Model Viewer), you should know what this does. Body does nothing yet. It's there if you're a cool guy who wants to add some variation even to the aliens. There's also 6 bodys for the spike numbers. Next to it, it says how many there are (In the model, though, it goes 0, then 6, and then decreases to 1).

Okay, on to the "fun" stuff. Create the class. Type this hunk of code in. Or copy it. I'm a tutorial, not a cop:
class CPitDrone : public CBaseMonster
{
    public:
        void Spawn( void );
        void Precache( void );
        int iSpikes;
        int Classify ( void );
        void SetYawSpeed( void );
};
Class is obvious, and so is CPitDrone. Then it says that we're taking the base as CBaseMonster. Next, we say that our public definitions (for other classes to edit) are spawn and precache. Precache means that the model is preloaded, so it doesn't make the game slower. There's a limit to 512 precaches. iSpikes is the current amount of spikes loaded into him...WRITE NOTHING IN THE COMMENTS ABOUT THAT LINE. Classify makes him choose what faction he wants to join, and SetYawSpeed changes how fast he turns.

Okay, now to make him get into the game as an entity (so you can put him in, duh).
LINK_ENTITY_TO_CLASS( monster_pitdrone, CPitDrone );
Now you can use monster_pitdrone as an entity. Editing the FGD will come later. For now, you can't use "SmartEdit".

Now, put on your Frankenstein labcoat (not his Monster, you un-nerds!) and prepare to set him to life.
void CPitDrone :: Spawn()
{
    Precache( ); // So the model loads

    SET_MODEL(ENT(pev), "models/npcs/pit_drone.mdl"); // So you can see him

    UTIL_SetSize(pev, VEC_HUMAN_HULL_MIN, VEC_HUMAN_HULL_MAX); // Let's make his size human. If you're smart enough (or have lots of patience) you can get replace the VEC_ stuff with "Vector( x, y, z)".

    pev->solid = SOLID_SLIDEBOX; // They see me slidin', they hating. This actually tells the engine for it to be Solid. Snakes can GTHO.
    pev->movetype = MOVETYPE_STEP; // 'cause monsters walk - they don't drive (Nightmares will follow)
    m_bloodColor = BLOOD_COLOR_GREEN; // Green blood - just like this comment. Freaked out much? (The blood's actually yellow, though)
    pev->health = 100; // Health - let's keep it as an integer, as opposed to a changeable variable for now.
    pev->view_ofs = Vector ( 0, 0, 20 ); // Eyes' offset (He sees you doing stuff you shouldn't)
    m_flFieldOfView = 0.5; // How far he can see.
    m_MonsterState = MONSTERSTATE_NONE; // Afet he spawns, make him sit there like an idiot, doing nothing.

    MonsterInit(); // Starts the monsters AI

    iSpikes = 6; // Default, he's fully loaded with spikes. AGAIN, NO PUNS YOU SICKOS!
}
I've commented everything so you can easily understand.
void CPitDrone :: Precache()
{
    PRECACHE_MODEL("models/pit_drone_spike.mdl"); //Loads the model for the spike
    PRECACHE_MODEL("models/npcs/pit_drone.mdl"); //Loads the NPC model in the game

    // Bunch of pretty self-explanatory sounds
    PRECACHE_SOUND("pitdrone/pit_drone_melee_attack1.wav");
    PRECACHE_SOUND("pitdrone/pit_drone_melee_attack2.wav");
    PRECACHE_SOUND("pitdrone/pit_drone_attack_spike1.wav");
    PRECACHE_SOUND("pitdrone/pit_drone_eat.wav");
    PRECACHE_SOUND("pitdrone/pit_drone_die1.wav");
    PRECACHE_SOUND("pitdrone/pit_drone_die2.wav");
    PRECACHE_SOUND("pitdrone/pit_drone_die3.wav");
    PRECACHE_SOUND("pitdrone/pit_drone_hunt3.wav");
}
Okay, now you have a monster, but he has to epic voice chose a side end epic voice. There's 13 original CLASS_es, but I've added some of my own for my mod. Here, I'll use an original one. See the adding new monster factions tutorial for how to do this.
int CPitDrone :: Classify ( void )
{
    return CLASS_ALIEN_MONSTER;
}
Finally, make him turn, turn, turn.
void CPitDrone :: SetYawSpeed( void ) {
    pev->yaw_speed = 90;
}
Say hi!Say hi!
Okay, you should have a monster sitting in your room...if you've added him to one.

Setting up AI schedules

If you approach the pitdrone right now, you should notice that he TRIED to attack back, even though nothing happens. He even chases you! This is thanks to our base - that's what's doing the work for us. In the QC (which you can find when you decompile a model), in the lines with the animations, you'll see some have ACT_s. These are invoked when necessary by the code.

Here's an example line. The act is in red.:
$sequence "range" "range" fps 30 ACT_RANGE_ATTACK1 1 { event 1 11 } { event 1008 1 "pitdrone/pit_drone_attack_spike1.wav" }
If you look at the QC again, you'll see some events. These highlighted in blue. The first event is called "1" and is played at frame 11. The next is called "1008"" and is played at frame 1. This plays a sound - in this case, "pitdrone/pit_drone_attack_spike1.wav". Since this isn't a QC tutorial, I won't talk about that anymore.

On to the events. How do we get him to shoot while he's playing the event? First, add these two lines to the class's public definitions:
void HandleAnimEvent( MonsterEvent_t *pEvent );
Schedule_t *GetSchedule( void ); // Handles some schedules
After you've done that, it should look like this:
class CPitDrone : public CBaseMonster
{
    public:
        void Spawn( void );
        void Precache( void );
        int iSpikes;
        int Classify ( void );
        void SetYawSpeed( void );
        void HandleAnimEvent( MonsterEvent_t *pEvent );
        Schedule_t *GetSchedule( void ); // Handles some schedules
};
HandleAnimEvent, obviously, handles the animation events. But before we get to that, we have to start coding the projectile. At the top of the file, add this:
class CPitDroneSpike : public CBaseEntity
{
    public:
    void Spawn( void );
    void Touch( CBaseEntity *pOther );
    Vector waterSpeed;
};

LINK_ENTITY_TO_CLASS( pitdrone_spike, CPitDroneSpike );

void CPitDroneSpike :: Spawn( void )
{
    Precache();

    pev->solid = SOLID_SLIDEBOX;
    pev->movetype = MOVETYPE_FLY;
    pev->classname = MAKE_STRING( "pitdrone_spike" );

    SET_MODEL( ENT(pev), "models/npcs/pit_drone_spike.mdl");
}

void CPitDroneSpike :: Touch ( CBaseEntity *pOther )
{
    if ( !pOther->pev->takedamage ) {
        // If the entity doesn't take damage
        if (UTIL_PointContents(pev->origin) == CONTENTS_WATER)
        {
            pev->velocity = waterSpeed; // Go slower while in water
        }
        else {
            pev->solid = SOLID_NOT;
            pev->movetype = MOVETYPE_FLY;
            pev->velocity = Vector( 0, 0, 0 );
            UTIL_Sparks( pev->origin );
            EMIT_SOUND( ENT(pev), CHAN_VOICE, "npcs/pitdrone/pit_drone_eat.wav", 1, ATTN_NORM );
        }
    }
    else {
        // If it does take damage
        pOther->TakeDamage ( pev, pev, gSkillData.pitdroneSpikeDmg, DMG_GENERIC ); // Give damage to whatever it is
        UTIL_Remove( this ); // Remove it
    }
}
Everything is explained in the comments. Start a new function in the end of the .cpp file:
void CPitDrone :: HandleAnimEvent( MonsterEvent_t *pEvent ) {
Now we can start listing our animation events. This is where the earlier stated definitions come to place. Let's start a switch statement and use the anim events.
    switch ( pEvent->event ) {
        case PDRONE_RELOAD:
            iSpikes = 6;
            SetBodygroup( BODYGROUP_SPIKES, 1 );
            break;
This starts a switch statement, meaning that it switches the output depending on the case. Here, we're saying that, depending on our event, we do different stuff, like the case with the PDRONE_RELOAD. It sets our loaded spikes to 6 and sets the body to match. break; means that the case has ended.

Get ready, here's the second case that shoots the spike, and it's long as hell.
        case PDRONE_FIRE_SPIKE:
            // Define the vectors - an offset and a direction
            Vector vecspikeOffset;
            Vector vecspikeDir;

            // This stores pev->angles into 3 vectors, v_forward, v_up, and v_right, so we can use them in offsets.
            UTIL_MakeVectors ( pev->angles );

            // Move the origin to a relative offset
            vecspikeOffset = ( gpGlobals->v_forward * 22 + gpGlobals->v_up * 40 );

            // Now make the origin absolute, by adding the monster's origin
            vecspikeOffset = ( pev->origin + vecspikeOffset );

            // Setting the Direction, by taking the enemy's origin and view offset (so we hit him in his head, not his feet) and the spike offset, and then normalizing it, so 1 is the maximum.
            vecspikeDir = ( ( m_hEnemy->pev->origin + m_hEnemy->pev->view_ofs ) - vecspikeOffset ).Normalize();

            // Randomizing the Direction up a bit, so he's not a perfect shot.
            vecspikeDir.x += RANDOM_FLOAT( -0.01, 0.01 );
            vecspikeDir.y += RANDOM_FLOAT( -0.01, 0.01 );
            vecspikeDir.z += RANDOM_FLOAT( -0.01, 0.01 );

            // Create the spike, place it and turn it.
            CPitDroneSpike *pSpike = (CPitDroneSpike *)CBaseMonster::Create( "pitdrone_spike", vecspikeOffset, pev->angles, edict() );
            // Actually setting the velocity. This is why we normalized it, so we can have different speeds.
            pSpike->pev->velocity = vecspikeDir * 900;
            // Same for water velocity
            pSpike->waterSpeed = vecspikeDir * 300;

            // Remember the pitdrone, so we can say who killed the enemy
            pSpike->pev->owner = ENT(pev);

            // No friction FTW
            pSpike->pev->friction = 0;

            // Set the angles to correspond to the velocity
            pSpike->pev->angles = UTIL_VecToAngles(pSpike->pev->velocity);

            // Take a spike out
            iSpikes--;

            // Set the body to match the spikes.
            if (iSpikes == 0)
            {
                SetBodygroup( BODYGROUP_SPIKES, 0 );
            }
            else
            {
                SetBodygroup( BODYGROUP_SPIKES, GetBodygroup( BODYGROUP_SPIKES )+1 );
            }
            break;
I commented on everything, so you know what it does.

Now for the melee! NINJA STYLE.
        case PDRONE_MELEE_LEFT:
            // Only gonna comment on this one, cuz the rest are basically the same.
            // This gets the enemy and attacks at the same time.
            // The parameters after CheckTraceHullAttack are distance, amount of damage, and type
            CBaseEntity *pHurt = CheckTraceHullAttack( 85, 20, DMG_SLASH );

            // If you did hurt someone...
            if ( pHurt )
            {
                // ...make him change his view angle a bit (only players)...
                pHurt->pev->punchangle.y = 15;
                pHurt->pev->punchangle.x = 8;
                // ... and push him back a bit.
                pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_up * -100;
            }
            break;
        case PDRONE_MELEE_RIGHT:
            CBaseEntity *pHurt = CheckTraceHullAttack( 85, 20, DMG_SLASH );

            if ( pHurt )
            {
                pHurt->pev->punchangle.y = -15;
                pHurt->pev->punchangle.x = 8;
                pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_up * -100;
            }
            break;
        case PDRONE_MELEE_BOTH:
            CBaseEntity *pHurt = CheckTraceHullAttack( 85, 30, DMG_SLASH );

            if ( pHurt )
            {
                pHurt->pev->punchangle.x = 15;
                pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_up * -100;
            }
            break;
I still commented on it. Remember, this is for all three of his melees - left, right, and both, because we changed up his angles a bit. Read the comments. Let's close off the switch statement now. Add this right afterwards.
        default:
            CBaseMonster::HandleAnimEvent( pEvent );
            break;
    }
}
What that, basically, did was check to see if the base has any events to use, and closes the switch and function.

Okay, if you compile now (don't), you'll see that he doesn't reload. He just fires infinitely. He also doesn't fight very efficiently. Remember, his attacks aren't very accurate, so should come closer, and run after you if you're too close.

Now, we'll get down to choosing schedules. We won't invent new schedules or tasks because the pitdrone doesn't need them. Look at the end of the tutorial for that.

Here's the code for the GetSchedule function:
Schedule_t* CPitDrone::GetSchedule(void)
{
    // Call another switch class, to check the monster's attitude
    switch (m_MonsterState)
    {
        // Manly monster needs to fight
        case MONSTERSTATE_COMBAT:
            if (HasConditions(bits_COND_ENEMY_DEAD))
            {
                // The enemy is dead - call base class, all code to handle dead enemies
                // is
                // centralized there.
                return CBaseMonster::GetSchedule();
            }

            // Can I attack melee style?
            if (HasConditions(bits_COND_CAN_MELEE_ATTACK1))
            {
                // Randomize my melee attacks, so it's a bit different
                switch (RANDOM_LONG(0, 1))
                {
                    case 0:
                        return GetScheduleOfType(SCHED_MELEE_ATTACK1);
                        break;
                    case 1:
                        return GetScheduleOfType(SCHED_MELEE_ATTACK2);
                        break;
                }
            }

            // I can range attack! HELLZ YEAH!
            if (HasConditions(bits_COND_CAN_RANGE_ATTACK1))
            {
                // TOO CLOSE! USE MELEE.
                if ((pev->origin - m_hEnemy->pev->origin).Length() <= 256)
                {
                    return GetScheduleOfType(SCHED_CHASE_ENEMY);
                }
                if ((pev->origin - m_hEnemy->pev->origin).Length() <= 512)
                {
                    // Do I have spikes?
                    if (pev->body != BODY_NO_SPIKES)
                    {
                        // Yes. Fire!
                        return GetScheduleOfType(SCHED_RANGE_ATTACK1);
                    }
                    else
                    {
                        // No.
                        if ((pev->origin - m_hEnemy->pev->origin).Length() <= 312)
                        {
                            // I'm close, I can go and attack.
                            if (HasConditions(bits_COND_CAN_MELEE_ATTACK1))
                            {
                                switch (RANDOM_LONG(0, 1))
                                {
                                    case 0:
                                        return GetScheduleOfType(SCHED_MELEE_ATTACK1);
                                        break;
                                    case 1:
                                        return GetScheduleOfType(SCHED_MELEE_ATTACK2);
                                        break;
                                }
                            }
                            // Lemme get a bit closer, so I can attack
                            else
                            {
                                return GetScheduleOfType(SCHED_CHASE_ENEMY);
                            }
                        }
                        // He's too far to melee. I'll just reload
                        else
                        {
                            return GetScheduleOfType(SCHED_RELOAD);
                        }
                    }
                }
                // Too far to either fire the spikes or melee, so lemme just get closer,
                // so I'm more accurate.
                else
                {
                    return GetScheduleOfType(SCHED_CHASE_ENEMY);
                }
            }
            // If I can do nothing, just chase after him
            return GetScheduleOfType(SCHED_CHASE_ENEMY);
            break;
    }

    // The base probably knows what to do
    return CBaseMonster::GetSchedule();
}
I've commented on the reason I put what where, but, just so you know, return GetScheduleOfType( SCHED_WHATEVER ); returns the schedule, so he can know what to do.

Now, you should have a well-functioning pit drone. You can use this base for most other enemies. For more advanced coding, read on. I'll discuss how to add schedules, and some other stuff. It's long, I know, so take a short break.

Additional NPC AI information

Attack checks

There are four functions:
virtual BOOL CheckRangeAttack1( float flDot, float flDist );
virtual BOOL CheckRangeAttack2( float flDot, float flDist );
virtual BOOL CheckMeleeAttack1( float flDot, float flDist );
virtual BOOL CheckMeleeAttack2( float flDot, float flDist );
That work pretty much the same. They check whether the monster is allowed to range or melee or not.

Here's a basic Melee2:
BOOL CBaseMonster :: CheckMeleeAttack2 ( float flDot, float flDist ) {
    if ( flDist <= 64 && flDot >= 0.7 )
    {
        return TRUE;
    }
    return FALSE;
}
That basically checks if the flDist (Distance, obviously), and flDot (I believe that's how-in-sight he is).

GetScheduleOfType

In the GetSchedule command, there were a lot of `GetScheduleOfType`-s, right? Those switch the schedule name to the name of the list, and return that.

EXAMPLE:
Schedule_t* CBaseMonster :: GetScheduleOfType ( int Type )
{
    switch ( Type )
    {
        case SCHED_CHASE_ENEMY:
            return &slChaseEnemy[ 0 ];

        default:
            ALERT ( at_console, "GetScheduleOfType()nNo CASE for Schedule Type %d!n", Type );

            return &slIdleStand[ 0 ];
            break;
    }

    return NULL;
}
In the example, you get the Type from the GetSchedule, where the function is called from. You put it into a switch statement. If it's SCHED_CHASE_ENEMY, release the proper schedule, in this case, &slChaseEnemy[ 0 ]. Otherwise, send an alert at the console, and give the standing schedule. If even THAT doesn't work, return nothing at all (that should never happen).

Creating new schedules

Okay, now to go on to making new schedules themseleves.

In your class, be sure that you have CUSTOM_SCHEDULES in the definition:
    ...
    void Precache( void );
    CUSTOM_SCHEDULES;
};
Now, go on to the schedule names.
enum
{
    SCHED_GET_BEST_ITEM = LAST_TALKMONSTER_SCHEDULE + 1,
    SCHED_COVER_AND_GET_ITEM,
    LAST_HEVSCIENTIST_SCHEDULE,        // MUST be last
};
That's an example from my HEV scientists (who can fight back and pick up weapons). LAST_COMMON_SCHEDULE can replace LAST_TALKMONSTER_SCHEDULE if you're using CBaseMonster instead of a Talking Monster. LAST_HEVSCIENIST_SCHEDULE should be renamed to fit your own monster, and the two schedules (SCHED_GET_BEST_ITEM and SCHED_COVER_AND_GET_ITEM) could be changed. The second can be removed or duplicated. The ` = LAST_TALKMONSTER_SCHEDULE + 1,` and LAST_HEVSCIENTIST_SCHEDULE, NEED to be there, but the text depends on the monster.

Now, you can write down the name of the lists.
DEFINE_CUSTOM_SCHEDULES( CHEVScientist )
{
    slGetBestItem,
    slCoverAndGetItem,
};

IMPLEMENT_CUSTOM_SCHEDULES( CHEVScientist, CTalkMonster );
The CHEVScienist s should be changed to match your class, and the sl -s should match something that resembles your SCHED_s. The IMPLEMENT_CUSTOM_SCHEDULES is necessary, and, again, the values should be substituted for your class and base respectively.

Now, you should add something along the lines of this, where you get to add your actual lists:
Task_t tlHeal[] =
{
    { TASK_MOVE_TO_TARGET_RANGE,      (float) 50                 },    // Move within 60 of target ent (client)
    { TASK_SET_FAIL_SCHEDULE,         (float) SCHED_TARGET_CHASE },    // If you fail, catch up with that guy! (change this to put syringe away and then chase)
    { TASK_FACE_IDEAL,                (float) 0                  },
    { TASK_SAY_HEAL,                  (float) 0                  },
    { TASK_PLAY_SEQUENCE_FACE_TARGET, (float) ACT_ARM            },    // Whip out the needle
    { TASK_HEAL,                      (float) 0                  },    // Put it in the player
    { TASK_PLAY_SEQUENCE_FACE_TARGET, (float) ACT_DISARM         },    // Put away the needle
};

Schedule_t slHeal[] =
{
    {
        tlHeal,
        ARRAYSIZE ( tlHeal ),
        0,    // Don't interrupt or he'll end up running around with a needle all the time
        0,
        "Heal"
    },
};
A LOT of stuff going on here. First of all, this is the heal schedule, and, again, replace everything you need to. From the top, we started the task list. This part is pretty obvious. We type up the different tasks we're gonna use. For example, the first one: TASK_MOVE_TO_TARGET_RANGE says the task - that he should get close to the target (the player), and (float)50 defines the parameter - the distance. This is not always necessary, and you should use (float) 0 when nothing else is necessary. You can set different schedules and different acts, too.

Down in the schedule. This is more like the base information about the task list. Now, tlHeal is the list, and ARRAYSIZE( tlHeal ) says the size of the list. The next two variables are the interruption conditions, and the other one is the "sound" interruption conditions. "Heal" is the name of the schedule.

Creating new tasks

Now, to move on to making new tasks. First of all, define the task names in a similar way to new schedules:
enum
{
    TASK_GET_PATH_TO_BEST_ITEM = LAST_TALKMONSTER_TASK + 1,
    TASK_PICK_UP_ITEM,
    LAST_HEVSCIENTIST_TASK,            // MUST be last
};
To say what it does, use this in the class:
void StartTask( Task_t *pTask );
Now, you can create the function itself:
void CHEVScientist :: StartTask( Task_t *pTask )
{
    switch ( pTask->iTask )
    {
        case TASK_RELOAD:
            m_IdealActivity = ACT_RELOAD;
            break;
        default:
            CTalkMonster::StartTask( pTask );
    }
}
Basically, it takes the task through a switch, and either calls the baseclass if it's not found, or does what's said - calling an ACT_.

NOTE: This only is called when we START the task. Use a similar function - RunTask, for every moment that it's on.

Okay, you should now have the know-how to make even the best NPCs. I could go in to talking monsters, but that's easy, and the tutorial's getting rather long. So - mortals, I hope your eyes are still intact, and that you've learnt something.

25 Comments

Commented 13 years ago2011-08-08 14:16:01 UTC Comment #101004
What file should I be edditing? Maybe add some details about what file for what page.
Commented 13 years ago2011-08-08 18:19:14 UTC Comment #101005
"Also, decide whether you want to start a new .cpp file (best choice for cleanliness, but requires a bit more space byte-wise (not too much), or to use the baseclass's .cpp file."
Commented 13 years ago2011-08-08 22:29:24 UTC Comment #101006
thanks, didn't even notice that line. Thanks for the tutorial but I don't think I'll be able to code it for a while. Thanks again.
Commented 13 years ago2011-08-10 00:08:09 UTC Comment #101007
omg I should totally get back to doing this. I wish I had some of dimbark's free time.
Commented 13 years ago2011-08-14 05:51:18 UTC Comment #101008
That's probably what EVERYONE wishes.
Commented 13 years ago2011-08-24 16:02:59 UTC Comment #101009
Looks really well done sir, even though i don't map for HL anymore.. Really fine work nonetheless!
Commented 13 years ago2011-08-31 09:53:19 UTC Comment #101010
you didnt define PDRONE_RELOAD and it doesnt range attack in mine it plays reload animation then NPC freezes (not game, just NPC) and when i get closer it follows me again and attacks with melee attack(which works fine)
Commented 13 years ago2011-09-07 01:37:02 UTC Comment #101011
@Motherfat: Really sorry - I'll fix it soon, but I've got both, school, and TWO enormous mods to run.

Thanks to all the commenters!
Commented 13 years ago2011-09-24 01:54:05 UTC Comment #101012
more plz thx :D
Commented 12 years ago2012-01-24 22:08:01 UTC Comment #101013
Hi.
I use this tutorial and is very good but i have only 3 "errors":

First, the pitdrone doesn´t fire.

Second, the melee attack left and right doesn´t make damage to the player.

and finally, when the pitdrones kill the player, using the attack "double", and i press a key to automatically restore the game i have this message error:

"ED_ALLOC = No edits yet."

**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--

Ok, for the first, i dont need them for the moment, my new monster doesn´t fire, so i think if i need that in the future, i can use the bullchicken code, or whatever.

For the second, i dont see where is the error...if anyone can help me.

For the last one, ("the most important"), i read about the "edits", they are the "slots" for the monsters, all hl monsters have one...so, ¿you need to "define" a slot for the pitdrone? how do that? heeelp! i am so close to finish my mod...i can´t release them until i have coding at least three new npc´s....

So, thanks for all, I hope someone can help me.
Commented 12 years ago2012-05-03 02:48:04 UTC Comment #101014
¿Is my english so bad? why nobody answer???...damn..
Commented 12 years ago2012-06-10 19:25:27 UTC Comment #101015
Well done....the crickets are more usefull than you, Thanks for nothing.
Commented 12 years ago2012-06-22 06:40:04 UTC Comment #101016
"Let's close off the switch statement now. Add this right afterwards.

default:
CBaseMonster::HandleAnimEvent( pEvent );
break;
}
} "

Right after what exactly??
Commented 11 years ago2013-07-21 16:32:22 UTC Comment #101017
Wow. Sorry I missed all this, guys. I'm going to download the code and fix any problems as soon as possible.
Commented 11 years ago2013-08-01 01:03:32 UTC Comment #101018
Finally......there is still hope, still hope.
Commented 10 years ago2013-12-02 17:48:23 UTC Comment #101019
..i dont fucking get what this guy i saying...??!!!!
Commented 10 years ago2014-04-14 03:35:51 UTC Comment #101020
Any solution to this thing?
Commented 9 years ago2015-02-08 20:02:54 UTC Comment #101021
okay... So call me mad for even getting into this, but i am a COMPLETE noob at c++, I only know the basics and have been messing around with them in code::blocks... So, my question is, How on earth do you actually put this INTO THE GAME? All I have is the HLSDK files in a folder on my hdd,with no clue on how to compile it or whatever you have to do... Please help me! (Not really expecting an anwser tho.. Last post was ~1 year ago..)
Commented 8 years ago2016-01-17 01:37:42 UTC Comment #101022
Does anybody besides me get an error saying "PDRONE_RELOAD: case not constant"?

Before I got to the reload part, everything compiled fine.
Commented 8 years ago2016-02-19 04:05:12 UTC Comment #101023
How do I get the NPC into hammer, I mean can you tell me what I need to put in the .fgd file and where to put it. Thanks in advance
Commented 8 years ago2016-03-08 03:55:37 UTC Comment #101024
Never mind about my "case expression not constant" error, I defined it already... along with fixing my other personal problems with this code.

But, every time the I enter the pit drone's FOV, the game exits to the main menu, literally disconnecting me from the game.
Commented 5 years ago2019-10-10 13:25:17 UTC Comment #102322
I love this tutorial lol, Thanks
Commented 3 years ago2021-01-05 04:59:14 UTC Comment #103180
"#include" is added in our npc's cpp file, right?
Commented 3 years ago2021-01-05 11:14:42 UTC Comment #103181
@Saw: those includes must be put in the same file as the rest of the code, so if you've created a new .cpp file for your NPC, then yes, they should be put at the top of that file.
Commented 2 months ago2024-08-28 09:12:28 UTC Comment #106340
since everyone has been having trouble with this and i needed to learn projectiles, i decided to work on fixing the bugs in this tutorial.
The integers that represent the animation events were not the same as what was written in the .qc for the melee attacks and reload so you had to change it to this
#define PDRONE_FIRE_SPIKE ( 1 )
#define PDRONE_MELEE_LEFT ( 2 )
#define PDRONE_MELEE_RIGHT ( 4 )
#define PDRONE_MELEE_BOTH ( 6 )
#define PDRONE_RELOAD ( 7 )
the return NULL under GetScheduleOfType was causing crashes so i changed it to:
return CBaseMonster::GetScheduleOfType(Type);
i had to make a custom checkAmmo function and override checkRangeAttack1 and explicitly call them in getSchedule.

and thats about it. heres a video preview of the changes i made and the showcasing it working. I will share the gitHub Code below so you can reference it.
PROJECTILE NPC
GITHUB FILES:
https://github.com/TylerMaster/ICE1/blob/BRANDON_SMITH_Ex1/wip_monster.cpp
https://github.com/TylerMaster/ICE1/blob/BRANDON_SMITH_Ex1/wip_monster.h

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