VERC: Half-Life AI, Schedules and Tasks Last edited 2 years ago2022-09-29 07:54:44 UTC

This tutorial assumes you have prior knowledge on how the HL SDK works. If you don't have this knowledge you'll have a hard time understanding this tutorial.

Note: Some code was slightly modified to allow it to fit on this page better.

The Half-Life AI is made mostly of two things, tasks and schedules. A task is a specific action performed by a monster, like playing a sequence, running to take cover, or throwing a grenade. A schedule is a set of tasks performed in a specific order, like finding a path to a corpse, running to the corpse, and performing a specific animation. Half-Life has a set of basic AI functions but most of the monsters have there own added schedules and tasks to give them there own unique AI or override the default AI. Below is a schedule of the victory dance a human grunt does.
//=========================================================
// Victory dance!
//=========================================================
Task_t    tlGruntVictoryDance[] =
{
    { TASK_STOP_MOVING, (float)0    },
    { TASK_FACE_ENEMY, (float)0    },
    { TASK_WAIT, (float)1.5        },
    { TASK_GET_PATH_TO_ENEMY_CORPSE,(float)0    },
    { TASK_WALK_PATH, (float)0    },
    { TASK_WAIT_FOR_MOVEMENT, (float)0    },
    { TASK_FACE_ENEMY, (float)0    },
    { TASK_PLAY_SEQUENCE, (float)ACT_VICTORY_DANCE    },
};

Schedule_t    slGruntVictoryDance[] =
{
    {
        tlGruntVictoryDance,
        ARRAYSIZE ( tlGruntVictoryDance ),
        bits_COND_NEW_ENEMY        |
        bits_COND_LIGHT_DAMAGE    |
        bits_COND_HEAVY_DAMAGE,
        0,
        "GruntVictoryDance"
    },
};
The slGruntVictoryDance[] is the actual schedule, the tlGruntVictoryDance[] is the list of tasks for that schedule. The stuff after ARRAYSIZE ( tlGruntVictoryDance ), are the different things that can interrupt this schedule. In this case, seeing a new enemy, and taking light or heavy damage. The 0, are different sounds that can stop the schedule. No sounds can stop this schedule. The "GruntVictoryDance" is the name of the schedule, if you do impulse 103 while looking at a monster it will give you a report on its current AI status. Now looking at tlGruntVictoryDance[] you'll see a list of tasks to be performed for this schedule. The (float)0 followed by each task is the value of its data. Most tasks don't need anything here except (float)0 but some need a value, for example: TASK_PLAY_SEQUENCE need an ACT to play, most sequences in a monster a linked to specific ACTs, like ACT_DIE, ACT_RELOAD, etc. Farther down in the hgrunt.cpp file you'll see this:
DEFINE_CUSTOM_SCHEDULES( CHGrunt )
{
    slGruntFail,
    slGruntCombatFail,
    slGruntVictoryDance,
    slGruntEstablishLineOfFire,
    slGruntFoundEnemy,
    slGruntCombatFace,
    slGruntSignalSuppress,
    slGruntSuppress,
    slGruntWaitInCover,
    slGruntTakeCover,
    slGruntGrenadeCover,
    slGruntTossGrenadeCover,
    slGruntTakeCoverFromBestSound,
    slGruntHideReload,
    slGruntSweep,
    slGruntRangeAttack1A,
    slGruntRangeAttack1B,
    slGruntRangeAttack2,
    slGruntRepel,
    slGruntRepelAttack,
    slGruntRepelLand,
};

IMPLEMENT_CUSTOM_SCHEDULES( CHGrunt, CSquadMonster );
That code defines all the custum schedules and implements them into the monster so it can use them.

