Ai Optimization: Part 2

Spawning Enemies Rant

Spawning enemies has always been problematic for us during our Mortal Rite journey. Spawning enemies is not something that I’ve seen people complain about on the internet about Unreal Engine, but, it seems like, there’s a lot that is just accepted when it comes to issues with Unreal Engine. E.G.: Something can affect one dev team and not another simply because of the type of project that they are working on -OR- because a Dev Team doesn’t know the proper way to do something… yet.

Originally we just spawned enemies using spawn actor nodes and that worked for the most part. This was in the time long, long ago.

All was good using spawn actor nodes until all wasn’t good using spawn actor nodes. What changed? Something about some of the enemies that we have created did not play well with the spawn actor nodes and caused huge amounts of lag (~10 seconds).

It was at this point that we decided to pre-spawn enemies. We went through several iterations of pre-spawning enemies: Pre-Spawning an enemy for each enemy needed in the level; Pre-Spawning a pool of enemies that can be re-used so that there are not as many in the level. Probably some other iteration that I don’t remember even though I made it.

The complexity added by pre-spawning enemies and making sure every enemy was properly recycled or cleaned up properly was significant. Each time new systems were added or changes to existing systems were made that affected enemies, which most do, it could cause something to not get cleaned up correctly.

After a lot of trial and error, we’ve now gone back to spawning enemies on demand using spawn actor nodes. What changed? We found out what was causing the huge lag and fixed it (Initializing cloth simulations that caused most of it). Well, it wasn’t all us. There was a fix introduced in a newer version of Unreal Engine that fixed a lot of it.

Optimization: Spawning. Where we are now

We use Enemy Placers to setup and place enemies in the world. Having all of the settings in a common place regardless of what enemy you are dealing with is good.

We now use a distance check between any player and each enemy to determine when they should spawn or de-spawn. This distance check can be done between some arbitrary location that can be literally anything in a level or something specifically put into a level to check against, or it can be the placer itself.

Configurable Placer Ranges (via slider) for each placed enemy:
Hearing Range, Aggro Range, Logic Enable, Spawn Range, Deaggro Range

All of these ranges have defaults so not all of them need to be set for each enemy. Each enemy also has their own melee attack range, missile attack range, defense range, etc. that are editable per enemy type and don’t need to be changed often.

There is also logic that doesn’t let someone kite an enemy outside of the original distance check to despawn or cheese the enemy. The enemy will stick with targets until they lose their target regardless of spawn distance.

These spawn distances further optimize the number of enemies that the Enemy Manager needs to process moment to moment. Before, in Ai Optimization 1, we had well over 100 enemies in the test level that needed to be dealt with and optimizations were being done to make having that number of enemies be performant.

With this distance-based spawning and despawning in play, we now only have to deal with around ~30 enemies in the test level at most and around 10 enemies on average. This is with no appreciable change to what the player experiences.

New Trigger Mode: Trigger Volume

I also added a new spawn mode called ‘Volume’ that references a volume instead of a distance check. The logic is basically the same, but uses a placeable volume instead of a radius.

Trigger Mode: Volume

The volume can be placed anywhere, and any number of placers can reference it. Meaning: A Level Designer can spawn in an army or one enemy using a volume.

Optimization: Spawning too many enemies at the same time

One thing that I noticed once we had this ‘Volume’ trigger working is that spawning a bunch of enemies within the same frame caused huge hitches. Hitches are bad. Anything over an 8ms hitch is going to be noticed by most players.

It also wasn’t just because of the volume: In the Test Level there were times when spawning in multiple enemies just because of enemy placement and spawn radiuses caused hitches with just a few enemies here and there.

I don’t have numbers for a packaged game, but spawning in 12 enemies in a single frame caused hitches of up to 140ms (!).

Solution: Spawn one enemy per frame.

My solution was to update the Enemy Manager to only spawn one enemy per frame instead of on demand without any limit.

