FlowVitae is a memory and performance efficient 2D grid library written in .net designed for small to large scale procedural worlds. Can be easily integrated with most render engines.
$ dotnet add package Venomaus.FlowVitaeFlowVitae 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:
Tested with:
Different grid layouts
Infinite chunking terrain
Easy to use
FlowVitae grids use 2 generic types
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 reloadedThis 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.
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<TCellType>
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<TCellType> 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 criteriaChecking 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
OnChunkUnloadSome 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.