I've been reading a few threads and bug reports lately, and I've seen how many people complained about the FPS in the new version.
Lots of people have also said that the most likely culprit is pathfinding.
So, I studied a bit about it, and read
this, and as a CS student, I've come up with an idea.
First, though, about the way the paths are calculated:
It uses the A* algorithm and the Manhattan heuristic.
It also uses a connectivity chart so that pathfinding doesn't use too many resources on dead ends.
The problems presented there?
Manhattan isn't the best heuristic: X+Y+Z isn't very efficient when units can move in diagonal directions.
(this can be easily changed to Z + max(X, Y) * 1.4 + min(X, Y))
Connectivity charts take lots of resources to update, and when there's lots of changes (bridge raising/lowering, door locking/unlocking, water) it has to update more often. It also doesn't really speed up pathfinding: it only cancels paths which are absolutely not possible.
My proposed solution:
A* is an algorithm where one assigns a value to every node that is added to the list of possible nodes a unit will travel.
At the current time, if a dwarf wants to path the other side of the map, he/she has to path the entire map.
##########################
# # # # #
# = # ======= #
# ===#= # =# D#
####=#####=####=##########
# ===# ======#=======#
# ===# #=======#
# ===# #=======#
#####=##############=#####
# ===# =======# ======#
# ============= =#
# S# # #
##########################
S is Start,
D is Destination,
# is a wall, and
= is a path the dwarf would look for.
In DF, every tile is a potential node.
What if, instead, we had another structure, a macro node? One that is placed in places like doorways or intersections, or on a large plain, in intervals of 8/10/16 tiles?
Of course, finding a way to place these will be a pain, but what if we left it to the user to define? If anything, we can define nodes for the outside, and so that would be a much faster whenever one has to path outside.
Nodes will have a connectivity chart with its closest distance to another node, pre-calculated. While calculating, you could toggle a flag so that pathfinding goes back to standard A* without the nodes.
Macro nodes will only need an X, Y, and Z co-ordinate, and any node room they're connected to.
Another structure, a 'node room', sort of, will contain a list of nodes. Simply put, it will make a boundary using its list so that it is easier to calculate starting/ending nodes.
However, when it's finished calculating the connectivity chart, the speed will be much faster.
The letters on the left are the node it's pathing from.
The letters on the top are the node it's pathing to.
A numeral is the length of the shortest path, rounded.
A letter is an intermediate length: this is just so I have less work. In the real table, it could use that, or it could calculate all paths.
The diagonal is grayed out: the computer shouldn't look at these paths.
A B C D E F G H I
A 0 d 4 3 d c c c c
B e 0 e e 3 e e e e
C 4 a 0 a a 4 f f f
D 3 e a 0 6 a a a a
E d 3 d 6 0 d d d d
F c c 4 c c 0 h 5 h
G i i i i i i 0 i 4
H f f f f f 5 i 0 9
I h h h h h h 4 9 0
##########################
# # #b-e# #
# a-c a a-d# b b- #
# # # # D#
####c#####d####e##########
# # # #
# c-f # d-e # g- #
# # # #
#####f##############g#####
# # # #
# f-h h h-i i g-i #
# S# # #
##########################
This way, the computer does these steps:
- Dijkstra's algorithm for the start path; it uses the node room list to find which one it's in.
Dwarf is in room f-h, but the computer does not know that.
The computer has its list of node rooms.
Dijkstra starts from Dwarf.
In a few steps, it finds node H.
The list is narrowed down to f-h and h-i.
The a few more steps, it finds node F.
The list is narrowed down to f-h.
Because it's the only one left, it returns f-h as its node room.
- Dijkstra's algorithm for the end path,
- The start path chooses a random node from its room,
- Calculate the macro-nodes, which is pretty fast (use Manhattan heuristic with the X, Y, and Z of the nodes),
- Use the room node closest to the end of the path as its start,
- Path to the first node,
- On each step onto the node, or in its vicinity, path to the next node.
- On the last node, path to the destination.
This way, pathing should require less resources and time, as the path will be much more direct.
S -> f -> c -> a -> d -> e -> b -> D
##########################
# # # # #
# ==a # b====== #
# = #= # =# D#
####c#####d####e##########
# = # ==== # #
# = # # #
# = # # #
#####f############## #####
# = # # #
# = #
# S# # #
##########################
Calculations:
9 heuristics calcs for Macro
~10 for S -> f
~10 for f -> c
~7 for c -> a
~3 for a -> d
~10 for d -> e
~3 for e -> b
~15 for b -> D
Total: ~ 67
Highest amount of memory used at a time:
15 nodes' worth
The old way:
~130-150 heuristic calculations and 150 nodes' worth of memory.
In the examples listed above, the old way would take ~130 to ~150 calculations, and the new way would take ~70, and the highest amount of memory needed at once is 150 nodes' worth compared to 15. This might not be much of a difference, but it's a very small proof-of-concept example that would be expanded upon exponentially when on an actual DF map, so it would probably increase FPS rate dramatically.
tl;dr: nodes and stuff
Thoughts?