0

The game runs at about 2000 to 3100 fps in normal window mode. If i set the JFrame component to fullscreen and scale up my JPanel to also the same resolution, the fps drops to 20-70. (This is a prototype, hardcoded resolutions will be later swapped out)

This is my relevant code (if this is not enough, I can provide more):

Game.java

import javax.swing.JFrame;

public class Game {

  public static void main(String[] args) {
      JFrame window = new JFrame("Platformer Test");
      window.setContentPane(new GamePanel(window));
      window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      window.setResizable(true);
      //window.setUndecorated(true);
      window.pack();
      window.setVisible(true);  
  }
}

GamePanel.java

package Main;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;

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

// custom imports
import GameState.GameStateManager;

@SuppressWarnings("serial")
public class GamePanel extends JPanel implements Runnable, KeyListener{

  // dimensions
  public static final int WIDTH = 320;
  public static final int HEIGHT = 240;
  public static final int SCALE = 2;
  
  // Graphic Device (used for fullscreen)
  static GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0];
  private JFrame frame;
  
  // game Thread
  private Thread thread;
  private boolean running;
  private double GameTicks = 60;


  // image
  private BufferedImage image;
  private Graphics2D g;
  boolean renderFPS = false;
  int frames = 0;

  // game state manager
  private GameStateManager gsm;


  public GamePanel(JFrame frame) {
    super();
    this.frame = frame;
    // set Window Size
    setFocusable(true);
    
    setFullscreen(true);    
  }
  
  private void setFullscreen(boolean t) {
      if(t) {
          setPreferredSize(new Dimension(1920, 1080));
          device.setFullScreenWindow(frame);
          requestFocus();
      }else {
          setSize(new Dimension(WIDTH * SCALE, HEIGHT * SCALE));
          requestFocus();
      }
  }

  public void addNotify() {
    super.addNotify();
    if (thread == null) {
      thread = new Thread(this);
      addKeyListener(this);
      thread.start();
    }

  }

  private void init() {

    // create image --> Game is drawn on here
    image = new BufferedImage(
        WIDTH, HEIGHT,
        BufferedImage.TYPE_INT_RGB
        );
    // get graphics component of game image
    g = (Graphics2D) image.getGraphics();

    // starts game clock
    running = true;

    // adds new GameStateManager
    gsm = new GameStateManager();
  }

  @Override
  public void run() {

    init();

    //game loop setup
    double ns = 1000000000 / GameTicks;
    double delta = 0;
    long lastTime = System.nanoTime();
    long timer = System.currentTimeMillis();
    int ticks = 0;

    // game loop
    while(running) {
      long now = System.nanoTime();
      delta +=  (now - lastTime) / ns;
      lastTime = now;
      while(delta >= 1) {
        update();
        ticks++;
        delta--;
      }
      if(running)
        render();
      frames++;


      if(System.currentTimeMillis() - timer > 1000) {
        timer += 1000;
        System.out.println("FPS: " + frames + ", ticks: " + ticks);
        renderFPS = true;
        frames = 0;
        ticks = 0;
      }
    }

  }

  private void update() {
    gsm.update();
  }

  private void render() {
    gsm.render(g);
    int fps = 0;

    // Draw To Screen
    Graphics g2 = getGraphics();
    g2.drawImage(image, 0, 0, WIDTH * SCALE, HEIGHT * SCALE, null);
    //g2.drawImage(image, 0, 0, 1920, 1080, null);
    if(renderFPS) {
        fps = frames;
    }
    g2.setColor(Color.red);
    g2.drawString("FPS: " + fps, 100,100);
    g2.dispose();
  }


  @Override
  public void keyTyped(KeyEvent e) {
    // TODO Auto-generated method stub

  }


  @Override
  public void keyPressed(KeyEvent e) {
    // TODO Auto-generated method stub
    gsm.keyPressed(e.getKeyCode());
  }


  @Override
  public void keyReleased(KeyEvent e) {
    // TODO Auto-generated method stub
    gsm.keyReleased(e.getKeyCode());

  }

}
Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
  • 1
    Don't use `Thread`, Swing is not thread safe. Don't use `getGraphics`, this is not how custom painting is done. Don't use `KeyListener`, it's not worth all the random pain (use Key bindings instead) – MadProgrammer Jun 13 '22 at 22:05

1 Answers1

1

Swing is not thread safe, you shouldn't be updating the UI, or the state the UI relies on, from outside the context of the Event Dispatching Thread. This means you shouldn't be using Thread as your "game loop".

See Concurrency in Swing for more details and How to Use Swing Timers for the most common solution.

