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.
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.
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.
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.
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)
What I’m trying to do next are:
Test prevent Unit walk on Rock Ground.
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
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);
}
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);
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:
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.
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);
}
}
}
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
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.
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.