Search code examples
javaswinguser-interfaceanimationpaint

Why does the JFrame glitch when multiple objects are painted?


I am making an asteroid game. Every so often an asteroid needs to be generated and fly across the screen. For some reason when more then 1 asteroid is created, the screen glitches out. If you maximize the screen you will be able to see the glitching. I have tried using paint instead of paintComponent. I have also tried extending JFrame instead of JPanel but that just makes it worse. The class below sets up the screen and handles the game loop

public class Game extends JPanel {
    static ArrayList<Asteroids> rocks = new ArrayList<Asteroids>();

    //This variable determines whether the game should keep running
    static boolean running = true;

    //Counter to access arraylist
    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        //Creating the window
        JFrame frame = new JFrame("Asteroid Game");
        frame.getContentPane().setBackground(Color.BLACK);
        frame.setSize(1100, 1000);
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);  

        Asteroids a = new Asteroids();
        frame.add(a);

        //Game loop
        while(running) {
            if(counter % 4 == 0) {
                rocks.add(new Asteroids());
                frame.add(rocks.get(rocks.size() - 1));
            }

            for(int i = 0; i < rocks.size(); i++) {
                rocks.get(i).repaint();
                rocks.get(i).move();
                if(!rocks.get(i).isPosFine()) {
                    rocks.remove(i);
                    i--;
                }
            }



            Thread.sleep(17);
            counter++;

        }
    }
}

The class below sets up the asteroids

public class Asteroids extends JPanel {
    //These arrays store the coordinates of the asteroid
    private int[] xPos = new int[8];
    private int[] yPos = new int[8];

    //Determines whether asteroid should be generated from top or bottom
    private int[] yGen = {-100, 1100};

    //Determines the direction the asteroid shold go
    int genLevel;

    /**
     * @param g Graphics
     * This method paints the asteroid
     */
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D r = (Graphics2D)g;
        r.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        r.setColor(Color.decode("#52575D"));
        r.fillPolygon(xPos, yPos, 8);
    }

    /**
     * This constructor sets up the asteroid location points
     */
    public Asteroids() {
        int x = (int)(Math.random() * (700 - 1) + 100);
        int y = yGen[(int)(Math.random() * (1 + 1 - 0))];
        updateAsteroids(x, y);
        genLevel = y;
        System.out.println("Created!");
    }

    /**
     * @param x int
     * @param y int
     * This method generates the asteroid based on the points passed in
     */
    public void updateAsteroids(int x, int y) {
        xPos[0] = x;
        xPos[1] = x + 20;
        xPos[2] = x + 40;
        xPos[3] = x + 35;
        xPos[4] = x + 40;
        xPos[5] = x + 4;
        xPos[6] = x - 16;
        xPos[7] = x - 20;

        yPos[0] = y;
        yPos[1] = y + 7;
        yPos[2] = y + 20;
        yPos[3] = y + 40;
        yPos[4] = y + 80;
        yPos[5] = y + 70;
        yPos[6] = y + 40;
        yPos[7] = y;
    }

    /**
     * This moves the asteroid
     */
    public void move() {
        int moveSpeedx = (int)(Math.random() * (10 - 1) + 1);
        int moveSpeedy = (int)(Math.random() * (10 - 1) + 1);
        for(int i = 0; i < 8; i++) {
            if(genLevel > 0) {
                xPos[i] -= moveSpeedx;
                yPos[i] -= moveSpeedy;
            }
            else {
                xPos[i] += moveSpeedx;
                yPos[i] += moveSpeedy;
            }

        }
    }

    /**
     * @return if the asteroid should be kept on the screen or not 
     */
    public boolean isPosFine() {
        for(int i = 0; i < 8; i++) {
            if(xPos[i] > 1250 || xPos[i] < -150)
                return false;
            if(yPos[i] > 1250 || yPos[i] < - 150)
                return false;
        }
        return true;
    }
}```

Solution

  • Your biggest problem that I can see is that you made your Asteroids class extend JPanel, making it much heavier weight than it should be, and making it difficult for more than one to show and for them to interact well and easily.

    I recommend that you:

    • Make Asteroid a non-component logical class,
      • one that knows how to draw itself by giving it a public void draw(Graphics2D g2) method
      • one that knows how to move itself in response to your game loop's tick
    • Create one JPanel just for drawing the entire animation
    • Give this JPanel a collection of Asteroid objects, say in an ArrayList
    • In this JPanel's paintComponent, loop through all Asteroids in the collection, calling each one's draw(...) method
    • Drive the whole animation in a Swing thread-safe and controllable way using a Swing Timer. In this timer's actionPerformed, tell each asteroid to move, and then call repaint() on the drawing JPanel
    • Don't call .repaint() within the loop, but rather after the loop is finished
    • Create a small BufferedImage sprite from your Shapes, and draw those as the asteroid

    A simple example illustrating what I mean:

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.Image;
    import java.awt.Polygon;
    import java.awt.RenderingHints;
    import java.awt.image.BufferedImage;
    import java.util.ArrayList;
    import java.util.List;
    import javax.swing.*;
    
    @SuppressWarnings("serial")
    public class Game2 extends JPanel {
        private static final int PREF_W = 1000;
        private static final int PREF_H = 800;
        private static final int TIMER_DELAY = 20;
        private List<Asteroid2> asteroids = new ArrayList<>();
    
        public Game2() {
            setBackground(Color.BLACK);
            int rows = 5;
            int cols = 5;
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    Asteroid2 asteroid = new Asteroid2();
                    asteroid.setX(j * (PREF_W / cols));
                    asteroid.setY(i * (PREF_H / rows));
                    asteroids.add(asteroid);
                }
            }
            new Timer(TIMER_DELAY, e -> {
                for (Asteroid2 asteroid2 : asteroids) {
                    asteroid2.move();
                }
                repaint();
            }).start();
        }
    
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            for (Asteroid2 asteroid : asteroids) {
                asteroid.draw(g);
            }
        }
    
        @Override
        public Dimension getPreferredSize() {
            if (isPreferredSizeSet()) {
                return super.getPreferredSize();
            }
            return new Dimension(PREF_W, PREF_H);
        }
    
        private static void createAndShowGui() {
            Game2 mainPanel = new Game2();
    
            JFrame frame = new JFrame("Game2");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(mainPanel);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> createAndShowGui());
        }
    }
    
    class Asteroid2 {
        private static final int[] POLY_X = { 20, 40, 60, 55, 60, 24, 4, 0 };
        private static final int[] POLY_Y = { 0, 7, 20, 40, 80, 70, 40, 0 };
        private static final Color ASTEROID_COLOR = Color.decode("#52575D");
        private Image image;
        private int x;
        private int y;
    
        public Asteroid2() {
            Polygon poly = new Polygon(POLY_X, POLY_Y, POLY_X.length);
            image = new BufferedImage(60, 80, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2 = (Graphics2D) image.getGraphics();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2.setColor(ASTEROID_COLOR);
            g2.fill(poly);
            g2.dispose();
        }
    
        public void move() {
            x++;
            y++;
        }
    
        public void setX(int x) {
            this.x = x;
        }
    
        public void setY(int y) {
            this.y = y;
        }
    
        public void draw(Graphics g) {
            if (image != null) {
                g.drawImage(image, x - 20, y, null);
            }
        }
    }