Melee Enemy AI and Behaviour - UQdeco2800/2022-studio-1 GitHub Wiki

Overview

AI Overview

Enemy AI is handled by the AITaskComponent, which is attached to the Enemy entity on creation. The AITaskComponent holds a list of priority tasks (defined on creation), each of which implements the 'PriorityTask' interface. On every update() cycle, the AITaskComponent calculates which task has the highest priority using each task's getPriority() function, then switches to that task if it is not already running (i.e. calls stop() on the current task, then start() on the desired task, then update() on that task until it is no longer the highest priority task.

Behaviour

Melee enemies target the crystal, and will only attack the crystal at night. During the day (if any remain) they do not act aggressively, and 'wander' around. They also continually attack all non-enemy entities they collide with, while colliding.

Pathfinding

For melee enemies, pathfinding is collision-based, and is implemented in the MeleeAvoidObstacleTask. In essence, when the owner entity collides with an object and stops moving, it turns 90° away from the center of the object and continues moving until the collision ends. If it collides with another object, it should rotate 180° and try to go around the other side of the object. More in-depth explanation can be found below.

UML Class Diagram of Melee Enemy AI Structure

image

AI Tasks

WanderTask

WanderTask was provided with the base game engine. In melee enemies, it has a priority of 1 at all times, making it the highest priority task during the day (using DayNightCycleService) and the lowest priority task at night.

On creation: pass in the default priority (should be 1) and wander range. getPriority() returns 1

MeleePursueTask

MeleePursueTask causes its owner entity to move in the direction of its target, regardless of distance or line of sight. It has a priority of 0 during the day, and 2 during the night (i.e. when DayNightCycleService time is either DUSK or NIGHT)

On creation: pass in the target to be pursued getPriority() returns 0 during the day, 2 at night

MeleeAvoidObstacleTask

MeleeAvoidObstacleTask is the pathfinding 'algorithm' used for melee enemies. It works using collision listening.

On creation: pass in the target to be pursued getPriority() returns -1 during the day, or when there is no non-attackable obstacle preventing movement, 3 otherwise

How it works

When a collision starts, if the entity causing the collision is not destroyable, it is added to the task's collisionEntities array. When collision ends, the entity is removed from the array.

getPriority() works differently depending on whether the task is active or inactive. If the task is inactive, getPriority() will return 3 iff collisionEntities is not empty, and the owner entity is moving. If the task is active, getPriority() will return -1 iff collisionEntities is empty and adjusting == false.

start() sets a new movementTask orthogonal to the way to the target, turning 90 degrees away from the center of the last entity in the collisionEntities array (note that this logic assumes the last entity to collide is the one causing the obstruction), and starts the movementTask.

update() performs several checks, and adjusts behaviour accordingly:

  • if the entity is not moving, rotate 180 degrees and begin moving in that direction
  • if collisionEntities is empty, set adjusting = true and rotate opposite to the direction previously assigned (e.g. left if you have been turning right), then begin movement in that direction
  • if adjusting = true, adjust timer is decremented by 1, and entity continues moving in the 'adjust' direction
  • if the movementTask is not active, start the movementTask

The flowchart for this behaviour is shown below (although please note that collision listen happens asynchronously to the update() method, so collision checks happen once, rather than every tick, and are handled in onCollisionStart() and onCollisionEnd() )

image

Continuous Attack and the ContinuousAttackComponent

Melee enemies will continually attack all attackable, non-enemy and non-boss entities they collide with, while collision persists. This is achieved using the ContinuousAttackComponent rather than a task attached to the AIComponent to keep logic simple and allow pathfinding to continue while attacking. ContinuousAttackComponent works using the following logic:

On creation:

  • registers the Entity, entity's CombatStats, and entity's HitboxComponent
  • sets up event listeners for collisions
  • start a TimerTask which loops through the colliders array, attacking everything in it every 3000ms

During gameplay:

  • on collision start, adds colliding entities to the colliders array if and only if the colliding entity has a CombatStatsComponent (i.e. is attackable), and does not have the enemy or boss classification, as defined by the EntityClassificationComponent (entities without the EntityClassificationComponent will be added to the array, provided they have a CombatStatsComponent )
  • on collision end, removes the colliding entity from the colliders array if it was there

image

Notes, Bugs, Alternative Methods

Why not a maze-solving algorithm?

The design team came to the conclusion that having entities 'swarm' the castle in a relatively unintelligent manner would add more to the ambience of the game, as opposed to having all melee enemies converge on limited paths, which would have caused bottlenecking and potentially been a source of frustration for the player (in this version, players can place walls and have them be meaningful obstacles to the crabs, as opposed to having the crabs go entirely around them)

Don't they get stuck in 'buckets'?

Yes. Originally, they turned 90 degrees on a second collision, but this caused collision to end and normal pursue to re-begin (getting them stuck in a loop), so the 180 degree turnaround has the intention of continuing collision and therefore not deactivating pathfinding too early.

Anything else to know?

Initially, melee enemies were given an additional task that superseded all others (priority = 4 when active) that continually attacked attackable entities blocking movement (once every 10 ticks or so), as opposed to just attacking once on collision. However, this task was discarded for sprint 2, as it became clear there wouldn't be enough time in the sprint to both get the pathfinding working and fix the game-breaking bugs encountered in this task. Hit Michelle (team 4) up if you want access to that task, which has been deleted from the current game files to avoid code clutter and prevent game-breaking bugs. This was fixed and implemented in sprint 3 (see above)