javaswingtimerpaintcomponentgame-loop

Where should I put the game loop in the swing app?


I'm trying to make a simple 2D game in Java.
As far as I know, my game should consist of two threads: "event dispatch thread" (for GUI operations) and "game thread" (for game loop).
I created an outline but could not find where to place the game loop.
in short, I'm trying to create a game loop without freezing my UI thread.
I would be grateful if you could give any information about the things I did wrong.
That's my game loop (You can also give tips to create a better game loop):

while(true) {
    repaint();
    try {
        Thread.sleep(17);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class Main {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                createAndShowGUI();

            }

        });
    }
    private static void createAndShowGUI() {
        JFrame frame = new JFrame("Forge and Attack");
        frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
        frame.setUndecorated(true);
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setFocusable(true);
        frame.add(new MyPanel()); 
    }   
}
class MyPanel extends JPanel implements KeyListener, MouseListener {
    public MyPanel() {
        setBackground(Color.BLACK);
        setOpaque(true);
        addKeyListener(this);
        addMouseListener(new MouseAdapter(){
            public void mousePressed(MouseEvent e){
                
            }
        });
    }
    @Override
    public void paint(Graphics g) {
        
    }
}

Solution

  • I thought this an interesting topic to expand on... I have covered the questions you asked as well as showed the maybe a better or correct way of doing certain things like painting, and listening for keys pressed, as well as some others like separation of concerns and making the entire game more reusable/expandable.

    1. Where to place the game loop?

    So this isn't straight forward and can depend on each individuals coding style, but really all we seek to achieve here is to create the game loop and start it at an appropriate time. I believe code speaks a 1000 words (sometimes it might just be 1000 words :)), but below is some code which in the most minimally possible way (still producing a valid working example) shows where a game loop can be created/placed and used in the code, the code is heavily commented for clarity and understanding:

    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.event.ActionEvent;
    import java.awt.event.KeyEvent;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javax.imageio.ImageIO;
    import javax.swing.AbstractAction;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.KeyStroke;
    import javax.swing.SwingUtilities;
    
    public class MyGame {
    
        private Scene scene;
        private Sprite player;
        private Thread gameLoop;
        private boolean isRunning;
    
        public MyGame() {
            createAndShowUI();
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(MyGame::new);
        }
    
        /**
         * Here we will create our swing UI as well as initialise and setup our
         * sprites, scene, and game loop and other buttons etc
         */
        private void createAndShowUI() {
            JFrame frame = new JFrame("MyGame");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            player = new Sprite(/*ImageIO.read(getClass().getResourceAsStream("...."))*/);
    
            this.scene = new Scene();
            this.scene.add(player);
    
            this.addKeyBindings();
            this.setupGameLoop();
    
            frame.add(scene);
            frame.pack();
            frame.setVisible(true);
    
            // after setting the frame visible we start the game loop, this could be done in a button or wherever you want
            this.isRunning = true;
            this.gameLoop.start();
        }
    
        /**
         * This method would actually create and setup the game loop The game loop
         * will always be encapsulated in a Thread, Timer or some sort of construct
         * that generates a separate thread in order to not block the UI
         */
        private void setupGameLoop() {
            // initialise the thread 
            gameLoop = new Thread(() -> {
                // while the game "is running" and the isRunning boolean is set to true, loop forever
                while (isRunning) {
                    // here we do 2 very important things which might later be expanded to 3:
                    // 1. We call Scene#update: this essentially will iterate all of our Sprites in our game and update their movments/position in the game via Sprite#update()
                    this.scene.update();
    
                    // TODO later on one might add a method like this.scene.checkCollisions in which you check if 2 sprites are interesecting and do something about it
                    // 2. We then call JPanel#repaint() which will cause JPanel#paintComponent to be called and thus we will iterate all of our sprites
                    // and invoke the Sprite#render method which will draw them to the screen
                    this.scene.repaint();
    
                    // here we throttle our game loop, because we are using a while loop this will execute as fast as it possible can, which might not be needed
                    // so here we call Thread#slepp so we can give the CPU some time to breathe :)
                    try {
                        Thread.sleep(15);
                    } catch (InterruptedException ex) {
                    }
                }
            });
        }
    
        private void addKeyBindings() {
            // here we would use KeyBindings (https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html) and add them to our Scene/JPanel
            // these would allow us to manipulate our Sprite objects using the keyboard below is 2 examples for using the A key to make our player/Sprite go left
            // or the D key to make the player/Sprite go to the right
            this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "A pressed");
            this.scene.getActionMap().put("A pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    player.LEFT = true;
                }
            });
            this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "A released");
            this.scene.getActionMap().put("A released", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    player.LEFT = false;
                }
            });
            this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "D pressed");
            this.scene.getActionMap().put("D pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    player.RIGHT = true;
                }
            });
            this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "D released");
            this.scene.getActionMap().put("D released", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    player.RIGHT = false;
                }
            });
        }
    
        public class Scene extends JPanel {
    
            private final ArrayList<Sprite> sprites;
    
            public Scene() {
                // we are using a game loop to repaint, so probably dont want swing randomly doing it for us
                this.setIgnoreRepaint(true);
                this.sprites = new ArrayList<>();
            }
    
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                Graphics2D g2d = (Graphics2D) g;
                // this method gets called on Scene#repaint in our game loop and we then render each in our game
                sprites.forEach((sprite) -> {
                    sprite.render(g2d);
                });
            }
    
            @Override
            public Dimension getPreferredSize() {
                // because no components are added to the JPanel, we will have a default sizxe of 0,0 so we instead force the JPanel to a size we want
                return new Dimension(500, 500);
            }
    
            public void add(Sprite go) {
                this.sprites.add(go);
            }
    
            private void update() {
                // this method gets called on Scene#update in our game loop and we then update the sprites movement and position our game
                sprites.forEach((go) -> {
                    go.update();
                });
            }
        }
    
        public class Sprite {
    
            private int x = 50, y = 50, speed = 5;
            //private final BufferedImage image;
    
            public boolean LEFT, RIGHT, UP, DOWN;
    
            public Sprite(/*BufferedImage image*/) {
                //this.image = image;
            }
    
            public void render(Graphics2D g2d) {
                //g2d.drawImage(this.image, this.x, this.y, null);
                g2d.fillRect(this.x, this.y, 100, 100);
            }
    
            public void update() {
                if (LEFT) {
                    this.x -= this.speed;
                }
                if (RIGHT) {
                    this.x += this.speed;
                }
                if (UP) {
                    this.y -= this.speed;
                }
                if (DOWN) {
                    this.y += this.speed;
                }
            }
        }
    }
    

    2. Tips to create a better game loop

    This very much like the first point in my answer is very subjective to what you are trying to achieve and at what granularity will your problem be satisfied with. So instead of prescribing 1 type of game loop. Let us look at the various kinds we can have:

    First what is a game loop?*

    The game loop is the overall flow control for the entire game program. It’s a loop because the game keeps doing a series of actions over and over again until the user quits. Each iteration of the game loop is known as a frame. Most real-time games update several times per second: 30 and 60 are the two most common intervals. If a game runs at 60 FPS (frames per second), this means that the game loop completes 60 iterations every second.

    a. While loop

    This we have seen in the above example and is simply a while loop encapsulated inside a Thread with possibly a Thread#sleep call to help throttle CPU usage. This and the Swing Timer are probably the most basic you can use.

    gameLoop = new Thread(() -> {
        while (isRunning) {
            this.scene.update();
            this.scene.repaint();
            try {
                Thread.sleep(15);
            } catch (InterruptedException ex) {
            }
        }
    });
    

    Pros:

    Cons:

    b. Swing Timer

    Similar to the while loop, a Swing Timer can be used in which an action event is fired periodically, because it is fired periodically we can simply use an if statement to check if the game is running and then call our necessary methods

    gameLoop = new Timer(15, (ActionEvent e) -> {
        if (isRunning) {
            MyGame.this.scene.update();
            MyGame.this.scene.repaint();
        }
    });
    

    Pros:

    Cons:

    c. Fixed time step*

    This is a more complex game loop (but simpler than a variable time step game loop). This works on the premise that we want to achieve a specific FPS i.e. 30 or 60 frames per second, and thus we simply make sure we call our update and rendering methods that exact number of times per seconds. Update methods do not accept a "time elapsed", as they assume each update is for a fixed time period. Calculations may be done as position += distancePerUpdate. The example includes an interpolation during render.

    gameLoop = new Thread(() -> {
        //This value would probably be stored elsewhere.
        final double GAME_HERTZ = 60.0;
        //Calculate how many ns each frame should take for our target game hertz.
        final double TIME_BETWEEN_UPDATES = 1000000000 / GAME_HERTZ;
        //If we are able to get as high as this FPS, don't render again.
        final double TARGET_FPS = 60;
        final double TARGET_TIME_BETWEEN_RENDERS = 1000000000 / TARGET_FPS;
        //At the very most we will update the game this many times before a new render.
        //If you're worried about visual hitches more than perfect timing, set this to 1.
        final int MAX_UPDATES_BEFORE_RENDER = 5;
        //We will need the last update time.
        double lastUpdateTime = System.nanoTime();
        //Store the last time we rendered.
        double lastRenderTime = System.nanoTime();
    
        while (isRunning) {
            double now = System.nanoTime();
            int updateCount = 0;
    
            //Do as many game updates as we need to, potentially playing catchup.
            while (now - lastUpdateTime > TIME_BETWEEN_UPDATES && updateCount < MAX_UPDATES_BEFORE_RENDER) {
                MyGame.this.scene.update();
                lastUpdateTime += TIME_BETWEEN_UPDATES;
                updateCount++;
            }
    
            //If for some reason an update takes forever, we don't want to do an insane number of catchups.
            //If you were doing some sort of game that needed to keep EXACT time, you would get rid of this.
            if (now - lastUpdateTime > TIME_BETWEEN_UPDATES) {
                lastUpdateTime = now - TIME_BETWEEN_UPDATES;
            }
    
            //Render. To do so, we need to calculate interpolation for a smooth render.
            float interpolation = Math.min(1.0f, (float) ((now - lastUpdateTime) / TIME_BETWEEN_UPDATES));
            MyGame.this.scene.render(interpolation);
            lastRenderTime = now;
    
            //Yield until it has been at least the target time between renders. This saves the CPU from hogging.
            while (now - lastRenderTime < TARGET_TIME_BETWEEN_RENDERS && now - lastUpdateTime < TIME_BETWEEN_UPDATES) {
                //allow the threading system to play threads that are waiting to run.
                Thread.yield();
    
                //This stops the app from consuming all your CPU. It makes this slightly less accurate, but is worth it.
                //You can remove this line and it will still work (better), your CPU just climbs on certain OSes.
                //FYI on some OS's this can cause pretty bad stuttering. Scroll down and have a look at different peoples' solutions to this.
                //On my OS it does not unpuase the game if i take this away
                try {
                    Thread.sleep(1);
                } catch (Exception e) {
                }
    
                now = System.nanoTime();
            }
        }
    });
    

    This loop will need other changes such to take place to allow for interpolation:

    Scene:

    public class Scene extends JPanel {
    
        private float interpolation;
    
        @Override
        protected void paintComponent(Graphics g) {
            ...
            sprites.forEach((sprite) -> {
                sprite.render(g2d, this.interpolation);
            });
        }
    
        public void render(float interpolation) {
            this.interpolation = interpolation;
            this.repaint();
        }
    }
    

    Sprite:

    public class Sprite {
    
        public void render(Graphics2D g2d, float interpolation) {
            g2d.fillRect((int) (this.x + interpolation), (int) (this.y + interpolation), 100, 100);
        }
    
    }
    

    Pros:

    Cons:

    d. Variable time step*

    Usually used when a physics system is being implemented, or whenever a record of elapsed time is needed, I.e. animations. Physics/animation updates are passed a "time elapsed since last update" argument and are hence framerate-dependent. This may mean doing calculations as position += distancePerSecond * timeElapsed.

    gameLoop = new Thread(() -> {
        // how many frames should be drawn in a second
        final int FRAMES_PER_SECOND = 60;
        // calculate how many nano seconds each frame should take for our target frames per second.
        final long TIME_BETWEEN_UPDATES = 1000000000 / FRAMES_PER_SECOND;
        // track number of frames
        int frameCount;
        // if you're worried about visual hitches more than perfect timing, set this to 1. else 5 should be okay
        final int MAX_UPDATES_BETWEEN_RENDER = 1;
    
        // we will need the last update time.
        long lastUpdateTime = System.nanoTime();
        // store the time we started this will be used for updating map and charcter animations
        long currTime = System.currentTimeMillis();
    
        while (isRunning) {
            long now = System.nanoTime();
            long elapsedTime = System.currentTimeMillis() - currTime;
            currTime += elapsedTime;
    
            int updateCount = 0;
            // do as many game updates as we need to, potentially playing catchup.
            while (now - lastUpdateTime >= TIME_BETWEEN_UPDATES && updateCount < MAX_UPDATES_BETWEEN_RENDER) {
                MyGame.this.scene.update(elapsedTime);//Update the entity movements and collision checks etc (all has to do with updating the games status i.e  call move() on Enitites)
                lastUpdateTime += TIME_BETWEEN_UPDATES;
                updateCount++;
            }
    
            // if for some reason an update takes forever, we don't want to do an insane number of catchups.
            // if you were doing some sort of game that needed to keep EXACT time, you would get rid of this.
            if (now - lastUpdateTime >= TIME_BETWEEN_UPDATES) {
                lastUpdateTime = now - TIME_BETWEEN_UPDATES;
            }
    
            MyGame.this.scene.repaint(); // draw call for rendering sprites etc
    
            long lastRenderTime = now;
    
            //Yield until it has been at least the target time between renders. This saves the CPU from hogging.
            while (now - lastRenderTime < TIME_BETWEEN_UPDATES && now - lastUpdateTime < TIME_BETWEEN_UPDATES) {
                Thread.yield();
                now = System.nanoTime();
            }
        }
    });
    

    Scene:

    public class Scene extends JPanel {
    
        private void update(long elapsedTime) {
            // this method gets called on Scene#update in our game loop and we then update the sprites movement and position our game
            sprites.forEach((go) -> {
                go.update(elapsedTime);
            });
        }
    }
    

    Sprite:

    public class Sprite {
    
        private float speed = 0.5f;
        
        public void update(long elapsedTime) {
            if (LEFT) {
                this.x -= this.speed * elapsedTime;
            }
            if (RIGHT) {
                this.x += this.speed * elapsedTime;
            }
            if (UP) {
                this.y -= this.speed * elapsedTime;
            }
            if (DOWN) {
                this.y += this.speed * elapsedTime;
            }
        }
    }
    

    Pros:

    Cons: