AI Task

From The DarkMod Wiki
Jump to navigationJump to search

The purpose of a Task is to represent an atomic action which can be performed by a particular AI Subsystem. Once a Task is plugged into one of the AI's Subsystems, it gets initialised once and performed multiple times, until it returns.

About Tasks

A Task is at the end of the command chain: The Mind delegates thinking to its State, State delegates work into the Subsystems by plugging in Tasks. Therefore a Task is supposed to work independently of other Tasks that might run in the other Subsystems (for instance: the ChaseEnemyTask (which is plugged into the movement Subsystem during Combat) only handles the AI movement, and nothing else. It does not change the AI state (in fact it is not allowed to change it) and the only way to communicate is to use data in the Memory and/or the owning idAI class.

The Task::Perform() is invoked every other frame and is supposed to return a boolean value to indicate whether it is finished (TRUE) or not (FALSE). Note that Tasks are not guaranteed to be executed each frame, as exactly one Subsystem is allowed to perform its Task each frame (the calls are interleaved to prevent a possible performance impact when many Tasks are active). Once a Task is returning TRUE, it is finished and the optional Task::OnFinish() event is triggered to give the Task the opportunity to do some cleanup. After that, the Task is removed from the Subsystem's TaskQueue and the next Task is activated.

Note: The OnFinish() method is also called when the Task is interrupted in its course (e.g. if the State is switched and the Subsystem's TaskQueue gets cleared). This ensures that the Task always gets the opportunity to perform its cleanup routine.


Things to remember:

  • Tasks are atomic actions and are highly specialised and act independently of other Subsystems.
  • Tasks may not switch States.
  • Tasks are allowed to use the Memory and the idAI methods of its owning AI class.
  • Tasks are initialised once (Task::Init).
  • Tasks are performed often (Task::Perform), return TRUE if the Task is finished, FALSE otherwise.
  • Tasks can optionally implement an Task::OnFinish() routine to do some cleanup. This is invoked under the assumption that Task::Init() has been performed before. An uninitialised task's OnFinish() method won't be called.
  • Tasks are not guaranteed to be executed each frame, as exactly one Subsystem is allowed to perform its Task each frame.


Implementation Details

The best thing is to learn from existing Tasks, there are quite a few already in the codebase (look at DarkMod/AI/Tasks/*). At any rate, it might be useful to explicitly mention a few things to avoid headaches:

Task Names

Each Task must have a unique name, usually defined as constant in the Task's header. This name is returned by the Task::GetName() method. The constant should be in the format TASK_* like this:

#define TASK_IDLE_ANIMATION "IdleAnimation"

Task Instantiation

Each Task must provide a static TaskPtr Task::CreateInstance() method taking no arguments and returning the shared_ptr of the newly created instance. This is necessary to allow the Library to instantiate any named Task upon request.

The default constructor is ideally non-public (i.e. private or protected), as instantiation is performed through Task::CreateInstance(). However, a Task may provide public non-default constructors for convenience which take a few startup arguments, but there always must be a default constructor to allow construction without arguments. This is important for Saving and Loading too.

Self-Registering Tasks

Each Tasks is expected to register itself in the TaskLibrary at construction time. To do this, it's enough to define a global helper object in the source file (.cpp file, not the header!) which registers the given Task in its constructor (see IdleAnimationTask.cpp):

// Register this task with the TaskLibrary
TaskLibrary::Registrar idleAnimationTaskRegistrar(
	TASK_IDLE_ANIMATION, // Task Name
	TaskLibrary::CreateInstanceFunc(&IdleAnimationTask::CreateInstance) // Instance creation callback
);

I'll spare the technical details, but overall this Registrar takes the name of the Task (which should always be defined as a string constant in the header) and the Callback in form of a boost::function object (CreateInstanceFunc). The boost::function object is pointing at the static CreateInstance member and is associated with the Tasks's name in the Library, so that the Library can instantiate any Task upon request.

Saving and Loading

A Task must be able to restore its state completely from an idSaveGame. The general rules for Saving and Restoring apply as usual. When the Subsystems are saved, the names of all the Tasks in the queue are stored into the savegame. These names are read back in on restore and passed to the TaskLibrary as argument. Hence the TaskLibrary must always be able to restore a named Task upon request.