-1

I'm working on an app, where I show a question and a person can answer by using the arrow keys for yes/no, which loads a new question and so on. This works well, but I'm trying to implement a multiplayer mode aswell, where another person can use the a and d keys, which is causing problems.

Basically, if the two people answer very quickly one after another, the first input gets counted as the answer for the current question, which causes a new question to load and the second input gets counted as the answer for that new question, even though the person hadn't even seen that question when they answered.

I've tried updating the question later, but it still always remembered the second KeyPress and processes them one after another.

I've also tried working with boolean variables to indicate that a person has already pressed and that the other person's input shouldn't be counted, but it either didn't work as expected or created a deadlock.

I think it's probably because of the way Java handles the KeyEvents that the code that handles the events is processed in parallel, so the boolean variables weren't set in time for it to block from processing the second KeyEvent aswell.

My goal is that if two people answer while one question is showing, only the first person to answer can get the points for that (if both answered at exactly the same time, both would ideally get the points), then a new question is loaded and so on.

Minimal reproducable example:

public class Test extends JFrame{
    int right_score;
    int left_score;
    JLabel r_score = new JLabel();
    JLabel l_score = new JLabel();
    boolean answer;
    boolean clicked = false;
    JLabel answer_label = new JLabel();

    public static void main(String[] args) {new Test();}
    
    public Test() {
        addKeyBindings();
        r_score.setText("Right: 0");
        l_score.setText("Left: 0");
        answer_label.setText("" + answer);
        this.setLayout(new FlowLayout(FlowLayout.CENTER));
        this.setMinimumSize(new Dimension(100, 100));
        this.add(answer_label);
        this.add(l_score);
        this.add(r_score);
        newQuestion();
        this.setVisible(true);
    }
    
    private void newQuestion() {
        Random rand = new Random();
        answer = rand.nextBoolean();
        answer_label.setText("" + answer);
    }

    public void r_pressed_yes() {
        if (answer && !clicked) {
            clicked = true;
            right_score += 1;
            r_score.setText("Right: " + right_score);
            newQuestion();
            clicked = false;
        }
        else if (!clicked){
            clicked = true;
            right_score -= 1;
            r_score.setText("Right: " + right_score);
            newQuestion();
            clicked = false;
        }
    };
    
    public void r_pressed_no() {
        if (!answer && !clicked) {
            clicked = true;
            right_score += 1;
            r_score.setText("Right: " + right_score);
            newQuestion();
            clicked = false;
        }
        else if (!clicked){
            clicked = true;
            right_score -= 1;
            r_score.setText("Right: " + right_score);
            newQuestion();
            clicked = false;
        }
    };
    
    public void l_pressed_yes() {
        if (answer && !clicked) {
            clicked = true;
            left_score += 1;
            l_score.setText("Left: " + left_score);
            newQuestion();
            clicked = false;
        }
        else if (!clicked){
            clicked = true;
            left_score -= 1;
            l_score.setText("Left: " + left_score);
            newQuestion();
            clicked = false;
        }
    };
    
    public void l_pressed_no() {
        if (!answer && !clicked) {
            clicked = true;
            left_score += 1;
            l_score.setText("Left: " + left_score);
            newQuestion();
            clicked = false;
        }
        else if (!clicked){
            clicked = true;
            left_score -= 1;
            l_score.setText("Left: " + left_score);
            newQuestion();
            clicked = false;
        }
    };
    

    private void addKeyBindings() {
        Action r_pressed_yes = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                r_pressed_yes();
            }};
        Action r_pressed_no = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                r_pressed_no();
            }};
        Action l_pressed_yes = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                l_pressed_yes();
            }};
        Action l_pressed_no = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                l_pressed_no();
            }};
        r_score.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("LEFT"), "r_pressed_yes");
        r_score.getActionMap().put("r_pressed_yes", r_pressed_yes);
        r_score.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("RIGHT"), "r_pressed_no");
        r_score.getActionMap().put("r_pressed_no", r_pressed_no);
        r_score.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("A"), "l_pressed_yes");
        r_score.getActionMap().put("l_pressed_yes", l_pressed_yes);
        r_score.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("D"), "l_pressed_no");
        r_score.getActionMap().put("l_pressed_no", l_pressed_no);
    }
}
  • 1
    You already answered your own question. You use booleans to turn KeyListeners on or off. Edit your question to include a [mre] that we can copy into our IDE, compile, and run millions of tests to determine what the problem is. – Gilbert Le Blanc Jul 15 '23 at 16:46
  • I've tried to construct a minimal reproducible example now, maybe that makes my issue clearer. If you click fast enough, the boolean doesn't have the intended effect – Ulala Olala Jul 15 '23 at 22:56

1 Answers1

1

The problem is not that the user inputs of the two players are handled in parallel. That will not be possible when you compare the reaction time of the players and the computational speed of your computer. The problem is that the variable clicked is only true for an extremely short time (due to the computational speed of your computer). The block of user inputs you want to write has to be longer in time to work.

Depending on how you want your game to be played, you can "activate" the new question only after a short delay. So when one player pressed a key, the input is checked, but the new question isn't shown directly or isn't "active" yet. Only after some time is passed, like 500ms, the new question with the new answer is shown. During that time, no user input should be handled or should be simply ignored. After that time, the user input can begin answer the next question again.

You can use timers or threads to show the next question after a short delay. Keep in mind not to simply use Thread.sleep() in your actionPerformed() method as it might block the GUI thread and your application would look like it is hanging/stopped/unresponsive.

Progman
  • 16,827
  • 6
  • 33
  • 48
  • That makes sense, I think I tried to fix it with a delay before but it still always seemed to remember the key inputs and just worked them off slowly. I'll try some more though, thank you – Ulala Olala Jul 16 '23 at 17:54
  • Hmm, so I've tried to implemented now and added a Thread.sleep() line in every method before calling newQuestion() and it's the way it was before, that the key presses are still remembered for some reason and just processed slower. Should I call Thread.sleep() at another time? I also tried adding an if clause (checking that clicked is false) before calling the methods in the key bindings, but that didn't help either. – Ulala Olala Jul 16 '23 at 20:50
  • @UlalaOlala I explicit wrote in my answer that you shouldn't use `Thread.sleep()` inside your methods for that exact reason as it blocks the GUI thread, which might includes any event handling like `getInputMap()`/`getActionMap()`. Unhandled user inputs/events might be delayed because of this, which looks like the input is "remembered". Use a timer instead to show/activate the next question at a later time. – Progman Jul 16 '23 at 21:05
  • @UlalaOlala: you were explicitly told *not* to use Thread sleep. Best to use a [Swing Timer](http://docs.oracle.com/javase/tutorial/uiswing/misc/timer.html) for any Swing GUI related pauses. 1+ to this answer (given yesterday), and 1- to the question for ignoring the answer's advice. – Hovercraft Full Of Eels Jul 16 '23 at 21:22
  • Oh true, I read it wrong sorry – Ulala Olala Jul 16 '23 at 22:04