Pathfinding - ItsDeltin/Overwatch-Script-To-Workshop GitHub Wiki

Pathfinding

Overwatch Script To Workshop supports pathfinding players and dummy bots.

Some features include:

  • Take advantage of hero abilities using the attributes system.
  • Can be customized to prefer speed or performance.
  • Paths can be precalculated so the path to take from anywhere in the map to a certain destination will instantly be known.
  • Pathfinding can be adjusted according to the state of the game.

If you don't have OSTW already, see getting started.

Table of contents

Creating Pathmaps

The first step to pathfinding is to create a pathmap.

Copy the pathmap editor code by running the Overwatch Script To Workshop: Copy pathmap editor code command in Visual Studio Code. This will copy workshop rules used to create pathmaps to the clipboard. Paste the rules into Overwatch then enable the map you want to create a pathmap for.

Copy pathmap editor code command

The editor

Editor

There are 3 modes in the pathfinding editor: Free-Walk, Place Nodes + Segments, and Add Attributes. Press interact to cycle between them.


Pathmaps contain an array of nodes that will be navigated between. While in the Place Nodes + Segments mode, press primary fire to create a node (as shown by blue spheres). Pressing crouch + primary fire will delete the closest nodes and any segments connected to them.

Nodes

After creating nodes, press secondary fire to select them. Selected nodes will be green. Press ability 2 to connect the selected nodes with segments (as shown by grapple beams). Pressing crouch + secondary fire will deselect all nodes. Pressing crouch + ability 2 will delete all segments in-between the selected nodes.

Segments

With the nodes now connected, pathfinding can move between them.


At the top of the editor, you can see the Connect Mode text. This determines how all the selected nodes are connected with segments. There are 3 ways they can be connected: Connect All, Connect As Path, and Connect As Star. You can cycle through these by pressing reload while in the Place Nodes + Segments mode.

In this example, the nodes are selected in the order 3, 4, 5, 2, 6.

Connect All: Connects selected all nodes to each other.

Connect All Example

Connect As Path: Creates a segment between all the selected nodes in the order they were selected.

Connect As Path Example

Connect As Star: Creates a segment from the first selected node to all other selected nodes.

Connect As Star Example

Adding Attributes

Attributes can allow you to take advantage of hero abilities and block paths. For more information, view the attributes section.

Attributes can be added in the 'Add Attributes' mode. To add an attribute, select 2 nodes you want to add an attribute in-between, press reload and crouch + reload to set the attribute value, then press primary fire to toggle the current attribute value.

In the image below, CurrentSegmentAttribute will return [3] if the player is travelling from node 68 to 99, and will return [2] when traveling from node 99 to 68. To change the attribute when traveling from 68 to 99, select 68 first. Otherwise, to change the attribute when traveling from 99 to 68, select 99 then 68.

attributes

Compiling

When you are done creating your pathmap, execute the 'Acknowledge' communication to compile. (Default key is G.) You will see the messages Compiling... followed by Compiling Finished!. Open the inspector and click copy to clipboard current variables as CSV text. on the GLOBAL variable target.

Copy CSV values

With the CSV values copied to the clipboard, open vscode, press ctrl+shift+p then run the Overwatch Script To Workshop: Create pathmap from CSV clipboard command. This will reveal a prompt to save the exported pathmap file.

Create Pathmap Command Save File

Editing pathmap files

To open pathmap files in the ingame editor, run the Overwatch Script To Workshop: Copy pathmap editor code command with a pathmap file opened. Then paste the rules into Overwatch.

Copy pathmap editor code message File Editor

To save the edited file, follow the compiling steps again.

Using Pathmaps

The bulk of pathfinding functions found in OSTW are within the Pathmap class.

Pathmaps can be imported to your code like so:

globalvar Pathmap map = new Pathmap("path-to-file.pathmap");

Since OSTW supports object oriented programming, you can do something like this instead and it will work as expected:

globalvar Pathmap map;

rule: "Get map"
{
    // Get the Hanamura pathmap.
    if (CurrentMap() == Map.Hanamura)
        map = new Pathmap("hanamura.pathmap");

    // Get the Horizon Lunar Colony pathmap.
    else if (CurrentMap() == Map.Horizon_Lunar_Colony)
        map = new Pathmap("horizon.pathmap");

    // Get the Dorado pathmap.
    else if (CurrentMap() == Map.Dorado)
        map = new Pathmap("dorado.pathmap");

    // Current map not supported.
    else
        SmallMessage(AllPLayers(), <"The map '<0>' is not supported.", CurrentMap()>);
}

Or:

globalvar Pathmap[] maps = [new Pathmap("hanamura.pathmap"), new Pathmap("horizon.pathmap"), new Pathmap("dorado.pathmap")];

