xyz

I have forget to precise the units must move tile by tile, does it change something ?

So if i understand well, i just need to include these passable tiles (only red circles i mean, red arrows is to show the step passage) ?

  • xyz replied to this.

    TwistedMind If there are no obstacles in the straight line between start and end point then you don't need A* to calculate the path. Just "rasterize" the line that connects start and end point and you'll get the tile path. That's also the way to get paths between A* nodes when only corners are nodes. It's how A* is supposed to be used. Having every tile as an A* node is a massive overkill. A* should not be burdened with trivial non-obstacled movement from tile to tile.

    Ah... that radically change my interpretation about the A* system...

    Just "rasterize" the line that connects start and end point and you'll get the tile path.

    Excuse me, you lost me at this point. An exemple: My unit is in position (0, 0) and i click to the position (5, 0).

    I see, on my screen, there are no obstacles but there is actually no any kind of "connexions" too, how the unit supposed to do to "know" it must pass to (1, 0), (2, 0), etc ?

    Second example: if the unit must go to the same point 0, 0 -> 5, 0, but there are a rocks on pos (3, 0), it must use astar ?

    So that's means:

    If obstacle detected:
         move with astar
    else:
         move from or with something

    Something is dark here: how the unit may detects something without astar ?

    • xyz replied to this.

      TwistedMind Ah... this concept is radically change my interpretation about the A* system...

      Yes, you've been misusing A*, that's why you get this enormous slowdowns. I'd suggest to switch to Godot 4 if you can because AStarGrid2D will handle all of the described things for you. If you still want to use 3.5 then cram as much work as you can on AStar2D. However in that case you'll have to handle some things on your own.

      The main thing you need is a function that returns a list of tile coordinates that represent a direct linear path between two tiles. Something like:

      get_direct_tile_path(Vector2i(0, 0), Vector2i(5, 0))

      This should return [(0,0), (1,0), (2,0), (3,0), (4,0), (5,0)]
      Having this function is crucial for all of it to work.
      So once you have the function, you should setup A* as follows. This runs only once after the map has been generated:

      • Iterate through all tiles and determine which ones are corners. Those are your A* nodes.
      • For each corner (A*node) find all other nodes that can be connected to it with a linear path without obstacles. Here you can use your get_direct_tile_path() function to get the tiles and then check if any of them is an obstacle.
      • Now you have nodes and non-obstacled connections. This can be directly fed into AStar2D

      Now for each individual pathfinding:

      • Ask your AStar2D to get you the path between your start/end. It will return a list of A* waypoints, the start/end points may or may not be A* nodes. This list now represents a non-obstacled movement path from start to end via waypoints
      • And finally use your get_direct_tile_path() function on each two consecutive waypoints to get the final tile-to-tile movement path.

      I definitively can't use Godot 4.x because something is too old in my computer (i tested and it's catastrophic, probably graphics by old OS)... I dont have as choice to continue with 3.5.2.

      I don't know and i can't identifiate something similar to "get_direct_tile_path" but something comes in my mind: is it possible to draw a virtual line from A to B but is it possible to compare the "pixels" of this line with mapgrid ? Or maybe another option in another name ?

      • xyz replied to this.

        TwistedMind I don't know and i can't identifiate something similar to "get_direct_tile_path" but something comes in my mind: is it possible to draw a virtual line from A to B but is it possible to compare the "pixels" of this line with mapgrid ? Or maybe another option in another name ?

        Yep, you identified the problem correctly. It's basically the same thing as line raseterization/pixelation. There's a well known classic algorithm for that: Bresenham's algorithm. You can probably find many implementations around the web. Note that there will be slight difference in implementation depending on whether your system allows for diagonal movement or not.

        You can start by looking here, and search around some more.

        Ok, i think all almost in place: i have a list with all valid points for the build-in astar system and i adding these points to the Astar instance.

        for linkPt in range(0, test2map.size()-1):
        	astarInstance.add_point(linkPt, test2map[linkPt])

        But 2 things worry me actually:

        1. Astar system works with ID, i don't find how to catch the id of previously assigned points to connect the neigbors;
        2. Some points in the map arent't included, because they have more that 2 "corners", what append if the unit must go in these missing points (especially for the bulldozers).

        Does it means the points connexion is done just before the move order or should i connect the points first and how to connect from ->to with neigbors ?

        • xyz replied to this.

          Yes. To let Astar to find a way, it needs to have coordinates, right ?

          Each point has it's own id (this is like that i understand the doc), but it's strange: why they should have an id ?

          • xyz replied to this.

            TwistedMind Yes. To let Astar to find a way, it needs to have coordinates, right ?

            You did exactly the opposite of what I described in my breakdown of A* usage. You don't want all passable tiles to be A* nodes. This is again a massive overkill and it's not how A* is supposed to be used. Only the obstacle corner tiles should be A* nodes.
            Look at my pixelated diagram again. Nodes are only at the obstacle corners. And the actual node points should be at centers of such tiles.

            If black tiles on the above diagram are your non-passable tiles then red dots (and only red dots) should be A* nodes. No additional nodes are necessary.
            If you proceed with such massive number of redundant nodes, you may experience slowdowns again, even with engine's AStar2D doing the pathfinding.

            I have well understood the first part. Not the second aparently...

            So, here the points which should be A* nodes. At this step, what i have to do ?

            For each corner (A*node) find all other nodes that can be connected to it with a linear path without obstacles. Here you can use your get_direct_tile_path() function to get the tiles and then check if any of them is an obstacle.

            The previous screen is the result of this, but its not what you said ?

            • xyz replied to this.

              TwistedMind So, here the points which should be A* nodes. At this step, what i have to do ?

              This now looks mostly correct as far as the A* point selection goes. It seems, though, like couple of them are still missing, namely the ones that are at the multiple corners. So check how you determine if a tile is at the corner.

              Once you have this, you need to iterate through all possible pairs of those points, draw an imaginary line between two points in a pair and check if that line intersect any non-passable tiles. If it does not intersect such a tile (i. e. the points "see" each other) then those two points have an A* connection. You need to find all those connections and pass them to AStart2D along with the points. The points are added via AStar2D::add_point() and connections are made using AStar2D::connect_points()

              And that would be your initial A* setup you run only once for a map.

              Like this ?

              How i can connect these points, if the godot system check in this order ?

              • xyz replied to this.

                TwistedMind Like this ?

                Not quite. You don't need "inner" corners. To find out if a tile is at the corner I'd do something like this:
                if top-left adjacent tile is non-passable and top and left adjacent tiles are passable - we have a corner. Check respectively for 3 other diagonally adjacent tiles (top-right, bottom-left and bottom-right). If this is true for any of them then the tile is at the corner, otherwise it's not.

                To test for connections you'll need to implement that get_direct_tile_path() function I was mentioning earlier, hell or high water. This function should be pure math. It only has to deal with coordinates. Doesn't need to concern itself with contents of an actual TileMap.

                3 valid points them:

                var corners = [Vector2(1, 1), Vector2(-1, 1), Vector2(-1, -1), Vector2(1, -1)]
                # 5 = dirt cell
                for pickCell in get_used_cells_by_id(5): 
                	var valid = 0
                		
                	if pickCell.x > 0 and pickCell.x < sizeMapX and pickCell.y > 0 and pickCell.y < sizeMapY:
                		for point in corners:
                			var newPoint = pickCell + point
                				
                			if get_cellv(newPoint) == 5:
                				valid += 1
                		if valid == 3:
                			astarLinkMap.append(pickCell)

                To test for connections you'll need to implement that get_direct_tile_path() function I was mentioning earlier, hell or high water. This function should be pure math. It only has to deal with coordinates. Doesn't need to concern itself with contents of an actual TileMap.

                I'm sorry but i really not understand... I understand this actually, in practice:

                for astarCell in astarLinkMap:
                	for astarCell2 in range(0, astarLinkMap.size()-1):
                		if astarCell != astarLinkMap[astarCell2]:
                			var linePath = traceLine(astarCell, astarLinkMap[astarCell2]) # ----> this is "get_direct_tile_path()"
                
                			if linePath.has(astarLinkMap[astarCell2]):
                				for point in linePath:
                					if astarLinkMap.has(point) == false:
                						test2map.append(map_to_world(point)) # -----> for red point when i launch the game

                if i do that, this will give something like the result of the first screen i have done.

                • xyz replied to this.

                  TwistedMind 3 valid points them:

                  Your code for determining corners doesn't implement the proper check I described. It just checks if 3 diagonally adjacent tiles are not passable (or something like that, can't really tell because you use non-symbolic constants). This in not the sufficient criteria. It need to check if a diagonal is not passable AND if two related non-diagonals are passable. Something like this:

                  func is_corner(cell: Vector2i) -> bool:
                  	for i in range(-1, 2, 2):
                  		for j in range(-1, 2, 2):
                  			if not is_passable(cell + Vector2i(i, j)) and is_passable(cell + Vector2i(i, 0)) and is_passable(cell + Vector2i(0, j)):
                  				return true
                  	return false 

                  As for the second thing, we'll get to it when you fix the corner identification. One thing at a time please 🙂

                  Now it should be fixed. This time, the top-left of red squares are centered in center of the cell.

                  • xyz replied to this.

                    TwistedMind Yeah, I think this looks good now. For a nicer preview it would be better to align red square centers to the tile centers.

                    Now for the second thing. Your code was adding all points gotten from traceLine() to the AStar2D. This is not what you want to do. Only the red points from your last screenshot go into AStar2D. I'll repeat: only that points are A* points.

                    When you get path tiles between two red points from traceLine(), you need to iterate through them and check if all of them are passable. If yes, then you add a connection between endpoints of that line using AStar2D::connect_points(). You DON'T add all of the path points. That would defy the purpose of using A* in the first place. Again; no points should be added besides the red points from your last screenshot.

                    So if i understand:

                    • Add reds points position with a ID as 0 -> list_of_point.size()-1 in astar system;
                    • Selecting unit number 9, use traceline(), no obstacles detected;
                    • I must do AStar2D::connect_points(from ID start, to ID end)

                    How i get each points from -> to, to move the unit ? I suppose it's with astar.get_point_path() but i need to get the ID point with astar.get_closest_point() ?

                    If you take the unit number 30 ?

                    • Try yellow path, use traceline() from unit's position to redpoint, no obstacles detected;
                    • I must do AStar2D::connect_points(from ID start, to ID end), so that's means connects an existing point with an ID (the end), with a point which doesn't exist and with no ID (where unit 30 is actually);

                    Godot will undertand this anyway ?

                    And if i take the black path ? There are obstacles. What must be done at this step ?

                    • xyz replied to this.

                      TwistedMind
                      What you seemingly don't understand is that there are two major steps when using A*:
                      Step 1: Setup of the permanent A* data structure that lets you calculate any path
                      Step 2: Using that data structure many times to calculate particular paths during gameplay.

                      Step 1 is done only once in entire runtime, so step 2 can then be done efficiently as many times as needed. The main purpose of step 1 is to precalculate an acceleration data structure (A*) that will later be used for efficient pathfinding. In our case, giving proper points and connections to AStar2D is the step 1.

                      We're currently trying to do step 1. So until this is done you should not think about units at all. Just deal with the proper A* setup. When this is done, you'll see how it all comes together on step 2.

                      TwistedMind So if i understand:
                      Add reds points position with a ID as 0 -> list_of_point.size()-1;
                      Selecting unit number 9, use traceline(), no obstacles detected;
                      I must do AStar2D::connect_points(from ID start, to ID end)

                      Almost.

                      • Units are not important just yet, remember we're still on step 1: setting up an acceleration data structure.
                      • Put all red points in an array, and consider an array index of a point to be its ID (as you already concluded)
                      • In another array, put all ID pairs that can be connected with a line without hitting any obstacles (determined by the help of traceLine())
                      • Feed the first array to AStar2D as points
                      • Feed the second array to AStar2D as connections

                      If done properly, that would conclude our step 1.