New Pathfinding Modifiers - Studio Beta

Hello AI developers,

Currently, PathfindingService almost always returns just the shortest path between the source and the target points. However, the optimal path should often consider the character/agent abilities, terrain type and game rules.

For example - you may want an NPC to travel over a bridge instead of swim through water, cross a street in the crosswalk, or avoid an area that would damage the character.

Introducing PathfindingModifiers

PathfindingModifiers are instances used to annotate/modify parts or meshparts. Their purpose is to let game creators customize the cost of traversing the volume their parent defines. To do so, CreatePath() now also includes a parameter Costs to map PathfindingModifier.ModifierId to cost multipliers. For instance, a cost value of 100.0 makes the parent Part 100.0 times harder to traverse. Conversely, a cost value of 0.5 makes the volume twice as likely to be included in the final path.

Let’s review some practical use cases of this feature.

1. Can the agent step over this brick?

When writing the AI for NPC’s in a game with death bricks, it’s useful to let PathfindingService know that the agent wants to avoid them, instead of walking over them. Annotating the part with a PathfindingModifier instance lets us customize the resulting path, like so:

DeathBrick Small

local modifier = Instance.new("PathfindingModifier")
modifier.ModifierId = "DeathBrick"
modifier.Parent = deathBrickPart

local agentParameters = {
    AgentRadius = 2,
    AgentCanJump = True,
    Costs = {
        DeathBrick = 10.0
    }
}
local path = PathfindingService:CreatePath(agentParameters)
path:ComputeAsync()

The new Costs parameter maps the identifier(s) to a cost multiplier that is used during the path search to establish priorities for crossing over that region. The constant math.huge can be used to indicate the region should be avoided altogether, even if it is the only path available.

2. Don’t cross the street in the middle of the block!

Let’s say we are working on a city simulation game and we populate the streets with pedestrians that use crosswalks as much as possible. Splitting the street model into crosswalks/non-crosswalks might not be practical. To annotate arbitrary regions, PathfindingModifiers can be parented to volumes. Volumes are usually parts that are non-collidable, anchored, and transparent, used to establish a region for gameplay purposes. As a result, the modifier will annotate any world geometry that overlaps with it.

Using the code below, you can place an invisible part that indicates where the crosswalk is on the map.

Crosswalk

local modifierVolume = Instance.new("Part", workspace)
modifierVolume.CanCollide = false
modifierVolume.Anchored = true
modifierVolume.Transparency = 1.0
modifierVolume.Position = ...
modifierVolume.Size = ...

local modifier = Instance.new("PathfindingModifier", modifierVolume)
modifier.ModifierId = "Crosswalk"

local agentParameters = {
    Costs = {
        Crosswalk = 0.01
    }
}

3. Noobs can’t swim!

PathfindingService will automatically annotate regions with parts and terrain materials.
In the following example we include a high cost multiplier for water, which steers the NPC to the bridge when a bridge is present as an alternative path.

There’s no set rule on what the multiplier value should be - it should be tuned until the desired effect is achieved. The distance from the bridge and the amount of water that needs to be crossed will be factored into the weighting of the final path.

local agentParameters = {
    Costs = {
        Water = 100.0
    }
}

Cross bridge 2

4. Open the gates

PathfindingService sees all obstacles initially as having the same “cost”. The service doesn’t know how to differentiate a brick wall from an unlocked door. With PathfindingModifiers it is possible to set a priority for an entryway like a door.

The following example enables the PassThrough property in the modifier to indicate that all of the obstacles enclosed by it are traversable (in this case, the door that can be opened).

doorVolume.PathfindingModifier.ModifierId = "Door"
doorVolume.PathfindingModifier.PassThrough = true

local canCrossDoor = true or false
local agentParameters = {
    Costs = {
        Door = canCrossDoor and 1.0 or math.huge
    }
}

Door PassThrough

How to enable PathfindingModifiers

You’ll need to enable the Beta Feature first.

To enable the beta feature:

  • Go to File → Beta Features in Studio
  • Check the box to the left of “Pathfinding Modifiers”
  • Restart Studio

You can read more about Pathfinding and Modifiers on the DevHub:

Debug visualization

It is possible to see what annotations are used by the service. This especially useful when models and terrain overlap and it’s not clear what an agent will see.

To enable it, open the Studio settings and enable the following settings:

image

FAQ

What is the priority in resolving normal parts, terrain, and parts with modifiers?

  • The priority is: PathfindingModifier → Part Material → Terrain Material

What if several parts/volumes overlap with each other?

  • We suggest you avoid this where possible; the results are unpredictable

Known Issues

  • Parts that are close to the surface of terrain will sometimes recognize the terrain material instead of the part sitting on top (fix is in progress)

We want to hear your feedback

We are excited to make this beta available to you. Let us know what you think and send us cool uses you come up with for the feature.

Special thanks to @Cinderstock for engineering work on this feature.

