[SOLVED] [Q] What is a proper way to limit or slow down joystick events?

Hi guys.
First of all, I am not developer at all, just trying to make thing works.
Thing is working, I am pretty satisfied, and will try to be as short as possible with my question.

It is 16x16 LED matrix, where I'm playing Tetris, Snake and Breakout via PS4 controller, running this on ESP32 with ~2700 lines of code (it would be probably half if I am developer :rofl:), and combining this with one more ESP32 with WLED, so I can control WLED from PS4 controller as well. Nevertheless my question should be simple and certain, I'm writing all this however, because I'm aware that nobody likes snippets only, people like to see whole code when asking for help, so if whole code is needed, you can find shorter version here, without WLED implementation and without some features I added recently. This shorter version is fully working, and it is also completely related with the question regarding controller.

Question: Since PS4 controller is sending insane amount of events, even if button is pressed very short, games were not playable out of box. E.g. if I want to rotate a falling sprite in Tetris, even if I press rotate button very shortly, sprite would rotate 10 or 15 times, same for moving sprite left/right, it would hit the edge in most of the cases. So I had to slow down it somehow. As said before, I am not a dev (not even close), so I done it on most stupid way with delay(x);, but I don't know another:

    if(!PS4.isConnected()){
      currentApp = 4;
      return false;
    } else {
        if (PS4.Right()){
          currentInput = RIGHT;
          delay(140);
        }
        if (PS4.Down()){
          currentInput = DOWN;
          delay(50);
        }
        if (PS4.Circle()){
          currentInput = CIRCLE;
          delay(200);
        }
        if (PS4.Cross()){
          currentInput = CIRCLE;
          delay(200);
        }
        if (PS4.Left()){
          currentInput = LEFT;
          delay(140);
        }
        if (PS4.Options()){
          currentApp = -1;
          return false;
          delay(500);
        }
    }

Of course, delay(); is blocking whole code until it finish, so it can be even used for "cheating", cause e.g. if you are holding left or right, you will drastically slow the game, and this could be useful in hard situations when you need a time to thinking where to put current sprite :star_struck:

What would be right way to limit or slow down key events received from PS4 controller? Please note: I would like to keep a feature I currently have, if I'm holding the button, sprite keep moving in "button direction".

Thank you very much.

If you hold down a button, does the PlayStation send an insane amount of events?

I know nothing about PlayStation, so just tell me and other ppl who may know even less:

What events and at what rate do you get when you exactly

  • press down a button
  • hold that button for ~1.5 seconds
  • release that button

and nothing else.

Include any events that are sent that are unrelated to the button during that period.

If that is easy, do the same thing for the joystick if those are continuous variable kpjoysticks; if the joysticks are essentially switches, not ranges, confirm that they report events the same as any regular button.

Your problem is essay to solve, it's just not fully clear what needs to be done.

TIA

a7

1 Like

Huh, thanks for blazing fast reply, but I will have to wait that kids release second PS4 controller (my younger have a guest currently :innocent:), so I can slightly modify a code with some Serial.println(currentInput); and catch the logs. But basically, these logs will be bunch of lines with 0 or 1 or 2, etc. in short time, depending on button pressed.

In mean time I can say:

Yes.

Will have to measure this, but approximately 5-6 events for very short press and for hold, subtract 5-6 events with time. There is no "button pressed/button down" and "button released/button up" as I saw it before with keyboard and some my python project, if you are pointing on that (btn up/btn down). Here, with this PS4 joystick library, it is sending key events fast all the time button is pressed, and it stops sending when button is released.

And I'll be working on my taxes going to the beach, but I will promise you that I won't be looking at the dox for playstation programming.

That's on you!

So far it from your description this sounds very easy to solve, it looks like essentially debouncing a very slow (relatively speaking) switch, in combination with state change detection.

Since these are both things you want to know about anyway, learning up on them will never be a waste of time.

In the IDE, check out

Examples/02.Digital/Sebounce

and

Examples/02.Digital/State Change Derection

and of course Blonk Without Delay.

L8R

a7

:blush: Fair enough.
Thank you, I think you already helped me by pointing to above examples. I think that Blink Without Delay example with their (currentMillis - previousMillis >= interval), etc. stuff will done the job. Will try it later and follow up, when I got controller.

@alto777 : Thanks a lot for your hint! Problem was solved with (currentMillis - previousMillis >= interval) logic from Blink Without Delay example. Basically, this would be snippet after fine tuning (because gameplay feel is better if some buttons have bigger slow down than others):

  unsigned long previousMillis = 0;
  unsigned long moveInterval = 100;
  unsigned long rotateInterval = 250;
  unsigned long downInterval = 30;

