AI Framework
AI Framework
Ok, I'll try to explain in a few paragraphs, what the new AI code framework is about and how it can be used. I'll start off with a scetch of the current AI components.:
Subsystems
Each AI has four distinct Subsystems: Senses, Movement, Communication and Action. Each Subsystem can perform one single Task at a time.
The idea behind the Subsystems is that the AI should be able to perform several Tasks at the same time. Previously, many things in the script were handled via eachFrame loops, each of which had to “remember” calling the standard routines, like SensoryScan, which was tedious. The Subsystem approach is there to provide four Task slots which correspond to a logical part of the AI. Hopefully, this should allow for a more “high-level” programming of the AI’s behaviour.
For example, an attacking AI has several Tasks to perform: chase the enemy, watch out for his position and attack him when he's near enough. Translated into the Subsystem framework this would be: Push a ChaseEnemyTask into the Movement Subsystem, a CombatSensoryTask into the Senses Subsystem and the CombatTask into the Action subsystem.
The Subsystem::PerformTask() must be called by the Mind class only (see Mind). Subsystems can be enabled or disabled. Disabled subsystems return FALSE when calling PerformTask(), which is important feedback for the calling Mind class (Disabled Subsystems are skipped when iterating over them each frame).
To allow for serial task processing, each Subsystem has its own TaskQueue which gets exectuted, one Task after the other.
Public Subsystem Interface
The most recent documentation of the public Subsystem interface can always be found in the Subsystem.h header file. A short pseudocode summary is shown here:
class Subsystem { // Performs the currently active Task (returns TRUE if the subsystem is active and the task was performed) bool PerformTask(); // Adds a Task and makes it the active one. The previously active task is pushed back in the queue. void PushTask(TaskPtr); // Finishes the currently active task. Next time PerformTask() is called, a new Task is picked from the queue. void FinishTask(); // Replaces the foremost Task with the given one. void SwitchTask(TaskPtr); // Adds the given Task to the end of the queue. void QueueTask(TaskPtr); // Removes all Tasks and disables this subsystem. void ClearTasks(); // As the name states void Enable(); void Disable(); bool IsEnabled(); };
Mind
Each AI has a Mind, whose purpose is to control the Subsystems. The Mind is always in a certain State (e.g. Idle, Combat, Searching), which have a distinct priority assigned to them.
The Mind::Think() method is called from idAI::Think(), hence it happens each frame. When this happens the Mind chooses one of the four AI Subsystems and calls Subsystem::PerformTask(). Note that only one of the four subsystems is called each frame, so the calls are interleaved over the frames. The Mind increases its subsystem iterator each frame, disabled Subsystems are skipped. If all four Subsystems are filled with Tasks, each Subsystem gets called every fourth frame - if only one Subsystem is enabled, it gets called each frame.
The Mind also handles ongoing hiding spot searches and provides methods to perform sensory scan routines. Many of these were previously implemented in ai_darkmod_base::subframeTask_* script functions, the calls to which were scattered all over the scripts, that's why I moved them into the Mind class.
The abstract definition of a Mind is defined in AI/Mind.h, the standard implementation is the BasicMind class. In principle it's possible to swap an AI's Mind with a different implementation (perhaps a DrunkenMind or something like that).
The Mind holds its very own StateQueue, similar to the Subsystem's TaskQueue. States can be pushed/switched/ended as well as conditionally pushed/switched, depending on their priority (see interface below). When switching to a new States, the Mind class calls State::Init() once. This gives the State the opportunity to plug the right Tasks into the Subsystems and do some other one-time setup processing, whatever this might be.
The interface is extensively documented in the Mind.h header file, a short summary is shown here:
class Mind { // Gets called each frame by idAI::Think. void Think(); // Pushes the current state, the previously active one gets postponed. void PushState(StatePtr); // Switches to the given state, the previously active one gets cleared. void SwitchState(StatePtr); // Pushes the current state, but only if the priority is higher than the one of the currently active State // Returns TRUE if the State was accepted. bool PushStateIfHigherPriority(StatePtr, int priority);
// Analogous to the above, but this switches the state if the priority is higher bool SwitchStateIfHigherPriority(StatePtr, int priority); // Adds the State to the end of the queue void QueueState(StatePtr); // Ends the current state (gets destroyed next round) and a new one is picked from the queu void EndState();
// Clears all pending States and falls back to the default state. void ClearStates(); // Returns the currently active State StatePtr GetState(); // Returns the reference to the AI's memory (is always owned by this Mind class) Memory& GetMemory(); };
Note: It's possible to call SwitchState/PushState/EndState/etc. during one State's initialisation. It is ensured that the current State object is not being destroyed immediately (to avoid destroying objects in the middle of code execution), this happens at the beginning of the next Mind::Think() call. The same holds for the Subsystem Push/Switch routines - the current Task doesn't need to fear self-destruction when calling these methods.
Legacy documents:
AI Priority Queue
Please refer to this article for documentation about the priority queue scripts: AI Priority Queue
This article is meant to be expanded over time as the AI documentation project progresses.