Close to the top of the hgrunt.cpp file there is this little chunk of code.
//=========================================================
// monster-specific schedule types
//=========================================================
enum
{
    SCHED_GRUNT_SUPPRESS = LAST_COMMON_SCHEDULE + 1,
    SCHED_GRUNT_ESTABLISH_LINE_OF_FIRE,
    SCHED_GRUNT_COVER_AND_RELOAD,
    SCHED_GRUNT_SWEEP,
    SCHED_GRUNT_FOUND_ENEMY,
    SCHED_GRUNT_REPEL,
    SCHED_GRUNT_REPEL_ATTACK,
    SCHED_GRUNT_REPEL_LAND,
    SCHED_GRUNT_WAIT_FACE_ENEMY,
    SCHED_GRUNT_TAKECOVER_FAILED,
    SCHED_GRUNT_ELOF_FAIL,
};
That declares all the schedule types which will be used later. It is important that after the first schedule you have the = LAST_COMMON_SCHEDULE + 1, otherwise your schedule numbers will be all wrong and your monsters will be doing stuff they shouldn't. The next part of code looks like this.
//=========================================================
// monster-specific tasks
//=========================================================
enum
{
    TASK_GRUNT_FACE_TOSS_DIR = LAST_COMMON_TASK + 1,
    TASK_GRUNT_SPEAK_SENTENCE,
    TASK_GRUNT_CHECK_FIRE,
};
That peice of code declares all the task types. Again its important to have = LAST_COMMON_TASK + 1, after the first task. Now, moving on.

The code that tells a monster what schedule to do is contained mostly in two functions, GetSchedule and GetScheduleOfType.

GetSchedule checks the monsters current state (combat, idle, alert, etc.) and picks a schedule based in different situations. When these requirements are met GetSchedule then returns with GetScheduleOfType( SCHEDULE_TYPE ). GetScheduleOfType uses the schedule types to return the corret schedule. Take a look at these two sections of code.

In AI_BaseNPC_Schedule.cpp (schedule.cpp in SDKs older than 2.2), GetSchedule function.
if ( HasConditions( bits_COND_ENEMY_DEAD )
    && LookupActivity( ACT_VICTORY_DANCE ) != ACTIVITY_NOT_AVAILABLE )
{
    return GetScheduleOfType ( SCHED_VICTORY_DANCE );
}
And back in hgrunt.cpp, GetScheduleOfType function.
case SCHED_VICTORY_DANCE:
{
    if ( InSquad() )
    {
        if ( !IsLeader() )
        {
            return &slGruntFail;[ 0 ];
        }
    }

    return &slGruntVictoryDance;[ 0 ];
}
Now heres what that code does. In the GetSchedule code the game checks to see if the monsters enemy is dead and if it has a sequence linked the the ACT_VICTORY_DANCE activity. If it does have the act, then it calls the monsters GetScheduleOfType with SCHED_VICTORY_DANCE. In the hgrunts GetScheduleOfType it looks through all the schedule types. If the grunt is in a squad and is the leader then it returns the fail schedule, otherwise it returns the victory dance schedule (seen farther up this page).

What? The tutorial is all done? Nah, still have to go over tasks! Like schedules, two functions are used to control tasks, StartTask and RunTask. StartTask is called when the task first starts up. RunTask is then called every time the monster thinks until the task is complete. Lets look at the task that tells the grunt which way to face when he throws a grenade. In the StartTask function we have this.
case TASK_GRUNT_FACE_TOSS_DIR:
    break;
As you can see this task doesn't need to do anything when it first starts. Not all tasks use StartTask or RunTask (although they have to use one). Now in RunTask.
case TASK_GRUNT_FACE_TOSS_DIR:
{
// project a point along the toss vector and turn to face that point.
    MakeIdealYaw( pev->origin + m_vecTossVelocity * 64 );
    ChangeYaw( pev->yaw_speed );

    if ( FacingIdeal() )
    {
        m_iTaskStatus = TASKSTATUS_COMPLETE;
    }
    break;
}
Now this code is called every time the monster thinks. It tells the grunt to turn towards where he wants to throw a grenade and when he finally faces the right direction the task is complete. The code that tells the schedule that this task is complete is the m_iTaskStatus = TASKSTATUS_COMPLETE;. You can also use TaskComplete() and TaskIsComplete(). TaskComplete checks to see if the task failed or not, TaskIsComplete just returns m_iTaskStatus = TASKSTATUS_COMPLETE;. It is important that you complete the task somehow, otherwise the monster just performs the task for an infinate amount of time (aka it becomes a vegie).

There you have it, a basic synapses of how the HL AI system works. You should probably look around the defaultai.cpp and AI_BaseNPC_Schedule.cpp (schedule.cpp with SDKs older than 2.2) to see a list of all the default tasks and schedules and get a grasp on how the defualt AI works.
This article was originally published on Valve Editing Resource Collective (VERC).
The archived page is available here.
TWHL only publishes archived articles from defunct websites, or with permission. For more information on TWHL's archiving efforts, please visit the TWHL Archiving Project page.

Comments

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