Chunk tilemap physics - #102662
Conversation
|
More or less though. This PR makes the baking as built-in to the TileMaplayer itself, while navmesh baking is made on the navigation region side (like, you to define a region and bake it). |
|
From further testing:
|
Ah well, the colors are a bit randomized (to distinguish shapes a bit), but it is done per-chunk. So well, the first shape always use the same color so you cannot distinguish chunks. I guess making the seams visible should be doable, I'll give it a try.
Hmm, I don't think it would be easily solvable. Modifying the TileSet kind of has to re-generate the whole TileMap. I guess it could be optimized but that's sadly a toooon of work. So for now I don't really see a good solution for the problem. |
I meant modifying the TileMap itself. When you paint or erase tiles, it seems to re-create modified physics chunks for no reason. |
Oh ok. Well yeah, it updates chunk by chunk, like for rendering. So indeed changing a tile in a given chunk re-renders the whole chunk. Not sure it could be optimized more though. Maybe I could limit the chunk size in the editor, but It might hurt debugging I guess. In any case, the polygon merging can maybe be optimized a bit, I need to check. |
|
This would be great to have for light occluders also. I can say that merging tiles greatly reduced the impact of shadows in our game in 4.3. Clay has made some shadow performance improvements in 4.4 that I've yet to test, but I'm guessing the chunking will still help. |
That's a good point. It should be doable. |
|
It is definitely an improvement over the status quo.
No full fix to physics ghost collision. This technically can not fix physics seam issues as when you bake it to chunks you still have chunk seams. For physics to really remove all those internal edge ghost collision issues it would need to merge all cells together that create a combined shape instead of slicing them to chunk size.
Change from rendering the debug with the very inefficient canvas item polyline function to bake a chunk mesh to render, it is dirt cheap in comparison. |
Hmm, interesting idea. IIRC, I had issues with 2D meshes back in the days, but maybe it got better? I'll try it out. |
|
Maybe there were issues but by now the 2d navmesh, csg and other places with a lot of edge debug use it already with no reported issues so I think it should not be a problem for TileMap to adopt it as well to improve performance. |
You can set chunk size to cover the whole TileMap. I checked my biggest one, chunk size 512 and works without problems. It does not impact loading time, but modifying the tiles is slower (luckily I don't need to do it). |
Ok I did not thought about that option, just assumed there would be a limit of quadrant size of sorts, yeah that could work. |
I'm wondering if we should improve the usability of huge chunk sizes by limiting their size in the editor, so that you can use one in the project and not suffer from slow editing speeds. On the other hand, I know it's been discouraged in the past to have huge collision shapes spanning the whole level, as each dynamic object will have to test collisions against the whole shape regardless of its position. This is particularly the case in 3D where trimesh collision is generally much more complex than tile collision in 2D.
I think this is better solved at a character controller level by implementing skimming, so that you don't need to make the collision shapes more complex. At a core level, skimming refers to preserving the character's velocity when hitting something for a short amount of time. This means that you will keep sliding until you no longer hit the surface, and as soon as you are no longer colliding, you will keep the velocity you've had before hitting the surface. You could see it as a reverse coyote time of sorts. Skimming as a concept has been used in both 2D and 3D games, with varying implementations (some being more generous than others). Note that regardless of this, I don't think skimming (or adding chamfers to collision shapes) this resolves the issue of characters bumping when sliding on the ground. One way to minimize the issue is to disable gravity while on a flat surface. |
There was a problem hiding this comment.
Tested locally, it works as expected.
Testing project: test_tilemap_large.zip
Physics seams issues
Holding left arrow in the testing project's main scene.
Before
rolling_ball_before.mp4
After
Physics seams issues still occur occasionally, but are much rarer and less severe.
rolling_ball_after.mp4
After (physics quadrant size set to 2**31 - 1)
This fully avoids the issue, but makes editing slower in the editor and may have adverse performance issues in large scenes (due to each object having to check collision against the entire level's shape).
rolling_ball_after_max_quadrant.mp4
Performance
PC specifications
- CPU: Intel Core i9-13900K
- GPU: NVIDIA GeForce RTX 4090
- RAM: 64 GB (2×32 GB DDR5-5800 C30)
- SSD: Solidigm P44 Pro 2 TB
- OS: Linux (Fedora 41)
Command line used for testing (we render in a tiny window to avoid GPU bottlenecks1):
godot --path /path/to/project lots_of_tiles.tscn --resolution 64x64 --print-fps
1000 RigidBodies enclosed in a complex TileMap.
Before
We encounter a "physics spiral of death" phenomenon, hence the sudden decrease in FPS at some point.
Project FPS: 391 (2.55 mspf)
Project FPS: 577 (1.73 mspf)
Project FPS: 11 (90.90 mspf)
Project FPS: 8 (125.00 mspf)
Project FPS: 60 (16.66 mspf)
Project FPS: 9 (111.11 mspf)
Project FPS: 20 (50.00 mspf)
Project FPS: 590 (1.69 mspf)
Project FPS: 328 (3.04 mspf)
Project FPS: 399 (2.50 mspf)
Project FPS: 442 (2.26 mspf)
After
No more physics spiral of death, and overall performance is greatly improved.
Project FPS: 837 (1.19 mspf)
Project FPS: 488 (2.04 mspf)
Project FPS: 698 (1.43 mspf)
Project FPS: 876 (1.14 mspf)
Project FPS: 900 (1.11 mspf)
Project FPS: 872 (1.14 mspf)
Project FPS: 880 (1.13 mspf)
Project FPS: 649 (1.54 mspf)
Feedback
- The property should be documented in the class reference, and it should have a property hint added to set a suitable range. I'd suggest something like
1,1024,1as sizes above 1024 will likely run into the aforementioned performance issue, on top of being very slow for editing. - We could sidestep the editing performance problem at large quadrant sizes by clamping the size of the physics quadrant while in the editor to a value like 16 or 32.
Footnotes
-
--disable-render-loopbreaks--print-fpsreporting, so we can't use it to test physics performance. ↩
That depends. The chunk size is square, while the TileMap can be wide. My widest level is 128x1664, which is much smaller than 1024x1024, but with size limit it couldn't be covered by single chunk. Though with The slow editor editing should be fixed not circumvented. As I said, physics chunks don't need to be created in the editor if the shapes are not visible. |
|
For editing performance could just do as the navmesh baking does. The navmesh editing has a timer that triggers and sets the time on any editing. So only when the user stops editing the timer fires and the navmesh gets rebaked. Could work the same for this collision bake. |
There was a problem hiding this comment.
Usually I've seen error checks before guards, but not sure if it's consistent.
|
I would like to ask a question just as food for thought before we regret it later. Rotated, one-way platforms are not technically possible to register in the current TileSet workflow. My own project checks a vast area for collision RIDs that correspond to a specific tile. No need to go in the specifics, but if they do correspond, they are rotated from within the physics server. Does this PR affect this kind of routine? And how to work around it? |
Mickeon
left a comment
There was a problem hiding this comment.
Unsure if "quadrant" has ever been the most accurate word to describe this. Even in geometry it's not accurate, but, oh well. That's been their name.
| </member> | ||
| <member name="physics_quadrant_size" type="int" setter="set_physics_quadrant_size" getter="get_physics_quadrant_size" default="16"> | ||
| The [TileMapLayer]'s physics quadrant size. Within a physics quadrant, cells with similar physics properties are grouped together and their collision shapes get merged. [member physics_quadrant_size] defines the length of a square's side, in the map's coordinate system, that forms the quadrant. Thus, the default quadrant size groups together [code]16 * 16 = 256[/code] tiles. | ||
| [b]Note:[/b] As quadrants are created according to the map's coordinate system, the quadrant's "square shape" might not look like square in the [TileMapLayer]'s local coordinate system. |
There was a problem hiding this comment.
| [b]Note:[/b] As quadrants are created according to the map's coordinate system, the quadrant's "square shape" might not look like square in the [TileMapLayer]'s local coordinate system. | |
| [b]Note:[/b] As quadrants are created according to the map's coordinate system, the quadrants may not look like squares in the [TileMapLayer]'s local coordinate system, depending on the [member tile_set]'s [member TileSet.tile_shape]. |
Maybe it's too verbose?
This would affect the collision RIDs from my understanding, since this is basically just creating merged shapes out of each chunk. As a workaround you could set the quadrant size to 1. But if you still wanted the benefits of this PR, you could have a separate TileMapLayer with just the one-ways. Not an elegant solution but it'd work. |
Okay. It's among the things worth sharing around and documenting when this gets merged, so let's take note. |
|
Thanks! |
|
Performance improvements are always awesome. Of course it breaks the destructible tiles system I JUST finished implementing yesterday 🥲 Does anyone have a workaround for getting the cell coordinates from an Area2D script? func onBodyShapeEntered(bodyRID: RID, bodyEntered: Node2D, bodyShapeIndex: int, localShapeIndex: int) -> void:
if bodyEntered is TileMapLayer:
var cellCoordinates: Vector2i = bodyEntered.get_coords_for_body_rid(bodyRID)doesn't work anymore. |
|
You can set quadrant size to 1 and it should behave as before (including the performance). Bigger quadrants will result in slower updating, so you might want to disable them anyway. AFAIK there is no reliable way to get a tile from a position (see #35344). For some use-cases, where you don't need precise coordinates (like for detecting floor type), you can use a couple of raycasts to get tile data and approximate the result. |
|
WOW this PR is amazing. You still get the collision issues on corners of tilemaps but building Godot from source to get this change has saved my project! |
|
unfortunately this broke my enemy behavior. my enemies have an Area2D with the this was previously possible by using |
|
As discussed previously, you can get the old behavior back by setting the chunk size to 1. Unless you're saying that this is broken too...? |
yes, that did the trick! thank you and apologies for not combing through the entire thread. |
Chunk tilemap physics
Chunk tilemap physics


Right now, TileMapLayer physics is implemented using one collision body for each cell, which is likely lowering runtime performance (to be verified, but I doubt having thousands of colliders on a map has no impact). This PR reworks the TileMapLayer physics so that cells shapes get merged into bigger collision shapes, whenever possible. A
physics_quadrant_sizeparam allow changing the size of each collision chunk.This video illustrate the processing done:
2025-02-10.16-16-07.mp4
Note this PR breaks compatibility for
get_coords_for_body_rid, as with the PR, a single body can cover multiple cells.Bugsquad edit: Fixes #84163 Fixes #89458