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
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() )
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'sCombatStats
, and entity'sHitboxComponent
- sets up event listeners for collisions
- start a
TimerTask
which loops through thecolliders
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 aCombatStatsComponent
(i.e. is attackable), and does not have theenemy
orboss
classification, as defined by theEntityClassificationComponent
(entities without theEntityClassificationComponent
will be added to the array, provided they have aCombatStatsComponent
) - on collision end, removes the colliding entity from the
colliders
array if it was there
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)