Frame-Based Collisions
In January 2025 I started working on a game for the Boss Rush Game Jam and discovered the AnimatedSprite library which is a state machine based on the sprite of the playdate sdk for showing different images of a sprite sheet.
Animation basics
Going through the documentation I realized it has two great features:
- it let's you change the speed of the animation (tickStep)
- two you can compose new animations out of existing frames
The tickStep of the animation means for how many "render" frames is a frame of the animation visible on screen. With the reference of the images above, how long to show each single image on the screen. So the higher tickStep to slower is the animation.
To the change the composition of an animation means, you can split up the anticipation, the action and the follow-through frames of an animation into separate parts. Then change the behavior of each part, for example prolong the anticipation to is easier for players to spot an incoming attack.
Here an example were as long as the player holds down the button then attackAnticipation is played for repeated spinning, then once release the attackSweep is played. Because the anticipation is only two frames it's slower then the rest of the animation:
{ "name": "attackAnticipation", "tickStep": 4, "frames": [1,2], }, { "name": "attackSweep", "tickStep": 2, "frames": [3,4,5,6,7,8,9], },
See it in action at the end, when when the little character spins the blades then sweeps in with an attack:
These features of the AnimatedSprite are a great fit for me because I wanted to use animations from Penusbmic for my game jam game and have fine-grained control to use them in various ways.
This example shows the actual hit of the whole attack animations are happening only in a few frames and only then collisions for an attack can be triggered. I've tried counting the frames and timing the collision with a frameTimer kinda like this:
function enableCollisionWithReset(player, initialFramesDelay, framesUntilReset) frameTimer.new(initialFramesDelay, function () -- enable collision player.attackRect:setCollisionsEnabled(true) frameTimer.new(framesUntilReset, function () -- disable collision player.attackRect:setCollisionsEnabled(false) end ) end ) end
Even if I would land the exact timing, it's not an approach that can scale. I would need a more precise solution, something like a frame hook!
In javascript frameworks (vue or react) a hook is referred to binding an event to a certain trigger. Simplest example, you want a certain function to be called an a button is clicked.
Frame Hook
Apparently it already has event triggers for onFrameChangedEvent, onStateChangedEvent and some more. But these weren't during the animation itself.
I had to dig deeper and look into the code of the AnimatedSprite library. For our context AnimatedSprite:updateAnimation() and the local function processAnimation(self) are relevant. So two main things are happening in these functions, check if the animation needs to advance to the next frame and check if an existing event has to be triggered.
With this uncovered I would still need to implement two things:
- Mark the frames when the collision should happen
- Find a way to trigger the collision event when at a marked
- Assign event per animation to trigger the collision (enable and disable it again)
The 1st part is easy
Add it into the json definitions of the animations:
{ "name": "attackAnticipation", "tickStep": 4, "frames": [1,2], "sfxFrames": [1] }, { "name": "attackSweep", "tickStep": 2, "frames": [3,4,5,6,7,8,9], "collisionFrames": [6,7,8], "sfxFrames": [3] },
The 2nd part kinda hard
In the depth of the AnimatedSprite, once the number of the _currentFrame is part of the markedFrames ("collisionFrames") then execute the callback()
local function checkMarkedFrames(self, state, markedFrames, callback) if (markedFrames == nil or state.frames == nil or markedFrames == nil or callback == nil) then return end local actualAnimationFrameNumber = state.frames[self._currentFrame] local isMarkedFrame = isInTable(markedFrames, actualAnimationFrameNumber) if (isMarkedFrame) then callback(self) end end -- call it like this during processAnimation() checkMarkedFrames(self, state, state.collisionFrames, state.onFrameCollisionEvent) checkMarkedFrames(self, state, state.sfxFrames, state.onFrameSfxEvent)
The 3rd part
Is okay I guess:
function activateCollWithFrameCooldown(attackRect, cooldownFrames) if(not attackRect:collisionsEnabled()) then -- active the collision attackRect:setCollisionsEnabled(true) if (DEBUG == true) then local visibleImg = getHitRectImage(attackRect.width, attackRect.height, true); attackRect:setImage(visibleImg) end frameTimer.new(cooldownFrames, function () -- disable the collision attackRect:setCollisionsEnabled(false) if (DEBUG == true) then local clearImg = getHitRectImage(attackRect.width, attackRect.height, false); attackRect:setImage(clearImg) end end ) end end -- the state is the current animation, multiple the amount of collisionFrame entries -- with the tickStep to get the duration (in frames) the collision has to stay active local state = animationSprite:getCurrentState() local cooldownFrames = #state.collisionFrames * state.tickStep -- the attackRect is an empty sprite with setCollideRect() set activateCollWithFrameCooldown(attackRect, cooldownFrames)
And here in action

The collision doesn't always work here because of the slow motion testing, but I wanted to show this because you can also see the debug rect being visible when the collisions are active. And also slow motion is possible when turning up the tickStep :)
I think that's it for now. Let me know in the comment, if you also like to see the code to check the collision once the "attackRect" is active?
PS: I created a pull request for the integration of the the onFrameCollisionEvent and onFrameSfxEvent, so if you have the latest version of AnimatedSprite you can use it has described.
Cheers!
Doms Blog
Dom is blogging about Game Design and Software Engineering
Status | In development |
Category | Other |
Author | Dom |
Tags | blog, Game Design, sourcecode |
More posts
- Uncrankd Game Jam 2025 Winter EditionFeb 28, 2025
Comments
Log in with itch.io to leave a comment.
This looks awesome, i am also using penusbmic's asset to create a platfomer. Will def try out your game that was submitted for the jam!