Now Enemy Placers request for an enemy to be spawned and the spawn requests are dealt with in the order which they were received unless the request is no longer valid. In the case where a player clips into a spawn radius/trigger causing spawn requests to be issued then the player runs out of the radius/trigger before those requests can be dealt with, the requests are removed before they are processed.

Future considerations might be made on the order that enemies are spawned based on distance so that closer enemies to players are spawned in first, but with the current system and how the levels are at the moment I haven’t seen a need to add that functionality yet.

Showing the difference between spawning 12 enemies at once vs. spawning 12 enemies one per frame.

After implementing the one-enemy-per-frame spawning, the peaks went from 140ms for 12 enemies to around 40ms for 12 enemies. Again, this is in the Unreal Engine Editor and not representative of a packaged game. In the packaged game spawning will take much less than 40ms.

At the end of the day, the biggest optimization that we have for performance and enemy performance is better use of the enemies that we have and how many enemies we have in play at any one time. There’s only so much optimization that can be justified on things like abilities, logic to run abilities, etc. while reducing the number of enemies in play has much more significant effect on performance.

Ai Optimization: Part 1

We’re starting to dial in what performance should be like for the upcoming playtest since the playtest will be here before we realize it.

Criteria decided on for Ai Logic was ~2ms on the Game Thread per frame for 20 active Ai. Pretty much arbitrary and just some numbers that were presented so I went with it.

Active Ai means Ai that are doing things. That sounds reasonable, but doing things means available for combat and when Ai is available for combat that means that they need to be assessing all targets in the world that are in range.

Profiling 20 Ai in a level that were assessing all targets in the world was around ~3ms per frame when spread out over 10 frames.

I was optimistic at this point.

In this case all that had to happen was to increase the number of frames for Ai Logic from 10 to 20 and the time per frame dropped to less than 2ms. 20 frames was still responsive enough.

And that solved it. Thanks for reading.

So, are we to expect that the maximum number of Ai in any level in the game is… 20?

You, probably.

Taking a look at the level that the Level Team is working on right now we see that there are currently over 100 Ai placed in the level. The performance of 100 Ai in the level is well above the previous criteria, but then 100 Ai in the level is not the previous criteria anyway.

Previous to optimizations Ai Logic Time in the Real Level was 3x what we wanted!

So, lets see how much we can do with some optimizations while not giving up any capabilities for the Ai.

Optimization: Target Mapping

Ai get their targets by grabbing the list of players in the world, getting all of the destructibles within a range around them and by being given a target directly. This is no where near efficient because the Ai has to sort through ALL targets, determine if they are hostile and should be kept or if they are not hostile/attackable and should be rejected.

The idea of Target Mapping is to maintain a map of Factions to Targets for all targets in the world. This way the Ai only needs to pull from the known Factions in order to get hostile or attackable targets.

Implementation of Target Mapping is straight forward:

  • Add Character targets to the Target Mapping when they are added to the world.
  • Remove Character targets from the Target Mapping when they die or are removed from the world.
  • Add Enemy targets to the Target Mapping when they are added to the world.
  • Remove Enemy targets from the Target Mapping when they die or are removed from the world.
  • Add Destructible targets to the Target Mapping when they are added to the world and are attackable by Ai.
  • Remove Destructible targets from the Target Mapping when they are destroyed or are removed from the world.
  • Anytime that there is a Faction change, remove the current mapping and create a new mapping for the Target’s new Faction.

Once Target Mapping was implemented and I accounted for all of the non-hostile target interaction that Ai need to be able to support, the performance was better.

Target Mapping saved around ~35%

Optimization: Ai Range

Originally all Ai had the same range meaning that the Ai would pick up and lose targets at the same range. These ranged were set across Ai using the worst case settings: The Heavy Archer!

The Heavy Archer has a range of around 100 meters and therefore all Ai Ranges were set to that range, which is a lot for any melee Ai.

