VERC: Half-Life AI, Schedules and Tasks Last edited 22 years ago2002-04-08 22:22:00 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.

This article was recovered from an archive and needs to be reviewed

  1. The formatting may be incorrect as it was automatically converted to WikiCode from HTML, it needs to be revised and reformatted
  2. Some information may be out of date as it was written before Half-Life was available on Steam
  3. After the article is re-formatted and updated for Steam HL, remove this notice
  4. Please do not remove the archive notice from the bottom of the article.
  5. Some archive articles are no longer useful, or they duplicate information from other tutorials and entity guides. In this case, delete the page after merging any relevant information into other pages. Contact an admin to delete a page.
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 the Valve Editing Resource Collective (VERC).
TWHL only archives articles from defunct websites. 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.