3

I want to cause the "main thread" (the thread started which runs main()) to do some work from the actionPerformed() method of a button's ActionListener, but I do not know how to achieve this.

A little more context:

I am currently programming a 2D game using Swing (a flavour of Tetris).
When the application starts, a window opens which displays the main menu of the game. The user is presented several possibilities, one of them is to start the game by pushing a "Start" button, which causes the game panel to be displayed and triggers the main loop of the game.

To be able to switch between the two panels (that of the main menu and that of the game), I am using a CardLayout manager, then I can display one panel by calling show().
The idea is that I would like my start button to have a listener that looks like this:

public class StartListener implements ActionListener {
    StartListener() {}
    public void actionPerformed(ActionEvent e) {
        displayGamePanel();
        startGame();
    }
}

but this does not work because actionPerformed() is called from the event-dispatch thread, so the call to startGame() (which triggers the main loop: game logic update + repaint() call at each frame) blocks the whole thread.

The way I am handling this right now is that actionPerformed() just changes a boolean flag value: public void actionPerformed(ActionEvent e) { startPushed = true; }
which is then eventually checked by the main thread:

while (true) {
    while (!g.startPushed) {
        try { 
            Thread.sleep(100); 
        } catch (Exception e) {}
    }
    g.startPushed = false;
    g.startGame();
}

But I find this solution to be very inelegant.

I have read the Concurrency in Swing lesson but I am still confused (should I implement a Worker Thread – isn't that a little overkill?). I haven't done any actual multithreading work yet so I am a little lost.

Isn't there a way to tell the main thread (which would be sleeping indefinitely, waiting for a user action) "ok, wake up now and do this (display the game panel and start the game)"?.

Thanks for your help.

EDIT: Just to be clear, this is what my game loop looks like:

long lastLoopTime = System.currentTimeMillis();
long dTime;
int delay = 10;
while (running) {
    // compute the time that has gone since the last frame
    dTime = System.currentTimeMillis() - lastLoopTime;
lastLoopTime = System.currentTimeMillis();

    // UPDATE STATE
    updateState(dTime);
    //...

    // UPDATE GRAPHICS
    // thread-safe: repaint() will run on the EDT
    frame.repaint()

    // Pause for a bit
    try { 
    Thread.sleep(delay); 
    } catch (Exception e) {}
}
Rez
  • 615
  • 1
  • 6
  • 9

5 Answers5

4

This doesn't make sense:

but this does not work because actionPerformed() is called from the event-dispatch thread, so the call to startGame() (which triggers the main loop: game logic update + repaint() call at each frame) blocks the whole thread.

Since your game loop should not block the EDT. Are you using a Swing Timer or a background thread for your game loop? If not, do so.

Regarding:

while (true) {
    while (!g.startPushed) {
        try { 
            Thread.sleep(100); 
        } catch (Exception e) {}
    }
    g.startPushed = false;
    g.startGame();
}

Don't do this either, but instead use listeners for this sort of thing.

e.g.,

import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.*;

public class GameState extends JPanel {
   private CardLayout cardlayout = new CardLayout();
   private GamePanel gamePanel = new GamePanel();
   private StartPanel startpanel = new StartPanel(this, gamePanel);

   public GameState() {
      setLayout(cardlayout);
      add(startpanel, StartPanel.DISPLAY_STRING);
      add(gamePanel, GamePanel.DISPLAY_STRING);
   }

   public void showComponent(String displayString) {
      cardlayout.show(this, displayString);
   }

   private static void createAndShowGui() {
      GameState mainPanel = new GameState();

      JFrame frame = new JFrame("GameState");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(mainPanel);
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }
}

class StartPanel extends JPanel {
   public static final String DISPLAY_STRING = "Start Panel";

   public StartPanel(final GameState gameState, final GamePanel gamePanel) {
      add(new JButton(new AbstractAction("Start") {

         @Override
         public void actionPerformed(ActionEvent e) {
            gameState.showComponent(GamePanel.DISPLAY_STRING);
            gamePanel.startAnimation();
         }
      }));
   }
}

class GamePanel extends JPanel {
   public static final String DISPLAY_STRING = "Game Panel";
   private static final int PREF_W = 500;
   private static final int PREF_H = 400;
   private static final int RECT_WIDTH = 10;
   private int x;
   private int y;

   public void startAnimation() {
      x = 0;
      y = 0;
      int timerDelay = 10;
      new Timer(timerDelay , new ActionListener() {

         @Override
         public void actionPerformed(ActionEvent e) {
            x++;
            y++;
            repaint();
         }
      }).start();
   }

   @Override
   protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      g.fillRect(x, y, RECT_WIDTH, RECT_WIDTH);
   }

   @Override
   public Dimension getPreferredSize() {
      return new Dimension(PREF_W, PREF_H);
   }
}
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • `startGame()` calls the triggers the game loop, so the game loop is executed on the thread which calls `startGame()`. So if I call `startGame()` from `actionPerformed()`, it does block the EDT, but when i call it from `main()` (which is where I put the `while(true)...`), it doesn't block the EDT and works great. Also, aren't swing timer used in the EDT (which is not what I want to do here)? I would like to use things like listeners, but again the only listeners I know run on the EDT, and I want listeners that run on my main thread. – Rez Apr 21 '12 at 22:19
  • @Rez: Again, the game loop ***should not block the EDT***. Period. Swing Timers run on the EDT, but the timer delay part, the hidden `Thread.sleep(...)` does not, and this is key. Please see edit to answer above for more. – Hovercraft Full Of Eels Apr 21 '12 at 22:25
  • Thank you for your answer (what I said though is that the game loop does not block the EDT). I have read & runned your code, it seems that what you do all the work on the EDT (carefully using Timers in order not to block the whole thread)? (I just checked with `SwingUtilities.isEventDispatchThread()`, the work in `startAnimation()` and in the associated `actionPerformed` is done on the EDT). I thought that we weren't supposed to do all the heavy work (the game logic, state updates and all that) in the EDT, but in a separate thread, isn't that right? Sorry I'm a little confused. Thanks again. – Rez Apr 21 '12 at 22:53
  • 1
    @Rez: Heavy lifting, like reading in files, writing to files, getting bytes from sockets, etc... should be done on background threads, but I wouldn't consider most game logic and state updates to be heavy lifting. – Hovercraft Full Of Eels Apr 21 '12 at 22:56
  • 1
    Updating game state on the EDT also simplifies the threading concerns. Otherwise you need to be careful about painting while the updateState method is running. – Michael Krussel Apr 23 '12 at 16:28
  • @MichaelKrussel I guess you guys are right. I first used a SwingWorker, which worked pretty well, but eventually I switched to doing everything in the EDT using Swing Timer to be sure that there wouldn't be any synchronisation issue, and it works just as well. I guess that I was afraid that doing everything at the same time would slow the game (state updates, sound triggers, game events, repaints). But experience proved me wrong. So, thank you ^_^. Now I have another issue – applet blinking/flickering... http://stackoverflow.com/questions/10324444/java-applet-blinking – Rez Apr 25 '12 at 23:11
