I still think the HAA* is the way to go. It allows us to minimize the amount of data we need in memory and provides an efficient way to both deal with changes to the environment and all the different sizes and movement types. The algorithm would need a few adjustments but I think its manageable.
For the Annotated A* portion of the algorithm (ignoring the hierarchy portion of the algorithm) I can see it working like this.
Two types of clearance metrics (transportation types). One is a main type, in DF's case this would be Ground and Air. The other is a secondary type. For DF I think you'd need Water, Fire, Smoke, Magma, obstacle (all), and obstacle (no pet). Each main type would have a pre-calculated clearance value stored in the map. Each sub-type would be regulated for an on-the-fly clearance calculation. This saves a lot on memory and, as you can see by the sub-types, they're infrequently encountered and/or very dynamic. This makes calculating them on the fly the best option anyways.
Each tile would have to be tagged with a main type and optionally tagged with any relevant subtypes. In DF's case every Ground tile would also be an Air tile but not vice versa. Creatures can only navigate over tiles of the appropriate type (ground or air) and for each tile calculated in the path finding algorithm the subtypes would be checked before placing it on the priority queue used by A*. To check for subtypes for a given square you simply recalculate the clearance for a square up to the creature's size but counting any inappropriate subtypes as a blocking tile.
Handling non square creatures provides an interesting twist to the algorithm. The clearance based algorithm will actually have to be modified to handle non-square multi-tile creatures and the z-axis. I'd change the algorithm like this:
Each tile has the following stored (all short ints):
- Maximum x axis clearance measured left to right
- Maximum y axis clearance measured top to bottom
- Maximum z-axis clearance measured from low to high
- Maximum square clearance measured left->right and top->bottom
- Minimum z-axis clearance measured across the square above
It sounds like a lot to store but I don't really think it'll be too bad, some values like maximum z-axis clearance could be changed to be calculated on the fly as it may not be checked very often.
The way we can check if an entity can fit in a particular tile would go through these steps:
1. Is creature's x and y sizes < the Maximum square clearance and is the creature's height below the minimum z-axis clearance for this square (all pre-calculated values)? If so creature can fit on tile and end.
2. Starting at the specified tile and moving from left->right based on creatures x size check the following:
- Is Maximum y-size for this tile > creature's y length
- Is maximum z-size for this tile > creature's z height
3. Starting at the specified tile and moving top->bottom based on the creature's y size check the following:
- Is the maximum x-size for this tile > creature's x length
- Is maximum z-height for this tile > creature's z-height
4. For the remaining tiles that the creature would be occupying if he stood at our starting tile check the z-height to make sure he fits.
Essentially you are checking each tile one to make sure that each tile is clear if the creature stands at the rectangular tile given. Since we pre-compute all these values it's a O(n) algorithm where n = # of tiles a creature occupies, but it gets better than that. By using the square properties that are also pre-calculated this algorithm could be even further optimized. For example if we wanted to fit a 3x2 creature in a tile at x,y and our square size was 2 then if the square size of the tile at x,y-1 was 2 our creature could fit in the given location.
Handling updates to the world is relatively cheap.Yes you have to recalculate clearance values but it's super easy to do and only requires a minimal amount of clearance values to be updated. You only need to recalculate clearance values for tiles to the left (lower x value), 'above' (lower y value), and lower down (lower z-value). All other tiles will be unaffected. The way the algorithm would work is you'd run four loops
1. Would check all tiles in a straight right->left line until an obstacle was hit
2. Would check all tiles in a straight bottom->top line until an obstacle was hit
3. Would check all tiles in a upper->lower line until an obstacle was hit
4. Flood fill of all tiles with a lower x, y, and/or z value (basically all tiles not covered by the loops above). The tricky part here is knowing when you can stop checking tiles, I believe it's possibly to generally only update a minimal number of tiles with a clever algorithm to do this.
This is just a rough outline of how something like this could work, I'll organize and collect my thoughts more as I look into it but I really do think it's the way to go for this sort of problem. It provides a way to efficiently store all the data we need for multi-tile pathfinding while also providing a potentially efficient algorithm.
Also remember that if a multi-threading solution is used performance becomes much less of an issue (though still important obviously). Given that creatures can take >1 frame to make a movement we can potentially have leeway in how long we have to calculate their specific path.