#Performance problems on rendering of many 2D sprites

1 messages · Page 1 of 1 (latest)

wicked frost
#

Hello. I'm making an isometric pixel-art tycoon game. It's really simple visually, there are no lights or anything like that. Still. I'm starting to notice some performance problems when I have too many little people in my screen. In my gaming PC I start getting fps drops when I hit about 500 nodes, each with 3 sprites and a 2 frame animation. Sprites are 32x32 pretty light stuff in general.

I have isolated the problem to be simply in the rendering of the Nodes. I've simplified my physics processing, my pathfinding and my shader usage to no impact. The problem seems to reside purely on my sprite2D's. Once I move my camera away from them or make them invisible my fps shoots to the maximum again.

Is there something I could do about this? I'm planning on having as many of these sprites at a time as possible. I'm on compatibility renderer. I'm looking right now into using multimesh 2D, but I figured I'd ask before making any major changes to my current system.

Many thanks for any help 🙂

lone shoal
#

You could just replace the nodes with draw_*() functions.

#

draw_texture() would let you reduce the amount of _draw() calls by literally 500

#

Just use the position of the characters to offset the drawing position.

wicked frost
lone shoal
#

That would be more performant, idk by HOW much, but it would def. help.

Specially if an AnimationPlayer was involved. At least AnimatedSprite is a bit more performant. But AnimationPlayer is way heavier.

#

I'd start by just drawing each texture and seeing the difference in impact.

wicked frost
#

Cool, I'll experiment a bit with that. Thanks for pointing that out 🙂

wicked frost
# lone shoal That would be more performant, idk by HOW much, but it would def. help. Special...

So, simply removing the Sprite2D's and animation player and having all my peeps as a reference in an array that queues redraw every frame and loops over and draws all of them does allow me to place a ton more than before. But, just to be sure I'm not sure I'm not doing it wrong: I had to also ditch using my sprite sheets and I'll have to isolate all my sprites and do a bunch of ifs in my draw call to print all the correct things, am I right or is there a simpler way to do this?

#

Ah, maybe I can use texture rects and not have to isolate my sprites? 🤔

lone shoal
#

You can use an AtlasTexture to reference parts of a sheet.

#

Or just cycle the textures and keep an index of which one per character.

#

AnimatedTexture used to do this for you, but it is Deprecated

wicked frost
#

that seems messy

lone shoal
#

Not really.
You keep the same AtlasTexture, you just change its region

#

When you do so. The drawn texture will use the chosen region.

#

This DOES mean that you would need a separate Atlas per character, or at least separate per character that is in a different frame of animation.

#

But it does let you store each frame as a Vector2 instead of a new Texture. And you can always extend (i believe) the AtlasTexture to do the region setting stuff automatically.

wicked frost
#

Ah, I see

wicked frost
#

My brain is hurting a bit from this, I think I figured out how it's going to work, but I have not yet managed 100% to get there. I still haven't figured out very well how Rect2 is supposed to work regarding frames and global position.

There is also another problem that I have no idea how to deal with using this approach: for my sprites to Y sort correctly in the isometric projection I have to set their Z index in process. This was something I had to figure out how it was going to work early on for my game to work, but it won't work with this new approach to drawing I think...

This is hard 😭

wicked frost
#

So, I got it kind of working: I have figured out how to draw the correct sprites in the correct position for all my peeps. I have not yet written the if statements for all possible animations and I still have no idea if the Y sort thing can be solved. BUT, there are some performance gains with this method, not a ton, though

#

this is no visible sprites at all

#

this is how it was before (minus the animations and shader for random coloring)

#

and this is the new method. It's a lot better, but at that number of peeps I start getting fps drops again (the number on screen is actually peep groups, so it's more than 700 individual peeps drawn)

#

Now I'm wondering if it's worth it to go back and think of some other solution or if I keep going with the draw method

#

I think that it's worth it going this route if I can fix the damn y sort again lol, I pushed the number of peeps up to as high as I want

#

Oh

#

SHIT

#

maybe I could solve the z-index/y-sort problem by using the draw call for every single object in my game? lol

#

this would be nasty

#

Yeah I'm not going down that route lol

wicked frost
lone shoal
#

Sorry, i was asleep :p

