Best practices for button handling

Well, at @crait’s suggestion, I’m asking for advice here.

My first game has relatively easy button requirements, and I did the obvious thing, but it doesn’t feel very good. Some games do have the feel I want, so I expect it’s my code and not my hardware.

The requirements are straightforward - just give me solid, bounce-free buttons. But I also want the arrow buttons to auto-repeat part of the time. My first attempt just uses pressed() along with a delay at the end of the loop to debounce them, which I feel is SOP for arduino button handling. At times I’ve used libraries for this, but none of them are flexible enough for what I want. If you really want, I can post the code, but the relevant parts are “arduboy.pressed(…)” and “delay(game.delay)”.

I have seen some games that behave really well. But a quick scan of the source doesn’t show much They seem to use a framework of some kind, or not be using the arudboy library.

So, anyone got a best practice for handling buttons with auto-repeat? Anyone think maybe this - or something like it - ought to be part of the arduboy class?

2 Likes

From what I have heard, Team A.R.G. has some nice debouncing feature implemented in their library. I think @MLXXXp also implemented something in his well-written Arduboy2 library. When I get back to a computer, I will try to give a more detailed answer.

Hi,

We at TEAM a.r.g. actually use the button functions created by @Dreamer3.
You can use them too, if you use his extra library: https://github.com/yyyc514/ArduboyExtra
the function you want is: justPressed

1 Like

Yes, as @crait said, Arduboy2 contains 3 new functions taken from the Arglib library that Team A.R.G. uses for all their games: pollButtons() (Arglib calls this poll()), justPressed() and justReleased(). And as @JO3RI indicated, Arglib and Arduboy2 both got them from ArduboyExtra.

You would normally call pollButtons() at the start of a frame:

#include <Arduboy2.h>

Arduboy2 arduboy;

void setup() {
  arduboy.begin();
  arduboy.setFrameRate(60);
}

void loop() {
  if (!arduboy.nextFrame())
    return;
  arduboy.pollButtons();
  // ... Code to render the next frame
}

60 frames per second is 16.6ms per frame which is sufficient to debounce the buttons. Slower frame rates give even longer debounce times.

