-2

I am coding a little Asteroids game, but it seems to be lagging a little bit. I am using a swing.Timer in order to update my JFrame and display the graphics. I have two questions, the first one being:

"Could the timer be the reason for the lags?" and the second one being:

"Is using a Timer the optimal way to handle game programming in Java, or is it not?"

When browsing the net, it seemed like everyone is using a Timer in order to handle animations, but I can't help but feel that it is a suboptimal way of doing this. Can someone pls explain this to me? Thank you in advance :)

Here is the code of my Timer, if it helps. First the Base class:


import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;

import javax.swing.*;


public class Base implements ActionListener {

    // Attributes
    protected static int cd = 3;        // Length of Countdown in seconds
    private int nrOfAsteroids = 10;     // Amount of Asteroids spawned
    protected static int fps = 60;      // Frames-per-second

    // Various variables and constants
    protected static BufferedImage image;
    protected static int height;
    protected static int width;
    protected static boolean colorMode = false;

    // Variables needed for Key-register
    protected static boolean isWpressed = false;
    private boolean isQpressed = false;
    private boolean isEpressed = false;
    private boolean isSpacePressed = false;
    private boolean stop = false; // TODO remove after game is finished

    // Various complex-objects
    private static Base b = new Base();
    private Asteroid[] a = new Asteroid[nrOfAsteroids];
    private JFrame frame;
    private JButton start;
    private JButton colorButton;
    private JLabel dummy;
    private JLabel gameLabel;
    protected static JLabel screen = new JLabel();
    private ImageIcon icon;
    private Timer t;
    private static Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();


    public static void main(String[] args) {
        height = (int) (screenSize.height * 0.9);
        width = (int) (screenSize.width * 0.9);
        image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        screen.setSize(width, height);
        b.frameSetup(); 
    } // end main