Simply cutting the Ai Ranges in half from 100 meters to 50 meters gains another significant reduction because the Ai doesn’t need to consider as many targets. In the future, each instance of the Ai’s ranges will be carefully selected by the Level Team so that they act appropriately.

Reduced Ai Range dropped Ai Logic Time significantly.

Very close to the 2ms mark for 100 Ai! Making good progress.

Optimization: Reachable Queries

Reachable Queries are how I am determining what targets an Ai should consider for Melee Attacks. If an Ai knows that they cannot reach a target they shouldn’t consider that target.

Example of this is if you have a melee-only Ai and there is no way for that Ai to get to a target, then the Ai shouldn’t consider that target and should instead consider another target or go back to what it was doing.

For Ai that have Ranged Attacks that can hit the target in the above scenario, the Ai will use their ranged attacks instead as expected.

Reachable Queries were set to test up to 5 points around each target in order to determine the reachability of that target.

The optimization is turning that value down to 3 points around each target and then to orient those points around each target in a better pattern.

Now we’re cooking with gas.

Better reachable query helped get down below 2ms for 100 Ai.

Looking Back

So with these optimizations, where does the original scenario of 20 Ai at 2ms per frame sit?

20 Ai clock in at around 1.3ms across 10 frames post optimization.

Note the Frames column.

Conclusion

Going through this process was really educational. I learned a lot about the Ai system that I built and where its bottlenecks are. In the future I plan on doing further optimizations:

  • Better control of what Ai are active/spawned in the level so that only the Ai that need to be in the level are there. This is actually already in progress because the needed functionality for on demand spawning of Ai or on demand waking up of Ai have been added to the new project.
  • Decouple non-combat and combat portions of Ai logic so that only Ai that are in combat consider their combat abilities.
  • Use Async Traces.

Goals

My goal for our Ai is to get >100 Ai to below 2ms across 10 frames. Still a way to go.

Disclaimer

All of these numbers are generated on AMD Ryzen 5900X and AMD Ryzen 5950X at stock frequencies. In the near future I will be testing these numbers using the min spec machine, which, from now on, I will call The Beast (EDIT: First tests using The Beast (min spec machine) show times that are roughly double the above times (example: Ai logic that takes 1.8ms on 5900X takes ~3.6ms on The Beast)).

Enemy of Good & Significance.

Optimization is needed for every game in order to make the game run well. Optimization is one of those endless voids where time goes to die. You can spend an infinite amount of time optimizing a game and it’s a good idea to not optimize early.

Early optimization wastes time and by the time that the game is finished the parts of the game that were optimized early may not be the actual bottleneck or may not even be in the game at that point. It’s important to make each part of the game “Good Enough” for now and then improve it later. This is true for everything in the game.

Perfect is the Enemy of Good.

Voltaire

It takes discipline to stick to this idea, though. As an artist, a programmer, as a craftsman, you want what you are working on to be the best that it can be. If you are an artist that could mean adding in all the detail that you appreciate. As a programmer that can mean making the code clean, as readable as possible for the next programmer that needs to understand your code, which is usually future you, or it can mean making the code run as fast as possible. It’s easy to get in a trap where you spend way more time optimizing something than is needed.

Is anyone going to notice the small detail that you put into that art? Is anyone going to notice that a non-critical part of the game takes 0.5ms to run rather than 1.5ms?

The answer is complicated because one of the most rewarding things about being a craftsman is having someone else notice the details, the passion, or the time and effort that you put into something. Since you want that positive feedback as a craftsman, it’s tempting to spend way too much time on something to make it the best that it can be.

Game Thread & Tick

STAT GAME to show stats for the game thread.

