home >> a Bot AI framework >> Execution - Tasks
last update: oct 2006; last minor update:april 2007

Execution - Tasks

AI Objects: Tasks

A "BoundProcess" is by definition thereafter a process that does not go on forever: it has completion/cancellation conditions implemented as member functions which are checked after every execution in its computation loop.

A Task is by definition a process:
a- which has no own computation loop: its computation loop is implicitely the game computation loop ( frame), ie the task execution is only performed once per frame;
b- which requires at least one execution ie one frame to be completed;
c- if it has an end ( ie a goal) by design ( outside of possible abnormal terminations causes or timers), it is a BoundProcess and has consequently a selfCompletion() member function checking for its successfull completion; the Task is obviously designed/coded to strive for selfCompletion() to return OK after the minimum number of frames.
NB: In most cases a Task is designed to act upon the (game) world like a human player would, but not always.

Tasks as control (closed-loop) systems

Task and Task parent interface
When a decision has been made, it often can be implemented right away by setting Actual=Decided ( eg when selecting a target for shooting, which does not mean that the crosshair already points to the target). But when the decision pertains to wished values of some states of the world, implementation is not instantaneous in most cases: the world usually requires to be acted upon consistently over several frames to change states, and the human player has no zero reacion time( eg moving the cross hair on the decided target).
In case of non-instantaneous action/result, the DecisionNode must have a FBPerceptionSystem and a comparator, ( named CheckGoalReached thereafter).
The FBPerceptionSystem ( FB stands for FeedBack) continuously gets the relevant actual values from the world, ie those needed to be monitored to check if the decision is actually implemented, that is to say Actual becoming equal to Decided. The FBPerceptionSystem has nothing to do with the SelPerceptionSystem of a DecisionNode, which is devoted to getting the context of the decision.
The DecisionNode comparator CheckGoalReached is input by the Decided value and the Actual value from the FBPerceptionSystem, and tells if the Actual value is acceptable: acceptable can mean strict equality or being in a given range/radius,...
If not, the DecisionNode must create a Task designed to strive to eliminate the discrepancy, possibly with some constraint imposed by the DecisionNode.
Hence the Task must be input by the DecidedValue as Command, and may be some constraints. It must also have a FBPerceptionSystem for feed-back, and a comparator. Endly, a Task specific Think process using its comparator ouput(s) derives proper values for the low level bot controls ( mouse and buttons or equivalent).
Checking task completion, Task comparator
A parent DecisionNode CheckGoalReached comparator may be be different from its current child Task comparator, at least because CheckGoalReached returns only OK or KO, whereas the task comparator might provide on top some data needed by the Task "think" process. In any case, the comparator of the Task must never say OK when its parent's comparator would say KO. Actually, the task comparator is a unique process( named selfCompletion thereafter); there is not much leeway on how to code it as it is closely related to how the task works. It is when coding DecisionNodes which could be parent of the Task that the herabove parent-Task consistency requirement must be taken care of.

Example: a waypoint-nav task is designed to reach a point, and cannot do anything else on its own than stop the bot when the goal is reached. Hence, to tell if it has been completed or not, the Task comparator selfCompletion() just check if the point is reached and the velocity is ( close to) zero.
A parent-process requiring to pick-up some item will use the waypoint-nav task if far away, but its CheckGoalReached comparator says OK if the item is picked-up and KO if not; ie reaching the point where the item lies is a mean not an end from the parent standpoint.
Moreover, there is no point for the parent to keep requiring the waypoint-nav task until the task completion ( reaching the point) after its own goal (pick-up the item) has been reached.
Other examples would be: navigate to a point until the bot meets another player, or until a line of sight exists to that other point,..)

This leads to a very flexible parent and Task implementation, where
- the Task stand-alone comparator is identified as a selfCompletion() check, which is a fixed process;
- any parent has its comparator implemented as a function-object ( CheckGoalReached), a pointer to which is handed out to the Task when the parent creates the Task;
- a Task always checks for completion by calling its pointed-to parent CheckGoalReached comparator first, ie before its selfCompletion() check.
The consistency requirement from above then insures that the selfCompletion check should actually never be successful, unless the parent uses it for its own check.

Tasks implementation specifications

-A checkStop() member function encapsulate checks for successful completion, as well as checks for other interruption causes; it return a StopStatus value.
the Task checkstop() member function is called at every botframe start on Tasks that have been executed the previous frame. Doing it that late allows for checkstop() to take into account all world changes occurring between frames.
The pointed-to parent Comparator is executed: if the output says OK, the task calls its parent onTaskCompletion( Task *) member function, indicating successfull completion. If it says KO, the task checks if it can go on; if not, it also calls back its parent onTaskCompletion( Task *) member function with an indication of the failure, and if yes, do nothing. The parent onTaskCompletion( Task *) execution results in internal state changes and the parent is activated.
checkStop() is also the place for some Tasks to perform all internal null-time sub-processes, mainly logic( waypoint switching,...).

