Sprint 4: Enemy AI and Interactions - UQdeco2800/2021-studio-6 GitHub Wiki
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.
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
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 ranged enemy when injured:
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:
Example of dead small enemy:
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");
}