Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question The pain of trying to use Tilemap for open worlds - what are my remaining options?

Discussion in '2D' started by Walley7, Jun 16, 2023.

  1. Walley7

    Walley7

    Joined:
    Dec 4, 2019
    Posts:
    56
    ----------------------------------------
    TLDR version:
    Tilemap.SetTilesBlock is slow - very slow. Much too slow for open world streaming of a large 2d world. What are my options - is it as bad as having to roll my own alternative?
    ----------------------------------------

    I'm working on an open world 2d game, with a target world size of 2816x1536 in tiles (2.8km x 1.5km).

    Pretty early on it became apparent to that Tilemap wasn't up to the task out of the box:
    • It doesn't store tiles in memory efficiently (rough measurements show it's using about 88 bytes per tile).
    • It definitely doesn't serialize tiles efficiently (each individual tile takes up about 265 characters in the scene file - ouch!).
    But I devised a plan - use the Tilemap component for presentation only, and create my own memory structure to stream tile data from. Said structure organizes tiles into chunks (flexible, but I've been using 32x32 for now), and gets each tile down to a single ushort (2 bytes of memory), and gets serialized pretty effectively too (about 3 bytes per tile, by saving them in Base64). Not bad!

    I then created a set of streaming components to load chunks in and out relative to the camera, and got them working both in the editor and ingame. Great.

    And for a time... things were good. I thought I had it handled.

    Until some recent benchmarking slapped me in the face. What's the point of pain you ask? Tilemap.SetTilesBlock I answer. Right now on a Ryzen 5800 it's taking about 57ms to load 5 chunks - or about 11ms to load one. That's for chunks of 32x32, so 1024 tiles each.

    I've checked and double checked that SetTilesBlock is the bottleneck here - not any of my other surrounding code. It literally takes it that much time just to set those tiles. I *suspect* that under the hood it stores each tile as a position + tile element, and that the high price of this function is that it must be converting the data to that structure.

    Anyway - this is definitely not going to work for the game I want to make. The problem is compounded by the fact that I need multiple tile layers - at minimum a ground, walls and roofs layer (and probably more for overlays and doodads). So even if I were to get creative, whether with smaller chunk sizes or spreading the load out over multiple frames, it's still going to be a disruptive performance hit - especially on middle of the rung hardware.

    So, my question - what are my options here? Any suggestions?

    I'm not seeing a way to get the performance I need out of the Tilemap component itself without making serious gameplay compromises which I don't want to make. I'm suspecting I basically have to roll my own solution, along the lines of what CodeMonkey has done in his "Custom Tilemap in Unity with Saving and Loading (Level Editor)" video.

    I do have to say - it's pretty disappointing to realize just how handicapped Unity's Tilemap component really is. It's especially questionable that it's so greedy with memory - the devs really didn't anticipate that people would want to use it for more than small levels and single rooms?
     
    Last edited: Jun 16, 2023
  2. vonchor

    vonchor

    Joined:
    Jun 30, 2009
    Posts:
    238
  3. karderos

    karderos

    Joined:
    Mar 28, 2023
    Posts:
    376
    the solution is - dont use either settilesblock or settiles, instead use

    SetTile

    yes, SetTile has much worse performance than settiles or settileblock on its face BUT, you can do it one at a time, so you should make a coroutine that slowly does settile when loading chunks. How slow? Maybe 50 tiles per frame, maybe 10 tiles per frame.

    If your player is fast maybe you will have to load chunks in advance so the map loading is never visible


    - also for you to consider, there is no point to make chunks square for a 2d game, since all screens are rectangular you should do instead 32x18 for example, tho this might be besides the point
     
    Last edited: Jun 16, 2023
  4. Walley7

    Walley7

    Joined:
    Dec 4, 2019
    Posts:
    56
  5. vonchor

    vonchor

    Joined:
    Jun 30, 2009
    Posts:
    238
    That's surprising to me: I found it much faster.

    Not sure how you are implementing chunking: I do it by padding the camera bounds a bit and loading only what's within the camera bounds+padding and deleting chunks that go outside the bounds+padding.

    I don't have my benchmarks available as I did this testing last year sometime IIRC.
     
  6. Walley7

    Walley7

    Joined:
    Dec 4, 2019
    Posts:
    56
    Either that or I use SetTilesBlock with 1x32 and 32x1 slices. Fair point re the chunk sizes, it's an option - if I did stick with Tilemaps I'm going to need to squeeze every small gain I can.

    Another thing I'm considering is keeping Tilemaps on the editor side for the workflow of palettes and brushes, but at runtime load the data into my own alternative for fast streaming.
     
  7. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    8,988
  8. karderos

    karderos

    Joined:
    Mar 28, 2023
    Posts:
    376
    i have been doing a 2d open world also using tilemaps and another thing that I notice is that unity has been improving tilemaps performance over the years, for example when i upgraded from unity 2017 to 2019 i notice a big performance upgrade, and maybe a small performance upgrade going to 2021, so they are probably working on it over the years, tho its a longshot of course to rely on updates that might never come
     
  9. vonchor

    vonchor

    Joined:
    Jun 30, 2009
    Posts:
    238
    Actually I can provide an example but it takes a little explanation.

    I have a new Palette tool that aside from painting tiles can also paint multilayer chunks: ie, one or more tilemap 'layers' at a time.

    Painting a 31x30 chunk over four tilemaps with one layer (ground) filled in totally and others not so much (see image) took 32 msec although this includes internal overhead like locating the tilemaps by tag, updating tile GUIDs and a few other things.

    View attachment 1257850
     

    Attached Files:

  10. Walley7

    Walley7

    Joined:
    Dec 4, 2019
    Posts:
    56
    After a little fiddling I've realized the 57ms was being made worse by another contributing factor, so it's actually closer to 15ms on my machine - not as severe but still not ideal. Though it *might* be approaching the range where smaller chunk sizes and loading slices or individual tiles could start to be playable.

    The contributing factor is a bit of a funny one - while setting up the benchmark I was populating the tilemap data with random tiles from my full set of tiles - including some of my wall tiles which are not 1x1 - many of them are 2 or 3 tiles high (so 1x2 and 1x3). I've found that the Tilemap component does not like when you mix different tile dimensions - you'll start to see occasional "GetTransformInfoExpectUpToDate" errors in the console, and apparently it makes SetTilesBlock 3x slower as well (I setup a fresh scene to test this theory).

    It seems to somehow permanently corrupt the Tilemap as well, as even once I switched all of the tiles back to 1x1, the performance degradation remains - the only solution is to delete and recreate the Tilemap component.

    I've also seen the "GetTransformInfoExpectUpToDate" issue triggered by using tile pivots other than 0.5, 0.5. So there's clearly a few quirks under the hood.