-A detCmd() member function is the first part of the Task execution process, where by design the minimum computations required as inputs to the conflict-solving process are performed ( to avoid if possible useless CPU usage for Tasks that will be rejected later).

-An applyCmd() member function is the second part of the execution process, performed only by Tasks non-rejected by the conflict solving process; the Tasks orders computations are completed if necessary, and the orders are transfered to the bot.

-The Task's Parent-Process onTaskStop() member function allows for the Parent process to react to the different Task StopStatus (except obviously GoOn).

-The Task perform checks for stop on failures only on what prevents it to meet its own success condition; ie it checks that it has (still) the capacity to do its job, in the conditions specified by its parent: it never checks if it becomes "silly" to do the job; this does not relate to the task process, but to AI design considerations that must be taken into account at its parent level by the designer: it is the parent job to insure that requiring the task (still) makes sense from a human perspective.
This insures modularity in the AI code.

-CS_CheckGoalReachedBase_ct is a pure virtual function-object base class. It is introduced to increase flexibility in Tasks successful completion checks; derived classes are instanciated and hard-coded in the Tasks' Parent processes.
the Task's pCS_Parent member pointer points to its Parent's CS_CheckGoalReachedBase_ct-derived class instanciation; the pointer itself and some pointed function-object data are handed-out to the Task by its Parent-process when the Parent requires the Task.

-The isRequiredBy_s() Task static member function is called by a Parent process whenever it needs the Task with various Parent arguments; a call to isRequiredBy_s() in particular copies/hands-out-pointers-of Parent data to static Task class members data which are relevant to the Task ( possibly including a pointer to the Parent's specific CS_CheckGoalReached); if the requirement is OK, ie the Task class is authorized for creation, a Task is created as a child of the Parent-process; the use of static data and function are meant to avoid polluting a possibly already existing child before the new requirement is authorized.
The task then copies right away the static data into its own and same type member data; this allows to the Task to run all its member functions on its own once placed in the AI_global Task-to-be-executed batch ( after conflict solving and whenever not rejected). NB: isRequiredBy_s() does not perform Task execution initialization; it just tranfers parent's parameters the Task needs to memorize and add the Task as a child to the Parent.


//generic example of code for a parent process needing TaskZ_ct:

TaskZ_ct::inputs_s= Parent. inputs;
TaskBase_ct * pTaskZ_a= TaskZ_ct::isRequiredBy_s( Parent, Parent. Priority,....)
pTaskZ_a-> inputCmds();//copy TaskZ_ct::inputs_s to pTaskZ_a-> inputs



skeleton code for Task classes
//+++++++++++++++++++++++++++++++++++
class TaskBase_ct
{
public:
int bSuccess;//member, so it is available to pParent
int StopStatus;//member, so it is available to pParent
Process_ct * pParent;
virtual int checkSuccess() { return 0; }
virtual int checkStop() { return StopStatus= K_GoOn; }
virtual void detCmd() { }
virtual void applyCmd() { }
};
//++++++++++++++++++++++++++++++++++++++++++++++++
class TaskA_ct: public TaskBase_ct
{
static Input_t Inputs_s;
Input_t Inputs;
void inputCmds()
{
Inputs= Inputs_s;
}
int checkSuccess()
{
return selfCompletion();
}
int checkStop();
void detCmd();
void applyCmd();
static int isRequiredBy_s( Process_ct * pParent_i);//called by the parent requiring this task
int selfCompletion();
...
};
//+++++++++++++++++++++++++++++++++++++++++++
class TaskB_ct: public TaskBase_ct //a class allowing an external success function-object pCS_Parent
{
CS_CheckGoalReachedBase_ct * pCS_Parent;
int checkSuccess()
{
return (* pCS_Parent)();
}
int checkStop();
void detCmd();
void applyCmd();
static int isRequiredBy_s( Process_ct * pParent_i, CS_CheckGoalReachedBase_ct * pCS_Parent_i);
int selfCompletion();
...
};
//
skeleton code for a checkstop() member function
//-------------------------------------------
int TaskX_ct:: checkStop()
{
// called after each exec() call, at the beginning of the
// next computation cycle ( frame).

//preset values
bSuccess= FALSE;
StopStatus= GoOn;

if( checkSuccess())
{
bSuccess= TRUE;
StopStatus= K_Success;
pParent-> onTaskStop( *this);//delete the task, makes it sleep,...
return K_Success;
}


//check for self-completion or cancellation
if( cancellationCause#0())
{
StopStatus= K_cancellationCause#0;
pParent-> onTaskStop( *this);
}
else if( cancellationCause#1())
{
StopStatus= K_cancellationCause#1;
pParent-> onTaskStop( *this);
}
...
#if TaskB type //ie checkSuccess()!= selfCompletion()
else if( selfCompletion())
{
StopStatus= K_Completed;
pParent-> onTaskStop( *this);
}
#endif
else //Status== K_GoOn;
{
...possible null-time computations ( eg logic, like switching to next waypoint)
to prepare next execution.
/*Could be implemented at exec() start, but logic computations outputs are
in some cases required before ( eg: conflict-solving) */
}
return StopStatus;
}