Massive Tile Maps: Lots of Game Objects, or fewer large meshes?

Hi all, it’s been a great long time since I posted anything on the forums.

As the title suggests, I’m designing a Tile Map system for a project I’m working on. By the design, the Tile Map should allow for the generation of very large worlds (somewhere around 4096 x 2048 tiles for the average world, where each tile, as a sprite, is about 32 pixels across - .32 unity units). So far, I’ve worked out and tested two very basic implementations (both at 200x200 tiles), one that instantiates a lot of GameObjects (with no attached MonoBehaviors, only Sprite Renderers), and another that generates a set of meshes, where each mesh encompasses an area of 100x100 tiles. The meshes have a bumped diffuse shader that allows for per-vertex color changes (for tile lighting). Colliders are already managed by the tile map, and are only enabled when the player could ever touch a tile.

For the system, I’m trying to lay the tiles so that they slightly overlap (in other words each tile is about .33 units across, on a .32 unit grid) Using the GameObject method, this is really easy, but I doubt the performance will be particularly great, even if I’m careful about enabling or disabling the tile GameObject colliders. I am at a loss as to how to implement this with the mesh method. My first idea was to set the UVs on the mesh to be slightly larger than the space the tile will occupy, but this means that the edge tiles could be cut off. Would this method even work?

Basically, what I’m asking is whether or not I should build the tile map with Game Objects or meshes. Which would perform better? If the Game Object method works better, is there a way to apply normal maps to sprites, or would I have to write a shader for that?

Sorry for the massive post! Thanks for your time!

I solved this with SpriteTile by creating a pooling system, so there are individual GameObjects, but only as many as needed to fill the screen. So you can have 4096*2048 with no issues, with overlapping tiles and so on, and it uses much less memory than building meshes.

–Eric

2 Likes

Thanks for the reply!

I imagine the GameObject approach, as far as tiles go, would be much easier to implement, and would definitely look better code wise. Since the tile’s current light is stored per tile in my tile map system, I imagine a pooling system (something less generic than another pooling system I wrote) that works with my lighting shouldn’t be too hard to implement.

How expensive is it to change the Sprite Renderer’s color? If it’s cheap, then the Game Object approach looks like it would be the way to go.

It was actually quite hard to implement efficiently and is more complex than creating meshes, but has some pretty clear benefits. It seemed like it would be easier until I actually did the coding. :wink: Changing a sprite color is cheap since it doesn’t involve materials, but rather vertex colors.

–Eric

1 Like

The fact that coloring is cheap is good to hear! I’ll go ahead and start designing a tile pooling system once I finish up the basic tile map system. Thanks for all of the advice!

Well you’re looking at about 8192 32x32 tiles. That would thus be 8192 game objects. Now, processing of each game object does have a little bit of overhead, and when you’re getting into the thousands of objects range you might start to notice that adding up. For sure, fewer meshes where each mesh contains many tiles would drastically cut down on that overhead. Note that each gameobject, if it’s active, has to have its transform processed in order to either cull it or to position the object at the proper coordinates, so it’d be good to avoid all those calculations if possible. However, you’ll also find that working with a mesh containing multiple quads means your editing process is now harder. You will need some kind of tool ideally to let you easily add and remove and change what tile image is used for each tile in the tilemap, and then your mesh has to be updated with new UV coordinates to remap that tile to show the right image.

With regards to culling though, the more tiles you put on a single mesh, the more there will be triangles outside the visible screen, where the larger mesh overlaps the edges, and those have to be processed (by the gpu?) to get culled. So you’ll be adding some extra work there but I think it’s very fast. You could, if you used game objects, just disable all the objects that you know are outside the visible area. Then maintain some kind of window that moves around the world and activates objects based on the coordinates. You could do this by scanning for and storing all of the tile gameobject into a 2d array, one time, and then using that array to enable/disable gameobjects on the fly as you scroll. You could do that same thing with your meshes containing multliple quads also.

There are old-school scrolling techniques that might be useful to know about. They may not apply so much because the hardware is different now. In the old days, circa 1980’s, 1990’s, there wasn’t enough cpu speed to just blast a whole screen full of tiles up to the graphics display every frame. You had to keep the previous frame intact and use it again, and would typically use double-buffering or triple-buffering to switch between them, as part of getting the display to refresh without tearing. But since you could keep the backbuffer contents between frames, it was possible to only have to update a strip of tiles that were on the leading incoming edge, ie those new tiles appearing on the side of the screen as you scroll. You could also distribute the rendering of those incoming tiles over several frames, based on the scroll speed, so that the uploads were as smoothly spread out over time as possible to maintain maximum consistent framerates. The interesting part of the problem was how to keep the backbuffer contents (already visible) intact while also filling in new tile strips and not using a tonne of memory to make it scroll. There were basically two main approaches that I know of.