One place where optimization was needed was the game thread, which is basically CPU usage. Honestly a surprise since we are using Unreal Engine 5 with real time global illumination via Lumen. Our initial assumption was that Lumen was going to cause the most issues for framerate because the Steam Hardware survey told us that the most common GPU that people had at the time was an NVIDIA GTX 1060 (or equivalent). And, also important, EPIC was targeting 30fps at Ultra Quality for Lumen on current generation console hardware, which is roughly 3x the performance of an NVIDIA GTX 1060 (as a side note: it looks like EPIC is going to do better than 30fps at Ultra on the current generation consoles, but it remains to be seen how complex developers make their scenes in real games).

In the previous Mortal Rite play test and demo builds, we had really bad CPU performance. Mortal Rite version 0.4.12 was the first version that we took the time to optimize CPU performance and at that point it was mainly cleaning up when actors in the game ticked. This was done in the middle of one of the play tests, which wasn’t great timing on our part since there were probably a lot of people that tried the play test and stopped playing due to poor framerates.

For those are are not aware: Each “thing” in the game ticks. A tick in a game is a frame. So, if something is ticking it is doing something each frame on the CPU. To optimize for 0.4.12, our tick count was optimized from over 400 to around 100. The count isn’t really important because you can have one really complex thing ticking and that can cause a performance issue by itself. But this illustrates a problem that in my estimation is an issue for anyone using Unreal Engine: Everything ticks by default and most things have multiple places where tick needs to be turned off (such as components and meshes). It feels like everything is working against you when it comes to tick because everything wants to “auto activate”, which starts tick (in some cases) even when you have set that “thing” to not tick.

Significance Manager

One way that we are dealing with game thread optimization is to have characters (for now) have the concept of Significance: How significant the character is for each frame.

We use a Significance Manager that has every character register with it and then on tick the Significance Manager determines how significant that character is.

Significance Manager settings per character.

We use the significance value to control how fast that character updates. No one will notice that an Enemy way off in the distance is being animated slower than one up close (especially if that someone is playing the game and not studying the pixels of that enemy in the distance), but having that Enemy in the distance update slower saves CPU time.

Numbers to illustrate tick rate changing.

We haven’t dialed in the values for each character yet, but based on our early testing we’ve saved around 6ms (Stock AMD 5950x CPU) on the game thread by having characters tick less via significance and by cleaning up ticks again. This value of 6ms will vary from system to system (probably more than 6ms on our min spec machine! Will test that in the future) and will also depend on how many characters/enemies are close to your camera as the player, but this is huge.

Realistic numbers to illustrate how it may look in game.

Destructible Enemies? Kind of.

Feedback is good.

When you hit an enemy it’s fun to see that you really smacked that enemy.

Hit Reactions: Good.

Blood splattering: Good.

Effects when you hit the environment: Good.

Removing armor is also good. Let’s do that.

Le Sword Knight

All characters have the concept of Health Thresholds that can be setup to trigger things happening. In the case of Sword Knights, Health Thresholds are used to trigger when their armor comes off.

Health Threshold Configuration. There are more than just one for the Sword Knight.
  • Enabled: Toggle this threshold. Usually like to have enables on everything so that it can be isolated for testing.
  • Percentage: Percentage health to trigger this threshold
  • Material Slot: The material slot for the armor that needs to be removed.
  • Socket: Location where the effect will be played so that the armor looks like it’s breaking from the correct place.

We can choose to setup any enemy to lose armor or anything visual based on their current health, but today I am focusing on The Sword Knight.

The Sword Knight’s armor has been setup with different material sections so that we can hide each section whenever we want and we’ve chosen to do this when the Sword Knight’s health reaches certain thresholds. At the same time that the armor is hidden we also set off a Niagara effect that shows his armor flying off and landing nearby.

Like destructibles?

You, an avid reader of the Mortal Rite dev blog

Yup. Like Destructibles.

It’s perhaps misleadingly simple:

  1. Damage happens
  2. Threshold met
  3. Hide the armor
  4. Play the effect
  5. Decrease the enemy’s armor so that future hits do more damage
  6. Profit
