15.7 C
New York
Saturday, April 19, 2025

unity – Creating numerous GameObjects asynchronously


I’m making a 2D procedural terrain utilizing many Tile GameObjects. To make this infinite (and extra performant), I dynamically load and unload chunks based mostly on the gamers place and a variable renderDistance.

Because the chunks are made from particular person squares, a whole bunch/1000’s of GameObjects should be created in a single perform, this causes quite a lot of lag because it halts the principle sport loop/thread. Initially I believed it might be a easy repair, I may simply async the capabilities, that manner they run together with the principle thread and do not halt something. However after doing so, not one of the chunks generate. After some debugging I discovered it appeared to cease at creating the GameObject:

personal async Process LoadChunkAsync(int x)
{
    if (!m_chunks.TryGetValue(x, out Chunk chunk))
    {
        //                       v HERE v
        GameObject chunkObject = new($"Chunk: {x}");
        chunkObject.rework.SetParent(m_chunkPool.rework);
        chunkObject.rework.place = new Vector3(x * Chunk.CHUNK_WIDTH - Chunk.CHUNK_WIDTH / 2, 0);
        chunk = chunkObject.AddComponent();
        chunk.chunkX = x;
    }
    Terrain terrain = await m_terrainGenerator.GenerateTerrainAsync(x * Chunk.CHUNK_WIDTH - Chunk.CHUNK_WIDTH / 2, Chunk.CHUNK_WIDTH, Chunk.CHUNK_HEIGHT);
    await chunk.LoadAsync(terrain.GetTiles());
    m_chunks.Add(x, chunk);
}

I am not precisely positive why this happens, however I guessed it was to do with Unity not being thread-safe. To get round this difficulty, I made a UnityMainThreadDispatcher class:

utilizing System.Collections.Generic;
utilizing System;
utilizing UnityEngine;
utilizing System.Threading.Duties;

public class UnityMainThreadDispatcher : MonoBehaviour
{
    public static UnityMainThreadDispatcher Occasion { get; personal set; }

    personal Queue m_pending = new();

    personal void Awake()
    {
        if (Occasion != null)
        {
            Debug.Log("Greater than 1 UnityThreadDispatcher exists in Scene.");
            Destroy(gameObject);
            return;
        }
        Occasion = this;
        DontDestroyOnLoad(gameObject);
    }

    void Replace()
    {
        lock (m_pending)
        {
            whereas (m_pending.Depend != 0) m_pending.Dequeue().Invoke();
        }
    }

    public void Invoke(Motion a)
    {
        lock (m_pending) { m_pending.Enqueue(a); }
    }

    public Process InvokeAsync(Func func)
    {
        var tcs = new TaskCompletionSource();
        Invoke(() =>
        {
            strive
            {
                tcs.SetResult(func());
            }
            catch (Exception e)
            {
                tcs.SetException(e);
            }
        });
        return tcs.Process;
    }
}

This labored, for one chunk. It additionally did not work for the tiles inside the chunk. I have been at this for some time now with no resolution, it is troublesome to debug as its async, and I am not that nice with async capabilities to start with so I am hoping somebody will help.

Right here is my ChunkManager and Chunk class:

utilizing System;
utilizing System.Collections.Generic;
utilizing System.Linq;
utilizing System.Threading.Duties;
utilizing UnityEngine;

public class ChunkManager
{
    personal Dictionary m_chunks;
    personal GameManager m_gameManager;
    personal TerrainGenerator m_terrainGenerator;
    personal UnityMainThreadDispatcher m_mainThreadDispatcher;
    personal ObjectPool m_chunkPool;
    personal Queue m_loadQueue;
    personal Queue m_unloadQueue;
    personal bool m_isProcessingLoadQueue;
    personal bool m_isProcessingUnloadQueue;

    public ChunkManager(int seed)
    {
        m_chunks = new();
        m_gameManager = GameManager.Occasion;
        m_terrainGenerator = GameManager.Occasion.terrainGenerator;
        m_mainThreadDispatcher = UnityMainThreadDispatcher.Occasion;
        m_chunkPool = PoolManager.GetPool("Chunks");
        m_loadQueue = new Queue();
        m_unloadQueue = new Queue();
        m_isProcessingLoadQueue = false;
        m_isProcessingUnloadQueue = false;
    }

