Sprint 4: Enemy AI and Interactions - UQdeco2800/2021-studio-6 GitHub Wiki

UML

Enemies UML

Note: Right click and select open image in new tab to see the full quality image.

This is the UML class diagram that shows the connection between the enemy NPC related classes. For simplicity, only the classes immediately connected to the enemy NPC classes are shown in this diagram.

Enemy Reactions

Movement direction

If the enemy is moving to a target location then a calculation is made to determine which of the four basic directions is closest to that direction. The four basic directions are left, right, front (down), back (up). The correct directional animation is then played to display the enemy walking in that direction. Once the enemy stops walking then the stationary animation in the same direction is played until a new target location is selected.

Justification

Having each enemy face the direction they are walking/looking will create a more immersive world for the user. The immersion is increased due to the more natural and realistic movement.

Implementation

This was implemented using the updateAnimationDirection method within the NPCAnimationController class that was created for determining the friendly NPC animation direction in the previous sprint. The main change this sprint was to adjust all the enemy TextureAtlas files to contain the necessary to animations for the enttiy to be stationary and walking.

Enemy in darkness

The enemy AI and cosmetics changes slightly when the enemy is in darkness (determined by the DetectDarknessComponent).

AI changes

The following changes are made to the enemy AI:

  • If an enemy is in darkness then they now move at 3 times their normal speed.
  • If an enemy is in darkness then they now shoot at 2 times their usual rate.

Cosmetic changes

A part of the enemy will glow whenever they are in darkness (or low light conditions). These glowing components will be the only thing noticeably visible within the darkness.

Large enemy eyeglow Long range enemy eyeglow Tough long ranged enemy glow

Implementation

A new class was created for this feature called EnemyDarknessController. This component will be added to each enemy entity. When the enemy is created, it will listen to the "InShadow" and "InLight" events, which trigger depending on whether this entity has entered or exited all light sources.

entity.getEvents().addListener("inShadow", this::entityInDarkness);
entity.getEvents().addListener("inLight", this::entityInLight);

When one of these events are triggered, the appropriate component for the entity is then updated with the values for in the light/dark.

movementComponent.setSpeed(inDarkSpeed);
fireBulletTask.setFireDuration(inDarkFiringDuration);
glowingEyesComponent.displayOn();

Justification

The AI changes will reinforce the stories premise that the Shadow Crawlers thrive in the darkness. Having a consistent story increases immersion for the user, as everything is realistic within the world that is created.

The increased AI difficulty when they are in darkness also creates a new game-play mechanic, where the user needs to try to stay in the light or face more dire consequences. This hopefully will make the user fear the darkness.

The glowing parts (especially the eyes) in the darkness creates consistency with the prologue, where only the Shadow Crawlers eyes are seen. The glowing eyes will also hopefully create a sense of unease within the user that there are things watching them from the darkness.

Both fear and uneasiness are emotions that we aim to create due to the dark nature of the game (horror themed) and the dire world that the game is set in (post-apocalyptic). These emotions will cause tension in the user, which will be contrasted by relief they feel to be in the safety of the safehouse.

Player in darkness

If the player is in darkness then all enemies have their chase AI detection radius increased by 2.5 times their regular amount. This should feel like the enemies swarm a player when their torch goes out.

Enemy hit

When the enemy gets hit by the player they will:

  • Highlight red for a brief moment of time
  • Play the enemy hurt sound effect for that enemy

Large enemy flashing red

Justification

A user may feel frustrated when attacking some of the higher health enemies if they are not sure if their attacks are even hitting them. Having the enemy flash red when hit and play a sound will tell the user the enemy has been hit and hopefuly alleviate this frustration.

Having the enemies change with the users actions also creates a more "juicy" game design, as the user will feel the game is more interactive.

Implementation

The NPCAnimationController class now listens on the "hit" events for the entity.

this.entity.getEvents().addListener("hit", this::npcHit);

Once a "hit" event has been triggered, it will play the "hit" sound and play the increase the animation index so that the hit animations will loop for a small duration.

npcSoundComponent.playHit();
hitActive = true;
...
if (hitActive) {
   indexOffset = 4;
}
...
updateAnimationDirection(walkTarget, WALKING + indexOffset);

Enemy injured

When the enemy's health is equal or below half of their maximum health, the enemy will look like they are injured (for example wounds, blood, etc).

Tough ranged enemy when not injured:

Tough enemy normal sprite

Tough ranged enemy when injured:

Tough enemy injured sprite

Justification

A user may feel frustrated when attacking some of the higher health enemies if they are not sure how much damage they are doing, or how much life the enemy has remaining. Having the enemy display as bloodied/injured when they have equal or less than half their remaining health, tells the user the enemy is nearly dead and hopefuly alleviate this frustration.

Having the enemies change with the users actions also creates a more "juicy" game design, as the user will feel the game is more interactive.

Implementation

Once an enemy "hit" event is triggered, the NPCAnimationController will check the entities CombatStatsComponent to see if the enemy is now under half life; if they are then switch to the damaged sprite index offset.

