Monsters Programming - Schedules and Tasks Last edited 1 week ago2020-11-19 23:30:22 UTC

Half-Life Programming

If you go back to looking at the code of RunAI(), you can see there is a call to MaintainSchedule(). If you look at this method, you will see things about schedules, tasks, completion and failure.

Schedules and tasks are the two main components of the Half-Life's AI. A task is a set of specific actions executed by the monster like playing a sequence/animation, reload, throw a grenade and so on. A schedule is a list of tasks executed by the monster to achieve a bigger goal such as moving to a precise location, wave a hand, shoot and take cover to reload...

The Task_t structure in the code represent a task, here is its definition:
struct Task_t
{
    int iTask;    // The ID of this task (TASK_WAIT for example)
    float flData; // Additional data that this task may use (time to wait for TASK_WAIT for example)
};
Same thing with Schedule_t for a schedule:
struct Schedule_t
{
    Task_t *pTasklist;  // Array of tasks for this schedule
    int cTasks;         // The number of tasks (always use "ARRAYSIZE" of the value you use in "pTasklist" to be safe)
    int iInterruptMask; // Condition bit(s) allowed to interrupt this schedule if needed
    int iSoundMask;     // Sound bit(s) allowed to interrupt this schedule if needed
    const char *pName;  // Name of the schedule shown in the console when testing/debugging
};
Let us take a deeper look at how declared and defined a task and a schedule are by looking at how a monster make a small flinch:
Task_t tlSmallFlinch[] =
{
    { TASK_REMEMBER,     (float)bits_MEMORY_FLINCHED }, // Remember in my memory that I flinched
    { TASK_STOP_MOVING,  0                           }, // Stop moving
    { TASK_SMALL_FLINCH, 0                           }, // Make a small flinch
};

Schedule_t slSmallFlinch[] =
{
    {
        tlSmallFlinch,              // This schedule use the "tlSmallFlinch" tasks array
        ARRAYSIZE( tlSmallFlinch ), // "ARRAYSIZE" will return 3 here and tell this schedule has 3 tasks
        0,                          // No condition bit can interrupt this schedule
        0,                          // No sound bit can interrupt this schedule
        "Small Flinch"              // This schedule is named "Small Flinch"
    },
};
How the monster is aware that it should make a small flinch? This is where Schedule_t *GetSchedule() intervenes:
Schedule_t *CBaseMonster::GetSchedule()
{
    switch ( m_MonsterState )
    {

    // [...]

    case MONSTERSTATE_COMBAT:
        {
            // [...]

            else if ( HasConditions( bits_COND_LIGHT_DAMAGE ) && !HasMemory( bits_MEMORY_FLINCHED ) )
            {
                return GetScheduleOfType( SCHED_SMALL_FLINCH );
            }

            // [...]

            break;
        }
        default:
        {
            ALERT( at_aiconsole, "Invalid State for GetSchedule!\n" );
            break;
        }
    }

    return &slError[0];
}
In the above example, if the monster is in combat, has the "took light damage" condition bit enabled and does not remember in it's memory "having already flinched", then he will try to perform a schedule tied to SCHED_SMALL_FLINCH.

Now let's look at Schedule_t *GetScheduleOfType( int Type ) to see how the schedule is actually executed:
Schedule_t *CBaseMonster::GetScheduleOfType( int Type )
{
    switch ( Type )
    {

    // [...]

    case SCHED_SMALL_FLINCH:
        {
            return &slSmallFlinch[0];
        }
    default:
        {
            ALERT( at_console, "GetScheduleOfType()\nNo CASE for Schedule Type %d!\n", Type );

            return &slIdleStand[0];
            break;
        }
    }

    return nullptr;
}
You can notice that SCHED_ constants are used to identify schedules and tie them to a Schedule_t.

How about tasks? The StartTask( Task_t *pTask ) method is called whenever a task is requested to be performed. Let us look at task startup for small flinch:
void CBaseMonster::StartTask( Task_t *pTask )
{
    switch ( pTask->iTask )
    {

    // [...]

    case TASK_REMEMBER:
        {
            Remember( (int)pTask->flData );
            TaskComplete();
            break;
        }

    // [...]

    case TASK_STOP_MOVING:
        {
            if ( m_IdealActivity == m_movementActivity )
            {
                m_IdealActivity = GetStoppedActivity();
            }

            RouteClear();
            TaskComplete();
            break;
        }

    // [...]

    case TASK_SMALL_FLINCH:
        {
            m_IdealActivity = GetSmallFlinchActivity();
            break;
        }

    // [...]

    default:
        {
            ALERT( at_aiconsole, "No StartTask entry for %d\n", (SHARED_TASKS)pTask->iTask );
            break;
        }
    }
}
There are two interesting things to note here: How does TASK_SMALL_FLINCH notifies the AI that it has finished? This is the job of RunTask( Task_t *pTask ) which is called by RunAI(). A reminder that RunAI() is called every "thinking tick" until the monster dies.
void CBaseMonster::RunTask( Task_t *pTask )
{
    switch ( pTask->iTask )
    {

    // [...]

    case TASK_SMALL_FLINCH:
        {
            if ( m_fSequenceFinished )
            {
                TaskComplete();
            }
        }
        break;

    // [...]

    }
}
You can see here that the task will be marked as completed successfully when the sequence/animation has finished playing. We can also determine that the duration of the flinch is determined by the animation/sequence itself.

As you might have already guessed, there are scenarios where a monster cannot complete a task successfully as intended (a monster trying to follow a target but that same target is too far and/or in a place impossible to reach). In that case, the task is marked as failed through TaskFail().

There are handy methods about tasks available such as int TaskIsComplete() and int TaskIsRunning(). And you guessed it already, monsters can have their own tasks and schedules as well. You can find most of the common tasks in the defaultai.cpp file.

For the tasks that involves "cover": cover searches are made within a 764 units radius unless float CoverRadius() is overridden. There is also "normal cover" (BOOL FindCover( Vector vecThreat, Vector vecViewOffset, float flMinDist, float flMaxDist )) and "lateral cover" (BOOL FindLateralCover( const Vector &vecThreat, const Vector &vecViewOffset )), the difference between them is that the latter try to find cover directly to the left or right of the monster.
Monster specific tasks, schedules and IDs
When defining new tasks that are specific to a monster, it is very important that the first task ID is LAST_COMMON_TASK + 1 if your monster is based on CBaseMonster or CSquadMonster. If you are using CTalkMonster, use LAST_TALKMONSTER_TASK + 1 instead.

If you do not do that, you will override the common tasks that are used by all monsters.

The same applies to schedules with LAST_COMMON_SCHEDULE + 1 or LAST_TALKMONSTER_SCHEDULE + 1.

Comments

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