Movement cost in 2D Grid Graph

Hi

For your use case, I think using tags is much simpler.
You can set a tag on each node (e.g. tagging nodes as Water). Then on the Seeker component you can specify which tags a unit can traverse, and at what cost.

Take a look at this page for more info: Working with tags - A* Pathfinding Project

Currently, there’s no built-in support to calculate the tag from a tilemap. So you’d have to write that code yourself. You can find similar code here: Graph Updates during Runtime - A* Pathfinding Project
That code changes the walkability, but you can instead make it change the tag.

You may want to use a separate graph for flying units, if they are supposed to be able to traverse basically anything. This is because when a node is fully unwalkable, the system can use a few other optimizations to make things behave better. When nodes are just tagged, it cannot make as many assumptions.
Take a look at Multiple agent types - A* Pathfinding Project for more information about that.

Yeah, I already read that Tag post.

I have tried to learn how to Tag node but I’m still stuck.

The video quality is too old and low quality, I can’t see anything at all. Sorry, my eye is bad.

How do I mark some node in the graph with tag ?
I can only do that by asset to graph data in code and set it by position ? or I can do that in Unity Editor?

I’d iterate over all nodes in the graph (like in the first link in my previous post) when the game starts.

AstarPath.active.AddWorkItem(() => {
    var gg = AstarPath.active.data.gridGraph;
    for (int z = 0; z < gg.depth; z++) {
        for (int x = 0; x < gg.width; x++) {
            var node = gg.GetNode(x, z);
            node.Tag = 1; // Do something here to calculate the tag
        }
    }
}));

So the only way to change node tag is Using Script and I must know which position in the Graph is Water/Swamp/etc…

I can’t get my head over of how to know exactly which node position in graph is related to special tiles in TileMap.

Hmm… I must calculate the position of those special tiles by custom script like access to Tilemap data and get Cell position and store them into an array, then access to graph and use cell position to get node ? I don’t know if I understand it correctly.

You can also do it using the GraphUpdateScene component. But for your use case, using scripting seems like the better solution imo.

You probably want to use Unity - Scripting API: GridLayout.WorldToCell

Yeah, I use WorldToCell a lot to interact mouse with TileMap.

What I mean, is the position of Node in Graph also same position as cell position in Tilemap ?
If yes, then I can call WorldToCell using Node position to know on that cell position has which tile.

That depends on how you have configured your grid graph.
The node.position field is an Int3 coordinate in world space. You can convert it to a Vector3 world position using (Vector3)node.position that you can then pass to WorldToCell.

1 Like

Thank you for information, it’s enough information for now, that helps me a lot.

I will try to tackle them again, really like your asset. :blush:

See you later when I face another problem which I can’t solve by myself.

1 Like

Hi Aron,

I have succeed pairing tilemap and graph to mark node with tag, then using below code to generate possible move node

var path = ConstantPath.Construct(unit.transform.position, unit.MovementPoints * 1000 + 1);

        path.traversalProvider = unit.TraversalProvider;

        // Schedule the path for calculation
        unit.GetSeeker().StartPath(path);

And the log for each GraphNode is correct also (Tag: 0 is grass ground, Tag: 1 is rock ground)
image

What I’m trying to do next are:

  1. Test prevent Unit walk on Rock Ground.
  2. Test Unit can walk on Rock Ground but cost higher move point.

So, I have some question:

  • I have used Seeker.StartPath instead of AstarPath.StartPath to work with Tag, I can prevent traversable by untick it, but how do I apply penalty point correctly for move point cost? At penalty 0-point, Unit with 4 move point can move 4 nodes, how many penalty point do I need if Unit with 4 move point can only move 2 nodes on that penalty node? (I have played with it but can’t get the correct result)
  • Beside Seeker + Tag are simple way “but it is also very limited in what it can do” like you said, please teach me how to use ITraversalProvider, I feel like this one is really powerful and flexible but because of lacking example and document about it, I can’t use it at all :face_with_spiral_eyes:

