Efficient sprite animation? For a large number of sprite instances

Hello,
I have a project in mind, but it requires a large number of animated sprites, and I am looking to see if there is any way to do this more efficiently?

  • I am using the C-API
  • I need to instantiate a large number of animated sprites, say 1000 instances
  • All 1000 will be in the display list
  • Only some fraction, say 100, will be within the screen's bound at any one time and hence actually receive a draw call on any given frame.
  • All of the sprites share the same 8-frame animation.

I was hoping that calling setImage for a sprite would just have the sprite cache the pointer to the image, so I could just draw over the single master-image once per frame with the next frame of animation and then call markDirty on all 1000 sprites. But this does not work, so I assume each sprite copies the image.

So then my only option is to call setImage 1000 times per frame to update each sprite to the next frame individually, performing 1000 bitmap copies?

This doesn't seem very efficient. Are there any tricks I am missing here? Could this be a feature request?

Thanks!

setImage() on the sprite doesn't copy the bitmap data so your original assumption is correct.

If changing the image doesn't affect the sprites it might be an issue with updateAndDrawSprites() or markDirty() not being called properly or a bug in the SDK.

How are you modifying the image used by the sprites?

Thanks @Nic

That's promising to hear.

I was trying something like

playdate->graphics->pushContext(theMasterBitmap);
playdate->graphics->drawBitmap(theAnimation[++i % FRAMES], 0, 0, kBitmapUnflipped);
playdate->graphics->popContext();

to update the master bitmap.

If this is something which should in theory work then I can have another look at it tomorrow, maybe I missed something...

Tim

I have the following minimal example that seem to work as expected.

But I believe if you call setImage() on your sprites, this is a bit more straightforward since you still have to call markDirty() on your sprites anyway.

If you don't want to call it on all your sprites, you can also call querySpritesInRect() with the screen as Rect and you will get all the sprite that are displayed on screen.

LCDBitmap* img_anim01;
LCDBitmap* img_anim02;
LCDBitmap* img_sprite;

#define SPRITE_COUNT 10
LCDSprite* sprites[SPRITE_COUNT];

int anim_frame = 0;

static int update(void* ud)
{
	(void)ud;

	anim_frame = !anim_frame;

	pd->graphics->pushContext( img_sprite );
	pd->graphics->drawBitmap( anim_frame ? img_anim01 : img_anim02, 0, 0, kBitmapUnflipped);
	pd->graphics->popContext();

	for ( uint32_t i = 0; i<SPRITE_COUNT; i++)
	{
		pd->sprite->markDirty( sprites[i] );
	}

	// LCDBitmap* new_frame = anim_frame ? img_anim01 : img_anim02;
	// for ( uint32_t i = 0; i<SPRITE_COUNT; i++)
	// {
	// 	pd->sprite->setImage( sprites[i], new_frame, kBitmapUnflipped );
	// }

	pd->sprite->updateAndDrawSprites();

	return 1;
}


DllExport int
eventHandler(PlaydateAPI* playdate, PDSystemEvent event, uint32_t arg)
{
	(void)arg;

	if ( event == kEventInit )
	{
		pd = playdate;
		pd->display->setRefreshRate(5);
		pd->system->setUpdateCallback( update, NULL);

		img_anim01 = pd->graphics->loadBitmap( "anim01", NULL );
		img_anim02 = pd->graphics->loadBitmap( "anim02", NULL );
		img_sprite = pd->graphics->copyBitmap( img_anim01 );

		for ( uint32_t i = 0; i<SPRITE_COUNT; i++)
		{
			sprites[i] = pd->sprite->newSprite();
			pd->sprite->addSprite( sprites[i] );
			pd->sprite->setImage( sprites[i], img_sprite, kBitmapUnflipped);
			pd->sprite->moveTo( sprites[i], 20 + i * 50, 50);
		}
	}
	
	return 0;
}
1 Like

Thanks @Nic!

It worked! I think my problem was drawSprites instead of updateAndDrawSprites, thinking that the latter was only for use in conjunction with setUpdateFunction (I have not added my update functions yet)

Happy to know that the animation part shouldn't be a bottleneck, I will continue to investigate how many sprites I can get away with on the sim, and then on the hardware.

conveyors

Let me know if you want this early test profiling on device

Ok, I think I am ready to see how far the hardware can be pushed.

In the simulator I can obtain > 1000 conveyors carrying > 1000 items while maintaining the 50 FPS cap, but I doubt that the playdate can match this (I will be very happy if it can!).

cargo

@matt (or anyone else), would you be able to give this a quick test on-device?

UntitledFactoryGame.pdx.zip (29.6 KB)

B Button: Add a column of conveyor belts
A Button: Place some apples on the most recent column of belts

What number of conveyors & apples causes the FPS to drop below 30?

1 Like

Oh, someone finally creating factory game. Please continue to develop it, I think this type of games (and farming) are perfect fit for playdate!

I hit 28 fps at 180 conveyors and 32 cargo

Hi @Whitebrim if feasible, yes, but (thanks @ncarson9) ~200 conveyors is not that many for a game like this.

Very useful to know what the limits are here, I'll have to go off and think about this one some more.

1 Like

I am a bit surprised that the conveyor belts dip the frame rate that much to be honest.

Having a lot of moving and elements that requires a lot of redraw has a strong impact on performance, but here I don't think the redrawing is really the bottleneck.

If I understand correctly, each converter belt tile is a sprite you force redraw. I feel the issue might be that it just create too many sprites to iterate in the sprite system.
The conveyer belts are rather a static elements and as such I think we can optimise easily how it is handheld, not as sprite but as custom element. But I think we need to try different methods and benchmark them properly.

Is it possible to draw belts as pattern background?

I am a bit surprised that the conveyor belts dip the frame rate that much to be honest.

Yes, I may be wrong, but I am assuming similar here - that the main issue is coming from each conveyor sprite currently running an update fn on each frame which moves the cargo forward and attempts to move it on to the next conveyor when it reaches the end.

No doubt many optimisations are possible, this was just a simple first try.

Is it possible to draw belts as pattern background?

Without animation, sure - but I don't know how I would do this with animation.

All in all, I've somewhat over-scoped this thread, it was supposed to be about animation efficiency. And I am here managing to update all the conveyor animations by re-drawing to just two bitmaps (the North-South and South-North ones).

But then add on the update fn. for the conveyor sprites (the cargo sprites don't need an update fn.). and we see where we start to test the CPU.

Sorry I didn't have he chance to test was away from my computer yesterday.

For reference we hit 40 fps with 144 sprites in my Sparrow/Mahjong Solitaire game with a lot of effort. There does seem to be a lot of overhead with sprites.

Full screen can be redrawn at 50fps FWIW

Keep a set of patterns comprising the animation in memory and change the drawing pattern per tile. You could rotate a single patterns but this would introduce a lot of table garbage.