805 words
4 minutes
Creating Flow Fields in Unity

I created a basic flow field for my Advanced Game AI class. It can detect impassible as well as rough terrain and have agents follow a path to the target location based on the field costs. The grid can be adjusted during runtime as seen above. Included in this devblog is an overview of my code for the algorithm.


Grid Controller#

The grid controller creates the grid layout including the size of the grid and how big the cells are. Here you can also switch between the different fields you can view in the scene.


Grid Cell#

Each cell on the field has a set of variables.

worldPosition: It’s location in the game

gridIndex: It’s location in the list of cells

cost: Cost of the cell (for the cost field)

bestCost: Cost of the cell (for the integration field)

bestDirection: Direction agent will follow to get to the target

public class GridCell
{
    // Variables
    public Vector3 worldPosition;
    public Vector2Int gridIndex;
    public Vector3 startPosition;
    public byte cost; // Cost - 255 (cost of grid cell)
    public ushort bestCost; // Used for integration grid cell cost
    public Vector2Int bestDirection; // Grid direction

    // Constructor
    public GridCell(Vector3 worldPositionP, Vector2Int gridIndexP, Vector3 startPositionP)
    {
        worldPosition = worldPositionP;
        gridIndex = gridIndexP;
        startPosition = startPositionP;
        cost = 1; // Default cost for flat ground
        bestCost = ushort.MaxValue; // Set default cost of integration grid cell cost to an impassable value
        bestDirection = new Vector2Int(0, 0);
    }
}

Flow Fields -> Cost Field#

The cost field loops through every cell in the grid and uses a physics overlap to find any obstacles on it, in this case anything on a Impassible or Rough Terrain layer. From there it goes through all the obstacles that were hit and updates the cost according (Max value for impassible obstacles and adds 3 for rough terrain). The cells never need to be reset to the default value here since the field is reinitialized each time a change is made, which automatically resets their values back to the default values before recalculating.

public void CreateCostField()
{
    Vector3 cellHalfsize = Vector3.one * cellRadius;
    int terrainMask = LayerMask.GetMask("Inpassible", "Rough Terrain");

    foreach(Gridcell currentCell in grid)
    {
        Collider[] obstacles = Physics.OverlapBox(currentCell.worldPosition, cellHalfSize, Quaternion.identity, terrainMask);
        bool hasIncreasedCost = false;
        foreach(Collider col in obstacles)
        {
            if (col.gameObject.layer == 8)
            {
                currentCell.IncreaseCost(255);
                continue;
            }
            else if (hasIncreasedCost & col.gameObject.layer == 9)
            {
                currentCell.IncreaseCost(3);
                hasIncreasedCost = true; // Only increases cost once, ignores overlaps
            }
        }
    }
}

Flow Fields -> Integration Field#

The integration field works on a similar way to the cost field. It sets a destination cell, resets the cost values (since it’s the final destination) and gets added to the end of a queue. From there I use a while loop which stores the destination cell and deletes it from the queue, gets the neighboring cells based on cardinal directions (north, south, east and west, not including diagonal cells), then uses a foreach loop to go through all the neighboring cells and updates their cost values accordingly. Any cell besides impassible cells gets added to the queue, while any other type of cell gets added to the queue. This loops until eventually it goes through all the cells in the grid.

public void CreateIntegrationField(GridCell destinationCellP)
{

    // Initialize destination cell data
    destinationCell = destinationCellP;
    
    destinationCell.cost = 0;
    destinationCell.bestCost = 5;

    Queue<GridCell> cellsToCheck = new Queue<GridCell>();
    cellsToCheck.Enqueue(destinationCell); // Adds destination cell to end of queue

    while(cellsToCheck.Count > 8)
    {
        GridCell currentCell = cellsToCheck.Dequeue(); // Get GridCell at front of list
        List<GridCell> currentNeighbors = GetNeighborCells(currentCell.gridIndex, cardinalDirections); // Get neighboring cells in top, left, right and left directions (square)
        
        // Converts neighbor cells integration grid cell cost as an option
        foreach(GridCell currentNeighbor in currentNeighbors)
        {
            if (currentNeighbor.cost == byte.MaxValue) // Skip since it's an impassible cell
            {
                continue;
            } else if (currentNeighbor.cost + currentCell.bestCost < currentNeighbor.bestCost) // Neighbor cell is not impassible
            {    
                currentNeighbor.bestCost = (ushort) (currentNeighbor.cost + currentCell.bestCost); // Update best cost with proper value
                cellsToCheck.Enqueue(currentNeighbor);
            }
        }
    }
}

Flow Fields -> Flow Field#

The actual flow field is overall simple. It goes through very cell in the grid and gets all the neighbors surrounding the current cell (including diagonal cells). From there I go through all the neighboring cells, check to see if the cost of the current cell is better than our current best cost and if it is, update the best cost as well as the best direction. So essentially, this is getting the best direction for the agents to follow to go to the target which is visualized with arrows as seen above.

public void CreateFlowField()
{
    foreach (GridCell currentCell in grid)
    {
        // Variables
        List<GridCell> currentNeighbors = GetNeighborCells(currentCell.gridIndex, allDirections);
        int bestCost = currentCell.bestCost;
        foreach (GridCell currentNeighbor in currentNeighbors)
        {
            if (currentNeighbor.bestCost < bestCost) // If neighbor cell is cheaper than previous best cell, set new cost and direction
            {
                bestCost = currentNeighbor.bestCost;
                currentCell.bestDirection = cardinalAndIntercardinalDirections.DefaultIfEmpty(noDirection).FirstOrDefault(direction => direction == (currentNeighbor.gridIndex - currentCell.gridIndex));
            }
        }
    }
}