Unity based Terrain Analyzer that filters maps by strategic regions based on custom attributes
VIDEO TO COME...
The Terrain Analyzer explained below is a tool created to automatically analyze maps based on characteristics defined by the user. It returns to following:
- A traversability graph representing a grid based map of all traversable points and their connections (For use in pathfinding)
- SMALL MODIFICATION NEEDED TO IMPLEMENT THIS FEATURE
- An analysis graph, representing strategic regions on the map (For use in AI reasoning, and strategy)
- Analysis graph includes system for users to write custom attributes by which each node will be analyzed and given a value (For more precise AI reasoning, and different AI strategies)
Fig 1: Above is a UML diagram representing the current code. Major scripts roles are listed below
- Terrain Analysis: Master script that controls the analysis procedure. Responsible for input and output
- Analyzer: Analyzes map returning a traversability map and Veronoi graph (Used to find choke points)
- VoronoiFinalizer: Culls nodes in Veronoi Graph until only open regions and choke points are remaining
The Algorithm works using the following steps:
- Create a 2D grid representing height values and their positions
- Make a traversability map representing all movable positions and their connections
- Make a boarder map representing the outline of the traversability map
- Using border map, create a Voronoi graph
- Prune all edges with non-traversable nodes
- Compute a radius for all remaining Voronoi graph nodes, representing the nodes distance from an untraversable obstacle.
- Use various culling methods to remove all unneeded or unwanted nodes
- Return a simple analysis graph
Using the Terrain Analyzer
- Set up a Unity Terrain (DOES NOT WORK ON NONE UNITY TERRAIN MAPS, FEATURE COMING IN LATER VERSIONS)
- Tweek preferences. Parameter explainations listed below:
- World Distance
- Terrain X (TX): The size in unity globalspace units of the terrain along X axis
- Terrain Y (TY): The size in unity globalspace units of the terrain along Z axis
- Percentage of Real Space
- Grid X Percentage (GX): Each X axis grid line will be percent of real X length
- Grid Y Percentage (GY): Each Y axis grid line will be percent of real X length
- Map Preferences
- Max Reachable Height (MRH): Defines a height above which units cannot reach
- Max Traversable Slope (MTS): Defines max change in height still defined as movable between grid squares
- Radius Computation
- Iteration Distance (ID): Defines distance checked per iteration of radius calculation
- Num of Directions (#D): Defines number of directions circle is broken into for checking radius calculations
- Pruning Settings
- Min Node Radius (MNR): Defines the minimum radius that any one node can have without being pruned
- Min Region Radius(MRR): Defines the minimum region size that two nodes can occupy
- Max Region Height Difference(MRHD): Defines the maximum height difference two nodes can have and still be considered in the same region
- Corridor Percentage: When culling corridor nodes, controls possible variance in same corridor node sizes
- Corridor Constant (CC): Gives a constant variance for corridor size, to stop small corridors retaining nodes.
Creates 2D float array of size 100/GX by 100/GY. Grid at position (i,j) represents world space position (i * (TX * GX), j * (TY * GY)). Populates grid using Terrain.sampleHeight(), per grid square. This leaves a grid of height values.
Due to float number error, a GX and GY percentage must be chosen which always represents numbers of limited digits. Default percentages are 0.25, 0.5, 1 and 2. Smaller number represent denser grids, more accurate map outputs and more computation time.
Cycle nodes creating nodes for each grid square within Max Reachable Height. Create edges between nodes that abide by Max Traversable Slope. (See Fig 3)
Fig 2: Complete traversability map of example map based on parameters
Create a node map made up of nodes that count as border nodes outlining the moveable area. (See Fig 4)
Fig 3: Border map created on test terrain “Crater” with grid density = 0.5%
Code used from https://code.google.com/archive/p/fortune-voronoi/ library created by codeproject user BenDi with Mozilla Public License, v. 2.0
Using the above library, a Voronoi graph is created over all nodes.
Border nodes do not make up a polygon. This method was chosen because Voronoi graphs only work over 2D surfaces, and height is an important aspect in these terrains. Instead the naive Voronoi is generated, then culled using various methods. Edges containing vertices outside of map or at unreachable heights are pruned (See Fig 5).
Fig 4: Voronoi graph after non-traversable edges are pruned
Step 5: Compute a radius for all remaining Voronoi graph nodes, representing the nodes distance from an untraversable obstacle.
Iteratively increases radius until an object is hit (See Fig 6)
ID defines how much the radius expands iteration. An ID = 1 would check radius =1,2,3,4,....n until an obstacle is hit or the map bound is passed.
#D tells the radius how many angles to cut the circle into when iterating. #D = 4 would check every angle between 0 and 360 with increments of 90 degrees.
Stopwatch tests have shown that this is the most costly part of the algorithm.
Fig 5: Shows radius of every node after computation
Systematically remove nodes from the graph using different properties. The right combination of culls is needed to efficiently remove any unimportant nodes. Culling methods are listed below. (See Fig 7)
- Min Radius Cull: Removes all nodes with radius below certain number.
- Largest Nodes First: Starting at node with largest radius, removes all nodes within radius of node.
- Region Merge: The MRR and MRHD can be tweaked. Region merge looks at each node n and checks if n.neighbours() contains a node within MRR distance. If a node is found, the smaller neighbour is merged into the larger neighbour.
- Leaf Prune: Any node with one neighbour is considered a leaf. It’s neighbour is considered the parent. If the parent has a larger radius than the leaf, then the leaf is merged into the parent. Repeated until no nodes are pruned.
- Triangle Cull: For each node, checks both neighbours. If both neighbours are also neighbours, merges smallest node with largest node.
- Corridor Prune: If a node n has degree = 2, checks the radius of both neighbours. If both neighbours are within n.Radius() * corrdiorPercentage + corridorConstant - n.Radius, then remove n and connect n’s neighbours.
- Prune Zero Children: (Optional) Nodes can occur that have no children. Especially in regions that are more disconnected and so should only be represented by a single central node. When flying units that can bypass terrain are available this might not be desirable. Otherwise, culling all zero children nodes may be a better option
The order in which culling is performed in the presented algorithm is as follows:
- Cull Below Radius
- Largest Nodes First
- Region Merge
- Triangle Cull
- Leaf Prune
- Corridor Prune
- Prune Zero Children
Fig 6: Example of completely pruned Voronoi Graph
The final output of the algorithm feeds AI the following code structure (See Fig 8)
IAttribute is an interface allowing anyone using the code to write their own attributes. Attribute calculation code is written in the calculate() function and can represent anything. It must output a number between 0 and 1. Sometimes for unity world calculations, the creation of a calculator class extending MonoBehaviour is necessary since MonoBehaviour is required to interact with unity game objects.
Fig 8: Example attribute graph. Takes same structure as analysis graph, but applies float values as attributes based on IAttribute interface. The above graph is using a custom "hill" attribute which gives each node a value based on it's height compared to it's lowest neighbour.