0
\$\begingroup\$

I'm developing a top-down 2D tile-based canvas game. I have a single sprite sheet of all my tiles, and then a JSON tile map array of where they're placed.

Previously I would render all the layers of tiles out to a single canvas element once, and then re-render that every frame. This would give me 300+ FPS because it would just be a single draw call each frame to redraw this canvas.

However, the issue with this is that the map is not just a "flat" image. Sometimes tiles need to be destroyed, or sometimes the player needs to stand behind certain tiles (such as when the player is above them), which doesn't seem possible if the map is just a static image.

As a result, I believe I instead need to re-render all the tiles every frame. However, even if I only re-render the tiles that the user can see, the FPS drops tremendously. With just two tile layers and a 100x100 map, the FPS drops to 20.

I would have assumed that, despite there being 100 * 100 * 2 canvas.context.drawImage calls per frame, that it would still be fine, because it was leveraging the GPU, and I'm using a single sprite sheet, so there should be no texture swapping. Apparently this is not the case.

I was looking for a way to possibly speed this up, but the recommendations seem to be extremely heavy-handed and opinionated libraries like three.js and pixi.js. These libraries would be great, except they do too much. In PIXI and Three.js you need to add objects to a scene and they're automatically redrawn each frame. This is not what I need, as my current ECS architecture demands an immediate-mode renderer, and I have my own scene graph already, so I can't (and don't want to) use theirs.

Is there a solution here? The only thing I really need to do is draw images clipped from a spritesheet quickly. Can this be done using immediate-mode rendering? Or is it doomed to be 20 FPS?

\$\endgroup\$
0
1
\$\begingroup\$

Even though the map is "small" at 100x100 with two layers, that's still 20,000 draw calls per frame.

One optimization is to only draw the tiles that are visible such as limiting your camera's viewable area and zoom levels so you can't see more than a few hundred tiles at once. You'll have to implement the ability to query if a tile is visible to the camera though.

A second optimization (after the above is implemented) is to group the visible tiles into one big draw call.

\$\endgroup\$
2
  • \$\begingroup\$ The 100x100 is the current viewable drawing area. Yeah, it seems like I need to do a single giant draw call. I suppose I'll do a bunch of webgl reading tomorrow to see if I can make any headway with that. \$\endgroup\$ – Ryan Peschel Feb 24 at 7:32
  • \$\begingroup\$ @RyanPeschel If this answer solved your problem, please mark it as accepted by clicking the checkmark in the upper left under the voting buttons. \$\endgroup\$ – Casey Feb 24 at 17:27
0
\$\begingroup\$

What do you think about rendering 2-3 states of the game world? Like, one which is "the stuff below the player", another which is "the stuff above the player", and more to suit your needs. Then simply render these in their respective order stuff below player -> player -> stuff above player

Then instead of re-collecting the image data every time a modification is made to the game world, perhaps there's another solution where you can manually edit the image's imageData object using math to near-instantly hunt down the exact pixels which need updating? This part is a bit fuzzy, but perhaps you could make it work? (Resulting in 2-3 calls to the renderer every frame, and a single one-tile-wide edit to image data every time a world-changing action is taken)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.