Using Dawksin to remove Sword Knight armor.

New Dog, Old Tricks.

Back in the time long, long ago, in the previous Mortal Rite project, we had fully functional placers that allowed for control over what mode an enemy used when spawned. The options were something like Normal, Guard, Melee, Patrol, Ranged, Ready, Wander, Debug, Test Abilities Only, Test and Initial Abilities, Special Use 1, Special Use 2, Arena Mode 1, and Arena Mode 2. You know, just off the top of my head.

Old Tricks.

Spawn Mode Options in Previous Project

Spawn Modes

So many spawn modes. Most of them ended up not being used because of what we ended up needing.

  • Do Not Use: Don’t use this.
  • Normal: Spawn the enemy with normal functionality.
  • Guard: Spawn the enemy so that they guard an area and pick up targets on damage or vision.
  • Melee(Not used): Spawn the enemy so that they prefer melee combat.
  • Patrol: Spawn the enemy in patrol mode moving from patrol point to patrol point.
  • Ranged(Not used): Spawn the enemy so that they prefer ranged combat.
  • Ready: Spawn in a ready to attack stance rather than going through a spawn animation that takes time.
  • Wander: Wander around to random points within a range.
  • Debug: Reacts to damage and picks up on targets. No attacks or movement.
  • Test Abilities Only: Whatever the test abilities are setup up as.
  • Test Abilities and Initial Abilities: Whatever the test abilities and Initial Abilities are setup as.
  • Special Use 1(Not used): Abilities that are defined in special use 1 for this enemy.
  • Special Use 2(Not used): Abilities that are defined in special use 2 for this enemy.
  • Arena Mode 1(Not used): Arena 1 abilities that are defined for this enemy.
  • Arena Mode 2(Not used): Arena 2 abilities that are defined for this enemy.

Spawn Criteria

Old Tricks: Spawn Criteria

Spawn Criteria determines when an enemy will spawn.

  • Immediately: As soon as Enemy Manager initialized and there was an enemy available in the enemy pool (pools are no longer needed in the new project).
  • Placer Event: An event such as a death event from another placer’s enemy. Allowed spawning in new enemies to fight when one or more other enemies diead.
  • Trigger Volume: When a player or enemy overlapped with a trigger volume.
  • Proxy: When a proxy triggered, which could be because of any placer event or trigger volumes. Pretty much anything that called the trigger of the proxy.
  • Wave Spawner: When the wave spawner determined that the enemy should spawn. Wave Spawner was only ever seen for a little bit, but has gone the way of the Dodo because it wouldn’t open doors to let players proceed when needed and we ran out of time to fix it for the previous playtest(s).

New Dog.

New Dog: Spawn Mode & Trigger Mode

The new placer now has the following spawn mode options:

New Dog: Spawn Mode Options

The only spawn modes that we are going to use are Debug, Guard, Patrol and Wander. Special 1 through Special 3 are reserved for any special handling that we need, but we may not need to use these special settings because in most cases it’s easier just to make a different enemy that does what you need it to do.

Example is if you want an enemy to do something specific. Something specific means a new ability or multiple new abilities that a version of an enemy has that the basic enemy does not have.

Because everything is really modular in the new project it’s really easy to add a new Ability Set that has the new ability or abilities that you need in it and then define new rules for the Ai so that the Ai uses those abilities. New Ability Set and New Ai Rules are just two configuration files. The abilities would need to be created, but those would have to be created for the older system as well. This is a net gain.

Trigger Mode

New Dog: Trigger Modes

Trigger Mode is what has replaced Spawn Criteria and there are literally just two options for this: Begin Play and External Call.

Begin Play is what it sounds like: when the Game/Level begins.

External Call is also what it sounds like: when something, anything, external to the placer determines that it’s time for the enemy to spawn.

Tools

Having the ability to spawn an enemy in a specific mode at a specific time is necessary for us to be able to control when a player will face enemies. So, I made some tools to help with this that replace the plethora of tools that helped with the old placers.

