Search code examples
javaswingjframejpanelgraphics2d

Java Tile Flickering


Whenever I move the camera in a java game I'm working on, the edges of the tiles begin to flicker, and gaps appear between the seams, shown in the picture provided. image flickers

I was following a tutorial series made by RyiSnow on YouTube, and this bug occurred when I got to the 5th tutorial in the series.

I suspect this may be a performance problem, however I am unsure of what the problem may be. What's strange is that while moving the camera up and to the left, the tiles flicker, but moving the camera down and to the left doesn't cause the tiles to flicker. This bug is even more apparent in full screen mode, even if all of the tiles were already rendered when in windowed mode.

A minimal reproducible example:

Main.java:

package src.main;

import javax.swing.JFrame;

public class Main //the main class
{

    public static void main(String args[])
    {
        GamePanel gamePanel = new GamePanel();

        JFrame window = new JFrame(); //create a new JFrame
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //task will terminate when window is closed
        window.add(gamePanel); //adds the game panel to the window frame
        window.pack(); //sets size to preferred size
        window.setVisible(true);
        window.setLocationRelativeTo(null); //center window
    }
}

GamePanel.java:

package src.main;

//import necessary libraries
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JPanel;

public class GamePanel extends JPanel implements Runnable //class for the game screen
{ 
    Thread gameThread; //the thread in which the game operates

    BufferedImage sprite;
    public int cameraY = -1400;
    public int cameraX = -1400;

    public GamePanel()
    {
        this.setPreferredSize(new Dimension(480, 320)); //set preferred size to the variables previously defined
        this.setDoubleBuffered(true); //all drawing done in offscreen buffer; improves game performance
        this.setFocusable(true);

        //load the tile image
        try
        {
            sprite = ImageIO.read(getClass().getResourceAsStream("/res/Grass.png"));
        }
        catch(IOException e)
        {
            e.printStackTrace();
        }
        gameThread = new Thread(this); //instantiate a new thread
        gameThread.start(); //start the thread
    }

    @Override
    public void run() //runs when thread is started
    {
        double drawInterval = 1000000000/60; //set the interval to 1000000000 divide by the fps (60)
        double delta = 0;
        long lastTime = System.nanoTime();
        long currentTime;

        while(gameThread != null) //repeat while gamethread is active
        {
            currentTime = System.nanoTime();
            delta +=(currentTime - lastTime) / drawInterval;
            lastTime = currentTime;

            if(delta >= 1)
            {
                cameraY++;
                cameraX++;
                repaint();
                delta--;
            }
        }
    }

    public void paintComponent(Graphics g) //In this case, this method is used to draw the tiles on a grid
    {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g; //create a new variable equal to the variable g converted to Graphics2D

        for(int x = 0; x<100; x++)
        {
            for(int y = 0; y<100; y++)
            {
                g2.drawImage(sprite, x*16+cameraX, y*16+cameraY, 16, 16, null);
            }
        }

        g2.dispose(); //clear the system recources used to paint objects
    }

}

Any help would be greatly appreciated.


Solution

  • This...

    Works for me, but, Swing is not thread safe. This means you should avoid update the UI or any state the UI relies on from outside the context of the Event Dispatching Thread.

    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javax.imageio.ImageIO;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public class Main {
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        JFrame frame = new JFrame();
                        frame.add(new GamePanel());
                        frame.pack();
                        frame.setLocationRelativeTo(null);
                        frame.setVisible(true);
                    } catch (IOException ex) {
                        Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            });
        }
    
        public class GamePanel extends JPanel implements Runnable //class for the game screen
        {
            Thread gameThread; //the thread in which the game operates
    
            BufferedImage sprite;
            public int cameraY = -1400;
            public int cameraX = -1400;
    
            public GamePanel() throws IOException {
                sprite = ImageIO.read(getClass().getResourceAsStream("/resources/Grass.png"));
                gameThread = new Thread(this); //instantiate a new thread
                gameThread.start(); //start the thread
            }
    
            @Override
            public void addNotify() {
                super.addNotify();
                if (gameThread != null) {
                    return;
                }
                gameThread = new Thread(this);
                gameThread.start();
            }
    
            @Override
            public void removeNotify() {
                super.removeNotify();
                Thread reference = gameThread;
                gameThread = null;
                if (reference != null) {
                    try {
                        reference.join();
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(480, 320);
            }
    
            @Override
            public void run() //runs when thread is started
            {
                double drawInterval = 1000000000 / 60; //set the interval to 1000000000 divide by the fps (60)
                double delta = 0;
                long lastTime = System.nanoTime();
                long currentTime;
    
                while (gameThread != null) //repeat while gamethread is active
                {
                    currentTime = System.nanoTime();
                    delta += (currentTime - lastTime) / drawInterval;
                    lastTime = currentTime;
    
                    if (delta >= 1) {
                        cameraY++;
                        cameraX++;
                        repaint();
                        delta--;
                    }
                }
            }
    
            public void paintComponent(Graphics g) //In this case, this method is used to draw the tiles on a grid
            {
                super.paintComponent(g);
                Graphics2D g2 = (Graphics2D) g; //create a new variable equal to the variable g converted to Graphics2D
    
                for (int x = 0; x < 100; x++) {
                    for (int y = 0; y < 100; y++) {
                        g2.drawImage(sprite, x * 16 + cameraX, y * 16 + cameraY, 16, 16, this);
                    }
                }
            }
    
        }
    }
    

    This...

    Also works for me, but makes use of a Swing Timer instead, which would generally be a better solution...

    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javax.imageio.ImageIO;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.Timer;
    
    public class Main {
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        JFrame frame = new JFrame();
                        frame.add(new GamePanel());
                        frame.pack();
                        frame.setLocationRelativeTo(null);
                        frame.setVisible(true);
                    } catch (IOException ex) {
                        Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            });
        }
    
        public class GamePanel extends JPanel //class for the game screen
        {
    
            BufferedImage sprite;
            public int cameraY = -1400;
            public int cameraX = -1400;
    
            private Timer timer;
    
            public GamePanel() throws IOException {
                sprite = ImageIO.read(getClass().getResourceAsStream("/resources/Grass.png"));
                timer = new Timer(16, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        cameraY++;
                        cameraX++;
                        repaint();
                    }
                });
            }
    
            @Override
            public void addNotify() {
                super.addNotify();
                timer.start();
            }
    
            @Override
            public void removeNotify() {
                super.removeNotify();
                timer.stop();
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(480, 320);
            }
    
            public void paintComponent(Graphics g) //In this case, this method is used to draw the tiles on a grid
            {
                super.paintComponent(g);
                Graphics2D g2 = (Graphics2D) g; //create a new variable equal to the variable g converted to Graphics2D
    
                for (int x = 0; x < 100; x++) {
                    for (int y = 0; y < 100; y++) {
                        g2.drawImage(sprite, x * 16 + cameraX, y * 16 + cameraY, 16, 16, this);
                    }
                }
            }
    
        }
    }
    

    I doubt it's my system as it can run other games without any problems

    You base code works for me, although I'd discourage you from building a system this way, it points the finger towards your system. It could be the OS, I've had issues with different hardware/driver combinations which do strange things on selected systems, so it's not easily discounted.

    Made in Java

    Swing is not the only available graphics library. Swing also makes use of passive repaint engine and it's paint process is generally out of your control, so it's not entirely suitable for more complex or intensive games/animation.

    You could also make use of BufferStrategy (and the JavaDocs which has a decent example) which will provide you with the lowest level access to the hardware which Java allows out the box.

    You could also look at JavaFx or even libGDX or LWJGL