Enemy AI Tasks - UQcsse3200/2026-studio-1 GitHub Wiki

Enemy AI Tasks

Overview

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.

The framework (ai/tasks/)

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

The tasks (components/tasks/)

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

How priority picks an attack

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.

Which enemy has which tasks

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.

How a task reaches the rest of the enemy

A task never calls an attack component directly. It triggers an event on its owner's entity:

  • MeleeAttackTask.update() triggers meleeAttack with the target.
  • RangedAttackTask.update() triggers its event name (default rangedAttack) with the target and a ProjectileType. start() triggers the same name with Start on the end.
  • ChargeTask.start() calls ChargeComponent.startCharge with the target's position.
  • WanderTask and ChaseTask trigger wanderStart and chaseStart, which the animation controllers use.

Movement tasks set a target on PhysicsMovementComponent, which does the walking.

Adding a new task

  1. Extend DefaultTask and implement PriorityTask.
  2. Return a negative number from getPriority() whenever the task should not run.
  3. Pick a priority relative to the existing ones: wander 1, chase 10, attacks 12 to 15.
  4. Add it in the enemy's factory method with aiComponent.addTask(...).

UML

Class diagram of the task hierarchy: AITaskComponent, the Task and PriorityTask interfaces, DefaultTask, and each task.

AI task class diagram

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

Task and component class diagram


Enemy AI Tasks: Testing Plan

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.

AITaskComponentTest, DefaultTaskTest

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
  • DefaultTask tracks its status and its owner

ChaseTaskTest, WanderTaskTest, MovementTaskTest, WaitTaskTest

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

PlatformWanderTaskTest, BoundedPlatformWanderTaskTest

Tests were used to ensure the following situations were working:

  • An enemy stays on its platform instead of walking off a ledge
  • The wanderStart event is still sent, as it is for WanderTask
  • A bounded wander stays inside its radius of the starting point

MeleeAttackTaskTest, ChargeTaskTest, RetaliateTaskTest

Tests were used to ensure the following situations were working:

  • Each task reports its priority only when it applies
  • MeleeAttackTask triggers meleeAttack with the target
  • ChargeTask starts a charge when the target is in the aggro radius and the charge is ready
  • RetaliateTask only fights back once the NPC has been provoked

RangedAttackEventContractTest

Tests were used to ensure the following situations were working:

  • RangedAttackTask sends both the target and the projectile type with its event

Manual testing

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.