    private void frameSetup() {
        // Frame Setup
        frame = new JFrame("yaaasssss hemorrhoids");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().setBackground(Color.BLACK);
        frame.setBounds((int) (screenSize.width * 0.05), (int) (screenSize.height * 0.03), width, height);
        frame.setLayout(new GridBagLayout());

        // creating a "color" button
        colorButton = new JButton("CLASSIC");
        GridBagConstraints cb = new GridBagConstraints();
        cb.weightx = 1;
        cb.weighty = 1;
        cb.gridx = 2;
        cb.gridy = 0;
        cb.anchor = GridBagConstraints.FIRST_LINE_END;
        cb.insets = new Insets(10, 0, 0, 10);
        colorButton.setPreferredSize(new Dimension(100, 30));
        frame.add(colorButton, cb);

        // creating a "ASTEROIDS" Label
        gameLabel = new JLabel("ASSTEROIDS");
        GridBagConstraints gl = new GridBagConstraints();
        gl.weightx = 1;
        gl.weighty = 1;
        gl.gridwidth = 3;
        gl.gridx = 0;
        gl.gridy = 1;
        gl.anchor = GridBagConstraints.CENTER;
        gl.fill = GridBagConstraints.BOTH;
        gameLabel.setPreferredSize(new Dimension(100, 30));
        gameLabel.setFont(gameLabel.getFont().deriveFont(60.0f));
        gameLabel.setForeground(Color.WHITE);
        gameLabel.setHorizontalAlignment(SwingConstants.CENTER);
        frame.add(gameLabel, gl);

        // Dummy Component
        dummy = new JLabel();
        GridBagConstraints dc = new GridBagConstraints();
        dummy.setPreferredSize(new Dimension(100, 30));
        dc.weightx = 1;
        dc.weighty = 1;
        dc.gridx = 0;
        dc.gridy = 0;
        frame.add(dummy, dc);

        // creating a "start" button
        start = new JButton("START");
        GridBagConstraints sb = new GridBagConstraints();
        sb.weightx = 1;
        sb.weighty = 1;
        sb.gridx = 1;
        sb.gridy = 2;
        sb.anchor = GridBagConstraints.PAGE_START;
        sb.insets = new Insets(15, 0, 0, 0);
        start.setPreferredSize(new Dimension(100, 30));
        frame.add(start, sb);

        // Implementing a function to the buttons
        start.addActionListener(this);
        colorButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                if (colorButton.getText() == "CLASSIC") {
                    colorMode = true;
                    colorButton.setText("LSD-TRIP");
                } else {
                    colorMode = false;
                    colorButton.setText("CLASSIC");
                }
            }

        });

        // Show Results
        frame.setVisible(true);
    }

    private void addImage() {
        // Implementing the Image
        icon = new ImageIcon(image);
        screen.setIcon(icon);
        frame.add(screen);
    }


    protected void setWindowSize() {
        width = frame.getBounds().width;
        height = frame.getBounds().height;
        screen.setSize(width, height);
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        // Cleaning the screen
        frame.remove(start);
        frame.remove(gameLabel);
        frame.remove(colorButton);
        frame.remove(dummy);

        // Checking if Window has been resized, and acting according to it
        setWindowSize();

        // Creating the image
        for (int i = 0; i < nrOfAsteroids; ++i) {
            a[i] = new Asteroid();
        }
        gameStart();
    }


    private void gameStart() {
        t = new Timer(1000/fps, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                clearScreen();
                for (int i = 0; i < nrOfAsteroids; ++i) {
                    a[i].drawAsteroid();
                }
                // Managing Controlls
                if (isWpressed) {}
                if (isQpressed) { }
                if (isEpressed) { }
                if (isSpacePressed) { }
                if (stop) { }

                // Updating the screen
                b.addImage();
            }

        });
        t.setInitialDelay(0);
        actions();
        t.start();
    }


    private void actions() {
        // Defining all the constants for more order when handling the actions
        final int focus = JComponent.WHEN_IN_FOCUSED_WINDOW;
        String move = "Movement started";
        String noMove = "Movement stopped";
        String shoot = "Shooting started";
        String noShoot = "Shooting stopped";
        String turnLeft = "Rotation left started";
        String noTurnLeft = "Rotation left stopped";
        String turnRight = "Rotation right started";
        String noTurnRight = "Rotation right stopped";
        String stopIt = "stop"; // TODO remove when game is finished

        // Getting the input and trigger an ActionMap
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("W"), move);
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("released W"), noMove);
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("SPACE"), shoot);
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("released SPACE"), noShoot);
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("Q"), turnLeft);
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("released Q"), noTurnLeft);
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("E"), turnRight);
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("released E"), noTurnRight);
        screen.getInputMap(focus).put(KeyStroke.getKeyStroke("S"), stopIt);

        // Triggered ActionMaps perform an Action
        screen.getActionMap().put(move, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                isWpressed = true;
            } });

        screen.getActionMap().put(noMove, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                isWpressed = false;
            } });

        screen.getActionMap().put(shoot, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                isSpacePressed = true;
            } });

        screen.getActionMap().put(noShoot, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                isSpacePressed = false;
            } });

        screen.getActionMap().put(turnLeft, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                isQpressed = true;
            } });

        screen.getActionMap().put(noTurnLeft, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                isQpressed = false;
            } });

        screen.getActionMap().put(turnRight, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                isEpressed = true;
            } });

        screen.getActionMap().put(noTurnRight, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                isEpressed = false;
            } });

        screen.getActionMap().put(stopIt, new AbstractAction() {
            private static final long serialVersionUID = 1L;
            @Override
            public void actionPerformed(ActionEvent e) {
                stop = true;
            } });
    } // end actions()


    private void clearScreen() {
        Graphics2D pen = image.createGraphics();
        pen.clearRect(0, 0, Base.width, Base.height);
    }


} // end class


Now the Asteroid class:


import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Polygon;

public class Asteroid { 

    // Attributes
    private int amountOfCornerPoints = 12;
    private int size = 50;
    private int rotationSpeed = 2;
    private int movementSpeed = 3;

    // Fields needed to construct the Asteroid
    private Polygon asteroidShape;
    private int xCenter = (int) (Math.random() * Base.width);
    private int yCenter = (int) (Math.random() * Base.height);  
    private int[] y = new int[amountOfCornerPoints];
    private int[] x = new int[amountOfCornerPoints];
    private int[] random = new int[amountOfCornerPoints];
    private int rmax = 20;              //Das Maximum für r
    private int rmin = -rmax;           //Das Minimum für r

    // Field needed to transport the Asteroid
    private boolean transporting = false;

