Table of Contents

Implementing the IGrid Interface

This article explains how to implement the IGrid interface to create custom grids.

Section1

Suppose we want to define our own grid, but still want to use many of the grid functions (such as those defined in @Gamelogic.Grids2.Algorithms). To do this, we need to let our grid implement the IGrid interface. This ensures our grid will have all the methods called by Algorithms. In this example, we will implement some irregular grid, with neighbors defined on a special component that can be configured in the inspector:

[Serializable]
public class Neighbors
{
    public int point;
    public List<int> neighbors;
}

[Serializable]
public class NeighborConfiguration : GLMonoBehaviour
{
    public List<Neighbors> neighbors;
}

Point Types

The first thing to do is choose a point type. You can use one of the existing point types, or define your own (in which case, that point type should implement the IGridPoint interface). For this example, we will use int, since it is suitable for grids with either a 1D structure (such as the built-in Grid1), or no important 2D or 3D structure (for example, irregular grids).

Underlying Data Structure

The second thing is to choose an underlying data structure to hold our cells. You can use a built-in grid for this, or an array, or a dictionary, or any data structure that makes sense for your application. You may even choose to use multiple data structures to optimize the various queries. In our case, we will use a dictionary. We will use the NeighborConfiguration to make queries about neighbors.

public class ExampleGrid<TCell> : IGrid<int, TCell>
{
    // Used in GetNeighbors function
    private static readonly int[] Empty = new int[] { };

    private Dictionary<int, TCell> cells;
    private Dictionary<int, Neighbors> neighbors;

    private NeighborConfiguration neighborConfiguration;

    public ExampleGrid(NeighborConfiguration neighborConfiguration)
    {
        this.neighborConfiguration = neighborConfiguration;

        cells = new Dictionary<int, TCell>();
        neighbors = new Dictionary<int, Neighbors>();

        foreach (var neighbor in neighborConfiguration.neighbors)
        {
            neighbors[neighbor.point] = neighbor.neighbors;
        }
    }
}

Implement the Bounds property

You should use the right bounds type for your grid. For 1D, 2D, and 3D grids that use the standard grid points, you can use one of the built-in types. For other grids, you need to use your own implementation of AbstractBounds.

What bounds to use

Grid Point Type Bounds Type
int GridInterval
GridPoint2 GridRect
GridPoint3 GridBounds
For any other type T A type that extends from @Gamelogic.Grids2.AbstractBound`1

Since we are using int as grid points, we use the GridInterval class. For this example, we will implement the bounds property to return a fixed interval.

public class ExampleGrid<TCell> : IGrid<int, TCell>
{
    private readonly AbstractBounds<int> bounds = new GridInterval(0, 100);

    // Other members...

    public override AbstractBounds<int> Bounds
    {
        get { return bounds; }
    }
}

Implement the Points property

This property should return all legal points for our grid. Don't think because we use a dictionary we only need to return the dictionary keys. This would be valid in two cases: if we assigned empty slots to all valid keys in the constructor, or if we wanted to implement a dynamic grid. We are not doing this here though.

Instead, we return all the points of our bounds interval:

public override IEnumerable<int> Points
{
    get { return bounds.Points; }
}

Implement the Cells property

This method should return a cell for each point returned by the Points property.

public IEnumerable<TCell> Cells
{
    get { return Points.Select(point => this[point]); }
}

Implement the Item property

This property is what is used to get a single cell. It should always return a value for a point that falls within the bounds. In this example, we check the point - if it is in the dictionary, we return the associated cell, otherwise, we return the default value for the TCell.

public override TCell this[int point]
{
    get
    {
        if (!bounds.Contains(point)) throw new Exception();
        if (!cells.ContainsKey(point)) return default(TCell);

        return cells[point];
    }
    
    set
    {
        if (!bounds.Contains(point)) throw new Exception();

        cells[point] = value;
    }
}

Implementing the Contains method

This method tells whether the grid contains a given point. This does not mean the grid is initialized for that point (that there is a valid cell at that point). This simply means the cell falls within the bounds of the grid. This method determines the shape of the grid. It is sometimes limited by the underlying data structure (if we used a list, for example, we would check whether the point falls inside the bounds of the list). In this case, we are not more limited than our bounds allow, and so we can use that to implement this method. For more specific "shapes" you will have more complicated logic.

public override bool Contains(int point)
{
    return bounds.Contains(point);
}

Implement the GetEnumerator methods

These methods allow us to iterate over all point-cell pairs in the grid. The implementation is pretty straightforward:

public IEnumerator<PointCellPair<int, TCell>> GetEnumerator()
{
    return Points.Select(
        point => new PointCellPair<int, TCell>(
            point,
            this[point])).GetEnumerator();
}

We also need a non-generic version, which is implemented by calling the method above:

IEnumerator IEnumerable.GetEnumerator()
{
    return GetEnumerator();
}

Implement CloneStructure

This method allows us to build a new grid with the same shape in a different type. We implement it by simply constructing a grid with the same data.

public override IGrid<int, TNewCell> CloneStructure<TNewCell>()
{
    return new ExampleGrid<TNewCell>(neighborConfiguration);
}

Customizations

Finally, you add whatever other methods you need. In this example, the whole point is to keep track of custom neighbors, so we expose a method that can be used to get this information.

public IEnumerable<int> GetNeighbors(int point)
{
    if (cells.ContainsKey(point))
    {
        return cells[point];
    }
    else
    {
        return Empty;
    }
}

When we use a method like the GetPointsInRange method, we can use this GetNeighbors method

var path = Algorithms.GetPointsInRange(
    grid,
    0, 
    grid.GetNeighbors,
    p => true,
    (p, q) => 1,
    2);