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.

New Blood

New progress this week / week and a half (I know I posted a little while ago).

We need some more blood my friend.

Abraham Lincoln

So to kick things off, as Anthony mentioned last week, we did have enemies spilling their contents on the floor, but boy oh boy, wouldn’t it be neat if also hit their meshes? We think so.

We had dabbled a little with a plugin that would let us do this in the last project, but we couldn’t get it working last time, but with a fresh set of eyes, we implemented it again, and voila, blooooood.

Yay it works! So basically what’s happening here is we find where you’re weapon hit, and we tell the system, “Please spawn a decal at this location, on this mesh”

very cool

But it would be a lot cooler, if it also splashed onto you. So we added one for the player’s mesh as well, and said hey let’s do a trace to figure out kinda where the blood would end up if it splashed from the hit location, back to you!

covered in sweet-n-sour sauce

So that’s pretty cool, BUT we didn’t want it to look like you were heavily bleeding from dealing damage to someone. So we figured we could differentiate a splatter, from a wound (for lack of better terminology).

The way we’d do this, is by having the wounds stick around for like 30 seconds, whereas the splatter would stick around for like 5 seconds. The only problem was the plugin didn’t differentiate.

uh oh

So I took a peek under the hood to figure out how it worked. Turns you can specify one image per mesh, which then gets drawn to a RenderTextureTarget.

This just means you can draw something to a texture real-time and do something with it. (In this case, projecting it to the mesh).

Since there wasn’t support for multiple textures, there also wasn’t support for different timings which is what I wanted for the splatter vs the wound.

no options… what’s that giant node?

So I took a look at the guts of that node and found out its…

HLSL

Hey a manual way of specifying how shaders are rendered.
I’ve never worked with this before!

I’m not stressed about this at all

Pulling in Anthony, we took a deeper look into this code, and found that if you want to fade out the blood, it fades right away, and you can only specify how long the total fade time is.

So doing some quick math we figured out how to add not only an initial delay, (So it wouldn’t start fading right away) but also a switch for whether it was a wound or a splatter so we’d have a different timings for when the fade started.’

We’re reading texture data. How Fancy!

And so with that done, here’s a sped up version of the blood drying quickly

His arm dries up. Much like a desert

And so with that we move onto…

Defense

In the last demo we only had two defense types. Block or Parry.

We were talking about it and figured we could give more options depending on what character you choose. So what does that look like?

  • Block
  • Deflect
  • Parry
  • Perfect Block
  • Evade

So Block is what enemies typically have and for the most part it’ll stay that way, so not much is new there.

Deflect, the main goal for this is to avoid attacks if timed perfectly. This works well for smaller weapons where it wouldn’t make much sense to block.

Deflect in action

Next up is Parry, this is similar to what Dawksin had in the demo. If he successfully parries, he will ignore damage and perform a counter-attack.

Parry in action

Perfect Block, this is what Shold had in the demo. Arguably one of the strongest defense types since successfully timed blocks cause enemies to stagger. Useful for protecting your teammates

Perfect Block in action

And finally Evade, this will most likely end up being Fia’s defense type. Successful timing will result in you repositioning behind your attacker.
(Thanks to Jensen for helping me work through this)

Evade in action

It’s using some of Dawksin’s vfx at the moment, but hey it works!


Now I could wrap it up there, but what about the part of the week where I hit road blocks with fundamental systems? Systems so vital, when they don’t work it ruins your whole experience? That’s right baby we’re talking about

Animation Locks

Animation locks are what determine if you’re locked into an animation or if you can break out of it and do something else. This is what lets things like chaining of melee attacks be possible, and having abilities not be interrupted mid-cast.

Version 1 of this system was in our last demo, and was notorious in multiplayer for failing when using Dawksin’s grappling hook, causing players to get stuck in a cast animation until they got hit by an enemy.

So we took another look at animation locks and implemented a pretty big change. Apart from reworking the whole thing into an ability so we could debug it better, we also made it so that locks ALWAYS have an expiry time.

This makes it to where even if the network hitches, or some ability flies off the rails in some crazy edge case, the lock WILL release.

Ultimately this ends up with us not needing to worry so much about getting stuck and some fancy new nodes for controlling when you can and can’t break out of animations.

fresh and exciting!

That last node also has some fancier functionality, to automatically read animations and find lock releases.

Here’s where Dawksin’s first attack releases the lock

Now I know what you’re thinking, wow Alex that’s a lot of stuff, we should end it here ri-

WRONG

I also went ahead and:

  • Updated Dawksin’s Reticle size so you could see more clearly
  • Fix a bug where anytime you got hit ALL OF YOUR PASSIVE ABILITIES GOT CANCELED
  • Fixed an issue where AI would cancel it’s own animation lock ability, making it get stuck forever.
  • Added in more entries to the Gameplay Cue Translator to allow different hit impacts which Anthony detailed in his blog post last week.
HIT ME WITH THOSE DIFFERENT MATERIAL TYPES
  • Fixed a bug where due to the new animation lock system dodge was no longer allowing dodge attacks, so a new “Dodge – Cancelled” ability was added that keeps your dodge state active for a little longer so you can chain an attack.

  • Fixed an issue where when blocking while locked onto an enemy with Shold his torso would kinda split in half.

Okay. Catch y’all next week