The first approach was possible only if you could take the backbuffer content and split it horizontally and vertically at the visible top-left coordinate. Remember that within the buffer there was no scrolling at all, it was more like the window looking at the buffer itself would scroll across the buffer, and the tiles that were drawn in place in previous frames would stay exactly where they were in relation to the top left of the buffer itself. So then you had this split which marked the left edge of the visible area, and as a result of the split, once the display window hit the right edge of the buffer it would have to be able to wrap around back to the left side. This was actually possible in hardware such as on the Amiga, and so you could implement a sideways scroll using just 1 screen + 2 extra strips of incoming tiles (one vertical strip for tiles that were incoming that were already partially visible, drawn earlier, and one vertical strip for totally hidden tiles that were incoming beyond that). It is actually possible to implement this in Unity using a rendertexture, if you can split the geometry into 4 quads and adjust the UV coordinates to scroll within it. But without a rendertexture it isn’t really feasible unless you want to upload slowly to a regular texture from the cpu (might be doable if spread out). There would also be a horizontal split in the display which again on the Amiga was accomplishable in hardware, by showing the bottom portion of the buffer first and then altering the address of where in graphics memory the display would output from to start drawing the top of the buffer after it. This made it possible to do very efficient, highly optimized scrolling that used the least possible cpu resources.

When the hardware support for split displays isn’t there, there is an alternative. If you maintain a 2x2 screen cache buffer, with a 1x1 screen window of visibility which scrolls around inside it, you can fake that the buffer is endless by uploading 2 copies of every tile. One you draw in its normal incoming tiles strip, the other you draw in a duplicate strip 1 screen to the right (or below). Then when your window gets to the edge of the buffer due to scrolling you can simply subtract 1 screen’s width (or height) from the window position to flip to viewing an exact copy of what you were looking at - the user has no visual idea that the flip occurred or that the view window is now at a totally different position, because both views show exactly the same tiles. This does require a doubling of what you upload, but it also allows you to create a scroll system that works in place. You could do this in Unity with a camera that stays within a confined 2x2 screen area, and uploading each tile twice. You then only have to upload tiles at the incoming edges of the visible area, plus the duplicate. The camera then doesn’t have to constantly move further and further in coordinates, because its position can wrap around. But this might take some adjusting of the rest of your logic. It does though allow you (if you use gameobjects, or a render texture) to keep the tiles you uploaded already.

Either way, on modern hardware there is no keeping of the backbuffer so ALL visible tiles have to be rendered every frame, regardless of whether they were uploaded before or not.

3 Likes

Thanks for the information!

I went ahead and setup the tile manager to build both ways and implemented a basic resource management system for each. Strangely, the mesh method, with my current management techniques runs faster, albeit while using about twice the memory. I think it has something to do with the way Unity handles Game Objects, since each object, even without any excess components still has to store the transform, and Unity cycles through each object to call any available Update functions and what not. With the camera’s size, the screen could, at the very worst, have somewhere around 60x40 (or 2400) tiles, which means I’d have to keep up to 2400 instances pooled, then modify them to hold the lighting and tile data when they’re placed. Since the mesh method uses far less objects (even when the tiles are pooled), it ends up running much faster. The manager just clears the meshes when they aren’t on screen, which reduces the memory impact; plus rebuilding meshes is really fast, even when calculating normals, tangents, colors, and all of that other mesh-y stuff.The mesh method was also the only way I could think to have normal mapped tiles, seeing as there isn’t a normal mapped sprite shader (using the following shader on textured quads just seemed too “hacky” to me), and a transparent, vertex colored, bumped diffuse shader wasn’t too hard to write.

Thanks for all of the help! By the way Eric5h5, SpriteTile is an amazing package. I picked it up a while ago to help prototype the tile engine, but ended up rolling my own for some stuff that wasn’t included out of the box. :slight_smile:

Cool. Yes as I imagined the mesh method has much less overhead when you’re dealing with such high numbers of game objects.

A quad with a normal-map shader on it is the same thing really as a mesh with lots of quads and a normal-mapped shader. Call it a sprite, call it a quad, whatever. It’s the same, not hacky.