Ai Death Actor

Red Cube: Ai Death Actor. Green Cubes: Placers.
When N Death Placer enemies die, spawn N Placer enemies.

Probably need a better name for this, but this guy just cares about when things die and when those things die it spawns new things. Simple.

Ai Death Actor in action.

Ai Trigger Actor

Pink Cube: Ai Trigger Actor. Green Cubes: Placers.
When Linked Trigger Volume is overlapped, spawn N Placer enemies.

This guy just cares about when a trigger volume is overlapped and when that trigger volume is overlapped it spawns new things. Also simple.

Or is it….

Ai Trigger actor can use tags to filter out things that shouldn’t trigger spawns. If you only want a player to trigger spawns or if you only want a specific faction of enemy to trigger spawns, this is the way. This tag filtering is similar to how we filter what can and can’t destroy Destructibles. Still simple, I think.

Ai Trigger Actor in action.

Now with feeling

Of course Ai Trigger and Ai Death actors can be used in sequence. For the above videos the configurations were the following:

Sequence of enemies spawned in on death using Ai Death Actor.
Sequence of enemies spawned in on trigger and then death using Ai Trigger Actor and Ai Death Actor.

Wouldn’t it be cool…

Ai Vision is what determines whether an enemy can see a target or not, and it’s trickier than I thought.

First, when I started working on Mortal Rite, I had not ever worked on creating Ai for a game so creating an Ai that works well and meets or exceeds the Ai in other established games was an order taller than Shold. Shold is tall. Bigly tall. I was eager to make a really good Ai so I went for it and have been learning the whole time.

Mortal Rite Steam update about the Road Map being released.
Mortal Rite Steam update about the Road Map being released. Shold(left) is super tall compared to normal sized humans, and this image doesn’t even do his height justice!

What does Ai Vision do exactly?

Enemies need to be fair and predictable for players to have a good gaming experience. This means that a player should expect an enemy to acquire their character as an attackable target when it is in line of sight(LOS) and within a certain range.

Easy enough: The Ai just needs to do a line trace to see if a target is visible (e.g.: Not blocked by something that should block sight). Trace from the location of the enemy’s eyes to the location of the potential target. Record whether or not the target is in line of sight and that helps determine if the enemy should attack that target or not.

Line Trace Logic, sort of.

Hol’ up. What are potential targets?

You, the reader.

Right. What are potential targets?

Potential targets are… targets that are attackable. Done.

Me

Real talk: How do we know what targets to trace against? We use the teams system built into Unreal Engine to control what is hostile to what. Entities on the same team are friendly. Entities on different teams are hostile. There are roughly two types of targets that an Ai should worry about:

  • Characters – Other enemies that are hostile or players that are hostile.
  • Destructibles – Objects in the world that can be destroyed (such as barrels, obstacles (walls potentially)) that are hostile. By default all destructibles are hostile to all characters.

Characters are fairly easy because we maintain a list of the characters in the world that the Ai can iterate through and choose the hostile characters. EZ.

Destructibles are fairly easy to get as well. I chose to use a sphere trace so that each Ai only needs to worry about destructibles within its attack range. Originally I used a sphere trace for Characters as well, but that’s silly when I have a curated list of characters at my disposal.

Get Targets. Are targets in LOS?

Non-Characters are more tricky. If we want an Ai to be able to do damage to a destructible and have the capability of destroying a destructible, we need that destructible to be hostile relative to the Ai. Hostility gates direct damage. This presents a new problem which is that if we put a lot of fun destructibles all over the world, the Ai will potentially run around killing those destructibles rather than fighting the player.

Solution to the Ai running around killing destructibles is to not allow the Ai lock on to those targets. Destructibles that the Ai cannot lock onto are destructibles like barrels, torches, some walls, etc.

Lock On Interface

