Designing AI for multi-level ground

I need some tips or advice how to I can possibly handle and implement navigation of enemies using navmesh.

Consider following example:

There is player and enemies who can fire bullets. The game has top-down (isometric) view, and pojectiles can travel only only on single Y plane. That means that position on Y axis never changes and bullet never goes up or down.
There are enemies who want to keep their “sniping” positions, but there are “close” shooters as well who chase the player.

Using various navmesh functions it’s possible to find proper places to snipe, chase, hide or other actions, however we wanted to create levels (height) in the game and it makes scripting AI very hard.
As you can see in the picture there can be some ground (blue) and ramp to elevated ground (green).

The problems is that navmesh does not provide any functions to check if ground is higher or lower, raycast is capable of going up or down and does not stop on ramp, it’s hard to check if enemy is on the ramp and even harder if there is line of sight to see player. So for example it’s hard to find proper position to shot player on elevation and also force enemy to not stand on it. I can check relative height of player and enemy, but it does not help to find proper positions.

I want to know if someone knows some tricks or ideas to overcome those problems.
Currently I have scripted some kind of wacky solution, but it does not work in many situations, so I start to think to ditch the idea of “levels” and stay on single flat ground, or maybe to make enemies capable of shooting on all axis (up/down too), but that would be probably bad design as camera is mostly from the top and it’s hard to see projectiles traveling in those angles.

1 Like

I would just author in some specific vantage points into the edges of your levels.

From those vantage points you can test if the player would be visible in order to decide if the enemy should go to that point.

You can even author in some killzones… this lets the enemies ask, “if player is in this killzone box, then these vantage points give the best views, pick the one closest to you, go to it and start shooting at the player.”

The more you author up front, the less tricky clever code your agent needs.

You can just sample the navmesh at that location and check for yourself… I think this might be what you’re looking for:

Well, I could define some points, but I really want AI to be dynamic and react properly to situation, without checking fixed points.

Well, yes and I use it, but sampling random points can be often expansive when navmesh is “unfortunate”, plus the way it works also is not perfect. Even if I get point I need some additional checks if target is on the same “height”, what is not as simple as comparing positions in my case, but what is more important I still need to check if player can be seen from that position.

The problematic part is the ramp, it’s hard to define behaviour for this place, enemy can be on “kind of” the same level, but still unable to shoot or block other units. The whole thing becomes complicated, because there are no proper tools to write scripts without hacks or a lot of conditions.

Have you tried doing the “line of sight” check between player and NPC with the NavMesh.Raycast() function? The “ray” can go up/down the slope, in a straight direction, and gets interrupted only by NavMesh edges.

Hi, yes, in general I tested everything that is available in API and I can say that creating decent AI on “multi-level” is simply hard. The problem with navmesh raycast and (actually other functions too) that it becomes complicated with obstacles and and it does not take several cases into account, for example I might want to allow some “holes” in navmesh to be not traversable, but be shotable and navmesh raycast can’t jump/go through it.

In my case the main problem is lack of API to find proper positions on navmesh. AFAIK unreal has navigation query system and there is nothing similar inside unity.
The only solution is just randomly sample points and checking if they meet criteria, but this is also a bit wacky, there is no way to use jobs for that, everything need to run on main thread.

I’m having a bit of trouble understanding your exact problem but it sounds like you want to find a reliable method of finding locations for enemies such that they can stand on a navmesh position that is the same height of the player thus allowing them to shoot at the player while also not having 3D environments block their shot to the player?

If the above is the case, my approach in the past has been to use a two-step method that can loop until a solution is found. It also has a limit on the number of iterations in that loop so that it doesn’t take too long or lock up the game if no solution is valid. It involves using the NavMesh’s SamplePosition function followed by a Raycast for final line-of-sight checks.

Typically, I use a distance of 1.0 in the SamplePosition and make many attempts either randomly or using some kind of algorithm or defined scanning pattern. If a position is found that matches the required height I then a simple raycast to ensure that nothing will be blocking the shot from the sampled position and the player.

The other option that I’ve used it to simply modify the AI if the heights don’t match… in one case I made the baddies lob grenades or other ballistic projectiles that could get around issues of height differences.

Yes, this is mostly the main problem. Let’s see example of one of these problems I face:


Red circle is enemy and green circle is player. If I want enemy to reach the closest shooting position to player it would be yellow path (assuming navmesh determines that). However consider this is long or medium distance unit, and it does not want to be too close, so to find nice position we could extend last part of path and raycast some orange point in distance. If I use navmesh raycast it would probably work if that surface was flat, but orange point can be on higher or lower level, so finding such a position becomes harder. If we take into account that navmesh is actually not good to determine line of sight it becomes even more complicated.
One note - I have scripted visibility system running on job system with raycasts, so it is way easier to check some things, but it still does not solve various other problems.

I try to do something similar, but SamplePosition is also a bit tricky, as it projects from the top to hit, so distance must be adjusted, plus if I wanted to make greater height distance check, then I would need to cast it more from the top/sky and that is way more expansive. In any case there is no other way than sampling navmesh randomly or in some kind of pattern. I also try to limit number of tries.
Good to see someone tried to do something similar, did your approach work well in the end?

I mean, it did. But that had more to do with the extremely strict limits of the game. All of the action took place on the screen or just slightly off of the edge. And while the NPCs did a rough check for LoS it wasn’t a hard and fast rule. If there were obstacles in the way that’s because I placed them there specifically as something the player could hide behind while bullets were flying around everywhere. Think of a Beat 'em Up mixed with a Shmup and you kinda get the picture.

Originally the NPCs could shoot up and down but due to the extremely flattened perspective it was hard for players to judge depth accurately. So I dropped that idea and decided to limit shots to the same height just like you. Since players could jump however, it was pretty easy to find positions where they’d remain invulnerable to all attacks while still being able to kill everything in sight. So I came up with NPCs lobbing explosives as a way to force players to not try to stay in such positions for more than a couple of seconds. There was also the case of significant height differences in which case the NPC skipped looking for an optimal position and instead just looked for any position near the player that was at the same height at which point it could resume like normal.

I think in your case it’s fine to use the random/pattern sample method since most of the time it will just work when things are relatively flat. In the cases where they aren’t, maybe you’ll get lucky and you’re NPCs will find a valid location to move to. You can give them some kind of backup state like I did as a means to force players to avoid camping places that are next-to-impossible for them to reach like short ramps. And for extreme height differences it’s just easier to switch to a different state entirely that purely focuses on finding something very near the player or goes to some default objective they can protect and wait. This is where Kurt’s suggestion of some data baked into the level itself can really help.

1 Like

The biggest (and to me overwhelming) advantage of authoring in data is to let you move forward.

Put some vantage points in, associate or mark them with rectangle “killzones” that they overlook, and move forward.

Now when your target is in the killzone, have the agent go for the associated vantage point and do a raycast (or spherecast) check to see if they really can see the target, and if so, fire.

The problem with agonizing over making the agent clever enough is that:

a) it is hard to make intelligent agents that will work in all future level designs
b) you can think you got it right but then have degenerate behaviour in random conditions
c) you invest all this massive complicated work only to find that it doesn’t play well, but it took you weeks and months of tinkering to find that out.

I always lean towards move forward. Moving forward lets you learn and reason about things faster.

Being faster lets you succeed faster, but more importantly lets you fail faster and move to the next thing.

1 Like