There's a couple ideas i had in mind:

  • Group peeps that are close enough togheter into a single sprite programatically, modify their texture to include the other ones and turn them into a single entity while they are togheter. OR just replace them with a pre-made sprite.

  • For Y sorting, just draw them in the order of their Y axis. Lower values first, higher values later.
    Sorting them in a collection to draw them in order is the tricky part. You can store a Dictionary of float : Peep to sort their keys, with the float being their Y coordinate. Dictionaries suffer no performance penalties based on their size, unlike Arrays.
    From there you could maybe sort the float values? (idk how performant it would be) Then use the sorted floats to access the Peeps using the Dictionary. var peep_at_top: Peep = y_to_peep_dict[y_pos_array[0]]

  • Maybe batch some of the calls over separate frames, like, the animation code could be limited to X amount of peeps each frame or to X amount of time (by using the built-in TIME Singleton to track current time), then break out of the loop until next frame. As a result some peeps may take 1 or 2 extra frames to change their animation, but, it would be a siginificant performance increase.

wicked frost
# lone shoal Sorry, i was asleep :p There's a couple ideas i had in mind: - Group peeps tha...

Cool. So, later yesterday I started paying attention to draw calls and noticed that it's the single culprit (for now) that I need to reduce in order to make my game work as I want it to. I noticed that, although peeps are a problem at this point, I would also have this problem once the map was filled with any kind of placeable object. I was able to reproduce the fps drops with any kind of sprite2D. So the problem is not animations or processing or anything else, it's draw calls, thanks for pointing that out to me!

I could barely sleep, and I thought of a possible way of handling this which I'm going to start working on now:

For every visible object in my game I'm calculating their Z-index on instantiation so that they sort correctly. I posted about it on reddit a while back when I first had to solve this:

https://www.reddit.com/r/godot/comments/1f2z0y6/how_to_solve_this_cursed_problem_regarding_y_sort/

What I'm thinking of doing is: having a manager node with a dictionary for each object in my game that needs to be drawn (name, texture and z-index), then, having a node for each possible z-index with that corresponding z-index, and a script that fetches from the manager dictionary all textures it has to draw respectively. For this, I'm assuming that the drawn textures are going to keep their parent node z-index. I'll have to test this. I'll probably also need to sort for y in this case, so yea, I'm wondering just how performant this would be, but I think it's worth the test.

If this works like I'm thinking, then I'll have a fixed amount of draw calls which will be linked to map size. But that value will be lower than the number of draw calls that start becoming a problem.

Reddit

Explore this post and more from the godot community

wicked frost
#

Duuuuuude this is crazy. I have not yet adapted my code for peeps, but I just made exactly what I described above and I was able to plop like 5 times the amount of tree sprites I could before to not a single fps drop yet. They are z-sorting correctly also, but not y-sorting yet. For y-sort I think I'll have to sort the arrays which might add some overhead, but for now, this is insane.

wicked frost
#

Hmmmm, I actually just figured out that my performance is about the same with this method and with simply drawing Sprite2D's. The problem actually seems to be one the shadows. Once I introduced them to the _draw() method I was back to square 1 😭

wicked frost
# lone shoal Sorry, i was asleep :p There's a couple ideas i had in mind: - Group peeps tha...

Sorry for bothering again, I want you to bat an eye at one thing:

Replacing my sprite2D's with the draw method for my tree sprites does reduce the amount of draw calls. It's a bit better than plain Sprite2D's, not a big lot, but enough to warrant using this method.

However, there is something weird going on when I try to add my shadow sprite. It is a plain sprite, I'm not applying transparency or anything, and this seems to happen both using Sprite2D's or drawing texture, look:

#

This a bunch of trees being drawn via code. Note the low number of draw calls.

#

This happens when I draw two textures. The draw calls simply explode. This happens both with sprite2D and the draw method.

#

Does this make any sense? lol

wicked frost
#

Yea, the work around is to simply have the shadow and the sprite as a single thing. Then there is no real difference between drawing them normally I think. Draw calls are the same sometimes, sometimes not, I don't know, it's really confusing.

#

yea, I think I'm done experimenting

#

My conclusions are:

