Reputation: 199
For a unity project. Procedural terrain generation system that runs in generations. The output of each generation becomes the input of the next, so they must be run sequentially.
They are also slow enough that they need to run on another thread.
The below code works by generating a "sector" and storing it in a dictionary with its "generation" number as the key. It also stores a small initialization object with just the data required to initialize a given generation, so that I can destroy sectors to save memory and re instantiate them backwards down the chain.
Increment()
finds the highest key, and generates a new sector with the previous one as its input.
Task.Run()
does work to generate sectors without blocking the rest of the game. The problem is that it's possible to request a new sector before the previous one finished generating, etc.
What's the best pattern to prevent generation 3 being generated before generation 2 is finished?
public class WorldGeneratorAsync : MonoBehaviour
{
public Dictionary<int, SectorController> SectorContollerDict = new Dictionary<int, SectorController>();
public Dictionary<int, TerrainGraphInput> GraphInputDict = new Dictionary<int, TerrainGraphInput>();
public int globalSeed;
public TerrainGraph terrainGraph;
async void Initialise()
{
DestroyAllSectors();
await InstantiateSectorAsync(0);
}
async void Increment()
{
var highestGeneration = SectorContollerDict.Keys.Max();
await InstantiateSectorAsync(highestGeneration+1);
}
async Task InstantiateSectorAsync(int generation)
{
if (generation == 0) // if we are first generation, init with dummy data
{
var inputData = new TerrainGraphInput(globalSeed, generation); // dummy data
var outputData = await Task.Run(() =>terrainGraph.GetGraphOutput(inputData)); // slow function
lock (SectorContollerDict)
{
SectorContollerDict[generation] = SectorController.New(outputData);
}
lock (GraphInputDict)
{
GraphInputDict[generation] = inputData;
}
}
else // we take the init data from the previous generation
{
int adder = generation > 0 ? -1 : 1;
TerrainGraphInput inputData;
if (GraphInputDict.Keys.Contains(generation))
{
inputData = GraphInputDict[generation];
}
else if (SectorContollerDict.Keys.Contains(generation + adder))
{
var previousSectorController = SectorContollerDict[generation + adder];
inputData = new TerrainGraphInput(
previousSectorController.sectorData,
previousSectorController.sectorData.EndSeeds,
generation,
globalSeed
);
}
else
{
throw new NoValidInputException();
}
var outputData = await Task.Run(()=>terrainGraph.GetGraphOutput(inputData)); // slow function
lock (SectorContollerDict)
{
SectorContollerDict[generation] = SectorController.New(outputData);
}
lock (GraphInputDict)
{
GraphInputDict[generation] = inputData;
}
}
}
private void DestroyAllSectors()
{
SectorContollerDict = new Dictionary<int, SectorController>();
GraphInputDict = new Dictionary<int, TerrainGraphInput>();
foreach (var sc in GameObject.FindObjectsOfType<SectorController>())
{
sc.DestroyMe();
}
}
}
Upvotes: 1
Views: 161
Reputation: 199
Thanks to Orace - their idea worked. Simpler that I expected - just switch the dictionary of sectorControllers to tasks, and await the previous generation in the instantiation function.
public class WorldGeneratorAsync : MonoBehaviour
{
public Dictionary<int, Task<SectorController>> TaskDict = new();
// public Dictionary<int, SectorController> SectorContollerDict = new();
public Dictionary<int, TerrainGraphInput> GraphInputDict = new();
public int globalSeed;
public TerrainGraph terrainGraph;
async void Initialise()
{
DestroyAllSectors();
TaskDict[0] = InstantiateSectorAsync(0);
}
async void Increment()
{
var highestGeneration = TaskDict.Keys.Max();
int newGeneration = highestGeneration + 1;
TaskDict[newGeneration] = InstantiateSectorAsync(newGeneration);
}
async Task<SectorController> InstantiateSectorAsync(int generation)
{
SectorController sc;
if (generation == 0) // if we are first generation, init with dummy data
{
var inputData = new TerrainGraphInput(globalSeed, generation); // dummy data
var outputData = await Task.Run(() =>terrainGraph.GetGraphOutput(inputData)); // slow function
sc = SectorController.New(outputData);
GraphInputDict[generation] = inputData;
}
else
{
int adder = generation > 0 ? -1 : 1;
TerrainGraphInput inputData;
if (GraphInputDict.Keys.Contains(generation))
{
inputData = GraphInputDict[generation];
}
else if (TaskDict.Keys.Contains(generation + adder))
{
// var previousSectorController = SectorContollerDict[generation + adder];
var previousSectorController = await TaskDict[generation + adder]; // await previous generation
inputData = new TerrainGraphInput(
previousSectorController.sectorData,
previousSectorController.sectorData.EndSeeds,
generation,
globalSeed
);
}
else
{
throw new NoValidInputException();
}
var outputData = await Task.Run(()=>terrainGraph.GetGraphOutput(inputData)); // slow function
sc = SectorController.New(outputData);
GraphInputDict[generation] = inputData;
}
return sc;
}
private void DestroyAllSectors()
{
GraphInputDict = new Dictionary<int, TerrainGraphInput>();
TaskDict = new();
foreach (var sc in GameObject.FindObjectsOfType<SectorController>())
{
sc.DestroyMe();
}
}
}
Upvotes: 2