void setup() {
  // Some setup code

}

  boolean loop() {
  //Lot of other code....
    if(!PS4.isConnected()){
      currentApp = 4;
      return false;
    } else {
        if (PS4.Right()){
          unsigned long currentMillis = millis();
          if(currentMillis - previousMillis > moveInterval) {
            currentInput = RIGHT;
            previousMillis = currentMillis;
          }
        }
        if (PS4.Down()){
          unsigned long currentMillis = millis();
          if(currentMillis - previousMillis > downInterval) {
            currentInput = DOWN;
            previousMillis = currentMillis;
          }
        }
        if (PS4.Circle()){
          unsigned long currentMillis = millis();
          if(currentMillis - previousMillis > rotateInterval) {
            currentInput = CIRCLE;
            previousMillis = currentMillis;
          }
        }
        if (PS4.Cross()){
          unsigned long currentMillis = millis();
          if(currentMillis - previousMillis > rotateInterval) {
            currentInput = CIRCLE;
            previousMillis = currentMillis;
          }
        }
        if (PS4.Left()){
          unsigned long currentMillis = millis();
          if(currentMillis - previousMillis > moveInterval) {
            currentInput = LEFT;
            previousMillis = currentMillis;
          }
        }
        if (PS4.Options()){
          currentApp = -1;
          return false;
          delay(200);
        }
  }
//some other code...

P.S. Don't ask me why I'm using boolean loop() and not void loop(), I don't know that :innocent:
This is a snippet from .h file, not from .ino file, boolean loop() was used in initial code I forked, it is working and I am happy :blush:
Writing this just because someone told me in one another thread that I shouldn't use boolean loop(), but void loop().

Hi there,

I think there may be an even better solution for your problem. From what I'm understanding your problem is that if you press your button the controller sends a e. g. 1 and otherwise a 0. So if you were to print every loop the current state it would look like this, even if you press it very shortly:

0
0
0
1
1
1
1
1
0
0
0
0
Is that correct? If so you could implement a very simple logic:

you declare a byte lastState;

byte lastState;

in your loop:

byte currentState = getCurrentState(); // getCurrentState is what your controller inputs e.G. 1 for left
if(lastState != currentState() && currentState() == 1) { //I'm assuming state 1 means LEFT
  //button left is pressed logic
}
lastState = currentState;

with that code you would enter the logic block only once and would only enter it again if you release it and press it again. So holding it down would turn it once and then never again.
You could also combine the two and reset your lastState to 0 every X millis so you could either press the buttons or hold them.

With my code you have the advantage, that you won't miss a very short press and also can mash the button if you want to turn it very fast, but it won't turn 5 times per secon on accident.

Yes.

Your solution makes sense, thanks a lot. Most probably will implement it later or tomorrow, I am whole day on PC, enough for today :stuck_out_tongue_winking_eye:

But really, it sounds lot better than my implementation, and will try it for sure.

EDIT: You are totally right. I just played long game of Tetris :slight_smile:. It is lot better than it was, but I had several misses, when button not react in time. Will definitely implement your logic these days, thanks once again.

Yes. That is essentially "state change detection".

The events are equivalent to what you would get if you digitalRead() a pushbutton in the loop, and the event nature of the input means any debouncing has been taken care of.

This is the full pattern you'll now spot in variations all over the place:

  byte currentState = digitalRead(theButtonPin); 

  if(lastState != currentState()) {   // state changed so...
    if (currentState() == 1) {

// do whatever you need/want when the button goes down

    }
    else {

// do whatever you need/want when the button was released

    }

    lastState = currentState; 
  }

If the logic is reversed, you could just invert the digitalRead right when you do it.

  byte currentState = !digitalRead(theButtonPin); // ! negates the logic value

In a larger sketch, the buttons might all be read at the top of the loop, that is also where you might fix "upside down" logic.

@stiw47 when you make something work with this, using events instead of digitalRead, please post it for us to admire, even a small example what works.

TIA

a7

1 Like

Will not be today, but will do it for sure. Thanks once again.

I won't speak for @blackspin but seeing ppl get over state change detection is one of the best parts of reading on these fora.

a7

After one more long Tetris game, I think everything is super cool now. Didn't noticed that I missed some button hit. This would be my code. Maybe it could be more simple, but IDK that and I'm satisfied with this :blush:

  unsigned long previousMillis = 0;
  unsigned long moveInterval = 100;
  unsigned long rotateInterval = 250;
  unsigned long downInterval = 30;
  byte lastJoystickState;

void setup() {
  // Some setup code

}

  boolean loop() {
  //Lot of other code...
    if(!PS4.isConnected()){
      currentApp = 4;
      return false;
    } else {
      if (PS4.Right()) {
        byte currentJoystickState = 4;
        unsigned long currentMillis = millis();
        if ((lastJoystickState != currentJoystickState && currentJoystickState == 4) || currentMillis - previousMillis > moveInterval){
            currentInput = RIGHT;
            previousMillis = currentMillis;
            lastJoystickState = currentJoystickState;
          }
      }
      if (PS4.Down()) {
        byte currentJoystickState = 2;
        unsigned long currentMillis = millis();
        if ((lastJoystickState != currentJoystickState && currentJoystickState == 2) || currentMillis - previousMillis > downInterval){
            currentInput = DOWN;
            previousMillis = currentMillis;
            lastJoystickState = currentJoystickState;
          }
      }
      if (PS4.Circle()) {
        byte currentJoystickState = 6;
        unsigned long currentMillis = millis();
        if ((lastJoystickState != currentJoystickState && currentJoystickState == 6) || currentMillis - previousMillis > rotateInterval){
            currentInput = CIRCLE;
            previousMillis = currentMillis;
            lastJoystickState = currentJoystickState;
          }
      }
      if (PS4.Cross()) {
        byte currentJoystickState = 5;
        unsigned long currentMillis = millis();
        if ((lastJoystickState != currentJoystickState && currentJoystickState == 5) || currentMillis - previousMillis > rotateInterval){
            currentInput = CIRCLE;
            previousMillis = currentMillis;
            lastJoystickState = currentJoystickState;
          }
      }
      if (PS4.Left()) {
        byte currentJoystickState = 3;
        unsigned long currentMillis = millis();
        if ((lastJoystickState != currentJoystickState && currentJoystickState == 3) || currentMillis - previousMillis > moveInterval){
            currentInput = LEFT;
            previousMillis = currentMillis;
            lastJoystickState = currentJoystickState;
          }
      }
        if (PS4.Options()){
          currentApp = -1;
          return false;
          delay(200);
        }
        if (PS4.L1()){
          byte currentJoystickState = 9;
          unsigned long currentMillis = millis();
          if((lastJoystickState != currentJoystickState && currentJoystickState == 9) || currentMillis - previousMillis > rotateInterval) {
            if(--currentPalette < 5 ) currentPalette = 20;
            previousMillis = currentMillis;
            lastJoystickState = currentJoystickState;
          }
        }
        if (PS4.R1()){
          byte currentJoystickState = 9;
          unsigned long currentMillis = millis();
          if((lastJoystickState != currentJoystickState && currentJoystickState == 10) || currentMillis - previousMillis > rotateInterval) {
            if(++currentPalette > 20) currentPalette = 5;
            previousMillis = currentMillis;
            lastJoystickState = currentJoystickState;
          }
        }
    }
//some other code...

The only thing I'm missing at the moment is that I cannot move and rotate the sprite same time. Most probably this could be implemented with more complex IF logic (aka more IF mess in my code :stuck_out_tongue_winking_eye:). This is what I just experimented and it seems it can read two buttons pressed same time:

#include <PS4Controller.h>

void setup() {
  Serial.begin(115200);
  PS4.begin();
  Serial.println("Ready.");
}

void loop() {
  // Below has all accessible outputs from the controller
  if (PS4.isConnected()) {
    if (PS4.Right()) Serial.println("Right Button");
    if (PS4.Left()) Serial.println("Left Button");

    if (PS4.Square()) Serial.println("Square Button");
    if (PS4.Cross()) Serial.println("Cross Button");

    if (PS4.Right() && PS4.Square()) Serial.println("Right and Square");
    if (PS4.Left() && PS4.Cross()) Serial.println("Left and Cross");
    
    delay(1000);
  }
}

Output:

02:01:53.482 -> Left Button
02:01:53.482 -> Cross Button
02:01:53.482 -> Left and Cross
02:01:54.444 -> Left Button
02:01:54.444 -> Cross Button
02:01:54.444 -> Left and Cross
02:01:55.444 -> Left Button
02:01:55.444 -> Cross Button
02:01:55.444 -> Left and Cross
02:01:56.443 -> Left Button
02:01:56.443 -> Cross Button
02:01:56.443 -> Left and Cross
02:01:59.447 -> Right Button
02:01:59.447 -> Square Button
02:01:59.447 -> Right and Square
02:02:00.454 -> Right Button
02:02:00.454 -> Square Button
02:02:00.454 -> Right and Square

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.