Repository navigation
Enemy AI Tasks
An enemy decides what to do through its AITaskComponent. The component holds a list of PriorityTasks. Every frame it asks each task for its priority and runs the one with the highest value. A task returns a negative priority when it does not apply, so it is never chosen.
When the highest-priority task changes, the old task is stopped and the new one is started. This is the whole decision system: there is no state machine, only tasks competing by priority.
| Type | Role |
|---|---|
Task |
the interface: create, start, update, stop, getStatus
|
PriorityTask |
a Task that also reports getPriority()
|
DefaultTask |
an abstract base class that tracks the status and the owner |
TaskRunner |
what a task is given so it can reach its entity |
AITaskComponent |
the component: holds the tasks, picks the highest priority each frame |
| Task | Priority | What it does |
|---|---|---|
WanderTask |
1 (always) | walks to a random nearby point, waits, repeats |
PlatformWanderTask |
1 (always) | a WanderTask that casts a ray down so the enemy does not walk off a ledge |
BoundedPlatformWanderTask |
1 (always) | a PlatformWanderTask that stays within a radius of where the enemy started |
ChaseTask |
10 when the target is in view | walks towards the target |
MeleeAttackTask |
15 when the target is in melee range | triggers meleeAttack every frame |
RangedAttackTask |
given, when the target is in range | triggers its ranged event every frame |
ChargeTask |
16 while a charge is running, or when the target is in the aggro radius and the charge is ready | starts a charge |
StandStillTask |
given | stops and faces the target (friendly NPCs) |
RetaliateTask |
given, when provoked | a StandStillTask that fights back once attacked |
MovementTask, WaitTask
|
none | helpers used inside the wander and chase tasks |
An attack task returns its priority only while the target is inside its range. It does not look at the cooldown: the attack component enforces that. So the task that runs is simply the highest priority among the tasks whose range covers the target.
The Cyclops uses this to choose between three attacks by distance:
| Distance to target | Task that runs | Why |
|---|---|---|
| up to 2.5 | stomp (priority 15) | highest priority and in range |
| 2.5 to 7 | laser (priority 13) | the stomp is out of range |
| 7 to 11 | rock (priority 12) | only the rock still reaches |
| beyond 11 | chase (priority 10) or wander (1) | no attack is in range |
A side effect is that the rock is never thrown up close and the laser is never fired at long range. That is deliberate.
| Enemy | Tasks, highest priority first |
|---|---|
| Skeleton | melee attack, chase, platform wander |
| Ranged Skeleton | ranged attack, melee attack, chase, platform wander |
| Minotaur | charge (16), melee attack (15), chase (10), platform wander (1) |
| Centaur | charge (16), ranged attack (12), chase (10), platform wander (1) |
| Cyclops | stomp (15), laser (13), rock (12), chase (10), platform wander (1) |
| Medusa | melee attack, ranged attack (gaze), bounded platform wander |
| Cerberus | melee attack |
| Zeus | melee attack, ranged attack (lightning), chase, platform wander |
| Harpy | melee attack, chase, wander |
| Ranged Harpy | ranged attack, chase, wander |
The charge has the highest priority (16) so that, once the target is inside the aggro radius and the charge is off cooldown, it wins over a melee or ranged attack. Every enemy built on the platformer base also carries that base's melee task, whether or not it has a melee attack component.
A task never calls an attack component directly. It triggers an event on its owner's entity:
-
MeleeAttackTask.update()triggersmeleeAttackwith the target. -
RangedAttackTask.update()triggers its event name (defaultrangedAttack) with the target and aProjectileType.start()triggers the same name withStarton the end. -
ChargeTask.start()callsChargeComponent.startChargewith the target's position. -
WanderTaskandChaseTasktriggerwanderStartandchaseStart, which the animation controllers use.
Movement tasks set a target on PhysicsMovementComponent, which does the walking.
- Extend
DefaultTaskand implementPriorityTask. - Return a negative number from
getPriority()whenever the task should not run. - Pick a priority relative to the existing ones: wander 1, chase 10, attacks 12 to 15.
- Add it in the enemy's factory method with
aiComponent.addTask(...).
Class diagram of the task hierarchy: AITaskComponent, the Task and PriorityTask interfaces, DefaultTask, and each task.

Class diagram of the tasks and the components and services each one depends on.

The framework tests are in ai/tasks/ and the task tests are in components/tasks/. Tasks are tested with a mocked GameTime and a real AITaskComponent, following the pattern in ChaseTaskTest.
Tests were used to ensure the following situations were working:
- The task with the highest priority is the one that runs
- A task with a negative priority is never chosen
- The old task is stopped and the new one started when the highest priority changes
-
DefaultTasktracks its status and its owner
Tests were used to ensure the following situations were working:
- An enemy chases a target that is in view and stops when it is not
- An enemy wanders to a point, waits, and repeats
- A movement task reaches its target and reports that it has finished
- A wait task finishes after its duration
Tests were used to ensure the following situations were working:
- An enemy stays on its platform instead of walking off a ledge
- The
wanderStartevent is still sent, as it is forWanderTask - A bounded wander stays inside its radius of the starting point
Tests were used to ensure the following situations were working:
- Each task reports its priority only when it applies
-
MeleeAttackTasktriggersmeleeAttackwith the target -
ChargeTaskstarts a charge when the target is in the aggro radius and the charge is ready -
RetaliateTaskonly fights back once the NPC has been provoked
Tests were used to ensure the following situations were working:
-
RangedAttackTasksends both the target and the projectile type with its event
Which attack the Cyclops picks is decided by priority and range together, so it is also checked by playing: stand at long range from a Cyclops and it throws a rock; at mid range it fires its laser; next to it, it stomps.