The Lock On Interface exists to determine what entities can lock on to other entities such as when a player locks on to a Sword Knight and you can see the little white dot that shows you are locked on to that target. Abilities (everything you can do in Mortal Rite is an ability, basically. Even melee attacks.) can then be made to reference the lock target or not.

Locked on to a Sword Knight

The Lock On Interface uses one or more Target Points, which are literally placed on the model by a developer, to indicate what parts of a target can be locked on to. In the case of the Sword Knight the target point is in the middle of his

Target Point (the RGB Axis Gizmo) on the Sword Knight in the UE5 editor.

I updated destructibles to allow them to be locked on to by:

  • No one – For things like barrels (fodder)
  • Only Ai – Player-created asset like Shold’s Wall
  • Only Players – A barrier that only a player needs to destroy to get somewhere
  • All

This means that we can have destructibles that only exist to be environmental fodder that accidentally gets destroyed or a player can choose to destroy just because. But most importantly an Ai will ignore it.

Updated logic flow becomes…

Ai Sight Logic with Hostile and Lock On checks.

Wouldn’t it be cool…

Someone in the office

Probably both the best and worst thing to hear around the office.

…if an enemy couldn’t reach its target because of a destructible it would swap to the destructible and destroy it to get to its target?!

A younger me

How hard could it be? Destructibles are already tracked by the Ai and attackable by the Ai. Now I just have to figure out what is blocking the Ai’s current target and, if the blocker is attackable, increase the blocker’s threat so that the Ai prefers that target over the Ai’s current target! Genius.

Updated logic flow!

Logic flow with Block threat and Blocker Threat resetting.

So this basically works. Other Alex showed off the Ai at this point in a TikTok video a while ago where an Evil Initiate attacked a wall in order to get to a target. It was a short clip, but it worked at that point.

The original scope of “The Ai should attack obstacles to get to targets” was Shold’s Great Wall that cuts off vision to the Ai’s target and also cuts off Navigation Mesh so that the Ai knows that it cannot move through the wall.

What’s navigation mesh, precious?

Gollum

Navigation Mesh is a procedurally generated mesh based on rules (such as radius and height (size) of an agent using the navigation mesh) that determines where an Ai can path. Navigation Mesh can be pre-computed so that at runtime it doesn’t have to be computed. Navigation Mesh can also be updated at runtime when obstacles move around so that Ai knows that it cannot path through moveable objects at runtime. Pathing calculations use Navigation Mesh in order to know where it can potentially path instead of having to compute pathing against the world’s geometry itself. Navigation Mesh is basically a simplification.

Green bit is the Navigation Mesh.

New Ai Vision Smell

What’s new is:

  • Better Multiple Target Point Evaluation
  • Better Base Target Threat
  • Better Reachable Evaluation

Multiple Target Point Evaluation

In the time long, long ago we determined that it was necessary to have multiple target points for larger targets such as the Constructor Boss. It was bad that players could only lock on to a single point on the Constructor Boss because it caused bad camera interaction. Solution was to add multiple Target Points and Alex updated player lock on logic to allow smooth cycling through those Target Points. It sounded like a good solution, but honestly wasn’t something that I thought I would like. But I do like multiple target points and I am happy that we have it.

What it means for the Ai is that the Ai can also have access to multiple target points for targets that are large. Since everything is modular and target points are defined by the Lock On Interface, it’s really simple to add N Target Points as needed.

Random Wall Destructible with multiple Target Points (yellow hand drawn Xs)

What’s really useful about multiple target points for the Ai is that it helps address a problem that most people probably wouldn’t think about: Whether an Ai can reach a target to attack it.

Half of this game is 90% mental.

