Best way to handle pressing or releasing two buttons simultaneously

On my quest to see how innovative we can all get with button input types on the Playdate, I'm looking to open a menu when both A+B are released. Currently, this way is working fine, but the way getButtonState() works, there's not a ton of room for error with how soon together someone has to release both buttons for it to register during the next update cycle.

function globalInputHandlers()
	local current, justPressed, justReleased = playdate.getButtonState()
	
	if justReleased == playdate.kButtonA|playdate.kButtonB then
		
		-- Open a menu
	end
end

For it to properly trigger, back-to-back cycles need to look like this:

-- Update cycle
current = 48,  justPressed = 0, justReleased = 0

-- Next update cycle
current = 0,  justPressed = 0, justReleased = 48

But if the user isn't relatively precise with releasing both buttons at the same time, it might look more like this:

-- Update cycle
current = 48,  justPressed = 0, justReleased = 0

-- Next update cycle
current = 32,  justPressed = 0, justReleased = 16

-- Next Next update cycle
current = 0,  justPressed = 0, justReleased = 32

... and then the original code won't get called. There's some obvious ways I can think to prevent this with a few extra conditionals and shenanigans inside of that if statement.. but just curious if this is something anyone else has encountered?

I don't find it particularly difficult to release both buttons synchronized enough to trigger it, but also I'm actively trying to do it and am aware of it. I'd hate for a user to feel frustrated that a button input works sometimes and doesn't work other times, and them not understanding why.

What's the best way to add some leeway for the input here?

You could store the previous frame's buttons state and do a bitwise OR operation on the that state and the current frame's state to very efficiently get a state that describes whether buttons were released in the current and/or previous frame. (untested)

2 Likes

I did something like this. I wouldn't claim it's the best way though! :grimacing:

local gfx = playdate.graphics
gfx.setColor(gfx.kColorBlack)

local release = {frames = 10, timer = 0, a = 0, b = 0}

function playdate.update()

    if aButtonBButton() then
        -- open menu or whatever
        spriteSquare(180, 100, 40, 40)
    end
    
    gfx.sprite.update()
    gfx.drawText("timer: " .. release.timer, 20, 20)
    
end

function aButtonBButton()
    if release.timer > 0 and playdate.buttonJustReleased('b') then release.b = 1 end
    if release.timer > 0 and playdate.buttonJustReleased('a') then release.a = 1 end
    if release.timer * release.a * release.b > 0 then
        release.timer, release.a, release.b = 0, 0, 0
        return true
    elseif playdate.buttonIsPressed('a') and playdate.buttonIsPressed('b') then
        release.timer = release.frames
    elseif release.timer > 0 then
        release.timer -= 1
    elseif release.timer == 0 then
        release.a, release.b = 0, 0
    end
    return false
end

function spriteSquare(x, y, w, h)
    local s = gfx.sprite.new(w, h)
    s.timer = 5
    local image = gfx.image.new(w, h)
    gfx.lockFocus(image)
    gfx.fillRect(0, 0, w, h)
    gfx.unlockFocus()
    s:setImage(image)
    s:moveTo(x, y)
    s:add()
    
    function s:update()
        if s.timer == 0 then
            s:remove()
        else
            s.timer -= 1
        end
    end
end
1 Like

Nice! @Nino's bitwise stuff was a little too galaxy brain for me, so here's what I landed on. It works quite well for now but is not optimized at all. I laughed when I saw that you used 10 frames as the timing for yours, because that's the same that I ended up using after testing :rofl: feels right, man

function playdate.update()
	playdate.frameTimer.updateTimers()
	customButtonHoldCheck()
end

local customButtonHoldTimer = nil
function customButtonHoldCheck()
	local current, justPressed, justReleased = playdate.getButtonState()
	
	-- Check if both buttons are held and timer isn't already running
	if current == kButtonA|kButtonB and customButtonHoldTimer == nil then		
		customButtonHoldTimer = playdate.frameTimer.new(10)
		ABButtonHeld()
	end
	
	-- Reset the timer if the input is abandoned before it's complete
	-- PreventMenuOpen() is used to check global things like if keyboard is visible
	if current == 0 or preventMenuOpen() then
		customButtonHoldTimer = nil
	end
end

function ABButtonHeld()	
	customButtonHoldTimer.timerEndedCallback = function(timer)
		-- Check that both buttons are still pressed when the timer is complete
		if playdate.buttonIsPressed(kButtonA) and playdate.buttonIsPressed(kButtonB) then
			-- Open the menu 
		end
	end
end
2 Likes