#
  • What causes draw calls to increase is simply using different textures. I can spam the map with a single texture to minimal draw calls, but once that object has two sprites, or I introduce another object, then draw calls increase sharply

  • If I have many different objects, then the draw method definetly does help in reducing draw calls. So I think I can keep using it, it frees up some performance which is going to be imporant as my game is for mobile.

  • The one single thing I can do to increase performance as much as I can is avoiding using too many textures. My peeps were made of head, body and shadow, if I make it into a single texture then it's a lot better.

dusk zodiac
#

I don't fully know about the performance, but I expect using modulate() to be ok (since I think that happens purely on the GPU).

So you could at least play with different (shades of) colors to add some "fake" variety to your peeps, trees, etc.

lone shoal
# wicked frost - What causes draw calls to increase is simply using different textures. I can s...

It makes sense that drawing more textures would increase the draw time.
Here's some ideas:

  • You can bake stuff onto the map itself, specially objects that are not going to interact with the peeps in this case. Objects that are near peeps and that they may pass trough can be done dynamically while the rest are baked.
  • Since baking a whole map is kind of a lot, you can divide it in quadrants (which each image being its own quadrant) and update only select quadrants at a time. Doom did it with physics calculations, and it is also an approach mobile GPUs use to reduce power consumption.
wicked frost
wicked frost
#

It's good that I figured this out now, so when I start making sprites for real I already know the best way to work with them.

lone shoal
#

Rendering is always the most problematic stuff. Turns out binary computers where not made to draw 3D graphics (and technically Y sorting falls under 3D).

It is also why shaders are so limited, there's a lot of compromises you have to make to have decent performance.

wicked frost
#

That is cool to know

#

Also out of curiosity I've just changed my renderer from compatibility to mobile and the game runs kinda different, so I guess I'll have to run some tests again on this one. Draw calls on idle are way higher but they don't increase as much. The batching thing definetly works differently in compatibility and mobile.

lone shoal
#

Maybe they went as far as to emulate the quadrant stuff in the mobile renderer?
It makes it suffer a lot with drawing bunched up 3D objects, but it is more performant otherwise.

Like, instead of redrawing the whole screen (which is faster) it tries to only redraw specific parts of the screen. Which can result in more calls overall if not properly optimized.

#

I saw it in a video once, can't really remember rn

wicked frost
#

The number of draw calls is wildly different. But it doesn't seem to lose performance on high numbers as it did before. I'll do some more testing and report back

wicked frost
#

So, all in all the mobile renderer seems to be a lot more suitable for my game rather than compatibility. I do not know the specifics, but using mobile the higher number of draw calls simply do not translate into fps loss like it did before. I've got to the point were the actual processing of the peeps is the bottleneck rather than the number of sprites on screen so, victory for now, i guess, and I can have a number of peeps on screen that suits what I envisioned for the game 🙂

The only weird thing I had to do was reduce my terrain layer "quadrant size" setting. It seems like it simply handles better the lower the number is. I think it was making one draw call for cell before which was crazy, but now it works decently to no aparent side effect that I can see.

civic jetty
#

What is your max unit count now?

wicked frost
# civic jetty What is your max unit count now?

About 450 peeps in mobile in my phone. It seems to me like the update of the peep's positions is the bottleneck right now.

My arrangement is the following:

I have peep groups that have a variable number of peeps from 1 to 5. These peep groups do the actual pathfinding and that does seem to be handling well right now. Then, the peeps themselves follow the group's position with an offset. Also, when peeps are offscreen no code other than position update runs.

Since it's already quite simple I do not know how to further optimize this other than to ditch gdscript for this particular processing. This is something I'm looking forward to try out a bit further ahead. I'm back to developing the rest of the game right now.

lone shoal
#

What are you using for path finding? AStar?

wicked frost
lone shoal
#

NavigationAgent uses AStar?
Because there is an actual separate AStar class.

#

It is WAY more performant than the Navigation system.

wicked frost
#

I think so, I'm not at the computer right now to be 100% sure though

#

But good to know there are alternatives

civic jetty
#

Navigation uses A*, but it has a ton of other stuff to and is built on a nav mesh. A* or some of the derivatives work really well for searches, since distance-to-end is a decent heuristic for which node to chose next

#

In theory with a nav mesh you could do a ton of stuff for lots of agents that make it a lot faster. For example, as you nav for units, if you maintain a nav cache of current nav searches, if another search hits an already used node (assuming a similar end goal), you can just return that, since the rest of the search will be the same.

Using clusters of units works really well too