Memory/Process Management Ideas/Tips

Really amazed by how fluid Gun Trails is and equally amazed by the memory management they put into the game (explained here: Shoot ’em up in style: the making of Gun Trails on Playdate - Playdate News). Would love any tips people have on their own management to make their games fluid as well.

For example, as my player runs around, little dust particles spawn. However, this bogs down the system and the fps starts to drop as it's adding new sprites for each dust animation. I'm thinking a better solution would be to reuse the sprite and instead of destroying it at the end of the frame, move to the starting position (under player) and reset the animation. Haven't tried it yet, but should work in theory?

1 Like

Yes, generally reusing images (and sprites) will be much faster than creating/destroying the same object over and over. The Playdate has a decent amount of memory so keeping things in memory and reusing them is a good strategy. The other tips we list in the docs are:

  • flatten loops as much as possible
  • localize frequently used global tables and functions, especially those used in loops
  • disk access is slow on the hardware, preload any external assets like images, fonts, and sound effects
  • pre-compute and cache the result of expensive computations
  • pre-render and cache the result of expensive drawing routines
  • pre-allocate and reuse tables
  • move table allocations out of loops
  • avoid excessive string concatenation with .., instead build a table of strings and use table.concat()
3 Likes

These are great tips! I'm not sure I'm entirely clear on the pre-render and cache. I think how I applied it so far is not to create objects with reference to the image directly in the init function. Otherwise, it's pulling the file each time. I noticed when I move something like local dustSpriteSheet = gfx.imagetable.new('images/sprite-table-16-16') outside of the init function, it saves on fps.

However, how would I go about storing/prerender something more complicated like a sprite that has multiple addStates and extends a parent object? Would I have the sprite store in a local table and call the Sprite parameters at the beginning of the world (EnemySprite example below), then call just the spriteSheet (CreateEnemy example below)?

local enemySheet = gfx.imagetable.new("images/enemy")

class('EnemySprite').extends(Enemy)

function EnemySprite:init(x)
	EnemySprite.super.init(self, x, enemySheet)
	self.health = 100
	self:addState("idle", 1, 3, {tickStep = 8})
	self:addState("run", 18, 27, {tickStep = 4})
    self:playAnimation()
end

Do I call this every time I want to create another instance of the sprite or is there a better way?

class('CreateEnemy').extends(EnemySprite)

function CreateEnemy:init(x)
	CreateEnemy.super.init(self, x, enemySheet)
end

Hi!

So, all that "pre-render and cache the result of expensive drawing routines" means is that if you have some expensive drawing (drawing a block of text, for example) that you can do once and cache to an image, it's faster to do that and then just redraw the cached image when you need to (or assign the image to a sprite).

Moving gfx.imagetable.new("images/enemy") out of your init class makes sense and is a good thing to do, because otherwise you're creating a new imagetable instance for each sprite you create, when instead the sprites can all share one instance.

I'm not clear on what the CreateEnemy sprite class is intended to do. Passing enemySheet in CreateEnemy.super.init(self, x, enemySheet) doesn't make sense because the init function of the EnemySprite class you are extending does not accept an imagetable argument.

The intended way of creating an instance of your "EnemySprite" class would be:
local enemySprite = EnemySprite(x)

If you wanted to re-use those sprite instances rather than creating new ones every time you need one, you could stash unused instances in a cachedSprites table or some such, and take them from there to re-use, if available, before creating a new instance.

Is it necessary for each instance of a sprite to maintain a reference to enemySheet? Would it work instead to just the file-local variable when it's needed? If each class does need a reference you could always just assign it directly in your init() function: self.enemySheet = enemySheet. No need to pass it as an init argument, unless of course it might be a different image table each time.

I'll just mention that it might be simpler conceptually to not use object-oriented sprites at all, and just create sprites directly via playdate.graphics.sprite.new(), but that's up to you :slight_smile:

2 Likes

Thanks Dan!

I do typically use local enemySprite = EnemySprite(x) to create a new instance. The issue I'm seeing is the stutter when new instances are created. The example above was me trying to figure out how to preload (which is definitely not correct) :sweat_smile:

For the cachedSprites example, that's probably what I want to do, but unsure of how to approach it.

Is something like this correct?

Call this early to create an instance and store in a cachedSprites table:

cachedSprites = {}

local enemy = EnemySprite(260)
table.insert(cachedSprites, enemy)

Let's assume the EnemySprites above gets created through the init function with the image path and all the animation states.
Then, would I call this separate function to use that stored sprite?

local newEnemy = CreateEnemy(330)

class('CreateEnemy').extends(gfx.sprite)

function CreateEnemy:init(x)
	self = gfx.sprite.new(cachedSprites[1])
	self:moveTo(x, 140)
	self:add()
end

Doesn't seem to render anything and doesn't look all that correct. How would I go about rendering the stored table? Thanks for the help!

No, that doesn't look right. Your init() method for your CreateSprite class doesn't have a call to CreateSprite.super.init(self), which is mandatory for subclasses, and it should not be assigning a value to self directly.

I still think using CreateEnemy for the name of the class is confusing, since create is a verb. Why not just create instances of your Enemy class directly?

I think what you might want is something more like:

local cachedSprites = {}

local function addEnemy(x, y)
	local enemySprite = nil
	if #cachedSprites > 0 then
		enemySprite = table.remove(cachedSprites)
	else
		enemySprite = EnemySprite() -- only create a new sprite if cachedSprites is empty
	end
	
	enemySprite:moveTo(x, y)
	enemySprite:add()

	return enemySprite
end

local function removeEnemy(enemySprite)
	enemySprite:remove()
	cachedSprites[#cachedSprites+1] = enemySprite
end

That said, you might not even need to cache sprites! You would just want to keep your initialization or setup code to a minimum, if that's what's causing the stutter you are seeing. I would only add it if you determine it to be necessary, otherwise it might just be overcomplicating things.

Please also note that you would not render a table of sprites manually - that is all done automatically for all "added" sprites when you call playdate.graphics.sprite.update() in your main update loop.

In order for a sprite to render you'll need to assign an image to it via playdate.graphics.sprite:setImage(), or provide a drawing callback function where you can do the drawing yourself.

2 Likes

I think this is the right path. Thank you!

1 Like

Does anyone know what ?? (missing) ([C]) is in the Simulator Sampler? It seems to be taking up processing time, but I can't isolate it. Thanks!

image

If you have a hybrid Lua/C game, that shows up when a function in your game's C code is being called from Lua and using a lot of Sampler time.

1 Like

Oh, I didn't realize how I got to doing hybrid Lua/C. Must be some of the open source scripts that may have them. Thanks!

If I see lots of [C] lines in the Sampler, I'm assuming this really is a hybrid then? Not sure how I'm getting them as some lines point to just the Playdate.update function.
image

Most of the C functions you're seeing seem to be Lua functions implemented in C behind the curtain, but I'm not entirely sure how you're getting a missing function if all your code is in Lua... unless you're calling a C function via an array table index. Those table indexes don't have names, so Lua can't uniquely track each one via the Sampler.

2 Likes

I'm not sure what the missing thing is, but even using pure Lua you'll see some ([C]) items because some of the Lua SDK functions are, behind the scenes, implemented in C for extra speed.

2 Likes