FlowVitae is a memory and performance efficient 2D grid library designed for small to large scale procedural worlds. Can be easily integrated with most render engines.
Supports:
- net7.0
- net6.0
- netstandard2.1
Tested with:
- SadConsole
- MonoGame
Different grid layouts
- Static grid
- Static chunked grid
- Procedural chunked grid
Infinite chunking terrain
- Chunking is automatically done
- Viewport and chunk size is configurable
- Method to center viewport on a coordinate, (this handles the chunking)
- Procedural generation algorithm can be passed straight to the Grid
Easy to use
- Possible to configure custom Grid, Cell, ProceduralGeneration classes
- Has some visualizer projects that serves as examples on how to integrate with a render engine, such as SadConsole or Unity.
FlowVitae grids use 2 generic types
- TCellType is constrained to a struct, and will represent the unique cell value kept in memory. (eg, an int or byte that points to the real cell id)
- TCell is the real cell that represents the TCellType, it uses the ICell interface
FlowVitae provides some basic implementations already out of the box.
Grid<TCellType, TCell>
Cell<TCellType>
Static Grid Creation
var grid = new Grid<int, Cell<int>>(width, height);
Static Chunked Grid Creation
var staticGen = new StaticGenerator<int, Cell<int>>(baseMap, width, height, outOfBoundsCellType);
var grid = new Grid<int, Cell<int>>(width, height, chunkWidth, chunkHeight, staticGen, extraChunkRadius = 1);
The baseMap here represents the full static grid.
Procedural Grid Creation
var procGen = new ProceduralGenerator<int, Cell<int>>(Seed, GenerateChunkMethod);
var grid = new Grid<int, Cell<int>>(width, height, chunkWidth, chunkHeight, procGen, extraChunkRadius = 1);
GenerateChunkMethod can look something like this:
public void GenerateChunkMethod(Random random, int[] chunk, int width, int height, (int x, int y) chunkCoordinate)
{
// Every position contains default value of int (0) which could represent grass
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// Add a random chance for a cell to be a tree
if (random.Next(0, 100) < 10)
chunk[y * width + x] = 1;
}
}
}
The random already has a unique seed based on the provided Seed in the ProceduralGenerator and the chunk coordinate. int[] chunk represent the chunk, int[] will be your TCellType[] and the chunkCoordinate is provided too, in case you want to sample noise based on coordinates.
Chunks are generated automatically and they will use this method as reference to build the chunk.
It is possible to set custom data, per chunk which can be directly retrieved from the grid. This custom data, can be any class that implements IChunkData interface. An example implementation:
internal class TestChunkData : IChunkData
{
public int Seed { get; set; }
public List<(int x, int y)>? Trees { get; set; }
}
// Custom chunk generation implementation
Func<Random, int[], int, int, TestChunkData> chunkGenerationMethod = (random, chunk, width, height, chunkCoordinate) =>
{
// Define custom chunk data
var chunkData = new TestChunkData
{
Trees = new List<(int x, int y)>()
};
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
chunk[y * width + x] = random.Next(-10, 10);
// Every 0 value is a tree, lets keep it for easy pathfinding access
if (chunk[y * width + x] == 0)
chunkData.Trees.Add((x, y));
}
}
return chunkData;
};
// Initialize the custom implementations
var customProcGen = new ProceduralGenerator<int, Cell<int>, TestChunkData>(Seed, chunkGenerationMethod);
var customGrid = new Grid<int, Cell<int>, TestChunkData>(ViewPortWidth, ViewPortHeight, ChunkWidth, ChunkHeight, customProcGen, extraChunkRadius = 1);
// Retrieve the chunk data, for the whole chunk where position (5, 5) resides in
var chunkData = customGrid.GetChunkData(5, 5);
Console.WriteLine("Trees in chunk: " + chunkData.Trees != null ? chunkData.Trees.Count : 0);
You can store the chunkdata within the internal cache buffer, which GetChunkData will then return instead
customGrid.StoreChunkData(chunkData);
customGrid.RemoveChunkData(chunkData, reloadChunk); // chunkdata only refreshes after chunk is reloaded
This works similar for Static chunked grids, you can pass along a chunk data generation method to the constructor.
// chunkGenerationMethod signature: (seed, baseMap, width, height, chunkCoordinate)
new StaticGenerator<int, Cell<int>, TestChunkData>(_baseMap, Grid.Width, Grid.Height, NullCell, chunkGenerationMethod, extraChunkRadius = 1)
FlowVitae provides an event that is raised when a cell on the viewport is updated
grid.OnCellUpdate += Grid_OnCellUpdate;
private void Grid_OnCellUpdate(object? sender, CellUpdateArgs<int, Cell<int>> args)
{
// Pseudo code
var screenGraphic = ConvertCellTypeToGraphic(args.Cell.CellType);
SomeRenderEngine.SetScreenGraphic(args.ScreenX, args.ScreenY, screenGraphic);
}
This event is by default only raised when the TCellType value on the viewport is changed during a SetCell/SetCells If you want this event to always be raised when a TCell is set, (even if CellType doesn't change, but some properties do) Then we also provided this functionality. You can adjust it like so:
grid.RaiseOnlyOnCellTypeChange = false;
There are some ways to convert the underlying TCellType to TCell.
- You can implement your own Grid class based on the GridBase<TCellType, TCell> and override the Convert method.
- You can call the method SetCustomConverter(converter) on the Grid which is then used in Convert instead of default new() constructor
Here is an example of the method:
_grid.SetCustomConverter(ConvertCell);
public Cell<int> ConvertCell(int x, int y, int cellType)
{
switch (cellType)
{
case 0:
return new Cell<int>(x, y, cellType);
case 1:
return new Cell<int>(x, y, walkable: false, cellType);
default:
return new Cell<int>(x, y, walkable: false, cellType);
}
}
It can be easily done by inheriting from CellBase or ICell
When you want your cell to be able to base of some render engine cell such as ColoredGlyph from Sadconsole, you can easily do it by using ColoredGlyph, ICell as your inheritance.
Here is an example of just a regular CellBase inheritance:
internal class VisualCell<TCellType> : CellBase<TCellType>
where TCellType : struct
{
public bool Walkable { get; set; } = true;
public bool BlocksFieldOfView { get; set; } // Some custom properties
public bool HasLightSource { get; set; } // Some custom properties
public VisualCell() { }
public VisualCell(int x, int y, TCellType cellType)
{
X = x;
Y = y;
CellType = cellType;
}
}
Getting and setting cells
var cell = grid.GetCell(x, y); // returns TCell
var cellType = grid.GetCelLType(x,y); // returns TCellType
var neighbors = grid.GetNeighbors(x, y, AdjacencyRule);
grid.SetCell(x, y, cellType, storeState);
grid.SetCell(cell, storeState);
var cells = grid.GetCells(new [] {(0,0), (1,1)}); // returns collection of TCell
grid.SetCells(cells, storeState);
grid.RemoveStoredCell(x, y);
grid.HasStoredCell(x, y);
Center viewport on a coordinate for procedural grids
This is especially useful when you want your player to always be centered in the middle of the screen. But during movement, the viewport adjusts to show the right cells based on the position of the player For this you can use the Center(x, y) method Grid provides. This method is also what controls the chunk loading.
// Pseudo code (make sure player doesn't actually move, or you'll end up with desync)
if (player.MovedTowards(x, y))
grid.Center(x, y);
Retrieve all cells within the viewport
// Returns all world positions that are within the current viewport
grid.GetViewPortWorldCoordinates();
grid.GetViewPortWorldCoordinates(cellType => cellType == 1 || cellType == 2); // with custom criteria
Checking bounds for static grids
// Returns true or false if the position is within the viewport
// Works only for screen coordinates if you're using a chunked grid
var isInBounds = grid.InBounds(x, y);
See if a cell is currently displayed on the viewport
var isInViewPort = grid.IsWorldCoordinateOnViewPort(x,y);
Reset grid state
grid.ClearCache(); // Removes all stored cell data
grid.RemoveStoredCell(x, y);
Resize grid viewport
This will resize the surface of the screen, reinitialize all the chunks and send out render updates for the new screen surface.
grid.ResizeViewport(width, height);
Be notified of main chunk loading/unloading
Following events will be raised when one of the chunks around the center chunk (center chunk included) gets loaded or unloaded.
OnChunkLoad
OnChunkUnload
Some chunk related methods: (x, y) is automatically converted to a chunk coordinate, so it can take any world position.
Grid.GetChunkSeed(x, y);
Grid.IsChunkLoaded(x, y);
Grid.GetChunkCoordinate(x, y);
Grid.GetChunkCellCoordinates(x, y);
Grid.GetLoadedChunkCoordinates();
Checkout the SadConsoleVisualizer project, it is an example project that integrates the FlowVitae grid with SadConsole and MonoGame.