#Tilemap SetTiles is slow, is there any fix?

1 messages · Page 1 of 1 (latest)

prime pilot
#

I have a game where the player is constantly moving, so the world is generated on the fly. Below I have included the function for generating a new chunk, my noise function isnt the slow part, the part that is taking up most of the time is the TileMap.SetTiles() where im passing in the array of tiles. Im wondering if there is a better way to do this, or a way to speed it up.

I prune old chunks since a large tilemap was causing the game to slowdown. I also make the tile arrays in tasks since that part doesnt interact with the UI so that should be fine. the timers say that scheduling and generating the tasks take about 4ms, Pruning chunks takes about 1ms per chunk, it usually is around 4-6ms, setting the chunks is always around 6ms. This means in total when a player hits a trigger to generate more chunks it takes about 16ms. This means its a noticeable stutter since everything else runs fairly smooth. I can tell exactly when a chunk is made especially the longer the level goes on the real worry is i have a pretty decent PC, I have tried building the game and even as a build i still see the stutter so i know its not just editor lag.

#
        private void GenChunkHandlerFromCoords(List<Vector2Int> chunkCoords)
        {
            Stopwatch sw = Stopwatch.StartNew();
            //Remove old chunks because a massive tilemap slows down game
            while (last.Count > Globals.lastChunks)
            {
                Stopwatch pruneTimer = Stopwatch.StartNew();
                Vector3Int[] positionsToRemove = last.Dequeue();
                world.SetTiles(positionsToRemove, new TileBase[positionsToRemove.Length]);
                pruneTimer.Stop();
                UnityEngine.Debug.Log($"Pruned chunk in {pruneTimer.ElapsedMilliseconds} ms");
            }
        UnityEngine.Debug.Log($"[GenChunkHandlerFromCoords] PRUNE time: {sw.ElapsedMilliseconds} ms");

        if (chunkCoords.Count == 0) return;
            //The vector3 is for all of the TIle Pos
            List<Task<(Vector3Int[], TileBase[])>> chunkTasks = new();
            foreach (Vector2Int coord in chunkCoords)
            {
                Stopwatch singleChunkTimer = Stopwatch.StartNew();
                //GenerateChunkDataAsync is a custom function I can provide if needed
                chunkTasks.Add(GenerateChunkDataAsync(coord));
                singleChunkTimer.Stop();
                UnityEngine.Debug.Log($"Scheduled chunk {coord} in {singleChunkTimer.ElapsedMilliseconds} ms");
            }

            Task.WaitAll(chunkTasks.ToArray());

            Stopwatch tileSetTimer = Stopwatch.StartNew();
            foreach (var task in chunkTasks)
            {
                world.SetTiles(task.Result.Item1, task.Result.Item2);
                last.Enqueue(task.Result.Item1);
            }
            tileSetTimer.Stop();

            sw.Stop();
            UnityEngine.Debug.Log($"[GenChunkHandlerFromCoords] Total time: {sw.ElapsedMilliseconds} ms");
            UnityEngine.Debug.Log($"[GenChunkHandlerFromCoords] Tilemap.SetTiles time: {tileSetTimer.ElapsedMilliseconds} ms");
        }
#

Example of current

fair pawn
#

I will say you seem to be doing a few unnecessary allocations here though. Cutting down on allocations will almost certainly help.

#

You didn't share the main code though

prime pilot
fair pawn
#

The profiler is made for exactly this.

Also are you using C# threading here? You could maybe look into the job system instead

prime pilot
prime pilot
#

i mean there is a clear spike there lol. I thought it was more than the 16ms i was logging but good to know this tool exists

fair pawn
prime pilot
#

yeah still learning how to do that lol

#

the graph says physics, which again just seems like part of the setTiles

#

If i had to guess its becasue i have a 2d collider map on my tilemap, Im using that with is trigger

fair pawn
prime pilot
#

ok so looking at this

I think i understand how to read this. My function GenWorld.Update() is 13ms while updating the collider is 33ms

#

this helps to know how to do this, but really just confirms what i thought Updating the tile map (specifically with a collider) is slow

fair pawn
#

but yeah

#

it looks like since you're updating the tilemap it's updating the collider

#

updating the collider is the slow part

#

Something you might try - instead of loading chunks into an existing tilemap, see if it's faster to instantiate a separate tilemap for each chunk

prime pilot
fair pawn
#

it might be that since it's all one tilemap and presumably a single CompositeCollider, that it needs to recreate the entire collider for the whole world each time you load a chunk

prime pilot
fair pawn
fair pawn
#

if so - doing that may also improve performance./

prime pilot
prime pilot
#
//Remove old chunks because a massive tilemap slows down game
            while (last.Count > Globals.lastChunks)
            {
                Stopwatch pruneTimer = Stopwatch.StartNew();
                Vector3Int[] positionsToRemove = last.Dequeue();
                world.SetTiles(positionsToRemove, new TileBase[positionsToRemove.Length]);
                pruneTimer.Stop();
                UnityEngine.Debug.Log($"Pruned chunk in {pruneTimer.ElapsedMilliseconds} ms");
            }
#

is there something specifically for Unloading? or is setting it to null work? becasue setting it to null does remov it from the collider

#

I can also make the total count smaller, but i have a use case for zooming out and seeing some of the history of where the player has been

#

if you still wanted Main here is what is calling that function