With your pathmap imported, you can run functions such as Pathfind, PathfindAll, or Resolve.

Common functionality

Static functions are called using the class itself, and object functions are called using an object reference. Parameters surrounded by [] are optional parameters.

Pathmap class

Pathmap map = new Pathmap(...);

// Object pathmap functions
map.Pathfind(player, destination, [attributes], [onLoopStart], [onNeighborLoopStart]); // Pathfinds a player to the specified destination.
map.PathfindAll(players, destination, [attributes], [onLoopStart], [onNeighborLoopStart]); // Pathfinds an array of players to the specified destination. Unlike the Pathfind function, this does not support moving players. If you are to pathfind an array of moving players, use the Resolve function instead.
map.PathfindEither(player, destinations, [attributes], [onLoopStart], [onNeighborLoopStart]); // Pathfinds a player to the closest position in the 'destinations' array.
PathResolve resolve = map.Resolve(destination); // Resolves a destination. This can be used to cache the path required to reach a destination and reduce server load usage.

// Static pathmap functions
define isPathfinding = Pathmap.IsPathfinding(player); // Determines if a player is pathfinding.
define isStuck = Pathmap.IsPathfindingStuck(player, [speed_scalar]); // Determines if a player takes longer than expected to reach the next node.
Pathmap.StopPathfind(player); // Stops the specified player from pathfinding.
define attribute == Pathmap.CurrentSegmentAttribute(player); // The current segment attribues of a pathfinding player.

PathResolve class

PathResolve resolve = map.Resolve(destination);

// Object PathResolve functions
resolve.Pathfind(players); // Pathfinds the array of players to the PathResolve's destination.

Not all of the pathfinding functions are listed here! Use the auto-completion to list all the pathfinding functions.

Pathmap functions

Attributes

Segments can be tagged with an array of attributes. Attributes can be used for many things, including:

  • Jumping over obstacles.
  • Taking advantage of hero abilities to navigate cliffs or terrain.
  • Blocking a path if a hero does not have the capabilites to transverse a route.
  • Blocking a segment if a payload gate is closed or opened.

You can assign different attributes to different hero abilties. For example, you can have an attribute 1 mean that pharahs will use their jump jets, or an attribute of 2 will have mei's create a wall underneath them. You can then program these actions in your code to activate when an attribute of 1 or 2 is reached.

If a segment is marked with an attribute, it will not be transversed unless the pathfinder function's attribute parameter is given the specified attribute.

Pathmap map;

// The player will pathfind through segments that have an attribute of 1 or 3, but not any segments that have any other attribute.
map.Pathfind(player: eventPlayer, destination: walkTo, attributes: [1,3]);

The Pathmap.CurrentSegmentAttribute function can be used to get an array of the current segment's attribute.

Example:

define eventPlayer: EventPlayer();

// This rule will cause the player to jump when an attribute of 1 is reached.
rule: "Pathfinder: Jump when an attribute of 1 is reached."
Event.OngoingPlayer
if (Pathmap.CurrentSegmentAttribute(eventPlayer).Contains(1))
{
    PressButton(eventPlayer, Button.Jump);
}

// Genji wall climb when an attribute of 2 is reached.
playervar define climbing;

rule: "Nav: Gengi: Climb Start"
Event.OngoingPlayer
Player.Genji
if (Pathmap.CurrentSegmentAttribute(eventPlayer).Contains(2))
{
    MinWait();
    StartHoldingButton(eventPlayer, Button.Jump);
    climbing = true;
}

rule: "Nav: Gengi: Climb End"
Event.OngoingPlayer
Player.Genji
if (climbing)
if (Pathmap.CurrentSegmentAttribute(eventPlayer).Contains(2) == false)
{
    StopHoldingButton(eventPlayer, Button.Jump);
    climbing = false;
}

Bakemaps

Bakemaps is a very fast pathfinding system. You can pathfind 12 players across the entire map and the shortest path will be found in a single tick (without waits!) and without crashing the server.

To create a bakemap:

// Live bakemap (slower to bake, pathmap and attributes can be set at runtime)
Bakemap bake = map.Bake(attributes:[0,1], printProgress: p => {
    // Show baking progress as a HUD text
    CreateHudText(AllPlayers(), <'Baking: <0>%', Min(100, p * 100)>, Location: Location.Top, SortOrder: 2);
});

// Compressed bakemap (*much* faster to bake, pathmap and attributes must be known at compilation)
Number i = 0;
Bakemap bake = map.BakeCompressed('original.pathmap', attributes:[0,1], printProgress: p => {
    // Show baking progress as a HUD text
    CreateHudText(AllPlayers(), <'Baking: <0>%', Min(100, p * 100)>, Location: Location.Top, SortOrder: 2);
}, onLoopStart: () => { // Override onLoopStart so baking is x5 faster
    i++;
    if (!(i % 5))
        MinWait();
});

