Java basic 2d game animation stutter

2020-02-07 07:02发布

问题:

So, I have been working on a 2d rpg for some time now and I can't seem to fix this one problem. The graphics seem to "jump" or stutter every few seconds for an unknown reason. This is getting quite annoying because I don't know what is causing it.

Here is a very basic program I wrote that just has a red square that moves from one side of the screen to the other side. Even in this very basic program the square still stutters every few updates and I really can't figure it out for the life of me.

public class Main extends JPanel {

int x=0, y=0;

public JFrame window = new JFrame("Window");

public Main(){
    window.setSize(1000, 500);
    window.setVisible(true);
    window.add(this);
}

public void paintComponent(Graphics g){
    super.paintComponent(g);
    g.setColor(Color.red);
    g.fillRect(x, y, 500, 500);
    x+=3;
    if(x>900){
        x=0;
    }
}

public void start(){
    while(true){
        repaint();
        try {
            Thread.sleep(16);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public static void main(String[] args){
    Main game = new Main();

    game.start();
}

}

If you run the class you will see what the problem is graphically. Obviously my game is made up of many more classes and is far more complex than this, but the same principal applies. If any one has any insight to my problem I would love to hear it. Thanks in advance.

Updated

Here are my two main classes:

Main Class:
package com.ultimatum.Main;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;

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

import com.ultimatum.Mangers.ImageLoader;
import com.ultimatum.Mangers.KeyStates;
import com.ultimatum.Mangers.ScreenUpdater;
import com.ultimatum.Mangers.UserInput;

@SuppressWarnings("serial")
public class Ultimatum extends JPanel {

    /**
     * @param x This is the start width of the screen and can be adjusted manually, but will not go any lower than this integer
     * @param y This is the start height of the screen and can be adjusted manually, but will not go any lower than this integer
     * @param contentPlaneX This is how much the width of the content plane is (Frame-Borders)
     * @param contentPlaneY This is how much the height of the content plane is (Frame-Borders)
     */
    public int x=850, y=610, contentPlaneX, contentPlaneY, middleX, middleY, tileSize=90;

    public Dimension minimumSize = new Dimension(x, y);

    public JFrame window = new JFrame("Ultimatum");//This makes the JFrame for the game
    public KeyStates keyStates = new KeyStates();
    public UserInput input = new UserInput(keyStates);
    public ImageLoader imageLoader = new ImageLoader();
    public static Ultimatum ultimatum;//Makes the object of this class
    public static ScreenUpdater su;//This is creating the object that is going to be making changes to the screen. For example, the animation.
    private BufferedImage screenImage;

    public boolean isWindowInFullscreenMode;

    private boolean imagesLoaded;

    public void initializeUltimatum(){
        toWindowedMode();

        addMouseListener(input);
        addMouseMotionListener(input);

        contentPlaneX=window.getContentPane().getWidth();
        contentPlaneY=window.getContentPane().getHeight();
        middleX=(int)contentPlaneX/2;
        middleY=(int)contentPlaneY/2;
        su = new ScreenUpdater(ultimatum, keyStates, imageLoader, "Test", tileSize);

        imageLoader.loadImages();
    }

    public void toFullscreenMode(){
        window.dispose();
        window.setUndecorated(true);
        window.setVisible(true);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setBounds(0,0,Toolkit.getDefaultToolkit().getScreenSize().width,Toolkit.getDefaultToolkit().getScreenSize().height);
        addListenersAndClassToWindow();
        isWindowInFullscreenMode=true;
    }

    public void toWindowedMode(){
        window.dispose();
        window.setUndecorated(false);
        window.setSize(x,y);
        window.setMinimumSize(minimumSize);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setVisible(true);
        window.setLocationRelativeTo(null);
        addListenersAndClassToWindow();
        isWindowInFullscreenMode=false;
    }

    public void addListenersAndClassToWindow(){
        window.add(ultimatum);//This connects paintComponent and the frame to this class
        window.addKeyListener(input);
    }

    public void paintComponent(Graphics g){
        if(imagesLoaded){
            super.paintComponent(g);
            //su.updateScreen(g);
            g.drawImage(screenImage, 0, 0, contentPlaneX, contentPlaneY, null);
        }else imagesLoaded = true;
    }

    public void update(){
        screenImage = su.updateScreen();
    }

    /**
     * This main class sets up the program. The while loop that keeps the game running is also contained inside this class. Most of this class is easily
     * readable so i'm not going to comment that much.
     */
    public static void main(String[] args){
        ultimatum = new Ultimatum();
        ultimatum.initializeUltimatum();

        final int FPS=60, TARGET_TIME=1000/FPS;

        long start, elapsed, wait;

        while(true){//This loops purpose is to keep the game running smooth on all computers 
            start = System.nanoTime();

            ultimatum.update();
            ultimatum.repaint();//This calls the paintComponent method

            elapsed = System.nanoTime() - start;

            wait = TARGET_TIME-elapsed/1000000;
            if(wait<0) wait = TARGET_TIME;


        try{//Catches the error in case the tries to give an error (which it won't)
            Thread.sleep(wait);//This is how long it waits it till the screen gets repainted
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
}

Screen Updater:

package com.ultimatum.Mangers;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.image.BufferedImage;

import com.ultimatum.Engine.BuildingGenerator;
import com.ultimatum.Engine.TextBoxGenerator;
import com.ultimatum.Entities.Character.Player;
import com.ultimatum.Gamestates.Buildings.HealingCenter;
import com.ultimatum.Gamestates.Menus.EscapeScreen;
import com.ultimatum.Gamestates.Menus.StartScreen;
import com.ultimatum.Gamestates.Menus.TitleScreen;
import com.ultimatum.Gamestates.Routes.RouteSuperClass;
import com.ultimatum.Gamestates.Towns.TownSuperClass;
import com.ultimatum.Main.Ultimatum;

public class ScreenUpdater {

    public Ultimatum ul;
    public Resizer rs;//This is the object that captures the resize in two integers
    public KeyStates ks;
    public ImageLoader loader;
    public Fader fader;
    public TextBoxGenerator textBox;
    public Initializer initer;
    public TileMap tm;
    public Player p;
    public BuildingGenerator bg;

    //Menus
    public TitleScreen titleScreen;
    public StartScreen startScreen;
    public EscapeScreen escScreen;

    //Towns
    public TownSuperClass towns;

    //Routes
    public RouteSuperClass routes;

    //Buildings
    public HealingCenter healingCenter;

    public final int TITLE_SCREEN=0, START_SCREEN=TITLE_SCREEN+1, LOAD=START_SCREEN+1, TOWN_ONE=LOAD+1, 
            ROUTE_ONE=TOWN_ONE+1, HEALING_CENTER=ROUTE_ONE+1, ESC_MENU=HEALING_CENTER+1;

    public int screenNo = TITLE_SCREEN;

    public int prevScreen=0;
    public boolean prevMenuState, menuState;//These variables are for the checkEsc method
    public boolean isMouseVisible=true, prevIsMouseVisible;//Simple boolean for setting the mouse from visible to invisible and vice versa

    public ScreenUpdater(Ultimatum ultimatum, KeyStates keyStates, ImageLoader imageloader, String location, 
            int tileSize){

        ul = ultimatum;
        ks = keyStates;
        loader = imageloader;
        fader = new Fader(ul, this);
        textBox = new TextBoxGenerator(loader, ks, ul);
        initer = new Initializer(fader, textBox);
        fader.sendIniterData(initer);

        p = new Player(ul, fader, loader, ks, initer, this);
        fader.sendPlayerData(p);

        tm = new TileMap(tileSize, loader, p);
        fader.sendTileMapData(tm);

        rs = new Resizer(ul, p);

        bg = new BuildingGenerator(ul, p, loader, tm);

        //Below are the game states being loaded

        //Menus
        titleScreen = new TitleScreen(ul, this, loader, ks, fader);
        startScreen = new StartScreen(ul, this, fader, loader, ks, textBox);
        escScreen = new EscapeScreen(ul, fader, loader, ks);
        rs.sendEscapeScreenData(escScreen);

        //Towns
        towns = new TownSuperClass(p, fader, bg, tm, this);

        //Routes
        routes = new RouteSuperClass(p, fader, bg, tm, this);

        //Buildings
        healingCenter = new HealingCenter(ul, fader, loader, ks, textBox);
    }

    public void clearScreen(Graphics g){
        g.setColor(Color.black);
        g.fillRect(0, 0, ul.contentPlaneX, ul.contentPlaneY);
    }

    public void checkEsc(Graphics g){
        if(ks.escReleased&&screenNo>LOAD&&!fader.fadingOut&&fader.fadingIn){
            if(screenNo<HEALING_CENTER&&!p.isMoving){


        menuState=true;
            prevScreen=screenNo;
        }
        else if(screenNo==ESC_MENU) menuState=false;
    }

    if(prevMenuState!=menuState){
        int toScreen;
        boolean mouseVisiblity;
        if(menuState){
            toScreen=ESC_MENU;
            mouseVisiblity=true;
        }
        else{
            toScreen=prevScreen;
            mouseVisiblity=false;
        }

        fader.FadeOut(g, 255, toScreen, false, "", 0, 0, false, 0, mouseVisiblity);//The zeros don't matter because the boolean is set to false
        if(!fader.fadingOut){
            prevMenuState=menuState;
            initer.initFader();
        }
    }
}

public void checkForF11(){
    if(ks.isF11PressedThenReleased){
        if(ul.isWindowInFullscreenMode) ul.toWindowedMode();
        else ul.toFullscreenMode();
    }
}

public void setMouseVisible(){
    ul.window.setCursor(ul.window.getToolkit().createCustomCursor(loader.cursor, new Point(0, 0),"Visible"));
}

public void setMouseInvisble(){
    ul.window.setCursor(ul.window.getToolkit().createCustomCursor(new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB), new Point(0, 0),"Clear"));
}

public void checkMouseState(){
    if(isMouseVisible!=prevIsMouseVisible){
        if(isMouseVisible) setMouseVisible();
        else setMouseInvisble();
        prevIsMouseVisible=isMouseVisible;
    }
}

public BufferedImage updateScreen(){
    BufferedImage screenImage = new BufferedImage(ul.contentPlaneX, ul.contentPlaneY, BufferedImage.TYPE_INT_ARGB);
    Graphics2D screenGraphics = screenImage.createGraphics();
    Color oldColor = screenGraphics.getColor();
    screenGraphics.setPaint(Color.white);
    screenGraphics.fillRect(0, 0, ul.contentPlaneX, ul.contentPlaneY);
    screenGraphics.setColor(oldColor);

    checkForF11();
    clearScreen(screenGraphics);
    switch(screenNo){
        case TITLE_SCREEN:
            titleScreen.titleScreen(screenGraphics);
            break;
        case START_SCREEN:
            startScreen.startScreen(screenGraphics);
            break;
        case TOWN_ONE:
            towns.townOne(screenGraphics);
            break;
        case ROUTE_ONE:
            routes.routeOne(screenGraphics);
            break;
        case HEALING_CENTER:
            healingCenter.healingCenter(screenGraphics);
            break;
        case ESC_MENU:
            escScreen.escapeScreen(screenGraphics);
            break;
    }
    checkEsc(screenGraphics);
    rs.checkForResize();
    ks.update();
    checkMouseState();

    //g.drawImage(screenImage, 0, 0, ul.contentPlaneX, ul.contentPlaneY, null);
    //screenGraphics.dispose();
    return screenImage;
}
}

回答1:

Don't update the state in the paintComponent method, painting can happen for any number reasons, many of which you don't initiate or will be notified about. Instead, the state should only be updated by your "main loop"

See Painting in AWT and Swing for more details about how painting works in Swing

Updated

Swing Timer based solution...

The example allows you to animate 1-10, 000 sprites, each sprite moves and spins independently. Obviously, I don't have collision detection, but the animation as a whole moves well

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                PaintPane pane = new PaintPane();

                JSlider slider = new JSlider(1, 10000);
                slider.addChangeListener(new ChangeListener() {
                    @Override
                    public void stateChanged(ChangeEvent e) {
                        try {
                            pane.setQuantity(slider.getValue());
                        } catch (IOException ex) {
                            ex.printStackTrace();
                        }
                    }
                });
                slider.setValue(1);

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(pane);
                frame.add(slider, BorderLayout.SOUTH);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public static class PaintPane extends JPanel {

        private static final int SPOOL_DELTA = 100;

        private List<Sprite> pool;
        private List<Sprite> sprites;
        private int quantity;

        public PaintPane() {
            try {
                BufferedImage img = ImageIO.read(getClass().getResource("/resources/Pony.png"));

                pool = new ArrayList<>(128);
                sprites = new ArrayList<>(128);
                Timer timer = new Timer(40, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {

                        if (sprites.size() < quantity) {
                            List<Sprite> toAdd = new ArrayList<>(SPOOL_DELTA);
                            int required = quantity - sprites.size();
                            if (pool.isEmpty()) {
                                for (int index = 0; index < Math.min(SPOOL_DELTA, required); index++) {
                                    int x = (int)(Math.random() * getWidth());
                                    int y = (int)(Math.random() * getHeight());
                                    toAdd.add(new Sprite(img, new Point(x, y)));
                                }
                            } else {
                                toAdd.addAll(pool.subList(0, Math.min(SPOOL_DELTA, pool.size())));
                                pool.removeAll(toAdd);
                            }
                            sprites.addAll(toAdd);
                        } else if (sprites.size() > quantity) {
                            List<Sprite> toRemove = new ArrayList<>(SPOOL_DELTA);
                            int required = sprites.size() - quantity;
                            if (sprites.size() > required) {
                                toRemove.addAll(sprites.subList(0, Math.min(SPOOL_DELTA, required)));
                                sprites.removeAll(toRemove);
                                pool.addAll(toRemove);
                            }
                        }

                        for (Sprite sprite : sprites) {
                            sprite.update(getSize());
                        }
                        repaint();
                    }
                });
                timer.start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }

            setFont(getFont().deriveFont(Font.BOLD, 18f));
        }

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

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            for (Sprite sprite : sprites) {
                sprite.draw(g2d, this);
            }
            String text = NumberFormat.getNumberInstance().format(sprites.size());
            FontMetrics fm = g2d.getFontMetrics();
            int x = getWidth() - fm.stringWidth(text);
            int y = (getHeight() - fm.getHeight()) + fm.getAscent();
            g2d.drawString(text, x, y);
            g2d.dispose();
        }

        public void setQuantity(int value) throws IOException {
            this.quantity = value;
        }

    }

    public static class Sprite {

        private BufferedImage img;
        private Point location;
        private double angle;

        private Point delta;
        private double angleDelta;

        public Sprite(BufferedImage cache, Point location) {
            img = cache;
            this.location = new Point(location);
            delta = new Point(rnd(), rnd());
            while (angleDelta == 0) {
                angleDelta = (Math.random() * 5) - 2.5;
            }
        }

        protected int rnd() {
            int value = 0;
            while (value == 0) {
                value = (int) (Math.random() * 9) - 4;
            }
            return value;
        }

        public void update(Dimension size) {
            location.x += delta.x;
            location.y += delta.y;

            if (location.x < 0) {
                location.x = 0;
                delta.x *= -1;
            }
            if (location.x + img.getWidth() > size.width) {
                location.x = size.width - img.getWidth();
                delta.x *= -1;
            }
            if (location.y < 0) {
                location.y = 0;
                delta.y *= -1;
            }
            if (location.y + img.getHeight() > size.height) {
                location.y = size.height - img.getHeight();
                delta.y *= -1;
            }

            angle += angleDelta;
        }

        public void draw(Graphics2D g2d, JComponent parent) {
            Graphics2D g = (Graphics2D) g2d.create();
            AffineTransform at = AffineTransform.getTranslateInstance(location.x, location.y);
            at.rotate(Math.toRadians(angle), img.getWidth() / 2, img.getHeight() / 2);
            g.transform(at);
            g.drawImage(img, 0, 0, parent);
            g.dispose();
        }

    }

}

You could also use a "time" based animation, instead of linear based animation, for example

  • Moving a square from a starting point to the position of a mouse click at a fixed speed
  • JPanel image flies from the screen
  • Java image move along points in list and use linear interpolation

And if you're feeling really brave, Moving JLabel to other JLabels - GUI and Move image in a spiral fashion in java which are examples of key-frame based animations (time based)

Updated

This is an update to the original posted code from the question that is using a time based animation and adds in some rotation to the object (and some other graphical updates).

You'll note that I've used a ReentrantLock around the critical points where the shape is either updated or painted, this should prevent possible race conditions or dirty read/writes from occurring

The following is the same animation at 10, 5, 2 and 1 second durations

One thing I did note, was, the smaller the update range (ie window), te better the animation, so you might consider using something like repaint(Rectangle) to reduce the amount of area the component tries to update

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Main extends JPanel {

    double x = 0, y = 0;
    private Rectangle2D shape;
    private double angel = 0;

    private ReentrantLock updateLock = new ReentrantLock();

    public JFrame window = new JFrame("Window");

    public Main() {
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setSize(1000, 500);
        window.add(this);
        window.setVisible(true);
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
        g2d.setColor(Color.red);

        updateLock.lock();
        try {
            g2d.setTransform(AffineTransform.getRotateInstance(Math.toRadians(angel),
                    shape.getCenterX(),
                    shape.getCenterY()));
            g2d.fill(shape);
        } finally {
            updateLock.unlock();
        }
        g2d.dispose();
    }

    public void start() {
        shape = new Rectangle2D.Double(x, y, 50, 50);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                long startTime = System.nanoTime();
                long runTime = TimeUnit.NANOSECONDS.convert(10, TimeUnit.SECONDS);
                System.out.println(runTime);

                double rotateFrom = 0;
                double rotateTo = 720;
                while (true) {

                    long now = System.nanoTime();
                    long diff = now - startTime;
                    double progress = diff / (double) runTime;
                    if (progress > 1.0d) {
                        progress = 0d;
                        startTime = System.nanoTime();
                    }

                    x = (getWidth() * progress);

                    updateLock.lock();
                    try {
                        angel = rotateFrom + ((rotateTo - rotateFrom) * progress);
                        shape.setRect(x, y, 50, 50);
                    } finally {
                        updateLock.unlock();
                    }

                    repaint();
                    try {
                        Thread.sleep(8);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.setDaemon(true);
        t.start();
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                Main game = new Main();

                game.start();
            }
        });
    }

}