Also I have found another problem, I’m trying to use AILerp component attached to unit but the moment this code call

void GeneratePossibleMoves(UnitController unit) {
        var path = ConstantPath.Construct(unit.transform.position, unit.MovementPoints * 1000 + 1);

        path.traversalProvider = unit.TraversalProvider;

        // Schedule the path for calculation
        unit.GetSeeker().StartPath(path);

        // Force the path request to complete immediately
        // This assumes the graph is small enough that
        // this will not cause any lag
        path.BlockUntilCalculated();

        foreach (var node in path.allNodes) {
            if (node != path.startNode) {
                Vector2Int roundedPosition = new Vector2Int(Mathf.FloorToInt(((Vector3)node.position).x), Mathf.FloorToInt(((Vector3)node.position).y));

                if (MapManager.Instance.Map.ContainsKey(roundedPosition)) {
                    OverlayTile overlayTile = MapManager.Instance.Map[roundedPosition];
                    overlayTile.Show();
                    possibleMoves.Add(overlayTile);

                    overlayTile.Node = node;
                }
            }
        }
    }

I will instantly get Exception: This function only handles ABPaths, do not use special path types even though I haven’t call ai.destination or ai.SetPath() at all.
The only place I call AILerp is this function in UnitController

public void MoveToNode(GraphNode node) {

        var path = ABPath.Construct(transform.position, (Vector3)node.position);

        path.traversalProvider = TraversalProvider;

        _ai.SetPath(path);
    }

How do I get around with this ?

The AILerp script listens to any paths that the Seeker component calculates, and will try to follow them.
Generally if you are calculating paths which are not for the agent to follow, you should do it using AstarPath.StartPath.

var path = ...;
// Copy some settings from the Seeker
path.enabledTags = seeker.traversableTags;
path.tagPenalties = seeker.tagPenalties;
AstarPath.StartPath(path);

won’t that also bypass modifiers attached to the seeker? If so - how to include them when requesting path?

Thank you, it works perfectly now.

About my previous post, can you give me some advice about it?

1 is about penalty point and other one is ITraversalProvider.

It’s OK if you are too busy to tell me more about TraversalProvider but I hope in future there will be more document or example about it.

But the important thing is Penalty point of Tag, please help me, how do I apply it correctly to make Unit limit number of nodes they can move.

Hi

Here’s an example script for you:

Due to some limitations right now, making this behave just like you want it to requires some somewhat ugly tweaks.
The cost of a path is defined as:

sum(traversalProvider.GetTraversalCost(node) for node in path) +
sum(connection cost between each adjacent node in path)

There are two things that need tweaking:

  1. I don’t think you want to consider the movement cost for the start node, right? In the example code below, I have compensated for that by returning 0 for the start node.
  2. The connection cost between nodes can currently not be modified by the ITraversalProvider. This is something I’m working on improving. However, we can compensate for it by subtracting the default connection cost when we run the ITraversalProvider.GetTraversalCost method. This is pretty ugly, but it works for your use case.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Pathfinding;

public class MovementPointTest : MonoBehaviour {
    public int movementPoints = 2;

    class MyCustomTraversalProvider : ITraversalProvider {
        public uint defaultCostToMoveBetweenTwoNodes;

		public bool CanTraverse(Path path, GraphNode node) {
			return DefaultITraversalProvider.CanTraverse(path, node);
		}

		public uint GetTraversalCost(Path path, GraphNode node) {
            if (node == (path as ConstantPath).startNode) return 0;

			uint c = 0;
            if (node.Tag == 0) {
                c = 1000;
            } else if (node.Tag == 1) {
                c = 2000;
            } else if (node.Tag == 2) {
                c = 4000;
            }
            return (uint)(c - defaultCostToMoveBetweenTwoNodes);
		}
	}