Then pathfind with the bakemap:

bake.Pathfind(players, destination);

Resolving paths

Paths can be precalculated and reused using the Pathmap.Resolve() function. After a destination is resolved, pathfinding will instantly know the path to take from anywhere on the map with almost no affect on the server load.

Pathmap map = new Pathmap("my_pathmap.pathmap");
PathResolve obj = map.Resolve(ObjectivePosition(0));

...

obj.Pathfind(eventPlayer);

Server load VS algorithm speed

You can customize the pathfinding functions depending on if server load or speed is more important.

Some pathfinding functions contain optional parameters that can be used to determine where and when waits will occur while pathfinding. There are 2 main loops that make up pathfinding, where one is nested in the other. By default, Wait(0.016) will occur at the start of each of these loops.

In the example below, a wait will occur every 4 iterations of the nested loop.

define waitIterate = 0;

hanamuraMap.Pathfind(lastDummyBot, LookingAtSpot, onLoopStart: () => {}, onNeighborLoopStart: () => {
    waitIterate++;
    if (waitIterate % 4 == 0) MinWait();
});

Hooks

Hooks can be used to modify how pathfinding-generated rules are created. More information on hooks is provided in their completion. Hooks are set by assigning them in the rule-level, like so:

globalvar define myVariable;

Pathmap.OnPathStartHook = () => {
    // Specify additonal code to run when the 'Pathfinder: Resolve Current' rule runs.
};

rule: "My rule" { ... }

Currently, there are 5 hooks.

  • Pathmap.OnPathStartHook: The code to run when a path starts.
  • Pathmap.OnNodeReachedHook: The code to run when a player reaches a node.
  • Pathmap.OnPathCompleted: The code to run when a player reaches their destination.
  • Pathmap.IsNodeReachedDeterminer: The condition used to determine wether or not a player reached a node.
  • Pathmap.ApplicableNodeDeterminer: The code used to get a node from a position.

Incorrect chosen node example

When pathfinding occurs, the starting node is chosen by the closest node to the player. This means in the image above, if a player were to start pathfinding while on top of the arch, the chosen node will be the one underneith. This will cause the pathfinder to run in place, futily trying to get to the node. Overriding the Pathmap.ApplicableNodeDeterminer hook to consider line-of-sight will fix this:

// Set the 'ApplicableNodeDeterminer' hook to consider line-of-sight.
Pathmap.ApplicableNodeDeterminer = (Vector[] nodes, Vector position) => {
    // Return the index of the closest node that is in line-of-sight.
    return nodes.IndexOf(nodes.FilteredArray(Vector node => node.IsInLineOfSight(position)).SortedArray(Vector node => node.DistanceTo(position))[0]);
};

Dynamically changing pathmaps

You can change pathmaps at runtime using functions like AddNode, AddSegment, AddAttribute, etc.

In the jump pad example, jump pads predict the landing location and connect segments as needed. I recommend checking out the source code for more information on dynamically changing pathmaps.

Smoothing Paths

These Pathmap functions can be used to smooth out the path of pathfinding players and make it look more natural. Try adding this to your script:

rule: 'Pathfinder: skip if visible'
Event.OngoingPlayer
if (Pathmap.IsPathfinding(EventPlayer())) // Make sure the player is pathfinding.
if (Pathmap.CurrentNode(EventPlayer()) != -1) // This ensures that we do not skip when the player is walking towards their destination.
if (Pathmap.CurrentPosition(EventPlayer()).Y - Pathmap.NextPosition(EventPlayer()).Y~Abs() < 1) // Height of position and skipped node must be < 1 meters.
if (IsInLineOfSight( // Make sure the player's position and the next node is in LOS.
    StartPos: PositionOf() + Up() * .25,
    EndPos: Pathmap.NextPosition(EventPlayer()) + Up() * .25
))
{
    Wait(0.1); // A small wait so the player has a chance to walk past the corner (so they do not bump)
    Pathmap.SkipNode(EventPlayer());
    Wait(0.1);
    LoopIfConditionIsTrue();
}

The reason why this isn't implemented natively is because there may be some cases with pathmaps where this will skip nodes unintentionally, but it will be fine in most cases.

Examples

Jump-pads

This example allows you to create jump pads. The jump pads will dynamically create paths so dummy bots can use them.

The entire map except the last point is mapped out, I recommend mapping the last point yourself by editing the pathmap file with the pathmap editor.

Source code

Import code: AFAAX

  • Press crouch to summon a dummy bot.
  • Communicate hello to pathfind the dummy bot to the position you are looking at.
  • Press interact to create a jump pad. Pathfinding dummy bots will use the jump pad if it will be faster to take to reach the destination.