You then use justPressed() to test if a button was pressed between the start of the last frame and this new frame:

  if (arduboy.justPressed(A_BUTTON) {
    jump();
  }

By using justPressed(), you don’t need any code to test if the button has been released. justPressed() won’t return true again unless that button was previously released.

If you need autorepeat you can use the pressed() function that you’ve already been using. If you want to slow down how fast the action repeats you can do it only after a number of frames has elapsed:

  if (arduboy.everyXFrames(4)) {
    if (arduboy.pressed(RIGHT_BUTTON) {
      moveRight();
    }
  }

You can also use your own local flags to keep track of the state of the game to allow buttons to behave differently at different times. Depending on the game state, you could use justPressed(B_BUTTON) for a single shot action, or pressed(B_BUTTON) to have it autorepeat.

You can also use justReleased() to detect that a previously pressed button has been released, but there aren’t many cases where it would be needed. (Arglib doesn’t even include this function.)

3 Likes

When I have worked through this in a couple of apps I built, I found that the delay() function ends up being out of place in any Arduboy app. While you are delaying, nothing else happens.

What you see in many of the examples here is part of a larger model for Arduboy apps, that you start at the top of the loop, and run full speed to the bottom of the loop. Along the way, you may change states depending on things happening. Detecting button presses can change internal states. Counters that increment each pass through the loop may change the state. The idea of what is on the screen may be changed.

Your button example maps out as being something like:

- capture all the buttons once
- is the buttonA pressed?
    no: do nothing
    yes:
    - is buttonA justpressed?
        yes: reset button down counter to zero and do any initial action
    - is the button down counter high enough to trigger repeats?
        yes: set flag to indicate repeats and set subcounter for loops per repeat
    - is the repeat flag on?
        yes: increment the subcounter
        - is the subcounter high enough for a repeat to fire?
            yes: reset the subcounter and do the repeat action
    - done repeats
- done button pressed

The key thing to notice is that nothing delays falling through this code. Larger and longer term timed events happen because you fall through the code multiple times, with a delay between each pass. But the delay is not at all obvious in the code.

This also allows for multiple events to be ongoing at the same time. If you have activities that classically implement as a for() loop, you need to unroll them, line them up, and drive each step off a higher level counter. The nice part of doing that is that something else - an exit button, for example - can override the completion of the loop, giving you both lots of flexibility AND great responsiveness to user input.

As an example, I had USB keyboard code that wrote several letters and then backspaced back over them. This complete action had to always complete, but it took long enough that it was possible to press and release a button before it finished. Building special button capture code (and delays!) inside that loop consumed code space and added complexity. Mapping the activities into the above structure meant that multiple passes through the main loop were necessary to finally send all of the USB commands - but a user button press, even at the very start, could be captured, and flagged as meaning “exit from this state when you next can”.

Over time, I have found this architecture to offer the most capability on almost any system. It adds both flexibility and resilience, as it is much harder to get trapped in a sub-function that cannot exit due to some missing condition – somewhere else in a pass through the loop, an exit command can still be processed and bail you out. Even simple micro-controllers with as little as a hundredth of the Arduboy’s capability will be structured this way, repeatedly driving their few lines of code from a regular hardware interrupt. Meanwhile, systems with hundreds of megabytes of main memory operate the same way, so as to not delay multi-process, multi-user, or multiple-same-user activities.

It does mean that code has to be studied to be understood. What may appear to be a simple action in concept ends up - due to time requirements - being spread out over the loop. Side effects must be closely watched for - your multistep looped process does not have exclusive control of the system while looping, as other activities and external feeds can change beneath you.

Finally, the Arduboy would appear to be incredibly limited in computing resources, yet it handsomely rewards time spent learning how to best use those resources.

2 Likes

@ChrisS, Your post is good advice. I’ll just add that most games that run full speed from top to bottom also include a synchronising pause at the top so frames are updated at a constant rate.

The Arduboy library, and its Arduboy2 and Arglib derivatives, include the setFrameRate() and nextFrame() functions for this purpose.

Doing this is good practice, rather than relying on the time it takes for the CPU to process the loop to control the pace of the game, because if you don’t:

  1. Your loop may take different times to execute depending on the game state and actions processed, thus causing the game to speed up and slow down at different times.
  • If your game uses functions from an external library, that library may be optimised to run faster at at later date, causing your game to run faster even though the game code itself didn’t change.
  • If your game gets ported to a much faster processor it may end up running far too fast, requiring delays to be added to slow it back down.
1 Like

Absolutely. In code I do that. I felt that I was already pushing the limits of reasonable length in the post.

That frame syncing has one final advantage - it effectively implements button debounce, almost for free. Since you can only detect a complete button cycle once every two frames, the classic button bounce problem goes away. This is still far faster than you can repeatedly press the button, so although it might appear slow from a processor point of view, it’s still many times faster than the human, which is ultimately what counts.

2 Likes

Great stuff, guys.

But it’s not quite appropriate for my use case. My fault for not posting source. So you can find the game repo at https://chiselapp.com/user/mwm/repository/1010/doc, and I’ll add some pseudo-code below.

I think my use case is common enough to be worth posting the solution. 1010 is a simple puzzle game that just waits for the user to move. So the architecture looks like:

loop () {
      wait for arduboy buttons.
      update game state
      display new state
}

Since there’s no animation, there’s no concept of “frames”, so using those for synchronization isn’t really an option. On the other hand, it also means that the nothing else that’s not happening when I call delay is nothing. Or more accurately, it’s spinning checking for button presses rather than waiting on the clock.

Which means I can improve the code by replacing the delay calls with my own loops that check for buttons that aren’t auto-repeating, so should be handled as soon as they’re pressed. Besides the call at the end of loop, I had a one in the title page handler. Oddly enough, they needed different code.

The one for the title screen is:

 if (state == MOVING) {
      unsigned long end = millis() + game.delay;
      while (millis() < end)
           if (arduboy.pressed(A_BUTTON) || arduboy.pressed(B_BUTTON))
                break;
 }

The button presses just cause the loop to exit, but aren’t handled here. So it uses pressed just to check them and exit. On the other hand, the one at the end of the title page handler is:

 do arduboy.pollButtons();
 while (!arduboy.justPressed(A_BUTTON));

Here, exiting the loop is the effect of the button, so it needs to use pollButtons/justPressed to consume them. Otherwise, the button will show up as pressed when the main loop starts.

The code is now behaving very well, so it’s on to the final UI tweaks. Thanks!

One aside. Considering that the Arduboy has more memory, a faster CPU and better graphics than my first computer in college, I don’t think it’s really “incredibly limited” so much as “incredibly retro”. Makes coding for it a lot of fun, though!

1 Like

So glad to have found this thread - I’m just getting started with Arduboy dev and TBH was a little surprised to find that pressed() didn’t handle debouncing for me - of course it’s nice to access the buttons at the plumbing level for some applications, but I was expecting the API to be a little more porcelain; so much great info in this exchange - love this community… :heart_eyes: :thumbsup:

Note that the Arduboy2 library can now be installed using the Arduino IDE library manager, so it’s now as convenient to use as the original Arduboy library. (The companion ArduboyTones library is also in the library manager, as well.)

1 Like

well where was this hiding a month ago :stuck_out_tongue:

In such a situation frames don’t harm anything though, and you still get the other benefits, power saving, etc. Just set your framerate to 15 FPS (a nice number for button polling and debouncing)…

ive even set the framerate to 1 :smiley:

That makes buttons very unresponsive. You may have to hold a button down for up to 1 second before it’s detected.

1 Like