void Update()
{
    Vector3 playerPos = player.transform.position;

    int playerChunkX = Mathf.FloorToInt(playerPos.x / chunkSize);
    int playerChunkY = Mathf.FloorToInt(playerPos.y / chunkSize);
    Vector2Int playerChunk = new(playerChunkX, playerChunkY);

    if (playerChunk != lastPlayerChunk)
    {
        lastPlayerChunk = playerChunk;
        GenerateChunksAroundPlayer(playerChunk);
    }

    if (playerPos.y < Globals.PlanetList[Globals.planetID].GetBattleDepth() - 10)
        LoadBattle();
}
private void GenerateChunksAroundPlayer(Vector2Int centerChunk)
{
    List<Vector2Int> chunksToGenerate = new();

    int radius = 1; // 1 = generate a 3x3 grid around the player
    for (int dx = -radius; dx <= radius; dx++)
    {
        for (int dy = -radius; dy <= 0; dy++) // only generate downward if needed
        {
            Vector2Int chunkCoord = new(centerChunk.x + dx, centerChunk.y + dy);
            if (!generatedChunks.Contains(chunkCoord))
            {
                generatedChunks.Add(chunkCoord);
                chunksToGenerate.Add(chunkCoord);
            }
        }
    }

    GenChunkHandlerFromCoords(chunksToGenerate);
}
prime pilot
dull cape
#

Breaking the world into chunks(separate tile maps) might help, as Unity wouldn't need to rebuild existing tile map colliders.

prime pilot
#

thats what that was above ^

fair pawn
#

how big are these chunks

prime pilot
#

cut the physics time in half

fair pawn
#

Good to see that actually worked.

Maybe a smaller chunk size would do the rest?

prime pilot
#

120 x 120

#

they are already fairly small

#

i base it off the screen width, so wide screen monitors need a larger chunk size

fair pawn
#

interesting - why do you base the chunk size off monitor size?

#

or pixels rather

prime pilot
#

becasue if someone wants to make their screen skinny and see more to the side im not going to stop them

#

they cant get over that far since i have a capped turning radius

fair pawn
#

But you don't need to change the chunk size for that

#

you can just change how many chunks are loaded at once

prime pilot
#

thats true i could change how many chunks i generate

fair pawn
#

It's not a direct comparison but Minecraft uses 16x16 chunks

#

(of course it's also 3D)

#

Don't you need a consistent chunk size anyway for your save format?

prime pilot
#

I also tried Co routines, but that was worse

prime pilot
#

that may change in the future, but i think i have a working solution

I was doing OnTriggerStay2D(Collider2D other)

but now im just doing a check after movement to see if there is a tile in the tilemap under my player collider. This removes the collider from the Tilemap

#

Now im back to my original script, which still has a 20ms frame (where the rest of the game is no sub 2ms) but i can reduce chunk size/look for better optimizations there

fair pawn
#

Oh yeah of you don't need real collision, just a coordinate check, you can just use the functions on the Tilemap to get which tile is at a given coordinate and which coordinate corresponds to a given world position

prime pilot
#

I might in the future but i cant think of anything now so ill deal with it then if it comes up again.

#

My script is still slow, maybe i try coroutines again

#

Tilemap.SetTiles is still half the time of the script

#

Doing deep profiler seems to make every take alot longer, but gives u the breakdown of the script

#

.setTiles is 49ms, where my actual gen function is 62. CLose to half the time is setting the tiles. Without Deep profile turned on its about 13ms which is what my consol.log was saying earlier

#

this time doing smaller more frequent placements might be fine though since i have removed the overhead of chaning the collider

fair pawn
#

Maybe I'm missing something but are you calling SetData for ALL the chunks in range each time?

#

Or are you only doing it for chunks that have newly entered or exited the range?

prime pilot
#
int radius = 1; // 3x3 grid
for (int dx = -radius; dx <= radius; dx++)
{
    for (int dy = -radius; dy <= 0; dy++) // only downward if needed
    {
        Vector2Int chunkCoord = new(centerChunk.x + dx, centerChunk.y + dy);
        if (!generatedChunks.Contains(chunkCoord))
        {
            generatedChunks.Add(chunkCoord);
            chunksToGenerate.Add(chunkCoord);
        }
    }
}

I have a hash of vector2ints with the chunks cords i have generated generatedChunks

fair pawn
#

Ok good. Maybe double check that's working properly but yeah looks about right I think.

#

Because you could easily run into trouble doing unnecessary calls if that logic isn't correct

dull cape
prime pilot
#

yeah i realized, im working on that now

The main goal of putting it in tasks is different threads can build the array of TIles and just return it

dull cape
#

Explanding SetTiles, might also provide some more clues

prime pilot
#

it gets messy fast, thats why i left it collapsed

#

seems like most of it is from ITilemap.GetAllTileData()

#

thats 26 out of 46 ms

#

TileBase.RefreshTile() is also called alot and so is Object.ForceLoadFromInstanceID()

but this is me just speaking becasue i have no clue what i can do about this since its unities code

#

potenntially do an ovverride or extend the class or something. It seems like it doing alot of extra stuff. I dont know why it needs to call all of this, should just be calling SetTile alot

dull cape
prime pilot
#

exact same numbers with setTileBlock

#

ill try tilechange data next i guess

#

roughly the same

dull cape
prime pilot
#

I did some refactoring and its good enough for now

#

just made it synchronous and chose block since it says its more performant

prime pilot
#

This is the deep profiler, still says 40% of the time is just the setTileBlock

#

guess thatll have to do for now, Later ill optimize what needs to be generated and do msaller blocks

dull cape
#

Ah, yeah, deep profiling would add a lot of overhead, especially if you're making a huge amount of calls.

prime pilot
#

well considering with collider method generating took 55ms, removing that brought me down to 16ms now im at 9, cant be to upset