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;
}