    // Field needed to rotate the Asteroid
    private int cornerAddition = 0;

    // Fields needed to detect Collision

    // Fields needed to determine the direction of the Asteroid
    private int direction = (int) Math.round((Math.random()*7));
    private int xMove = 0;
    private int yMove = 0;

    // Fields for determining the color of the Asteroid
    private Color col;
    private int red = 255;
    private int green = 255;
    private int blue = 255;

    public Asteroid() {
        // Activating colorMode
        if (Base.colorMode == true) {
            do {
                red = (int) Math.round((Math.random()*127));
                green = (int) Math.round((Math.random()*127));
                blue = (int) Math.round((Math.random()*127));
            } while (red < 64 && green < 64 && blue < 64); }
        col = new Color(red, green, blue); 


        // Zufallszahlen Generator
        for (int i = 0; i < random.length; ++i) {
            random[i] = (int) (Math.random()*rmax + rmin); }

        asteroidShape = new Polygon();

        whichDirection();
    }


    protected void drawAsteroid() {
        move();
        rotate();
        int degreeHolder;
        int degrees;

        for (int i = 0; i < amountOfCornerPoints; ++i) {
            degreeHolder = i*(360/amountOfCornerPoints) + cornerAddition;
            if (degreeHolder >= 360) {
                degrees = degreeHolder - 360;
            } else {
                degrees = degreeHolder;
            }

            x[i] = getXvalue(size + random[i])[degrees];
            y[i] = getYvalue(size + random[i])[degrees];
        }

        asteroidShape.invalidate();
        asteroidShape = new Polygon(x, y, amountOfCornerPoints);

        Graphics2D pen = Base.image.createGraphics();
        pen.setColor(col);
        pen.draw(asteroidShape);
        pen.dispose();
    }


    private void rotate() {
        cornerAddition += rotationSpeed;
        if (cornerAddition >= 360)
            cornerAddition = cornerAddition - 360;
    }


    private void move() {
        detectTransport();
        xCenter += xMove;
        yCenter += yMove;
    }


    private void detectTransport() {
        boolean transportImmunity = false;
        if (xCenter <= -size || xCenter >= Base.width + size) {
            if (transportImmunity == false)
                transporting = !transporting;
            transportImmunity = true;
            transport();
        }
        if (yCenter <= -size || yCenter >= Base.height + size) {
            if (transportImmunity == false)
                transporting = !transporting;
            transportImmunity = true;
            transport();
        }
    }


    private void transport() {
        while (transporting) {
            xCenter -= xMove;
            yCenter -= yMove;
            detectTransport();
        }
    }


    private void whichDirection() {
        switch (direction) {
            case 0: // Gerade Oben
                xMove = 0;
                yMove = -movementSpeed;
                break;
            case 1: // Diagonal Oben-rechts
                xMove = movementSpeed;
                yMove = -movementSpeed;
                break;
            case 2: // Gerade rechts
                xMove = movementSpeed;
                yMove = 0;
                break;
            case 3: // Diagonal Unten-rechts
                xMove = movementSpeed;
                yMove = movementSpeed;
                break;
            case 4: // Gerade Unten
                xMove = 0;
                yMove = movementSpeed;
                break;
            case 5: // Diagonal Unten-links
                xMove = -movementSpeed;
                yMove = movementSpeed;
                break;
            case 6: // Gerade links
                xMove = -movementSpeed;
                yMove = 0;
                break;
            case 7: // Diagonal Oben-links
                xMove = -movementSpeed;
                yMove = -movementSpeed;
                break;
        }
    } // end WhichDirection


    private int[] getXvalue(int radius) {
        int[] xPoint = new int[360];

        for (int i = 0; i < 360; ++i) {
            double xplus = Math.cos(Math.toRadians(i+1)) * radius;
            xPoint[i] = (int) Math.round(xCenter + xplus); }
        return xPoint;  
    }

    private int[] getYvalue(int radius) {
        int[] yPoint = new int[360];

        for (int i = 0; i < 360; ++i) {
            double yPlus = Math.sin(Math.toRadians(i+1)) * radius;
            yPoint[i] = (int) Math.round(yCenter - yPlus); }
        return yPoint;  
    }

}