1

you should be using a SwingWorker this will execute the code in doInBackground() in a background thread and the code in done() in the EDT after doInBackground() stops

ratchet freak
  • 47,288
  • 5
  • 68
  • 106
0

The easiest way: use a CountDownLatch. You set it to 1, make it available in the Swing code by any means appropriate, and in the main thread you await it.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • Thanks, I'm looking at this possiblity right now, this seems to be exactly what I expected :) – Rez Apr 21 '12 at 23:08
0

You can consider showing a modal dialog with the game panel using SwingUtilities.invokeAndWait() so that when the dialog is closed the control returns back to main thread.

Ashwinee K Jha
  • 9,187
  • 2
  • 25
  • 19
  • Thanks, this should work, however I would prefer to stick with just a menu panel if possible (I'd like to keep just one window, also, I am not sure if dialogs are well embedded in applets)? – Rez Apr 21 '12 at 22:31
  • Hmm, My answer was based on the idea that you want the first panel to be closed (i.e let user make selection) and then start another display. Probably the correct way would be to stop doing anything at all in the main thread and do everything asynchronously using swing timers as already suggested by other people. Generally in swing applications the main thread has limited job, just to set the frame or dialog to visible. – Ashwinee K Jha Apr 21 '12 at 22:38
0

You can make all code except the EDT run on single thread execution service and then just post runnables whenever you need some code executed.

Jakub Zaverka
  • 8,816
  • 3
  • 32
  • 48