arcadeperfect
arcadeperfect

Reputation: 199

Async task only run when previous is completed

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

Answers (1)

arcadeperfect
arcadeperfect

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

Related Questions