Don't use JPanel#getGraphics, this is not how painting in Swing is done. Instead, override paintComponent. See Painting in AWT and Swing and Performing Custom Painting for more details.

Don't use KeyListener, seriously, it's just not worth all the hacking around to make it work. Instead, use the key bindings API

The following, simple, example runs at roughly 171 updates a second (it separates the "timer ticks" and "paint ticks", as in Swing, it's not really possible to know when something is actually rendered to the screen) in both windowed and full screen

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public final class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0];

                JFrame frame = new JFrame();
                frame.add(new GamePanel());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);

//                device.setFullScreenWindow(frame);
            }
        });
    }

    public class GamePanel extends JPanel {
        private Timer timer;

        private int ticksPerSecond = 0;
        private int paintsPerSecond = 0;

        private int xDelta = 1;
        private Rectangle boxy = new Rectangle(0, 0, 50, 50);

        // Graphic Device (used for fullscreen)
        public GamePanel() {
            timer = new Timer(5, new ActionListener() {
                private Instant lastTick;
                private int ticks = 0;
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (lastTick == null) {
                        lastTick = Instant.now();
                    }
                    if (Duration.between(lastTick, Instant.now()).toMillis() >= 1000) {
                        ticksPerSecond = ticks;
                        lastTick = Instant.now();
                        ticks = 0;
                    }
                    ticks++;

                    boxy.x += xDelta;
                    if (boxy.x + boxy.width > getWidth()) {
                        boxy.x = getWidth() - boxy.width;
                        xDelta *= -1;
                    } else if (boxy.x < 0) {
                        boxy.x = 0;
                        xDelta *= -1;
                    }
                    boxy.y = (getHeight() - boxy.height) / 2;

                    repaint();
                }
            });
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }

        public void addNotify() {
            super.addNotify();
            timer.start();
        }

        @Override
        public void removeNotify() {
            timer.stop();
            super.removeNotify();
        }

        private Instant lastPaint;
        private int paints = 0;

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (lastPaint == null) {
                lastPaint = Instant.now();
            }
            if (Duration.between(lastPaint, Instant.now()).toMillis() >= 1000) {
                paintsPerSecond = paints;
                lastPaint = Instant.now();
                paints = 0;
            }
            paints++;

            Graphics2D g2d = (Graphics2D) g.create();
            FontMetrics fm = g2d.getFontMetrics();
            g2d.drawString("Ticks p/s " + ticksPerSecond, 10, 10 + fm.getAscent());
            g2d.drawString("Paints p/s " + paintsPerSecond, 10, 10 + fm.getAscent() + fm.getHeight());
            g2d.fill(boxy);
            g2d.dispose();
        }
    }
}

If you "really" need absolute control over the painting process, then you should be using a BufferStrategy, see BufferStrategy and BufferCapabilities and the JavaDocs which has an excellent example of how it should be used.

Lots of things might effect the performance of the paint process, for example

A crazy experiment...

So, this example allows you to change the number of entities been rendered on the screen. Each entity is moving in it's own direction and is rotating (no collision detection).

