MonoGame.Extended icon indicating copy to clipboard operation
MonoGame.Extended copied to clipboard

[Tiled] Make Tiled classes changeable in the run-time

Open rds1983 opened this issue 7 years ago • 5 comments

Right now, most of the Tiled classes constructors are internal, most of properties are read-only. It seems that TiledMap is supposed to be created only by ContentManager. I think it would be helpful if Tiled classes could be created/changed in the run-time. Moreover other MonoGame.Extended classes like TextureAtlas or BitmapFont have that feature.

rds1983 avatar Apr 08 '17 08:04 rds1983

It's only immutable because the renderer logic is tightly coupled. This is more of a rendering problem than anything.

There are two ways approaches for rendering Tiled maps; use SpriteBatch or go custom.

The trouble with SpriteBatch is that it is has to be refreshed every frame. This means that (1) all the memory of the map has to be touched leading to CPU cache trashing and (2) the tiles have to be added to the queue for rendering leading to time spent processing geometry on the CPU to upload to the GPU (which the upload process itself has a cost).

One idea that semi-fixes (1) & (2) is to use render targets for each layer, but we had complaints that they consume too much memory. Another idea is to quickly discard tiles that lie outside the camera frustum (orthographic most likely) before queueing them to SpriteBatch. This is probably what everyone is doing who use SpriteBatch for rendering maps in other framework and engines.

However to really fix (1) & (2) an observation need to be made: most games do not change the map at runtime and if they do change the map at runtime it is not usually every frame. For even a game that runs at 60 FPS, that is 16.66 milliseconds per frame. Does the game really need to change the content of the map every 16.66ms? Probably not. So then, it is possible to write a custom renderer (like we already have) that solves (1) & (2) by calculating or updating the render geometry for a tiled map on a "when-needed" policy at runtime. The trade off is that the CPU cost to render the map has been offloaded and cached at the expense of more video memory and the GPU now has more to process.

Is this realistic? How much memory? Well, here are some rough calculations... For sake of being complete, let us assume that every tile in each tile layer is used and is not empty. Let's also assume a fairly large map acting as a rough upper bound. 500x500 map is 250,000 tiles per tile layer Say there are 10 tiled layers, again acting as a rough upper bound. thus, 250,000 * 10 = 2,500,000 tiles Let's use the same primitive type as SpriteBatch does, triangle lists. Then each triangle vertex for a tile has a position (x, y, z) and texture coordinates (u, v) which are all 4 bytes each so each triangle vertex for a tile needs 20 bytes using indices for triangle vertices, only 4 vertices are needed per tile, with 6 indices needed to reference those 4 vertices Then the memory used for vertices is: numberOfTiles * bytesPerTileVertex * 4 = 2,500,000 tiles * 20 bytes per tile vertex * 4 is ~ 191 mebibytes (MB) The memory used for indices is: numberOfTiles * bytesPerTileVertexIndex * 6 = 2,500,000 tiles * 2 bytes per tile vertex index * 6 is ~ 29 mebibytes

So roughly the amount of video memory needed to run a 500x500 with 10 full tile layers is around 220 MB. Practically this is way too much for mobile which use unified memory for GPU and CPU, but this is not unrealistic for desktop with decent hardware. For a smaller more practical map of 100x100 with 10 full tile layers, it adds up to be about 10 MB all together. This is practical for mobile and desktop.

So yes, the trade off can be justified assuming small enough maps (tile size doesn't matter in these calculations). Performance boost is already seen on TiledMaps desktop demo of the custom renderer for non-dynamic maps, easily going beyond 2000 FPS on decent hardware.

lithiumtoast avatar Apr 08 '17 22:04 lithiumtoast

Thanks for the detailed explanation, Lucas. I think it could be made mutable using 'dirty' pattern.

rds1983 avatar Apr 09 '17 04:04 rds1983

It's only immutable because the renderer logic is tightly coupled. This is more of a rendering problem than anything.

The current renderer isn't intended to be tightly coupled to the model. Any tight coupling is merely coincidental. The best way to fix this would be to try and implement a second renderer and find the overlaps.

It's theoretically possible to have a second renderer completely independent of the first with different properties. It's true that each renderer is going to have some pros and cons. One of them might be good at rendering small maps that are changeable at run-time while the other is good at rendering large maps but stays static.

That said, having lots of renderers is also a bit of a maintenance problem. There'd have to be some pretty good reasons to do so. It would be interesting to see how it might work but I've got quite a few things on the to-do list already.

craftworkgames avatar Apr 09 '17 08:04 craftworkgames

Right, it's a problem of the relationship between the renderer and the model. The solution here for the model is decent for one scenario (could use some code review) but what about different scenarios? Should the model be allowed to change if it is linked to a static renderer?

lithiumtoast avatar Apr 09 '17 09:04 lithiumtoast

Easy solution would be to make a user responsible to notify the static render that the model had been changed. I.e.

  map.Layers[2].Tiles.Add(myTile);
  staticRenderer.Invalidate();

rds1983 avatar Apr 09 '17 10:04 rds1983