    public void UpdateSeed(int seed)
    {
        Process.Run(() => UpdateSeedAsync(seed));
    }

    public void ClearAllChunks()
    {
        Process.Run(() => ClearAllChunksAsync());
    }

    public void LoadChunksAroundPlayer(float playerX)
    {
        Process.Run(() => LoadChunksAroundPlayerAsync(playerX));
    }

    public void UnloadDistantChunks(float playerX)
    {
        Process.Run(() => UnloadDistantChunksAsync(playerX));
    }

    public async Process UpdateSeedAsync(int seed)
    {
        m_terrainGenerator.SetSeed(seed);
        Listing unloadTasks = new();
        foreach (var chunk in m_chunks.Values)
        {
            unloadTasks.Add(UnloadChunkAsync(chunk.chunkX));
        }
        await Process.WhenAll(unloadTasks);
        m_chunks.Clear();
    }

    public async Process ClearAllChunksAsync()
    {
        Listing unloadTasks = new();
        foreach (var chunk in m_chunks.Values)
        {
            unloadTasks.Add(UnloadChunkAsync(chunk.chunkX));
        }
        await Process.WhenAll(unloadTasks);
    }

    public WorldTile GetWorldTile(float x, float y)
    {
        Chunk chunk = GetChunkByX(x + 0.5f);
        if (chunk == null)
        {
            return null;
        }
        int tileX = (Mathf.FloorToInt(x + 0.5f) - Chunk.CHUNK_WIDTH / 2) % Chunk.CHUNK_WIDTH;
        if (tileX < 0)
        {
            tileX += Chunk.CHUNK_WIDTH;
        }
        int tileY = Mathf.FloorToInt(y + 0.5f);
        return chunk.GetWorldTile(tileX, tileY);
    }

    public async Process LoadChunksAroundPlayerAsync(float playerX)
    {
        int renderDistance = m_gameManager.settings.renderDistance;
        int playerChunkX = Mathf.FloorToInt(playerX / Chunk.CHUNK_WIDTH);

        for (int x = playerChunkX - renderDistance; x <= playerChunkX + renderDistance; x++)
        {
            if (!m_chunks.ContainsKey(x))
            {
                m_loadQueue.Enqueue(x);
            }
        }

        if (!m_isProcessingLoadQueue)
        {
            await ProcessLoadQueueAsync();
        }
    }

    public async Process UnloadDistantChunksAsync(float playerX)
    {
        int renderDistance = m_gameManager.settings.renderDistance;
        int playerChunkX = Mathf.FloorToInt(playerX / Chunk.CHUNK_WIDTH);

        foreach (var chunk in m_chunks.The place(chunk => Mathf.Abs(chunk.Key - playerChunkX) > renderDistance).ToList())
        {
            m_unloadQueue.Enqueue(chunk.Key);
        }

        if (!m_isProcessingUnloadQueue)
        {
            await ProcessUnloadQueueAsync();
        }
    }

    personal async Process ProcessLoadQueueAsync()
    {
        m_isProcessingLoadQueue = true;
        whereas (m_loadQueue.Depend > 0)
        {
            int chunkX = m_loadQueue.Dequeue();
            await LoadChunkAsync(chunkX);
        }
        m_isProcessingLoadQueue = false;
    }

    personal async Process ProcessUnloadQueueAsync()
    {
        m_isProcessingUnloadQueue = true;
        whereas (m_unloadQueue.Depend > 0)
        {
            int chunkX = m_unloadQueue.Dequeue();
            await UnloadChunkAsync(chunkX);
        }
        m_isProcessingUnloadQueue = false;
    }

