Accepting precise input timings for a rhythm game

,

Hi! I'm new to game development and am trying to prototype a simple falling-note rhythm game for the playdate. I don't have a device yet and am testing using the emulator (1.9.0) on macOS 12.2.1 (Monterey).

An issue I'm running into is detecting input timings with ~ms precision. I'd like to have that level of input precision so I can set some strict timing windows, ideally allowing ~15-17ms of error for the tightest one. Unfortunately, all of my tests so far for capturing precise input timings seem to be limited by framerate:

  • At the max fps of 50, playdate.update is called every 20ms, meaning detecting an input via playdate.buttonJustPressed within that callback can be off by up to that amount.
  • When I register a custom input handler or define the default callback methods like playdate.AButtonDown and record the time the callback is called relative to playdate.update using getCurrentTimeMilliseconds, the offset between the two is generally ~0-1ms. If I use getElapsedTime, the two values are identical. That suggests to me that the callbacks are run on the same loop as the update callback, but slightly before update itself is called. It also suggests to me that getElapsedTime is cached per loop, which is a bit surprising.
  • I looked briefly at the C API but haven't set up my dev environment to use it. It looks like it might be possible by listening for kEventKeyPressed events, but I'm hoping to avoid using C if possible.

My questions are:

  • I'm not sure what the button input polling rate is; is it high enough that I can get reliable inputs at the ms level, or will I need to work around precision issues at that level too?
  • Is there a way to access input timings more precisely through the Lua API? My testing suggests that all APIs are scheduled with frame updates, but it's possible that I'm misunderstanding my test results or that the emulator has different behavior than a physical device, which I can't yet test on.
  • If not possible in Lua, will the C API provide more precise timings?

Edit: Here's the code I'm testing with, if that helps:

AButtonDownAt = nil
playdate.display.setRefreshRate(50)

function playdate.update()
    if AButtonDownAt then
        print("a button press detected " .. playdate.getElapsedTime())
        AButtonDownAt = nil
    end
end

playdate.inputHandlers.push {
    AButtonDown = function()
        AButtonDownAt = playdate.getElapsedTime()
        print("inputHandlerFired " .. AButtonDownAt)
    end,
}

Thanks, folks!

ah this is an interesting problem.

First of all, regarding playdate.getElapsedTime(), you need to call playdate.resetElapsedTime() first. I think for your case this is something you want to use since this is the most precise timing available.

To get more precision when an input has been pressed, I think the best Lua out-of-the-box solution would be to use playdate.buttonIsPressed()
playdate.buttonJustPressed() and the callback are handheld only once per update to my knowledge, however playdate.buttonIsPressed() always return the actual state of the input and I think the hardware is doing it a 1kHz (maybe someone from Panic can confirm).

So I think the strategy would be to have some loops that would run during playdate.update() just to check to the state of the buttons and to detect a change yourself. during your update loop.
What I would try if I were you is to run the game at 50FPS. During the first 10ms I would just have a loop to check playdate.buttonIsPressed(). The last 10ms would be reserved for the game logic and rendering (might be tight).

You can also run with playdate.display.setRefreshRate( 0 ) and your playdate.update() will be called more often than 50 times per second.
I am sure @matt can explain you how he handled it in his game.

1 Like

Correct, dave confirms 1KHz here

With refresh (0) I do a very naïve frame limiter: I manage the waiting in the update myself, just an empty while loop watching elapsed time since loop start, waiting for it to pass 16ms (I run at 60Hz).

Running higher than 50fps means you will need to do partial screen updates (easiest way is to use the Sprite system). 60Hz with ~175 rows changing, 200Hz with ~75 rows changing, and a ramp in between those.

I do not use the SDK wait function for many reasons, but mostly because it winds down the CPU to energy saving speed, and when code resumes the CPU ramps again, this takes multiple ms of time. An empty while loop keeps the CPU running at normal speeds so I get better performance with an empty while loop.

I agree with Nic it's better to use buttonIsPressed() and not InputHandler() for this.

1 Like