if (!damagedActive && combatStatsComponent.getHealth() <= combatStatsComponent.getMaxHealth()/2) {
   damagedActive = true;
}
...
if (damagedActive) {
   indexOffset = 2;
}

Enemy melee attack

When the enemy attacks a player, the enemy attack sound effect will play for that enemy. The enemy will also keep attacking the player in regular intervals if the player stays close to an enemy.

Justification

The user already receives a visual feedback from being hit by an enemy by the player entity flashing red. Adding a sound effect of the enemy attack will assist in letting the user know they have been hit (and from whom).

The addition of an enemy attack sound effect will also add to the "juicy" design of the game as it is an additional interaction/feedback depending on where the player has moved to in the game.

Having the enemy attack in regular intervals when close to the player will remove the strategy of staying within the attack range of the harder enemies so they cannot attack. This strategy was not intended in the game and is not realistic within in the world we have created (i.e. the enemy should keep attacking if the player is near).

Implementation

A new EnemyMeleeAttackComponent class was created which listens to the collisionStart and collisionEnd events.

entity.getEvents().addListener("collisionStart", this::onPlayerClose);
entity.getEvents().addListener("collisionEnd", this::onPlayerFar);

These events determine whether the player is in range, and if so the hit method is called within the PlayerCombatStatsComponent every 2 seconds. This will also play the enemy attack sound effect.

if (!timeSource.isPaused() && playerInAttackRange && timeSource.getTime() >= endTime) {
	soundComponent.playMeleeAttack();
	playerStats.hit(myStats);
	endTime = timeSource.getTime() + (int)(attackInterval * ONE_SECOND);
}

Enemy shoots

When the enemy shoots, the enemy shoot sound effect will play for that enemy.

Justification

This sound effect attempts to help the user realise what is going on in the environment around them, which is very important when the shooting enemy is currently off screen.

The addition of an enemy shoot sound effect will also add to the "juicy" design of the game as it is an additional interaction/feedback depending on where the player has moved to in the game.

Implementation

Each time the DistanceFireBulletTask fires a bullet, the shoot sound effect is played.

this.owner.getEntity().getEvents().trigger("fire");
npcSoundComponent.playShoot();

Enemy detects player

When the enemy detects a player (starts to chase them), the enemy detection sound effect will play for that enemy.

Justification

This sound effect attempts to help the user realise what is going on in the environment around them, and to understand why an enemy is now charging towards them.

The addition of an detection sound effect will also add to the "juicy" design of the game as it is an additional interaction/feedback depending on where the player has moved to in the game.

Implementation

Each time the ChaseTask starts, the detectPlayer sound effect for that entity is played.

this.owner.getEntity().getEvents().trigger("chaseStart");
npcSoundComponent.playDetectPlayer();

Spawner spawns enemy

When a small enemy is spawned by the spawner enemy, it will:

  • Display an animation that relates to the spawner spawning.
  • Play the spawn sound effect

Justification

This visual indication and sound effect attempts to help the user realise what is going on in the environment around them, and to understand why an enemy has now appeared next to the spawner.

Implementation

Each time the SpawnerEnemyTask initiates the spawn event, it will play the spawn sound for that entity.

this.owner.getEntity().getEvents().trigger("spawn");
npcSoundComponent.playSpawn();

The NPCAnimationController listens to this spawn event and displays the spawning animation for a short duration. Note that the spawning animation will not display if the spawner is injured, as the top of the egg has been torn off when injured.

if (spawning && !damagedActive) {
  if (lastAnimation == null || !lastAnimation.equals("spawn")) {
	animator.startAnimation("spawn");
	lastAnimation = "spawn";
  }
}

Enemy dies

When the enemy dies, the enemy will no longer be instantly disposed of, instead the sprite is replaced by the respective dead enemy sprite. The enemy entity will then have various components disabled as not to attack or interfere with the player in any way. An enemy dying sound effect will also be played.

Early version of a generic dead body prototype:

Generic dead body prototype

Example of dead small enemy:

Small enemy dead body

Justification

Having the dead bodies of the enemies stay around when an enemy dies was to increase the realism inside the game world and therefore increase immersion. The dead bodies of the enemies also contribute to the horror themes of the game.

Implementation

The NPCAnimationController listens to the "hit" event for the entity.

this.entity.getEvents().addListener("hit", this::npcHit);

Once hit, the combatStatsComponent of the entity is checked to see if it is considered dead. A the dead sound effect will play if it is dead.

if (!isDead && Boolean.TRUE.equals(combatStatsComponent.isDead())) {
  isDead = true;
  npcSoundComponent.playDead();
}

When considered dead, components are disabled so the enemy body doesn't interfere with the player. For any priority AI tasks, a new DeadTask is added which makes the entity do nothing.

hitboxComponent.setLayer(PhysicsLayer.NONE);
aiTaskComponent.addTask(new DeadTask());
glowingEyesComponent.deactivate();

The dead body animation is then played.

if (isDead) {
	animator.startAnimation("dead");
}