    personal async Process LoadChunkAsync(int x)
    {
        if (!m_chunks.TryGetValue(x, out Chunk chunk))
        {
            GameObject chunkObject = await m_mainThreadDispatcher.InvokeAsync(() =>
            {
                GameObject obj = new($"Chunk: {x}");
                obj.rework.SetParent(m_chunkPool.rework);
                obj.rework.place = new Vector3(x * Chunk.CHUNK_WIDTH - Chunk.CHUNK_WIDTH / 2, 0);
                return obj;
            });

            chunk = await m_mainThreadDispatcher.InvokeAsync(() =>
            {
                Chunk newChunk = chunkObject.AddComponent();
                newChunk.chunkX = x;
                return newChunk;
            });
        }
        Terrain terrain = await m_terrainGenerator.GenerateTerrainAsync(x * Chunk.CHUNK_WIDTH - Chunk.CHUNK_WIDTH / 2, Chunk.CHUNK_WIDTH, Chunk.CHUNK_HEIGHT);
        await chunk.LoadAsync(terrain.GetTiles());
        m_chunks.Add(x, chunk);
    }

    personal async Process UnloadChunkAsync(int chunkId)
    {
        if (m_chunks.TryGetValue(chunkId, out Chunk chunk))
        {
            await Process.Run(() => GameObject.Destroy(chunk.gameObject));
            m_chunks.Take away(chunkId);
        }
    }

    personal Chunk GetChunkByX(float x)
    {
        int chunkX = Mathf.RoundToInt(x / Chunk.CHUNK_WIDTH);
        if (m_chunks.TryGetValue(chunkX, out Chunk chunk))
        {
            return chunk;
        }
        return null;
    }
}
utilizing System.Threading.Duties;
utilizing UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class Chunk : MonoBehaviour
{
    public const int CHUNK_WIDTH = 16;
    public const int CHUNK_HEIGHT = 256;

    public int chunkX;

    personal UnityMainThreadDispatcher m_mainThreadDispatcher;
    personal WorldTile[,] m_worldTiles;
    personal Tile[,] m_tiles;

    personal void Awake()
    {
        Rigidbody2D rb = gameObject.GetComponent();
        rb.bodyType = RigidbodyType2D.Static;
        CompositeCollider2D compositeCollider = gameObject.AddComponent();
        compositeCollider.geometryType = CompositeCollider2D.GeometryType.Polygons;
    }

    personal void Begin()
    {
        m_mainThreadDispatcher = UnityMainThreadDispatcher.Occasion;
    }

    public async Process LoadAsync(Tile[,] tiles)
    {
        await Process.Run(async () =>
        {
            m_tiles = tiles;
            m_worldTiles = new WorldTile[CHUNK_WIDTH, CHUNK_HEIGHT];
            for (int x = 0; x < CHUNK_WIDTH; x++)
            {
                for (int y = 0; y < CHUNK_HEIGHT; y++)
                {
                    Tile tile = m_tiles[x, y];

                    if (tile != null && tile.sprite != null)
                    {
                        GameObject tileObject = await m_mainThreadDispatcher.InvokeAsync(() =>
                        {
                            GameObject obj = new GameObject($"Tile_{x}_{y}");
                            obj.rework.SetParent(rework);
                            obj.rework.localPosition = new Vector3(x, y);
                            return obj;
                        });

                        m_mainThreadDispatcher.Invoke(() =>
                        {
                            SpriteRenderer spriteRenderer = tileObject.AddComponent();
                            spriteRenderer.sprite = tile.sprite;
                            WorldTile worldTile = tileObject.AddComponent();
                            worldTile.tile = tile;
                            m_worldTiles[x, y] = worldTile;
                            PolygonCollider2D polygonCollider = tileObject.AddComponent();
                            polygonCollider.compositeOperation = Collider2D.CompositeOperation.Merge;
                        });
                    }
                }
            }
        });
    }

    public void SetTile(int x, int y, Tile tile)
    {
        m_tiles[x, y] = tile;
    }

    public Tile GetTile(int x, int y)
    {
        return m_tiles[x, y];
    }

    public WorldTile GetWorldTile(int x, int y)
    
}

I’ve additionally seemed into varied different strategies as follows with no outcomes I am pleased with:

  • Coroutines: rely an excessive amount of on the principle sport loop (ideally I would like this to be fully impartial so it causes no lag).
  • Object pooling: would not work on this case since world era is infinite and I can not pool an infinite variety of tiles.

Any assist could be appreciated.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles