Double click in pulp!

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.

4 Likes

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?

2 Likes

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.

4 Likes

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
2 Likes

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
1 Like

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.

Your function tiles idea is really neat!

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.

1 Like

So for the layman following along, what is the best way to do this now?

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.

2 Likes

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.

1 Like