Creating a schedule: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
mNo edit summary
No edit summary
Line 1: Line 1:
{{npc tut}}
{{npc tut}}


==Defining a Schedule==
A schedule is a list of [[task]]s for an NPC to perform. A new schedule is chosen '''only''' when there is no active one; this might be because the NPC has only just spawned, because it has completed a schedule since it last [[NPCThink()|thought]], or because the schedule it was running previously encountered an interrupt [[condition]].
Custom schedules can be added by first declaring a new enum that will define the custom schedules.


<pre>
== Creating a new schedule ==
enum
{
    SCHED_DODGE_ENEMY_FIRE = LAST_SHARED_SCHEDULE,
};
</pre>


Then we would need to define our custom schedule using the <code>DEFINE_SCHEDULE</code> macro.
=== Enumeration ===
Schedules are defined in the NPC's [[AI_BEGIN_CUSTOM_NPC]] [[macro]] block. Before you start to do that however, you will need to add a new item to the class' enum:


<pre>
enum
AI_BEGIN_CUSTOM_NPC( npc_custom, CNPC_Custom )
{
DEFINE_SCHEDULE
      SCHED_DODGE_ENEMY_FIRE = LAST_SHARED_SCHEDULE,  
(
};
SCHED_DODGE_ENEMY_FIRE,


" Tasks"
{{todo|Elsewhere <code>BaseClass::NEXT_SCHEDULE</code> is used, not <code>LAST_SHARED_SCHEDULE</code>. What's the difference?}}
" TASK_FIND_DODGE_DIRECTION 3"
" TASK_JUMP 0"
""
" Interrupts"
        "              COND_LIGHT_DAMAGE"
);
AI_END_CUSTOM_NPC()
</pre>


When you define a schedule you provide two sections, [[Tasks]] and Interrupts.
=== Definition ===
Once the schedule is enumerated, we can start defining it. All schedules use the same structure:


An Interrupt is a condition that will cause the NPC to break out of the schedule and run a more appropriate one to the given condition and/or state. In this case if the NPC experiences <code>COND_LIGHT_DAMAGE</code> (any damage greater than zero) the NPC will break out of this schedule and play a more appropriate one which would be handled in <code>SelectSchedule()</code> or state specific variants.
AI_BEGIN_CUSTOM_NPC( npc_custom_class, CNPC_Custom )
DEFINE_SCHEDULE
(
SCHED_DODGE_ENEMY_FIRE,
" Tasks"
" TASK_FIND_DODGE_DIRECTION 3"
" TASK_JUMP 0"
""
" Interrupts"
        "              COND_LIGHT_DAMAGE"
);
AI_END_CUSTOM_NPC()


Some interesting methods are described below that make up <code>CAI_BaseNPC</code> class.
{{warning|The first character of each string (except for the 'gap') must be a space or a tab. Your schedule will otherwise be invalid!}}


==SelectSchedule==
As you can see, there are two parts to the definition:


In <code>CAI_BaseNPC</code> the function <code>SelectSchedule()</code> determines what schedule the NPC should run depending on its state and conditions.  
#'''<code>Tasks</code>''' is simply the list of sequential [[task]]s an NPC must perform in order to complete the schedule.
#*Every task requires a numeric value to be passed with it! If the task doesn't actually make use of one, just pass <code>0</code>.
#'''<code>Interrupts</code>''' is a list of [[conditions]] that will cause the schedule to be abandoned, and a new one chosen, if any are ever detected.


In <code>CAI_BaseNPC</code>, all of the default States are considered, and additional methods are called to select the schedule based on condition, for example, when the NPC is in the <code>NPC_STATE_IDLE</code>, the method <code>SelectIdleSchedule()</code> is called, this method is similar to <code>SelectSchedule()</code>, all it does is select a schedule based on the NPC's state and conditions.  
In the example schedule given above, the NPC will attempt to find a suitable direction to dodge in (checking in a maximum of three directions, judging by the parameter) before using the output of that task, stored somewhere in the class, to perform the movement itself. But if it encounters <code>COND_LIGHT_DAMAGE</code> (any damage greater than zero) during the process, it will abandon the schedule and select another.


Child classes can implement these schedule selection methods to select their own custom schedules based on their own custom conditions.  
The behavioural code that decides what the NPC actually ''does'' is defined in the component [[task]]s.


For instance, we could create our own schedule <code>SCHED_MEDICALPOINT_GO</code> and a condition <code>COND_NEEDS_MEDICAL_ASSISTANCE</code>.
== SelectSchedule() ==


We could implement the schedule selection in <code>SelectSchedule()</code> or a schedule selection method variation as follows
<code>SelectSchedule()</code> is called whenever an NPC finds itself without a schedule, and contains the logic that decides which should be selected to fill the gap. The NPC's current [[state]] and [[condition]]s usually play a large part in the decision.


<pre>
A schedule is selected if the function returns it's name - e.g. <code>return SCHED_DODGE_ENEMY_FIRE;</code>.
if(HasCondition(COND_NEEDS_MEDICAL_ASSISTANCE))
    return SCHED_MEDICALPOINT_GO;  
</pre>


The following switch statement has been taken from <code>SelectSchedule()</code> in <code>CAI_BaseNPC</code>, it shows how the different variations of schedule selection is chosen depending on the NPC's state.  
{{tip|If you expect to write a lot of schedule selection logic, you may find it useful to split it into sub-functions of your own making. <code>CAI_BaseNPC</code> for instance has <code>SelectIdleSchedule()</code>, <code>SelectCombatSchedule()</code>, etc. that are called depending on the NPC's state.}}


<pre>
=== Useful functions ===
switch( m_NPCState )
{
      case NPC_STATE_NONE:
            DevWarning( 2, "NPC_STATE IS NONE!\n" );
            break;
     
      case NPC_STATE_PRONE:
            return SCHED_IDLE_STAND;
      case NPC_STATE_IDLE:
            AssertMsgOnce( GetEnemy() == NULL, "NPC has enemy but is not in combat state?" );
            return SelectIdleSchedule();


      case NPC_STATE_ALERT:  
;<code>HasCondition([[int]] condition)</code>
            AssertMsgOnce( GetEnemy() == NULL, "NPC has enemy but is not in combat state?" );  
:True if the specified [[condition]] has been set for the current think. Of course, you'd use your enumerated names instead of passing an integer directly!
            return SelectAlertSchedule();
;<code>GetState()</code>
:Returns the NPC's state. You can also access m_NPCState directly, but GetState() is read-only and therefore safer.
;<code>return BaseClass<nowiki>::</nowiki>SelectSchedule()</code>
:There are very few situations where you won't want to call the base class' function. Do so at the ''end'' of your function, since a return obviously takes precedence over any code that comes after it.


      case NPC_STATE_COMBAT:
== TranslateSchedule() ==
            return SelectCombatSchedule();
     
      case NPC_STATE_DEAD:
            return SelectDeadSchedule();


      case NPC_STATE_SCRIPT:
<code>TranslateSchedule()</code> is called immediately after <code>SelectSchedule()</code>. It is designed to allow child classes to replace their parent's or parents' schedules with those of their own without having to duplicate selection logic.
            return SelectScriptSchedule();


      default:
int CNPC_Custom::TranslateSchedule( int scheduleType )
            DevWarning( 2, "Invalid State for SelectSchedule!\n" );
{
            break;
switch( scheduleType )
}
{
</pre>
case SCHED_IDLE_WALK:
 
{
==TranslateSchedule==
return SCHED_CUSTOM_IDLE_WALK;
 
break;
This function is called after the schedule selection. It can be used to translate parent NPC schedules to child specific schedules. It's useful if you want to change the default schedules to run your own ones instead.
}
 
}
The following code demonstrates how to translate schedules into child specific schedules.
 
return BaseClass::TranslateSchedule( scheduleType );
<pre>
}
int CNPC_Custom::TranslateSchedule( int scheduleType )
{
switch( scheduleType )
{
case SCHED_IDLE_WALK:
{
return SCHED_CUSTOM_IDLE_WALK;
break;
}
}
 
return BaseClass::TranslateSchedule( scheduleType );
}
</pre>
{{navbar-last|Creating an interaction|Creating an NPC}}
{{navbar-last|Creating an interaction|Creating an NPC}}
[[Category:AI Programming]]

Revision as of 09:51, 2 June 2008

A schedule is a list of tasks for an NPC to perform. A new schedule is chosen only when there is no active one; this might be because the NPC has only just spawned, because it has completed a schedule since it last thought, or because the schedule it was running previously encountered an interrupt condition.

Creating a new schedule

Enumeration

Schedules are defined in the NPC's AI_BEGIN_CUSTOM_NPC macro block. Before you start to do that however, you will need to add a new item to the class' enum:

enum
{
     SCHED_DODGE_ENEMY_FIRE = LAST_SHARED_SCHEDULE, 
};
Todo: Elsewhere BaseClass::NEXT_SCHEDULE is used, not LAST_SHARED_SCHEDULE. What's the difference?

Definition

Once the schedule is enumerated, we can start defining it. All schedules use the same structure:

AI_BEGIN_CUSTOM_NPC( npc_custom_class, CNPC_Custom )
	DEFINE_SCHEDULE
	(
		SCHED_DODGE_ENEMY_FIRE,

		"	Tasks"
		"		TASK_FIND_DODGE_DIRECTION	3"
		"		TASK_JUMP	 		0"
		""
		"	Interrupts"
	        "               COND_LIGHT_DAMAGE"
	);
AI_END_CUSTOM_NPC()
Warning.pngWarning:The first character of each string (except for the 'gap') must be a space or a tab. Your schedule will otherwise be invalid!

As you can see, there are two parts to the definition:

  1. Tasks is simply the list of sequential tasks an NPC must perform in order to complete the schedule.
    • Every task requires a numeric value to be passed with it! If the task doesn't actually make use of one, just pass 0.
  2. Interrupts is a list of conditions that will cause the schedule to be abandoned, and a new one chosen, if any are ever detected.

In the example schedule given above, the NPC will attempt to find a suitable direction to dodge in (checking in a maximum of three directions, judging by the parameter) before using the output of that task, stored somewhere in the class, to perform the movement itself. But if it encounters COND_LIGHT_DAMAGE (any damage greater than zero) during the process, it will abandon the schedule and select another.

The behavioural code that decides what the NPC actually does is defined in the component tasks.

SelectSchedule()

SelectSchedule() is called whenever an NPC finds itself without a schedule, and contains the logic that decides which should be selected to fill the gap. The NPC's current state and conditions usually play a large part in the decision.

A schedule is selected if the function returns it's name - e.g. return SCHED_DODGE_ENEMY_FIRE;.

Tip.pngTip:If you expect to write a lot of schedule selection logic, you may find it useful to split it into sub-functions of your own making. CAI_BaseNPC for instance has SelectIdleSchedule(), SelectCombatSchedule(), etc. that are called depending on the NPC's state.

Useful functions

HasCondition(int condition)
True if the specified condition has been set for the current think. Of course, you'd use your enumerated names instead of passing an integer directly!
GetState()
Returns the NPC's state. You can also access m_NPCState directly, but GetState() is read-only and therefore safer.
return BaseClass::SelectSchedule()
There are very few situations where you won't want to call the base class' function. Do so at the end of your function, since a return obviously takes precedence over any code that comes after it.

TranslateSchedule()

TranslateSchedule() is called immediately after SelectSchedule(). It is designed to allow child classes to replace their parent's or parents' schedules with those of their own without having to duplicate selection logic.

int CNPC_Custom::TranslateSchedule( int scheduleType )
{
	switch( scheduleType )
	{
		case SCHED_IDLE_WALK:
		{
			return SCHED_CUSTOM_IDLE_WALK;
			break;
		}
	}

	return BaseClass::TranslateSchedule( scheduleType );
}
Creating an interaction Return to Creating an NPC