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.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
.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
.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:
TASK_REMEMBER
, pTask->flData
is passed as value to the only parameter of the Remember()
method which is bits_MEMORY_FLINCHED
as defined in the task itself.TASK_REMEMBER
and TASK_STOP_MOVING
notify they are completed successfully through TaskComplete()
because those are tasks that don't need another "thinking tick" to be completed (or failed). This is not the case for TASK_SMALL_FLINCH
.TASK_SMALL_FLINCH
notifies the AI that it has finished? This is the job of RunTask( Task_t *pTask )
which is called by MaintainSchedule()
through 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.TaskFail()
.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.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 first uses the node graph (which we will study later) and the other try to find cover directly to the left or right of the monster.
One more thing, when being "restored" (loading a saved game for example), the monsters have their current schedule and task being "reset".
m_iTaskStatus
can be one of the following:
#define TASKSTATUS_NEW 0 // Just started
#define TASKSTATUS_RUNNING 1 // Running task & movement
#define TASKSTATUS_RUNNING_MOVEMENT 2 // Just running movement
#define TASKSTATUS_RUNNING_TASK 3 // Just running task
#define TASKSTATUS_COMPLETE 4 // Completed, get next task
If you go back in the MaintainSchedule()
method, we can see inside that loop with a scary "undone: this is just here so infinite loops are impossible" comment that if there is an actual schedule and the current task is complete, then a call to NextScheduledTask()
advance to the next task (which increase an index and set m_iTaskStatus
to TASKSTATUS_NEW
) in the current schedule or mark the latter as "done" if there are no more tasks.m_failSchedule
) set, then you will see sparks above the monster's head and the Schedule: <name> Failed
console message.SCHED_FAILED
is used but tasks can define a "custom" one through the usage of TASK_SET_FAIL_SCHEDULE
. Note that some monsters don't use this task and will "override" case SCHED_FAILED
in Schedule_t *GetScheduleOfType( int Type )
(the alien controller does that).GetIdealState()
if various conditions are met (enemy died during combat or schedule is interrupted by non-completion or schedule has completed and requested a state change). GetIdealState()
is responsible for checking various conditions bits and apply the right state as needed.StartTask()
. The activity is changed to the ideal one if needed and if the task is not complete and not a new one, we get out of that loop mentioned previously.RunTask()
is called. The ideal activity is set again for an edge case where two animations need to blend if RunTask()
changes it.You must log in to post a comment. You can login or register a new account.