a Bot AI framework
last update: oct 2006; last minor update: dec 2006
Priority
Each DecisionNode and Task has a Priority value as member data.
At each step of the AI engine, a DecisionNode/Task either sets its own priority to its parent's value, or sets it to
a higher value.
Priority values attached to DecisionNodes and how they are propagated down are set by the bot-ai designer. Priorities may vary between frames, as priority covers two different concepts: importance ( like fullfilling the mission) and urgency ( shooting back as somebody fires at me right now); something important becomes more and more urgent when time goes by( in time-limited games of course).
Bot low-level controls
Depending on how the game works, the player, hence the bot has several controls ( buttons and mouse for the player) that are interpreted as basic actions by the game-engine ( move eyes, reload weapon
, change weapon, walk/run, jump,...). Moreover, even triggered by different inputs/buttons, some actions are incompatible by game design( eg: reloading vs defusing the bomb in Counter-strike).
For conflict resolution purpose, Bot controls which cannot be used simultaneously must be regrouped. Again, incompatibility must not be judged in terms of making sense or not: this is the job of the thinking part of the AI; incomptability here is purely "mechanical" as restricted to what the game engine allows or not.
Conflict solving
The Task-To-be-Executed PriorityList contains all Tasks requested by all Decision Paths at bot-level.
Having more than one decision tree, running the AI-engine may result in having conflicting Tasks in this list, or the same Task class with different instanciations ( ie different Commands, Constraints,...).
As the list is sorted by priority, the ConflictSolver loops on each Task starting from the top.
For each task, the ConflictSolver...
1. calls the detCmd() Task member function, which performs the minimum computations to get the data required by the conflict-solving process: which controls the Task requires, with which values, and if the control access request is a must-have or a nice-to-have.
2. identifies and solve possible conflicts arising from the Task.
- If the Task is accepted, the ConflictSolver memorizes a copy of the Controls the Task requested, the values of the control, and the flag telling if it is must-have or a nice-to-have. These will be used for lower priority Tasks conflict checking.
- If a conflict arose, the Task is rejected, detached from the Task-To-be-Executed PriorityList and its parent is called-back ( usually to delete the task).
Note: Some bot-ai implementations just push the rejected Task on a stack for later re-activation, but one cannot be sure that the decision contexts in the following frames will be compatible with the one that lead to request this Task with its particular set of variables/parameters( Commands,...).
So the Conflict-Solver accumulates granted control requests, narrowing progressively in effect the set of available controls and values for lower priority Tasks. At the end of the process, the Task-To-be-Executed PriorityList only contains Tasks that will be executed.
Tasks for which at least one must-have control access is not granted is rejected by the Conflict-solver as a whole( eg: Task_Shoot requires access to the trigger-pulling control as well as the LookAtPoint control for aiming; it does not make sense to keep a Task_Shoot if one of those two controls is not accessible by the task). This is a key point when designing new actions that require parallel sub-actions: if the parallel sub-actions must never be performed separately , they should be part of the same Task.
By definition of "nice-to-have", rejected access to "nice-to-have" controls does not cause the requesting Task rejection.
When the ConflictSolver compares Controls values as requested by tasks, compatibility is not only based on value equality, but the implementation also allows for the use of ranges.
I implemented it after considering realistic moves and it is better explained with the following practical example.
Except maybe for short range moves, a human player looks in the direction where he/she is going. Hence a bot should do the same, and this implemented that way:
the Navigation_Task requests access to the Control setting a point to look at (LookAtPoint) with a value being its destination, with a nice-to-have flag; it also requests for another Control setting a range ( LookAtRange), with the range being approx twice the field of view centered on the preceding point, with a must-have flag.
Assuming a higher priority task -like shooting- has already pre-empted the LookAtPoint Control ( which is memorized in the ConflictSolver), the ConflictSolver processing the Nav_Task will most of the time reject access to its LookAtPoint control request, as the shooting target is probably not the destination point of the Nav_Task. As this is just a nice-to-have request from the Nav_Task, the only consequence is that its LookAtPoint control request of the Nav_Task is not taken into account anymore.
The ConflictSolver also checks the LookAtRange control: if the direction memorized by the ConflictSolver ( the shooting direction) is within the Nav_Task Range request, the Nav_Task is accepted; if not, it is rejected as a whole ( no move/stop move).
This works the other way round: let us assume that the Nav_Task has high priority hence its LookAtPoint control access has been granted though being only a nice-to-have; if a lower priority task request access to the LookAtPoint control with a must-have flag and a different value, and the value is within range of the Nav_Task range, the task is not rejected ,and the LookAtPoint control value is adjusted to this tasks's value.
This admittedly leads to a lot of classes, code and - I guess- CPU time, but it increases design opportunities a lot and I am presently not considering going back to a more traditional approach.