Hello all, I workshopped some quick code for detecting double clicks(and differentiating them from single clicks) in the playdate squad discord and thought I'd throw my (messy) code on the forum in case anyone wants to use it.
Obviously there are some limitations here. Because the game needs to wait to see if you're gonna do a double click, there's gonna be a 6 frame delay between your single b-press and the action assigned to the b-press. This is okay in an optimized game that runs at the full 20 fps, but is gonna be very very noticeable in a game that has a lot of slowdown. The double click action is gonna be more immediate. Because of this, you're better off using this to get extra input options in a game where the action on your single b click doesn't need to be too immediate. If b swings a sword, it's not gonna be great. If b opens a menu, it'll feel much more smooth.
Player script
if bPressed == 0 then
bPressed = 1
framesSinceBPress = 0
elseif bPressed == 1 then
if framesSinceBPress<5 then
say "good job, you double clicked 'B'"
end
bPressed = 0
framesSinceBPress = 7
end
end
game script
on loop do
if bPressed==1 then
framesSinceBPress++
if framesSinceBPress==5 then
say "that's a single b_press"
bPressed = 0
framesSinceBPress = 0
end
end
end
You can obviously change all the b's and cancels in here to a's and confirms. Replace the say functions with whatever code you want to execute on b click and double b click. And you can change the various frame count if you want bigger or smaller windows for double clicking/detecting single clicks. You should always set framesSinceBPress on line 10 of the player code to something higher than the number of frames it waits after a signel press in the game code.
The code is a bit messy, I might clean it up and edit this post later.
i suspect that for a lot of cases it would be ok to perform the single click actions while detecting the double click. like swinging a sword... it's ok if the sword swings while i'm still trying to press B for the second time.
5 frames ought to be 250ms for an optimized game running at full frame rate... seems microsoft suggested long ago that 500ms should be the double-click period in windows... i wonder, would 10 frames feel too slow these days? if you process the single click during those 10 frames, would that mitigate the sense of it being slow?
Yeah, it could be fine in a lot of cases. Just depends on what you want and what you're working on. Here's the (much simpler) code for doing it while still processing the single b presses.
player script
on cancel do
if framesSinceBPress<5 then
say "good job, you double clicked 'B'"
end
framesSinceBPress = 0
end
game script
on loop do
framesSinceBPress++
end
You'll run into problems if your single b press does anything like triggering a say or opening up a different menu, but if you're just swinging a sword, this code might be just fine. Otherwise you'll wanna use the code in the original post.
Going to propose a (mostly structurally) different approach. The idea being that you implement your single/double-press logic in the singleCancel , doubleCancel , singleConfirm , and doubleConfirm custom events that are defined in the player script, and the doublePressDelay variable in the load event in the game script lets you tweak the sensitivity.
The advantage here is mostly just code abstraction. Cleans up the game's loop and player's draw bodies by only having the single call "handlePresses" lines, and the events where you handle the single/double presses are completely devoid of any of the logic regarding handling of double presses.
Player script:
on singleCancel do
say "Pressed B"
end
on doubleCancel do
say "Double-pressed B"
end
on singleConfirm do
say "Pressed A"
end
on doubleConfirm do
say "Double-pressed A"
end
on cancel do
cancelPressed += 1
cancelLastPressed = frameCount
end
on confirm do
confirmPressed += 1
confirmLastPressed = frameCount
end
on clearCancelState do
cancelPressed = 0
doCancel = 0
end
on clearConfirmState do
confirmPressed = 0
doConfirm = 0
end
on handlePresses do
if doCancel==1 then
call "clearCancelState"
call "singleCancel"
elseif doCancel==2 then
call "clearCancelState"
call "doubleCancel"
end
if doConfirm==1 then
call "clearConfirmState"
call "singleConfirm"
elseif doConfirm==2 then
call "clearConfirmState"
call "doubleConfirm"
end
end
on draw do
call "handlePresses"
end
Game script:
on load do
doublePressDelay = 4 // Maximum delay in frames to wait for double press
end
on handlePresses do
frameCount += 1
if cancelPressed==1 then
delta = frameCount
delta -= cancelLastPressed
if delta>doublePressDelay then
doCancel = 1
end
elseif cancelPressed==2 then
doCancel = 2
end
if confirmPressed==1 then
delta = frameCount
delta -= confirmLastPressed
if delta>doublePressDelay then
doConfirm = 1
end
elseif confirmPressed==2 then
doConfirm = 2
end
end
on loop do
call "handlePresses"
end
And with regard to bitflung's suggestion of allowing single presses to trigger while waiting for a potential double-press this is a similar restructuring of zapzip2013's code here to expose custom events to hold the logic:
Player script:
on singleCancel do
log "single cancel"
end
on doubleCancel do
log "double cancel"
end
on singleConfirm do
log "single confirm"
end
on doubleConfirm do
log "double confirm"
end
on cancel do
cancelDelta = frameCount
cancelDelta -= cancelLastPressed
cancelLastPressed = frameCount
if cancelDelta>doublePressDelay then
call "singleCancel"
else
call "doubleCancel"
end
end
on confirm do
confirmDelta = frameCount
confirmDelta -= confirmLastPressed
confirmLastPressed = frameCount
if confirmDelta>doublePressDelay then
call "singleConfirm"
else
call "doubleConfirm"
end
end
Game script:
on load do
doublePressDelay = 4 // Maximum delay in frames to wait for double press
end
on loop do
frameCount += 1
end
This looks nice to me.
I'm a little concerned about eventual overflow of the frameCount variable though.
Also, I'm currently putting a lot of my functional stuff in scripts for a given tile - e.g. in this case I might push that logic into a tile called CLICK, then include some state variable to set whether double (or triple, etc) clocks should be detected.
I think, then, that the player script could just do something like:
on confirm do
mimic "CLICK"
end
on doubleConfirm do
// ...
end
on singleCancel do
// ...
end
// etc
Now you've got flexible code that depends only on importing this CLICK tile into your game.
I'd love to have a single "game" in my pulp library where I keep tiles like this and could export them to other games in my library directly... that's feature creep for pulp itself though and likely belongs in another thread.
So far I haven’t made anything too big in pulp, and since in all other parts of my life my code has to be very neat and organized, I’ve sorta thrown that to the wind, but eventually that’s gonna…get away from me.
As for overflowing, my original code accounted for that, as it didn’t count frames when it wasn’t looking for a double click, but hat was unfortunately lost in Nicholas’s otherwise STELLAR refactoring I’m sure it wouldn’t be too hard to come up with a way around that while mostly keeping Nichols’s structure, BUT I’m not sure how necessary it would be. Just did some testing where I multiplied a variable by 2 every frame and the last value before it hit pulp’s max and started just reading as “infinity” was 8.988467*10^307, so unless I’m misunderstanding something(which is possible, it’s past my bedtime) it’ll take you about…the age of the universe times 1x10^289 until your frame counter overloads in pulp. Which feels like a very long game.
I admit I still haven't actually delved much into Pulp other than to briefly noodle about in PulpScript so there are a lot of Pulp-centric "programming" quirks like using tiles to store reusable behavior that I still am not familiar with.
As far as overflowing frameCount is concerned, that is something to consider. I think the exceptionally large value zapzip2013 is seeing is more likely due to the simulator running in JavaScript in-browser, as that looks more like JavaScript's max number value. If we assume Pulp's userspace variables on hardware are being stuffed into a signed (32-bit) word then we could expect a maximum value of 2,147,483,647. Which would enable a frame counter for ...29,826 hours of consecutive play? The data type is just an assumption though, would need verification on that.
I think the code above is "good", and possibly heading towards "better". I'm not sure we'll know whether we are approaching "best" it not for some time though.
Oh man good call on the max number, I shouldn’t think that close to 2 am, lmao. that did seem a bit ridiculous for the maximum, but I was just like,”oh neat.”
daniel in the Discord server pointed out that everything is more likely stored as floating point numbers. It would definitely make more sense to be floating point. I was stuck thinking about this particular use case where the frame counter is functionally an integer, but the same data type is likely used for all numbers so it would need to be a float of some sort.
Bumping as I just referred to this in implementing a double press. I used event.frame to find the difference, sidestepping any worries around an overflowing frame count. I also ended up recording the position the player was at so that double presses only trigger if on the same tile, which suited my use case better.