Devlog 3 - Enemies and Interaction


Overview

Since the last devlog, I have added combat, enemy soldiers, friendly soldiers, hostages, a faction-based targeting system, corpses, and game states (win/lose).


Modular Entity Design

I have designed a very over-the-top component system for all entities. This was difficult to set up and is a somewhat unorthodox workflow (at least for me), but in the end I got it working smoothly.


Pictured above: All of Extrication's currently-existing scripts and components.

There is no unified enemy script, hostage script, player script, etc. Every single entity is composed of numerous small, generalised components. Most of these components are not even strictly tied to proper NPC behaviour. I could make a door that uses the targeting system to open for the closest entity, or a crate that breaks when you shoot it (using my hitbox component and a health manager). 

Making Extrication this way will allow me to make lots of entities with shared but ultimately very different behaviours. This style of design is very difficult with more traditional non-modular programming methods. The only real downside to my approach is that it is slightly abstract and hard to understand without knowing the code.


Pictured above: The hostage entity with its collider and hitbox showing.

The creation of the hostage is a perfect example of this workflow in action. In a more traditional programming style, this would have posed quite a difficult conundrum. Should both the enemy and hostage inherit from a base "Entity" class with all the essential features present? This could work, but what if the hostage or other future entities don't need basal entity systems like movement or targeting (which most of the entities thus far require). Should these features be included with the hostage but disabled? Should there be a slew of contrived inheritance classes to differentiate behaviours (think Entity, EntityMover, EntityMoverTargeter, etc.)?

But with my component system, I added a working hostage in minutes. I simply created a new entity, added a HealthManager, a Hitbox, and a CorpseHandler. Voila, a working, killable hostage!


Pictured above: Idle to aggro state management on the enemy. Enemies will begin shooting and chasing their target when aggro, and will slowly wander around and bounce off walls when idle.

One of my favourite components is the LOSStateManager (line-of-sight state manager), which can enable/disable components or make function calls when a given entity spots or loses sight of its current target. This component is the main work-horse of the enemy AI, and allows me to create all sorts of behaviours. I could, for example, make an enemy make a sound when it sees a target, or begin to patrol an area when it can't. It's very versatile and easy to use. If I get time, I intend to use it to make a varied cast of enemy characters, all with unique AI behaviour.

Faction and Targeting System

I have created a simple faction system to allow for dynamic entity targeting. Every entity that is part of the targeting system will be added to a jagged array of GameObjects with space for 30 entities per-faction. There are currently 4 factions: neutral, player, enemy, and hostage. I can very easily add new factions if needed.

With the faction system, I can duplicate an enemy while changing its faction to the player's and it will begin attacking its brethren (while otherwise behaving the exact same way as them).

The reason I added this system was because making enemies specifically target the player in-code felt like a dirty and limited solution. It would require extensive code rewrites if any other targetable entities were added. It also goes against my design of modularity by forcing every enemy-related component to have a reference to a global "player" variable which won't necessarily exist or be valid at runtime. This probably isn't the worst concession ever, but it irked me enough to the point where I felt compelled to create a faction/targeting system.


Pictured above: The targeting setup for the enemies. Enemies will only target the PLAYER faction, will be themselves added to the ENEMY faction, and will have their idle state enforced when they have no current target.

The targeting system works by checking for the closest entity at timed intervals and setting it as the target. The many rapid distance-checks this requires are a bit of a programming no-no, so I have employed several optimisations to reduce the load. Entities will only perform distance-checks on entities within their opposing faction(s), and if there is only one valid enemy target that exists (such as multiple enemies vs 1 player), they will not perform a distance-check at all. Additionally, the target updates are done at slightly different time-intervals per-entity, so that not everything is checking for a new target on the same frame. I have also employed some array-searching optimisations to reduce the number of loops required to check all valid targets.

Corpses

Corpses now appear when an entity is killed. This is controlled by a CorpseHandler component which allows me to customise the front and back corpse sprites per-entity. An entity will die with their front facing up when killed by a projectile hitting them on their front half, and will have their back facing up when killed from behind. The body will slide across the floor in the direction they were damaged from. It feels very satisfying and gives the shooting a sense of weight.

Pictured above: The VERY early-stages corpse art. Green one is the front, orange is the back.

Game States

The game can now be lost or won. To win the game, you must kill all enemies and save all hostages. To lose, you either need to die or accidentally kill a hostage (hopefully not deliberately).

This was trivially easy to implement. Hostages and enemies have a script attached which adds them to a running count on the GameManager class. When they die (enemies) or are saved (hostages), they are removed from the count. Once the enemy and hostage counts both reach 0 the game is won. The lose cases were even easier to add as they were just function calls to lose the game when either the player or a hostage dies. Unlike enemies, hostages are not removed from the count when they die, so don't worry about a potential scenario where you kill the last one but still somehow win.

The Problem of Friendly Fire


As a core pillar of my game, I have decided to make factionless bullets/projectiles. Every bullet can kill anything that has a hitbox (remember that a dead hostage is an instant game over). I think this will make the gameplay more interesting and force the player to be careful about when and where they choose to fight.

The problem is that entities don't know not to shoot their friends, or even that their friends exist. Initially, the enemy stationed at the back of a group would mow down its allies in front while attempting to hit the player (very funny but not ideal). While I never saw this happen, I was also worried that enemies may shoot hostages if there was one between you and them. I fixed these issues by adding an ally-check behaviour to the shooting component. All entities that have this feature enabled and properly configured will stop shooting whenever an ally is directly in front of them. They can still kill each other on rare occasion (due to bullet travel times and ally movement), but I think this is at an acceptable level and will surely add to the chaos.

Pictured above: Ally check preventing enemy from shooting near a hostage.

Pictured above: Ally check preventing enemies from shooting near each other. One of the enemies can still shoot because the player is between him and his friend.

The ally-check works by shooting multiple raycasts in a small cone from the tip of the gun before it is fired. If the first object hit by any of the rays is in an allied faction group, the shooting will be forcibly stopped. If they hit something else, like walls or an enemy entity, the shooting will continue as normal.

You as the player don't have an ally-check, so you can shoot literally anybody. Be careful where you aim that gun of yours.

Response to Previous Feedback

There were many comments given about shooting through walls, which is no longer an issue. A ray is cast from the entity's origin to the end of the gun. If it is hitting a wall, the gun will not shoot. This goes for enemies, friendlies and the player.

New Feedback

I will add feedback here once I receive it.

Leave a comment

Log in with itch.io to leave a comment.