394 Likes

This topic was automatically opened after 10 minutes.

Finally, we can pathfind through closed doors! :partying_face:

Can we use Pathfinding Modifier zones on paths that don’t exist? (e.g. a bridge that raises and lowers)

21 Likes

I can’t possibly be the only one thinking this, but annotating costs for materials should be annotated by the Material enum, not by a string that matches the material name.

39 Likes

Oh god finally. I’m happy to see that Pathfinding Modifilers. Good job guys.

8 Likes

This is an amazing update, can’t wait to test it out (Right now).

Would love to also experiment with Costs. Seems very interesting.

5 Likes

Awesome! This update is great. I love it!
I was just wondering, why do we need to set something to math.huge if they absolutely can not cross past it?
What is the scale exactly? shouldn’t a 100 be enough?

Door = canCrossDoor and 1.0 or math.huge

6 Likes

Very cool features, would’ve been neat to have this on a past project I never finished. Opens doors to a lot of cool AI in city maps.

However, pathfinding still has some major problems, such as not taking into account CollisionGroups when calculating a path, which is a problem for many games. Hoping for more improvements in the future.

7 Likes

it’s a reverse scale. more then 1 = more scary. less then 1 = less scary. Mark lava bricks at 100 to make the AI more scared of walking into them. Mark racing shortcuts at math.huge to make sure the AI never uses them at all.

Additionall, canCrossDoor is defined two lines up, and is a bool depending on if the door’s pathfindable or not. I imagine the cost for false is the same as 1 since it’s not a number value.

7 Likes

What happens if a negative cost value is specified, and, is that frowned on? I would assume that it’d make the target always cost less than anything else with a positive cost since a negative number is always less than a positive one. And, similarly, since the costs are multiplicative, would the opposite of math.huge for costs then be 0, or would it be -math.huge?

Additionally, having looked at this before it was in beta and not really being able to deduce much about how it worked, I’m super impressed with just how much flexibility this provides! I had been trying to wrap my head around how this might work in a way that offers flexibility beyond just whitelisting/blacklisting parts, but, I am genuinely pretty impressed with this.

Not only does the implementation make this feature extremely powerful, it makes it very easy to use even for people who haven’t done a lot of programming before :grinning_face_with_smiling_eyes:

Also, is it possible to allow agents to ignore floors altogether, and do 3D pathfinding? For example, in a zero gravity environment, or, with agents which can fly. This would be extremely important to a lot of people’s use cases.

11 Likes

false likely isn’t a valid cost and would probably throw an error, or be ignored. The code produces 1 if canCrossDoor is true, or math.huge if canCrossDoor is false by using an and/or chain.

I like to read it like English as if canCrossDoor, 1.0, or math.huge, or I guess a less direct translation, if canCrossDoor, 1.0, else math.huge as this makes a lot more intuitive sense to me than trying to read it the way that it is.

@MagikTheWizard

Because a path which is 10000 studs long would have a cost of 10000 (assuming the cost is 1 per stud, which, it likely is based on voxels rather than studs). If you have a pathfinding modifier which makes the cost 100 for that modifier, it means that a 100 stud path through that region would cost 10000 too. So, the AI might favor a 100 stud path through a kill brick over a path which is 10001 studs long.

There is no scale exactly, you can think of the regular cost like the length of the path. The lowest cost path is also the shortest path (because if cost = length, the lowest length is also the lowest cost). When you give a cost multiplier in the Costs dictionary, it makes the path cost 100 times more. So, basically, it makes the determined length of the path longer to the pathfinder.

@c0mmandhat

If I understand correctly, the answer is yes. If the bridge were to be raised, there’d not be a walkable path over the bridge. If the modifier zone still overlaps part of the bridge, it’d just modify the surfaces of the bridge it overlaps with, so, there’d still be costs in the middle.

5 Likes

This is an unexcepted, but VERY welcome update. Thank you so much!

I’m especially glad that volumes are allowed to exist here, this will make annotating terrain and other components useful. I’m also glad we can specify costs over all, that is very useful.

16 Likes

Pretty sure you can’t use an enum as a key in a dictionary

4 Likes

Is there a way to use it for navigating system? If so, it’s gonna be very useful for it.

4 Likes

This is Lua. You can use almost anything as a key in a dictionary.

9 Likes

This programming language is a mess. I’m all in for it being Enums then.

5 Likes

Ha this is dope, so many updates to things that I am adding in my game are coming out, thanks for this

3 Likes
local foo = {
    [Enum.KeyCode.F] = "sdkask"
}

just encapsulate it in brackets like you would a spaced or invalid string

4 Likes

Sudden realization that I’ve used this before and that I could’ve literally proved myself wrong

5 Likes

This update is great. I love it!
I have a small question regarding the new update.

I’m a bit confused. This line of the script. Can we set randomly the numbers? I mean can we change the “math.huge” to 50?

Any help would be appreciated!

6 Likes