Hello everyone, this post demonstrates how to build a custom grid system that can be utilized for various purposes such as pathfinding, heatmaps, grid-based building systems, grid-based combat systems, and much more. This grid system leverages the generic features of the C# language. It enables you to store and manage data in a two-dimensional arrangement of cells. Being generic means you can use it to store any type of data, from integers and strings to complex objects and custom classes in each cell.
This code creates a grid, and I believe it’s largely self-explanatory. However, if you encounter any confusion, feel free to ask in the comments.
public class CustomGrid<TItem>
{
public delegate void GridItemChangedDelegate(int coordinateX, int coordinateY, TItem obj);
public event GridItemChangedDelegate GridItemChanged;
public int Height { get; private set; }
public int CellSize { get; private set; }
public int Width { get; private set; }
public TItem[,] GridCellCollection { get; private set; }
public List<Vector2> GridPositions { get; private set; }
private readonly Func<Vector2> _originPosition;
public CustomGrid(int width, int height, int cellSize, Func<Vector2> originPosition = null)
{
_originPosition = originPosition;
Height = height;
CellSize = cellSize;
Width = width;
GridCellCollection = new TItem[width, height];
GridPositions = new();
// Initialize grid cells and positions
for (int i = 0; i < Width; i++)
{
for (int j = 0; j < Height; j++)
{
GridCellCollection[i, j] = default;
GridPositions.Add(GetWorldPosition(i, j));
}
}
}
private void CreateGrid()
{
for (int i = 0; i < Width; i++)
{
for (int j = 0; j < Height; j++)
{
//each cell data
}
}
}
private Vector2 GetParentPosition() => _originPosition?.Invoke() ?? Vector2.Zero;
private bool TryGetGridCoordinate(Vector2 pos, Vector2 parentPos, out (int x, int y) coordinate)
{
var x = Mathf.FloorToInt((pos.X - parentPos.X) / (CellSize));
var y = Mathf.FloorToInt((pos.Y - parentPos.Y) / (CellSize));
coordinate = (x, y);
return x >= 0 && x < Width && y >= 0 && y < Height;
}
private bool IsCellEmpty(int x, int y)
{
var obj = GridCellCollection[x, y];
return obj == null || EqualityComparer<TItem>.Default.Equals(obj, default);
}
private bool IsCellEmpty(int x, int y, out TItem item)
{
item = GridCellCollection[x, y];
return GridCellCollection[x, y] == null || EqualityComparer<TItem>.Default.Equals(GridCellCollection[x, y], default);
}
public bool IsCellEmpty(Vector2 pos)
{
return TryGetGridCoordinate(pos, GetParentPosition(), out var coordinate) && IsCellEmpty(coordinate.x, coordinate.y);
}
private Vector2 GetWorldPosition(int x, int y) => new Vector2(x * CellSize, y * CellSize) + GetParentPosition();
public bool TrySetItem(Vector2 pos, TItem obj, out Vector2 positionToSet)
{
if (obj == null)
{
positionToSet = Vector2.Zero;
return false;
}
var parentPos = GetParentPosition();
if (TryGetGridCoordinate(pos, parentPos, out var coordinate) && IsCellEmpty(coordinate.x, coordinate.y))
{
GridCellCollection[coordinate.x, coordinate.y] = obj;
positionToSet = new Vector2(coordinate.x * CellSize, coordinate.y * CellSize) + parentPos;
GridItemChanged?.Invoke(coordinate.x, coordinate.y, obj);
return true;
}
positionToSet = Vector2.Zero;
return false;
}
public void TriggerItemChangedEvent(int x, int y)
{
GridItemChanged?.Invoke(x, y, GridCellCollection[x, y]);
}
public bool TryGetItem(Vector2 pos, out TItem obj)
{
if (TryGetGridCoordinate(pos, GetParentPosition(), out var coordinate))
{
if (!IsCellEmpty(coordinate.x, coordinate.y))
{
obj = GridCellCollection[coordinate.x, coordinate.y];
return true;
}
}
obj = default;
return false;
}
public bool TryRemoveItem(Vector2 pos, out TItem removedItem)
{
if (TryGetGridCoordinate(pos, GetParentPosition(), out var coordinate))
{
if (IsCellEmpty(coordinate.x, coordinate.y, out removedItem))
{
return false;
}
GridCellCollection[coordinate.x, coordinate.y] = default;
return true;
}
removedItem = default;
return false;
}
}
But how does it all work? Let’s break down above code:
Variables
- GridItemChanged (event):
- This event is triggered when a grid item changes. It’s of type GridItemChangedDelegate, which defines the signature of methods that can be subscribed to this event.
- Height, Width, CellSize:
- These are properties that define the dimensions and cell size of the grid.
- GridCellCollection:
- It’s a 2D array of type TItem that holds grid items. Each cell in the grid can contain an item of type TItem. Since, TItem is generic, it can hold any type of data.
- GridPositions:
- It’s a list that stores grid positions as Vector2 and can be used to draw visual representation of grid.
- _originPosition:
- It’s a private field that holds a reference to a function returning a Vector2, representing the origin position of the grid.
Methods
- CustomGrid() constructor:
- This is use to Initializes a new instance of the CustomGrid class with specified width, height, cell size, and optionally, an origin position function.
- GetParentPosition():
- This method Returns the parent position of the grid. If _originPosition is not null, it invokes the function and returns the result; otherwise, it returns Vector2.Zero.
- TryGetGridCoordinate(Vector2 pos, Vector2 parentPos, out (int x, int y) coordinate):
- Given a world position and parent position, this method calculates the corresponding grid coordinate. This returns true if the coordinate is valid within the grid bounds.
- IsCellEmpty(int x, int y), IsCellEmpty(int x, int y, out TItem item):
- This method is use to check if the cell at the specified grid coordinates is empty (i.e., does not contain an item).
- IsCellEmpty(Vector2 pos):
- This is a overloaded method that checks if the cell at the specified world position is empty.
- GetWorldPosition(int x, int y):
- This method calculates and returns the world position of a grid cell based on its coordinates.
- TrySetItem(Vector2 pos, TItem obj, out Vector2 positionToSet):
- Tries to set an item at the specified world position in the grid. If the position is valid and the cell is empty, sets the item in the grid and returns true.
- TriggerItemChangedEvent(int x, int y):
- This is use to invokes the GridItemChanged event for the specified grid coordinates.
- TryGetItem(Vector2 pos, out TItem obj):
- Attempts to get the item at the specified world position in the grid, respectively. Returns true if successful, along with the item.
- TryRemoveItem(Vector2 pos, out TItem removedItem):
- Attempts to remove the item at the specified world position in the grid, respectively. Returns true if successful, along with the item so it can be cleanup.
This is how you can create a custom grid. It’s important to note that this implementation isn’t perfect or exhaustive. It’s a simple demonstration, and further optimization and feature additions can be made based on specific requirements. If you’re unsure how to use this grid, you can refer to the GitHub pages for a simple demo.
Check out my other post :
- Implementing Scene Management with Loading Screen in Godot Engine.
- How to implement Google Game Play Services in Godot using C#.
Great article! I really appreciate the clear and detailed insights you’ve provided on this topic. It’s always refreshing to read content that breaks things down so well, making it easy for readers to grasp even complex ideas. I also found the practical tips you’ve shared to be very helpful. Looking forward to more informative posts like this! Keep up the good work!