LOS is only half of the “Can the Ai attack a target” equation. The real question is “Can the Ai see a target and can that Ai get to a location that allows it to attack that target”. The Ai has two types of attacks at a really high level: Melee attacks and Ranged attacks. When an Ai decides what it can do it needs to know if it can use a melee attack or a ranged attack against a target. Part of Ai Vision Logic is determining if a target is Melee Reachable or Ranged Reachable.

  • Melee Reachable: True if there is a pathable location that is within the Ai’s attack range to a target.
  • Ranged Reachable: True if the target is in LOS and within Missile Range.

It might be becoming clear that all of the above Ai Vision logic talk was really just scratching the surface of what the Ai is actually doing, but I felt I needed to cover all that to setup a foundation for the real problems that I’ve solved recently.

A problem caused by needing to be within range of a target in order to attack it is that for large targets that have one target point the Ai may or may not be able to get within range of the target point, but it can get in range of the target’s location. This looks like the Ai should be able to attack the target, but the Ai, trying to be smart, is overthinking the situation and rejecting the target because it can’t reach the target point.

One target point near the pivot of the asset is attackable.
Level designer flips the asset 180 degrees for visual reasons

Multiple Target point handling allows the Ai to dynamically choose the best target point for a target. This happens every evaluation so that the Ai can handle a moving target that has multiple target points and have a better chance to always be able to attack that target.

Evaluate the best target point and use that.
Ai can’t see the two target points that are near the bottom because they are below the terrain. If these were the only target points then the Ai wouldn’t be able to see the target at all.
Multiple Target Points being evaluated by Sword Knight.

Base Target Threat

Originally the threat table assumed that any target with 0 threat should be removed. Everything on the Ai’s threat table was either Threat greater than 0 or Threat equal to 0. After talking over all of these issues with Alex, Alex suggested adding an way to define base threat per target. So, a player would have a higher base threat than a destructible, but the destructible still had to have a base threat of 1. This would allow an Ai to choose a player, which is a more desirable target for the Ai to attack, over a player. And BAM, now the Ai chooses the Players over the Destructibles when they are evaluated at the same time.

The original base threat change was good, but the ultimate solution was to allow Ai to track targets that have 0 threat and not reject them. The new paradigm is everything with Threat greater than or equal to 0 stays on the threat table and anything less than 0 is removed. Also, anything that has a threat of 0 is not chosen as a target.

Boiling this down with everything else talked about:

  • Targets can have multiple target points that allows them to be attacked because at least one target point will be in LOS and reachable
  • Destructibles start with 0 threat, which allows the Ai to track them, but prevents Ai from attacking them when they are the only targets.
  • Targets gain threat when they are a blocker of a target that the Ai wants and therefore becomes the Ai’s priority when they should be.
  • Blockers reset their threat when they are no longer blocking a target so that the Ai resumes its pursuit of the real target: the player.

Better Reachable Evaluation

Multiple Target Points allow for the Ai to better see targets that it could not see before due to a single target point being obstructed and also partially addressed the reachability issue because multiple target points increases the chance that a target point will be in range for the Ai (optimization for static targets would be to place fewer target points in better places than to just have a bunch of target points. Don’t worry: we’re not going to be stupid about the number of target points we use because performance is important).

It was because everything was working better that a new problem appeared: there can be obstacles on the ground that make a target partially unreachable for melee attacks.

In a setting where there is debris on the ground or anything that blocks a direct path to a target that is reachable in other ways the Ai would still decide to not attack that target, which is bad and makes the Ai look dumb. And we don’t want dumb Ai.

Nav blockers do not block LOS to Target, but do prevent the finding of a reachable location within range of the target.

The solution that I’ve put in place for this is to test a half circle of reachable locations from the chosen target point. This means that in the above case additional reachable locations would be tested to the sides of the red location.

The Ai can now reach the target due to the extra testable locations. As expected.

Conclusion. Finally.

This has become a long post. Hopefully interesting. Now for the system in action and potentially the TLDR of the whole post.

Everything in motion. Performance pretty horrible due to the unique way that Unreal Engine handles drawing debug shapes. Performance without the debugging is fine.