faiface/pixel

Drawing tens of thousands of sprites to a batch

jrcichra opened this issue · 7 comments

I have a 200x200 isometric level (just for the fun of it). With some tiles having more than one sprite layered on top, I'm coming to about 52015 draw calls to a single batch (all using the same batch, from one sprite sheet).

Why is performance not that great? Every frame, I am clearing the batch and redrawing all 52015 sprites. I could cut some corners, i.e when no tiles are updated, but being the game is isometric, and will likely have dynamic tile elements, this would be a minimal edge case. In most cases, (I believe) each frame I need to redraw the entire map.

I'm on Ubuntu 20.04 with a Ryzen 3900x and a WX2100 graphics card.

I was hoping adding elements all to the same spritesheet and a single batch would result in 100s of frames a second. I'm struggling to maintain 30.

Can you show code sample? I got 1200 fps on Intel Hd graphics but i grouping XxY tiles into batch chunks. You should render again only changed chunks. Separate animated object to different layers.

Sure, here's where the logic starts, it's designed so each tile has a reference to the batch it should draw to (which is the single batch):

https://github.com/jrcichra/gollercoaster/blob/c891f6d0a7c5c0d21124f43da197a773fa5cced9/game/game.go#L136

Draw calls to the buffer happen in a loop here (which calls N true draws per tile):

https://github.com/jrcichra/gollercoaster/blob/c891f6d0a7c5c0d21124f43da197a773fa5cced9/game/game.go#L217

It's harder to re-render only what's changed in an isometric game because of the painter's effect / angle.

I'm developing isometric game too. You can split everything into layers (ground, animated ground, animation overlay etc.). Also you can create batch in different goroutine. I take a look into your code later and prepare sample. it should take me 2-3 days.

Is it necessary to renter everything every frame? wouldn't it be better to render just tiles that are on screen? Just add a bounding rect to every tile and if it does not intersect projected screen bounds, don't render it.

@jakubDoka I was hoping a 200x200 grid would still perform at a high enough level where I didn't need to worry about that. I'm not ready for that performance optimization if it buys me nothing when fully zoomed out. Yes, if the map gets ridiculously massive, I would make this rendering optimization.

@jrcichra It’s not that simple.

In current state in "gollercoaster" project code needs to loop trough all map tiles over and over every frame. CPU can’t afford that.

Layers - everything should be spliced into different layers. You should use Canvas from 2d platform example. So in first frame you draw all visible objects from batch to canvas and redraw only canvas. if camera change draw all tiles again then clear canvas and redraw. (Loop trough the layers is slow. it’s better to use iota)

Animation - It's should depends on camera zoom. lower scale = less frames because player don't see that. Animation should be on another layer and routine.

Chunks/batch render - if you don’t want to use chunks you should draw only visible elements to batch. With chunks once created static chunk can be stored in memory until level changed.

Routines - static assets, animation and ui should be in different routines. It’s prevent application from blocking. 



I’m think I will create repo with utils and rewrite code from my apps. It’s will contain a:

  • Dynamic assets handling
  • Tools for building assets and assets configs.
  • Rendering helper (chunks, layers, animations)
  • UI lib
  • And many more…

Sorry it took me many months to revisit this thread. These suggestions are all great. I could use some clarification in terms of my specific example.

If I wanted to have the mouse highlight the selected tile on the map, are you saying the most efficient method would be:

  • layer 0 - render the 200x200 grid once, while "loading the map"
  • layer 1 - the mouse determines which tile it sits on, and draws a small white halo around that tile. I can somehow clear that layer and not affect layer 0?

I need to revisit my code, and see how layers work in pixel.