PS.: My computer is most likely not the cause, since it can run a lot bigger games with at least 100fps

Edit: None of the other methods, as for example the rotate() method, is causing the lag, as I have already tried the entire code with only the most essential methods and the result was the same.

Edit2: Maybe its worth noting, that the lag actually is only barely noticeable. However, for a game as small as an Asteroids is, there really shouldn't be any lag, especially if it only runs on 60 fps.

Edit3: MRE is added

Solo
  • 57
  • 1
  • 11
  • 1
    Without a proper [mre] it's hard to tell. We don't know if `drawAsteroid()` is reading an image everytime it's called or how you're handing rotation. So, yes, that could be causing lag. – Frakcool Sep 30 '19 at 16:08
  • @Frakcool There you go, I updated my post. First and foremost, I would be happy to simply know if the timer itself could be at fault, and especially if using a timer is the best approach. Determining the actual source of lag comes second for me. – Solo Sep 30 '19 at 16:23
  • 1
    This question (still) does not contain a [mcve] as suggested by @Frakcool. Voting to close. – Andrew Thompson Sep 30 '19 at 16:46
  • *"I would be happy to simply know if the timer itself could be at fault"* No, if used correctly. *"if using a timer is the best approach."* We have not enough context of your code structure, so cannot answer this. – Frakcool Sep 30 '19 at 16:48
  • Alright then, I added every method which is absolutly necessary, since all the others can be disregarded (removing them did not fix the problem). I hope this helps – Solo Sep 30 '19 at 16:59
  • 1
    Tip: If you're replying to someone add @AndrewThompson or whoever you're answering to, so they get notified (The `@` is important). It's still not a MRE, did you read the link? We cannot test. But you're 1) Creating and disposing a LOT of `Graphics` objects. 2) Creating a bunch of new `ImageIcons` 3) Why are you invalidating your `AsteroidShape` (And what are they)? – Frakcool Sep 30 '19 at 17:17
  • @Frakcool Thanks for the tip, I didn't know thats how it works. I am sorry, but if I were to post the entire script, we'd end at about 1.5k lines, which I simply deem a little too much. Besides, my question is very theoretical, so I don't deem it necessary. You're right, I create a lot of ImageIcons, I'll have a look at that. When I am disposing the Graphics objects, they shouldn't be a problem anymore, or are they? The AsteroidShape is a Polygon, containing the shape of the Asteroid. I understood invalidate to basicly "flush" the polygon, is that not how it works? Thank you for your help :) – Solo Sep 30 '19 at 20:12
  • Understand that: 1) Java is not a script language, so you're not speaking of "scripts". 2) I didn't request for the whole code, instead post a [mre] (A simple, yet complete project that demonstrates your issue), with a `main` method, that draws let's say 300 images (could be the same image over and over again). Yes, after disposing the objects they should not be a problem, but creating them, yes! For [`invalidate()`](https://stackoverflow.com/questions/4396583/java-swing-repaint-vs-invalidate) read that. – Frakcool Sep 30 '19 at 20:30

1 Answers1

1

I would suggest an/fps-limiting approach instead because the lag that can happen in the game gets amplified by the strict time intervals of the Timer class. After you set the frame to be visible add the following code(or something like it):

long time = System.nanoTime();

while(!gameOver) {
    long nTime = System.nanoTime();
    float diff = (nTime - time) * 0.000000001f;

    if(diff > 1.0f / fps) {
        time = nTime;

        // do rendering here and multiply any speeds or accelerations by diff
    }
}
J. Lengel
  • 570
  • 3
  • 16
  • I really like that approach, since it doesnt use a timer. Can you tell me, why you are using `System.nanoTime()` and not `System.currentTimeMillis`? I would imagine the latter one to be easier to work with. – Solo Oct 02 '19 at 09:49
  • Well not really as you do the same amount of divisions / multiplications and have higher accuracy but milliseconds work fine as well – J. Lengel Oct 02 '19 at 18:46
  • So I just tried what you suggested, but now I can't get past the startscreen. I am guessing, the windows processes are not being updated during the while loop. How do I fix that? – Solo Oct 06 '19 at 18:00
  • You have to call repaint() every time the loop runs on your context (JPanel) object – J. Lengel Oct 07 '19 at 19:17