    void Update() {
        var defaultCostToMoveBetweenTwoNodes = AstarPath.active.data.gridGraph.neighbourCosts[0];
        var maxGScore = 1000 * movementPoints;
        // +1 to make the path find all nodes up to *and including* the max g score
        maxGScore += 1;
        ConstantPath path = ConstantPath.Construct(transform.position, maxGScore, null);
        path.traversalProvider = new MyCustomTraversalProvider() { defaultCostToMoveBetweenTwoNodes = defaultCostToMoveBetweenTwoNodes };

        AstarPath.StartPath(path);
        path.BlockUntilCalculated();

        var allNodes = path.allNodes;
        foreach (var node in allNodes) {
            Pathfinding.Drawing.Draw.SolidBox((Vector3)node.position, Vector3.one * 0.5f, Color.red);
        }
    }
}

This produces a result like this:

where the blue region has tag 1, so it costs 2 movement points to traverse each node, using the above code.

It will indeed. But you you want to post-process the path using the Seeker’s modifiers you can use the seeker.PostProcess(path) method.
See Seeker - A* Pathfinding Project

Haha, even with limitations, it’s nice to see you still can give solution.

I got it, so with penalty point of tag alone won’t be enough to do it, must use TraversalProvider for this move cost.

Also thank you for example about TraversalProvider and the solution for move cost using it.

By the way, because I’m following your Block Manager code as below on UnitController

private void Awake() {
        TraversalProvider = new BlockManager.TraversalProvider(BlockManager, BlockManager.BlockMode.AllExceptSelector, new List<SingleNodeBlocker> { SingleNodeBlocker });
    }

And below is the modifed code for GeneratePossibleMoves base on your guide which include block all SingleNodeBlocker instance.

var path = ConstantPath.Construct(unit.transform.position, unit.MovementPoints * 1000 + 1);

        path.traversalProvider = unit.TraversalProvider;
        path.enabledTags = unit.GetSeeker().traversableTags;
        path.tagPenalties = unit.GetSeeker().tagPenalties;
        // Schedule the path for calculation
        AstarPath.StartPath(path);

Now it raises another problem, how to use BlockManager.TraversalProvider and MyCustomTraversalProvider together ?

You can put the block manager’s traversal provider as a field inside your own custom traversal provider. And then just forward the CanTraverse call to it.

1 Like

Hi, thank to your support, I have created CustomTraversalProvider to suite my need as below

public class CustomTraversalProvider : ITraversalProvider {

    private uint _defaultCostToMoveBetweenTwoNodes = AstarPath.active.data.gridGraph.neighbourCosts[0];

    private ITraversalProvider _blockManagerTraversalProvider;

    public CustomTraversalProvider(ITraversalProvider blockManagerTraversalProvider) {
        _blockManagerTraversalProvider = blockManagerTraversalProvider;
    }

    public bool CanTraverse(Path path, GraphNode node) {
        return _blockManagerTraversalProvider.CanTraverse(path, node);
    }

    public uint GetTraversalCost(Path path, GraphNode node) {
        if (path is ConstantPath && node == (path as ConstantPath).startNode) return 0;
        if (path is ABPath && node == (path as ABPath).startNode) return 0;

        uint cost = 0;
        if (node.Tag == 0) {
            cost = 1000;
        } else if (node.Tag == 1) {
            cost = 2000;
        }

        return cost - _defaultCostToMoveBetweenTwoNodes;
    }
}

It works perfectly for now, but I have encountered another problem relate to BlockManager.

Following document, I have attached 2 components into my Unit which are SingleNodeBlocker and UnitController, both of them have field public BlockManager BlockManager; which will be assign by Drag and Drop in Editor.

But when making my unit into a Prefab, the BlockManager can’t be assigned anymore, I can modify my UnitController to solve this but what about SingleNodeBlocker ?

How do you usually apply Blocking nodes method for prefab case ?

This is more of a general Unity question of how to handle prefabs.
Generally, you’d use a script to assign this reference at the same time that you instantiate the object into your scene.

1 Like

Yeah, I knew it, just asking to know if you handle your component in any build-in way.

Thank for your answer anyway. :wink:

1 Like