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. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Performance Advice for a Large 2D World

Discussion in 'Scripting' started by Megalogue1, Jun 9, 2022.

  1. Megalogue1

    Megalogue1

    Joined:
    Dec 17, 2020
    Posts:
    134
    My current project features a 2D world that is generated procedurally at the start of the game. It's tile-based, and therefore made up of blocks (similar to those in Minecraft). The problem? I want it to be large.

    Basic Setup: Instead of just directly filling the tilemap, I use a WorldGenerator script to generate a two-dimensional array of WorldBlock, which is an ordinary C# object. This array is stored in a GameWorld script. After generation, a separate GameWorldVisuals script reads the array and sets tiles in the tilemap accordingly. Finally, the Tilemap game object has a TilemapCollider2D attached, allowing the player to run around on the tiles, sidescroller-style.

    I think this architecture works pretty well, considering the needs of my project and my beginner/intermediate programming skill. But because I want the world to be large (1024x1024 tiles at minimum), performance is becoming a major concern. Here are the two approaches I've tried so far.

    Approach 1 (most naive): I set all the tiles, all at once, at the start of the game. This leads to a long loading time when I press "Play", as well as a very noticeable freeze whenever even a single tile in the tilemap is changed (e.g. the player breaks a block). My guess is that this lag is due to Unity needing to build/rebuild some or all of an EXTREMELY large tilemap collider.

    Approach 2: I generate the world data at the start like normal, but I set and clear tiles as the player moves around. A WorldLoader script tracks the player's movement, and whenever the player moves "X" distance from their last position, the tiles in a small area around the player are set, and tiles outside that area (e.g. ones that are offscreen) are cleared. This approach has better performance, but there is still a noticeable "hiccup" while running around the world. This is probably due to frequently looping through 30x30 grid positions, checking which ones should be loaded, setting/clearing each tile as needed, and then updating the tilemap collider.

    I've been considering an "Approach 3" where I use a chunk-like system (again, similar to Minecraft), but I can foresee performance issues here, too. It seems like whenever I change lots of tiles in a single frame, that will also lead to lots of collider updates, which will lead to lag. Maybe I could find a way to spread the changes across multiple frames, but this seems like it would require more programming finesse than I currently have.

    Are there other approaches I haven't thought of? Are there assets that might solve this problem for me? I'm aware of things like World Streamer, but that particular asset talks a lot about streaming world data from disk, which doesn't seem to fit with my goal of runtime procedural generation.

    Thanks for reading this far. All advice is welcome!
     
  2. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    401
    Depending on how simple or complex you need your tilemap collision to be, it might be worth sacking off the tilemap collider entirely and going it alone.

    What are your requirements in that respect?
     
  3. Megalogue1

    Megalogue1

    Joined:
    Dec 17, 2020
    Posts:
    134
    It's not as simple as some flat world borders at the edges, if that's what you mean. The world is generated with perlin noise to resemble lots of twisting and turning tunnels.

    I should also note that the TilemapCollider2D is paired with a CompositeCollider2D. Without this, my physics-based character controller frequently gets stuck while walking, even when on a surface that appears flat. This composite collider may also be adding to the lag when recalculating things.
     
  4. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    401
    No, I meant in terms of the amount of collision detail for each tile. Is it empty/solid tiles, partial tiles, curves...?
     
  5. Megalogue1

    Megalogue1

    Joined:
    Dec 17, 2020
    Posts:
    134
    I see. Currently each tile is just a square. As in, the collision type in the Tile asset is set to "Grid." So in that sense it is very simple.

    Is this helpful to my situation in some way?
     
  6. lordconstant

    lordconstant

    Joined:
    Jul 4, 2013
    Posts:
    389
    Your first step needs to be working out exactly where your bottlenecks are.

    You should use unitys profiler to get a sense of whats causing things to take so long when your turning on & off tiles.

    I would recommend going for your 3rd choice & working with chunks. Chunks are just a faster way of doing approach 2 so it will save you having to iterate through every tile to work out if anything need changes.

    If the collider issue is your problem try having a collider/tilemap per chunk instead of one giant one as then it gives the collider generation code less work to do. Use an object pool to avoid having to reinstantiate them as you run around.
     
  7. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    401
    If that's the way things are going to continue to work, absolutely. Tile-based collision is very straightforward to implement by hand and scales very well with map size.

    Chunking up your collision is another option, but you will still run into issues with catching on seams. You might be able to work around that by having your collision chunks overlap by a few tiles and only turning on one chunk at a time, but that might get messy once you start having other entities, projectiles or what have you.
     
  8. Megalogue1

    Megalogue1

    Joined:
    Dec 17, 2020
    Posts:
    134
    This is surprising to me. Isn't the built-in TilemapCollider2D built specifically for the purpose of tile-based collision? It was written by, presumably, a team of highly-qualified professionals. Are you saying that it's flawed and/or inefficient in some way that these professionals somehow missed? To the extent that making a better version from scratch would be "straightforward?"

    If working from scratch will help my project, that's great. I just struggle to see how the TilemapCollider2D isn't the best available option.
     
  9. PuppyPolice

    PuppyPolice

    Joined:
    Oct 27, 2017
    Posts:
    116
    Looking at approach 2 you should be possible to change tile checking from 30x30 to 30x2, max 30x4 tiles to check and alter.

    Since you know how big it is, everytime you move a tile to the right you remove the whole row furthest to the left and generate a new row to the right, you don't need to check everything in the middle.
     
  10. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    401
    A perfectly reasonable question!

    Remember when I asked what kinds of collision you were making use of, and you said just empty/full square tiles? Well, the specific problem of detecting and resolving simple collisions (eg axis-aligned boxes smaller than the tile resolution) with a regular square axis-aligned grid can be solved much, much more efficiently than the general problem of colliding arbitrary 2D collider shapes with a tilemap that could also contain irregular tiles.

    Simple tilemap collision can quite literally be a case of casting two floats to ints and looking up the X/Y result in the tilemap. That's going to be certainly hundreds and possibly thousands of times faster than a more general solution could ever be, and is instantly responsive to tile changes because the tile is what you are colliding with, as opposed to some separately calculated and stored representation of it.

    In your case, the performance of individual collision detections is not the issue, although it might be if you started adding thousands of moving objects. No: in your case, the relevant issues are:
    1. The enormous dimensions of the tilemap, which makes regeneration of the collision data noticeably time-consuming
    2. The fact that breaking it up into chunks reintroduces the trip-hazards that plague all general-purpose physics systems when objects share unwelded common vertices and internal surfaces

    Thus, I described making a 'better' version from scratch as straightforward, not because the Unity devs have missed something obvious, but because TilemapCollider2D is geared up to solve a far, far more complex problem than the one you actually have. In this case, 'better' means 'more fit for a highly specific purpose'.
     
    Last edited: Jun 13, 2022
  11. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    10,557
    The CompositeCollider2D was created way before the TilemapCollider2D existed. It was designed to help you merge geometry in a static set-up, not a huge scale dynamic set-up. This'll be the sole reason why you're seeing it taking a long time. A single tile change in a TilemapCollider2D only affects that tile. By adding a CompositeCollider2D you're asking that every single physics shape in a TilemapCollider2D which could equate to hundreds of thousands of shapes be merged into one large set of continuous edges. That's a global operation and cannot just add/remove a single tile. There's plenty of profiler instrumenting in Unity which will show this as the problem.

    The ghost collision problem is a pain in every single physics engine. Using a CompositeCollider2D on a TilemapCollider2D is a sledgehammer to crack a nut but unfortunately, without using a decent and smarter character controller that can handle small and unexpected collision normals such as are expected when climbing stairs then you're going to suffer from it.

    Not just specifically for tilemaps but for the larger problem of dealing with such ghost collisions is a subject we've been discussing and are actively planning workflows to remove this pain entirely. The initial focus is for the tilemap by allowing a smart/automatic joining of tiles without the use of the CompositeCollider2D making a tile add/remove/change a local operation for that tile only. Also the ability to only create the physics as part of the visual chunking system too is an option.

    Also, the seemless handling of dynamic elements such as a separate GameObject that contain platforms such that they can merge dynamically with "surface" elements in a tilemap is another use-case we're looking at. This is part of the solution for separate Collider2D being part of the same surface essentially. For instance, a whole bunch of BoxCollider2D next to each other.

    For now though, there's no solution beyond a smarter character controller when you have large scale dynamic collider changes. Again, though, it's not a bug but just how physics engines work. It is a frustraiting pain point we're actively looking at now.

    Certainly, for tilemaps, if you can split them up into static/dynamic elements or logical chunks then this'll help but joining those separate chunks isn't something that can be done now. That said, in combination with the CustomCollider2D you could do this but it's not something available out-the-box which is what we want to provide.
     
    Kurt-Dekker likes this.
  12. Megalogue1

    Megalogue1

    Joined:
    Dec 17, 2020
    Posts:
    134
    I can kind of see how this would work with a certain type of character controller--that is, one where you're directly modifying the player's transform.position to create walking/running/jumping movements. Can it also work with a physics-based controller--one that applies force to a rigidbody and thus allows other forces/collisions to act naturally on it? I'm having a hard time seeing how I could allow normal physics interactions (e.g. some boxes falling on the player) but also "tell" the physics engine that a collision with a tile has happened. I wasn't aware that one could intervene with the physics engine at all in that way.

    Hi MelvMay! Good to hear from you. I wasn't aware of the relationship (or lack thereof) between these two components, or how awkward it truly is to use them together.

    The unexpected difficulty involved here has led me to question whether block-breaking is actually an important part of my game design, and I'm not convinced that it is. Right now I'm exploring alternative ways to generate 2D levels, without the use of Tilemaps. Maybe I won't need this awkward problem to be solved after all.
     
    MelvMay likes this.
  13. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    401
    Absolutely! It can take some fettling depending on what you're trying to do, but it's absolutely possible to accessorise the physics with new behaviour. For example, I made this:

    https://assetstore.unity.com/packages/tools/physics/linkage-206044

    Adding custom tile-based collision detection and resolution should be no problem at all providing you use the correct method of moving the character to resolve collisions - typically rigidbody.MovePosition during FixedUpdate.
     
  14. Megalogue1

    Megalogue1

    Joined:
    Dec 17, 2020
    Posts:
    134
    Wow, nice asset! :)

    This might be drifting a bit too far outside the realm of the "Scripting" subforum, but doesn't rigidbody.MovePosition interfere with external forces? For example, I can envision a scenario where the player is using MovePosition to walk around and a gust of wind starts blowing. I can see things getting pretty weird with the wind adding a force to the rigidbody and the movement script basically forcing it to move to a certain position each FixedUpdate.

    I've been focusing solely on achieving movement with rigidbody.AddForce so far. I never know if I'm going to want external forces to act on my characters as my game grows, and I think AddForce helps keep that behavior natural, in case I ever need it.
     
  15. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    401
    The answer is "Yes, if you do it wrong."

    All the linked platforms in the Linkage video are moved using MovePosition and MoveRotation, and the entire purpose of the package is to respect all external forces applied to the objects.

    If you decide to pursue a roll-your-own collision system for the tilemap, let me know and I'll sort you out with an example of how it can be done while respecting external forces.
     
    Megalogue1 likes this.
  16. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    10,557
    It's not a case of interferring, it's a case of using two different movement methods at the same time which in itself makes no sense. In the end, you want a position to change. You have to choose one method to do that. Some change the position directly, some set the velocity directly which then gets integrated into a position change, some add a force which gets integrated into a velocity change then integrated into a positon change and some use MovePosition which is another move to position with a calculated velocity. You cannot move to a position AND use a force or set the velocity directly.
     
    Megalogue1 and Peeling like this.