Well, despite I’ve described already our approach in linked thread, but maybe worth to go a bit deeper here
In our game we have map, which can be any size (depends on loaded mission, endless mode ,challenge mode etc.), at level load we starting to generate all data required for map (as our levels procedural generated we can’t cache it AOT). In our game map built from map cells (each map cell is 5x5
Unity meters or 5x5
unit cells) for example map 100x100 in cell size will be 500x500 in Unity meters (unit cells 1x1
). Then we have implemented idea of Areas and Regions (combination of approaches mentioned in paper linked in my thread and here and approach which is developer of RimWold using). Region is a map segment with a maximum size of 20x20
Unity meters (unit cells) in our game, from every cell in the Region you are guaranteed to be able to go to any other cell in the same Region. Area is list of connected Regions - from every Region in Area you are guaranteed you can reach any other Region in the same Area, as result from every unit cell (1x1
) you are guaranteed you can reach any other unit cell in the same Area.
On image below, green cells - unit cells (1x1), gray cells - map cells (5x5), orange cells - maximum Region cells (but it doesn’t mean it’s always a Region of that size, it can be multiple Regions in that orange cell, I’ll describe that below)
Example below - green cells - is unit cells of one Region, rest of space in 20x20 Region cells are blocked as result we calculate bounds of Region, for simplifying calculations next and reducing unnecessary cells processing, yellow lines is borders of currently hovered Region. Blocks with white borders - is adjacent Regions connected with current one.
And as I mentioned before in one 20x20 cell can be multiple Regions
Example below is Areas - inside walls (closed area) is one Area, outside walls - another. Third one is example when we’ve opened gates, as result two Areas becomes one.
Every time in game something is built, destroyed, gates opened\closed, we invalidate
Regions in which bounds it’s happened, invalidate and remove flow fields generated for that region, and recalculate new
Regions on their place, update adjacent
Regions and recalculate
Area (in case new closed
Area appeared or previously closed becomes connected with any other). As result instead of recalculating whole map every time we only recalculate very small piece of map. All this data is enough for everything in the game - we always can easily early out when we need to check if unit can reach point or not (just compare
Area ID where unit stands and
Area ID where it should go - if they’re the same - he can if not - can’t). By this data we can easily build hierarchical A* pathfinding for crowds - we just finding the path on
Regions (20x20) cells and we can easily merge them if multiple units stands in the same
Region and finding path to the another
Region (again all requests path to the same
Region) we just finding path once for all of them. The same for building flow fields - instead of building field for every unit for whole map - we just build it for small region and for all units in the same
Region moving to the same
Region edge - it will be the same, as result we generate it once and cache, and when someone will find path through this region to the same region edge - we don’t need to recalculate that - it’s already been calculated we just use already created flow field, and many many other things.
How it’s implemented. We have our custom native container GlobalMap
which is associated with entity with class
(!) IComponentData, as result it’s stored on singleton entity, which can be tracked in dependency chains (we have custom mechanism for dependency tracking in systems which read\write from\to native containers, in case of GlobalMap
most of systems - read, and only one - write, as result dependency chain pretty straightforward). Inside this container we have many unsafe stuff. Arrays of different cell states (map cells) which indicates it’s water cell, or grass, or forest or cliff etc. Also it’s store multiple unsafe maps like UnitCell->RegionData, AreaId->Area, RegionLinkHash->RegionLinkData, PathKey->FlowFieldKey, and unsafe
FlowField native container which is storing map FlowFieldKey->RegionFlowField, where RegionFlowField is unsafe struct storing flow field vectors for every cell in this Region. And pathfinding itself doing some tricky unsafe stuff and allocate persistent memory right inside job when calculate and populate flow fields, we using our custom jobs for pathfinding, couple of additional unsafe structs for effective calculations, and heavily rely on IJobParallelForDefer.