I can get this to run up to roughly 20-25, 0000 entities before I start seeing a (significant) change in the number of updates per second. I rolled it to 100, 000 and it dropped to roughly 42 updates per second.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public final class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0];

                GamePanel gamePanel = new GamePanel();
                JSlider slider = new JSlider(1, 100000);
                slider.setValue(100);
                slider.addChangeListener(new ChangeListener() {
                    @Override
                    public void stateChanged(ChangeEvent e) {
                        if (slider.getValueIsAdjusting()) {
                            return;
                        }
                        gamePanel.setBoxCount(slider.getValue());
                    }
                });

                JFrame frame = new JFrame();
                frame.add(gamePanel);
                frame.add(slider, BorderLayout.SOUTH);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);

                device.setFullScreenWindow(frame);
            }
        });
    }

    public class GamePanel extends JPanel {
        private Timer timer;

        private int ticksPerSecond = 0;
        private int paintsPerSecond = 0;

        private List<Box> boxes = new ArrayList<>(100);

        // Graphic Device (used for fullscreen)
        public GamePanel() {
            for (int index = 0; index < 100; index++) {
                boxes.add(new Box());
            }

            timer = new Timer(5, new ActionListener() {
                private Instant lastTick;
                private int ticks = 0;

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (lastTick == null) {
                        lastTick = Instant.now();
                    }
                    if (Duration.between(lastTick, Instant.now()).toMillis() >= 1000) {
                        ticksPerSecond = ticks;
                        lastTick = Instant.now();
                        ticks = 0;
                    }
                    ticks++;

                    for (Box box : boxes) {
                        box.update(getSize());
                    }

                    repaint();
                }
            });
        }

        public void setBoxCount(int count) {
            if (count < boxes.size()) {
                boxes = boxes.subList(0, count);
                return;
            }
            while (boxes.size() < count) {
                boxes.add(new Box());
            }
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }

        public void addNotify() {
            super.addNotify();
            timer.start();
        }

        @Override
        public void removeNotify() {
            timer.stop();
            super.removeNotify();
        }

        private Instant lastPaint;
        private int paints = 0;

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (lastPaint == null) {
                lastPaint = Instant.now();
            }
            if (Duration.between(lastPaint, Instant.now()).toMillis() >= 1000) {
                paintsPerSecond = paints;
                lastPaint = Instant.now();
                paints = 0;
            }
            paints++;

            Graphics2D g2d = (Graphics2D) g.create();
            for (Box box : boxes) {
                box.paint(g2d);
            }
            FontMetrics fm = g2d.getFontMetrics();
            int yPos = 10 + fm.getAscent();
            g2d.drawString("Ticks p/s " + ticksPerSecond, 10, yPos + fm.getAscent());
            yPos += fm.getHeight();
            g2d.drawString("Paints p/s " + paintsPerSecond, 10, yPos + fm.getAscent());
            yPos += fm.getHeight();
            g2d.drawString("Count " + boxes.size(), 10, yPos + fm.getAscent());
            g2d.dispose();
        }
    }

    private List<Color> colors = new ArrayList<>(Arrays.asList(new Color[]{
        Color.BLACK, Color.BLUE, Color.CYAN, Color.DARK_GRAY, Color.GRAY, Color.GREEN,
        Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.RED, Color.WHITE,
        Color.YELLOW
    }));

    public class Box {
        private Color fill;

        private Rectangle shape;

        private int x;
        private int y;

        private int xDelta;
        private int yDelta;

        private int rotationDelta;
        private int angle = 0;

        public Box() {
            Random rnd = new Random();
            int width = rnd.nextInt(45) + 5;
            int height = rnd.nextInt(45) + 5;
            x = rnd.nextInt(400) - width;
            y = rnd.nextInt(400) - height;

            xDelta = rnd.nextInt(2) + 1;
            yDelta = rnd.nextInt(2) + 1;
            if (rnd.nextBoolean()) {
                xDelta *= -1;
            }
            if (rnd.nextBoolean()) {
                yDelta *= -1;
            }

            rotationDelta = rnd.nextInt(5);
            if (rnd.nextBoolean()) {
                rotationDelta *= -1;
            }

            shape = new Rectangle(x, y, width, height);
            Collections.shuffle(colors);
            fill = colors.get(0);

            shape = new Rectangle(0, 0, width, height);
        }

        public void update(Dimension bounds) {
            x += xDelta;
            y += yDelta;
            angle += rotationDelta;

            if (x + getWidth() > bounds.width) {
                x = bounds.width - getWidth();
                xDelta *= -1;
            } else if (x < 0) {
                x = 0;
                xDelta *= -1;
            }
            if (y + getWidth() > bounds.height) {
                y = bounds.height - getWidth();
                yDelta *= -1;
            } else if (y < 0) {
                y = 0;
                yDelta *= -1;
            }
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }

        public int getWidth() {
            return shape.width;
        }

        public int getHeight() {
            return shape.height;
        }

        public void paint(Graphics2D g2d) {
            g2d = (Graphics2D) g2d.create();
            g2d.translate(x, y);
            g2d.rotate(Math.toRadians(angle), getWidth() / 2, getHeight() / 2);
            g2d.setColor(fill);
            g2d.fill(shape);
            g2d.dispose();
        }
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • What is the worst that could happen, if I just leave this implementation as is. – jang1er Jun 15 '22 at 22:34
  • @jang1er You could end up with inconsistency between the model and the view (dirty read/writes); you could end up with rendering artefacts that are near impossible to replicate; you could end up in the same situation you're in right now. I've shown a workflow which can render nearly 20k elements without significant frame drop and 100k with a drop down to 40fps - yet you're implementation drops to 20fps just by switching to full screen mode – MadProgrammer Jun 15 '22 at 22:55
  • Swing uses a passive rendering workflow, this means that you're not in control. If you want to "as close to metal" control, then you need to use a `BufferStrategy` workflow – MadProgrammer Jun 15 '22 at 22:57
  • I narrowed the fps drop down to the window not being in focus. My Keylistener does not work in full screen as well. But i will just implement it now correctly to avoid further issues – jang1er Jun 16 '22 at 20:11
  • [Key bindings](https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html) <- less hair loss – MadProgrammer Jun 16 '22 at 21:27