Conversation
Collaborator
Author
Collaborator
Author
This is fixed now with the last commit, as far as I could tell from the testing I did. |
6b45c38 to
77a4dcc
Compare
The pathfinder's three std::map structures had significant per-iteration overhead from tree node allocation and O(log n) traversal: - openNodesList (map<float, vector<Node*>>): replaced with a std::vector<OpenListEntry> maintained as a binary min-heap using std::push_heap / std::pop_heap. Tie-breaking by insertion sequence number preserves deterministic expansion order. - openPosList (map<Vec2i, bool>): replaced with std::unordered_set<Vec2i, Vec2iHash> for O(1) average membership tests instead of O(log n). - closedNodesList (map<float, vector<Node*>>): eliminated entirely. The only use was finding the best partial-path node when the node limit is reached; this is now tracked with a single bestClosedNode pointer updated on each expansion. Also align pathFindNodesAbsoluteMax (pool size) with pathFindNodesMax (search limit): both are now 2000, so the extended retry path can actually find more nodes than the normal search. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When A* hits its node limit and the unit has been failing to find a path for 2+ consecutive frames, the direct route is likely blocked by a large obstacle (e.g. trees covering the front of a base with only a rear opening). In that case the unit should explore around the obstacle rather than keep pressing toward the blocked front. Introduce a heuristicWeight field on FactionState and an optional parameter on aStar(). Normally weight=1.0 (standard A*). On the extended retry triggered by repeated node-limit failures, weight=0.25 is used, which strongly reduces the heuristic's pull toward the goal and causes the search to expand much more uniformly — effectively closer to BFS — so it can discover routes that initially move away from the target before circling around. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 200-node cap applied to units failing 3+ consecutive frames was also being applied inside the exploratory retry call (heuristicWeight < 1.0), neutralising the wider search. Skip the cap when in exploratory mode so the retry actually gets the full node budget to search around large obstacles. Also trigger exploratory mode after just 1 consecutive failure instead of 2, so it kicks in on the first retry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The retry was gated on (maxNodeCount != pathFindNodesAbsoluteMax), but this PR equalised both to 2000, making the condition permanently false. The exploratory search never ran. Replace the dead guard with an isExploratoryRetry flag that prevents infinite recursion while allowing the retry to fire whenever the node budget is exhausted on a normal search. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
isLastPathfindFailedFrameWithinCurrentFrameTolerance() was hardcoded to return false, permanently disabling the exploratory retry path. Enable it with a 60-frame cooldown (~1.5 s at 40 fps) so units that exhaust their normal node budget get one retry per cooldown period. Also increase the node pool and exploratory budget from 2000 to 8000 nodes. With weight=0.25, 8000 nodes covers roughly a 50-cell radius in open terrain, giving the search a realistic chance of finding paths that require long detours around large obstacles like tree clusters or mountains. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
With a 60-frame cooldown, the 5-waypoint partial path from each exploratory retry was exhausted in ~10 frames, leaving 50 frames where only normal A* ran. Normal A* bestClosedNode points in a different direction each time, causing visible back-and-forth oscillation at 1x speed. 10 frames matches the typical partial-path lifetime so the exploratory retry fires again just as the previous path is consumed, keeping units consistently moving in the same direction rather than oscillating. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ldown When normal A* exhausts its node budget and triggers an exploratory retry, a short partial path is built toward the goal. Once that path is consumed (typically in 1-2 frames), the next A* call during the cooldown window previously fell through to bestClosedNode, which pointed in a different direction (toward open space), causing visible 1-tile back-and-forth oscillation in congested narrow corridors. Now, when we are still in cooldown for the same destination, we return tsBlocked instead of emitting a bestClosedNode path. The unit waits until the cooldown expires and a fresh exploratory retry fires. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
77a4dcc to
b3b9078
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Possible related to #277
We haven't tested this yet... I'll report back when we do.
The pathfinder's three std::map structures had significant per-iteration overhead from tree node allocation and O(log n) traversal:
openNodesList (map<float, vector<Node*>>): replaced with a std::vector maintained as a binary min-heap using std::push_heap / std::pop_heap. Tie-breaking by insertion sequence number preserves deterministic expansion order.
openPosList (map<Vec2i, bool>): replaced with std::unordered_set<Vec2i, Vec2iHash> for O(1) average membership tests instead of O(log n).
closedNodesList (map<float, vector<Node*>>): eliminated entirely. The only use was finding the best partial-path node when the node limit is reached; this is now tracked with a single bestClosedNode pointer updated on each expansion.
Also align pathFindNodesAbsoluteMax (pool size) with pathFindNodesMax (search limit): both are now 2000, so the extended retry path can actually find more nodes than the normal search.