【问题标题】:smooth animation in java for fast moving objectsjava中用于快速移动物体的平滑动画
【发布时间】:2015-02-25 05:36:38
【问题描述】:

我正在创建球以不同速度从屏幕一侧移动到另一侧的简单动画。问题是,在球的速度较高时,我可以看到球的明显闪烁,实际上很难解释,但是当部分球仍在上一步时,我可以看到重绘。

我尝试了很多方法,包括:

  1. 原生摇摆动画使用第一个线程/睡眠/repain,然后移至计时器

  2. 在 swing jframe 中切换到 javafx 画布/窗格。尝试了过渡和 AnimationTimer

  3. 修补 CreateBufferStrategy,对于 1、2、3 - 老实说,没有看到任何区别(也许我做错了什么......)

我的问题是如何提高平滑度以及我想要实现的目标是否可以使用本机 java 或者使用一些外部库更好?如果可以,你能推荐一些东西吗?

下面显示了我的第二/第三次尝试的示例代码。

import java.awt.Dimension;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;

import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;
import javax.swing.JFrame;

public class FXTrackerPanel extends JFrame {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    public int crSize = 30;
    public double xPos = crSize;
    public double yPos = 100;
    public int xSize = 100;
    public int ySize = 100;
    public Circle r;
    int dir = 1;

    public void updateScreenSize() {
        int screen = 0;
        GraphicsEnvironment ge = GraphicsEnvironment
            .getLocalGraphicsEnvironment();
        GraphicsDevice[] gs = ge.getScreenDevices();
        if( screen > -1 && screen < gs.length )
        {           
            xSize = gs[screen].getDisplayMode().getWidth();
            ySize = gs[screen].getDisplayMode().getHeight();
        }
        else if( gs.length > 0 )
        {
            xSize = gs[0].getDisplayMode().getWidth();
            ySize = gs[0].getDisplayMode().getHeight();
        }
        else
        {
            throw new RuntimeException( "No Screens Found" );
        }

        yPos = ySize / 2;
    }

    private void initFXPanel(JFXPanel fxPanel) {
        updateScreenSize();
        xPos = crSize;

        Group root = new Group();

        double speed = 5;

        int repeats = Timeline.INDEFINITE;

        r = new javafx.scene.shape.Circle(xPos, yPos, crSize / 2, Color.RED);
        TranslateTransition tt = new TranslateTransition(Duration.seconds(speed), r);
        tt.setFromX(xPos);
        tt.setToX(xSize - crSize * 3);
        tt.setCycleCount(repeats);
        tt.setAutoReverse(true);
        tt.setInterpolator(Interpolator.EASE_BOTH);
        tt.play();

        root.getChildren().add(r);

//      new AnimationTimer() {
//          
//          @Override
//          public void handle(long now) {
//              double speed = 20;
//              try {
//                  speed = Double.valueOf(TETSimple.mp.speedSinus.getText());
//              }
//              catch (Exception ex) {
//                  speed = 20;
//              }
//              double xMov = (speed * 4 * Math.sin( xPos * Math.PI / xSize ) );
//              if (xMov <= 0) {
//                  xMov = 1;
//              }
//              if (dir == 1) {
//                  if (xPos >= xSize - crSize)
//                      dir = 0;
//                  xPos += xMov;
//              } else {
//                  if (xPos <= 1)
//                      dir = 1;
//                  xPos -= xMov;
//              }
//              
//              r.setTranslateX(xPos);              
//          }
//      }.start();

        fxPanel.setScene(new Scene(root));
    }

    public FXTrackerPanel() {
        updateScreenSize();
        this.setSize(new Dimension(xSize, ySize));
        this.setPreferredSize(new Dimension(xSize, ySize));
        this.setVisible(true);
        this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        JFXPanel fxPanel = new JFXPanel();
        this.add(fxPanel);
        this.createBufferStrategy(3);

        Platform.runLater(new Runnable() {

            @Override
            public void run() {
                initFXPanel(fxPanel);               
            }
        });
    }

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

这里是swing代码的例子:

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.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;

import javax.swing.JPanel;
import javax.swing.Timer;

import java.lang.Math;
import java.util.Random;

public class TrackerPanel extends JPanel implements ActionListener {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    Shape cr;
    Color c;
    public int crSize = 30;
    public double xPos = crSize;
    public double yPos = 100;
    public double xPosPrev = crSize;
    public double yPosPrev = 100;
    public int xSize = 100;
    public int ySize = 100;
    int dir = 1; // left
    int timer = 50; // 50 - sins, 1500 - linear
    int method = 1; // 1 - jump, 2 - sinus
    int timeToChange = 1000;
    int passes = 0;
    Timer tt;
    boolean clickedClose = false;
    private int repeats = 0;

    // t - timer interval, m - method of ball movement: 1 - jump, 2 - sinus
    public TrackerPanel(int t, int m) {
        this.setPreferredSize(new Dimension(300, 200));
        this.timer = t;
        this.method = m;
        c = Color.red;
        repaint();
        this.updateScreenSize();
        tt = new Timer(t, null);
        tt.addActionListener(this);
        tt.start();
    }

    public void updateScreenSize() {
        int screen = TETSimple.suppMonitor;
        GraphicsEnvironment ge = GraphicsEnvironment
                .getLocalGraphicsEnvironment();
        GraphicsDevice[] gs = ge.getScreenDevices();
        if (screen > -1 && screen < gs.length) {
            xSize = gs[screen].getDisplayMode().getWidth();
            ySize = gs[screen].getDisplayMode().getHeight();
        } else if (gs.length > 0) {
            xSize = gs[0].getDisplayMode().getWidth();
            ySize = gs[0].getDisplayMode().getHeight();
        } else {
            throw new RuntimeException("No Screens Found");
        }

        yPos = ySize / 2;
        yPosPrev = ySize / 2;
    }

    public void actionPerformed(ActionEvent arg0) {
        if (method == 1)
            lineMovement();
        else
            sinusMovement();
        repaint(0, ySize / 2, xSize, crSize);
    }

    private Double parseText2Int(String literal) {
        try {
            return Double.valueOf(literal);
        } catch (Exception ex) {
            ex.printStackTrace();           
        }
        return 10.0;
    }

    private void checkFinishCondition() {
        if (passes + 1 > repeats && repeats != 0) {
            if (!clickedClose) {
                TETSimple.mp.bStop.doClick();
                clickedClose = true;
            }
            return;
        }
    }

    private void sinusMovement() {
        this.updateScreenSize();
        this.repeats = parseText2Int(TETSimple.mp.repeatsCount.getText()).intValue();
        checkFinishCondition();

        double speed = parseText2Int(TETSimple.mp.speedSinus.getText());
        double xMov = (speed * Math.sin(xPos * Math.PI / xSize));
        if (xMov <= 0) {
            xMov = 1;
        }
        if (dir == 1) {
            if (xPos >= xSize - crSize)
                dir = 0;
            xPosPrev = xPos;
            xPos += xMov;
        } else {
            if (xPos <= 1 + crSize) {
                dir = 1;
                passes++;
            }
            xPosPrev = xPos;
            xPos -= xMov;
        }
    }

    private void lineMovement() {
        this.repeats = parseText2Int(TETSimple.mp.repeatsCount.getText()).intValue();
        checkFinishCondition();

        double left = crSize;
        double center = xSize / 2 - crSize * 1.5;
        double right = xSize - crSize * 2;
        Random r = new Random();

        if (timeToChange <= 0) {
            passes++;
            if (xPos == left || xPos == right) {
                timeToChange = 300 + r.nextInt(12) * 100;
                xPos = center;
            } else if (xPos == center) {
                timeToChange = 300 + r.nextInt(7) * 100;
                if (r.nextBoolean())
                    xPos = left;
                else
                    xPos = right;
            }
        } else {
            timeToChange -= 100;
        }
    }

    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.green);
        g2d.fill(new Ellipse2D.Double(xPos, yPos, crSize, crSize));
        g2d.dispose();
    }
}

【问题讨论】:

  • 闪烁可能是由于不正确地更新输出而不使用适当的双缓冲或在尝试优化更新过程时更新屏幕的错误部分引起的
  • 您是否尝试过使用 BufferCapabilities 检查硬件上实际可用的内容?您的双缓冲和页面翻转很可能被忽略。在这种情况下,您需要自己动手。

标签: java animation javafx


【解决方案1】:

我认为这个问题是由 AWT 中 JFXPanel 的渲染引起的:在两个不同的系统线程(AWT 事件调度线程和 FX 应用程序线程)之间同步发生了一些复杂的事情。

如果您可以将其编写为“纯”JavaFX 应用程序(即没有 Swing/AWT 代码),它会运行得更顺畅:

import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;

public class FXAnimationTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        Group root = new Group();

        double speed = 5;

        int repeats = Timeline.INDEFINITE;

        Screen screen = Screen.getPrimary();
        Rectangle2D screenBounds = screen.getBounds();
        double xSize = screenBounds.getWidth();
        double ySize = screenBounds.getHeight();

        double crSize = 30 ;
        double xPos = crSize ;
        double yPos = ySize / 2 ;

        Circle r = new Circle(xPos, yPos, crSize / 2, Color.RED);
        TranslateTransition tt = new TranslateTransition(Duration.seconds(speed), r);
        tt.setFromX(xPos);
        tt.setToX(xSize - crSize * 3);
        tt.setCycleCount(repeats);
        tt.setAutoReverse(true);
        tt.setInterpolator(Interpolator.EASE_BOTH);
        tt.play();

        root.getChildren().add(r);

        Scene scene = new Scene(root, xSize, ySize);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

如果您必须在 Swing 应用程序中嵌入 JavaFX,并且您使用的是 Java 8(我在 8u20 上对此进行了测试),那么有一个系统属性可以在同一线程上运行两个 UI 工具包。我认为这目前仍处于试验阶段,因此使用风险自负,但请尝试

java -Djavafx.embed.singleThread=true FXTrackerPanel

这稍微改进了一些东西,但它仍然很不稳定,不如“纯 JavaFX”版本。

【讨论】:

  • 也是一个很好的答案,但我在项目中使用其他带有 JavaFX 的库时遇到了麻烦,现在回退到摇摆
【解决方案2】:

如果没有可运行的示例,很难确切知道可能出了什么问题,但我会尽可能避免混合使用 JavaFX 和 Swing,因为它们具有不同的呈现机制。

以下是一个非常简单的示例,它通过简单地改变球在每次更新时的移动量来增加动画球的速度...

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JLabel;
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 BouncyBall {

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

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

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new ControlPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class ControlPane extends JPanel {

        private JSlider speed;
        private JSlider quanity;

        private BallPitPane ballPitPane;

        public ControlPane() {
            setLayout(new BorderLayout());
            ballPitPane = new BallPitPane();
            add(ballPitPane);

            JPanel controls = new JPanel(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridx = 0;
            gbc.gridy = 0;
            gbc.anchor = GridBagConstraints.WEST;

            speed = new JSlider(1, 100, 4);
            quanity = new JSlider(1, 100, 1);

            controls.add(new JLabel("Speed:"), gbc);
            gbc.gridy++;
            controls.add(new JLabel("Quanity:"), gbc);

            gbc.gridx++;
            gbc.gridy = 0;
            gbc.weightx = 1;
            gbc.fill = GridBagConstraints.HORIZONTAL;

            controls.add(speed, gbc);
            gbc.gridy++;
            controls.add(quanity, gbc);
            add(controls, BorderLayout.SOUTH);

            speed.addChangeListener(new ChangeListener() {
                @Override
                public void stateChanged(ChangeEvent e) {
                    ballPitPane.setSpeed(speed.getValue());
                }
            });

            quanity.addChangeListener(new ChangeListener() {
                @Override
                public void stateChanged(ChangeEvent e) {
                    ballPitPane.setQuanity(quanity.getValue());
                }
            });
        }

    }

    public class BallPitPane extends JPanel {

        private List<Ball> balls;
        private int speed;

        public BallPitPane() {
            balls = new ArrayList<>(25);
            setSpeed(2);
            setQuanity(1);

            Timer timer = new Timer(40, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    for (Ball ball : balls) {
                        ball.update(getWidth(), speed);
                    }
                    repaint();
                }
            });
            timer.start();
        }

        public void setSpeed(int speed) {
            this.speed = speed;
        }

        public void setQuanity(int quanity) {

            while (balls.size() > quanity) {
                balls.remove(0);
            }
            while (balls.size() < quanity) {
                int radius = 4 + (int) (Math.random() * 48);
                Ball ball = new Ball(
                        randomColor(),
                        (int) Math.abs(Math.random() * getWidth() - radius),
                        (int) Math.abs(Math.random() * getHeight() - radius),
                        radius
                );
                balls.add(ball);
            }

        }

        protected Color randomColor() {

            int red = (int) Math.abs(Math.random() * 255);
            int green = (int) Math.abs(Math.random() * 255);
            int blue = (int) Math.abs(Math.random() * 255);

            return new Color(red, green, blue);

        }

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

        @Override
        protected 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);
            for (Ball ball : balls) {
                ball.paint(g2d);
            }
            g2d.dispose();
        }

        public class Ball {

            private Color color;
            private int x;
            private int y;
            private int radius;
            private int delta;

            public Ball(Color color, int x, int y, int radius) {
                this.color = color;
                this.x = x;
                this.y = y;
                this.radius = radius;
                delta = Math.random() > 0.5 ? 1 : -1;
            }

            public void update(int width, int speed) {
                x += speed * delta;
                if (x + radius >= width) {
                    x = width - radius;
                    delta *= -1;
                } else if (x < 0) {
                    x = 0;
                    delta *= -1;
                }
            }

            public void paint(Graphics g) {
                g.setColor(color);
                g.fillOval(x, y, radius, radius);
            }

        }

    }

}

此示例在 Swing 的绘制过程范围内工作,如果您需要对绘制过程进行更多控制,则需要使用 BufferStrategy(在 Swing 中工作)

【讨论】:

  • 选择了这个答案,因为我的代码更容易适应摇摆。但这仍然不是理想的解决方案,尤其是在大屏幕上闪烁很明显。问题是,根据我新发布的代码,我还能做些什么吗?特别是我可以与 BufferStrategy 一起使用的任何算法?也许使用更高级的图形库,如 opengl 或 direct3d ?另一个想法是,现在在大屏幕上闪烁可能与每个给定时间范围内的大移动步长有关(在您的示例中,我将定时器延迟设置为 40)?也许缩短步骤并减少延迟会有所帮助?
  • 真的很难说,比如可以用repaint(int, int, int, int) 来限制更新的区域,但是大的一步可能会导致图片出现在两个不同的地方不是因为硬件,而是人们眼睛的工作方式。 40 毫秒的延迟是 25 fps,您可以改为 16 毫秒,这会使您达到 60 fps,但我建议您关注更改的大小。你也可以采用基于时间的职业而不是线性进展,这意味着如果变化是基于时间的进展的(所以在 n 秒内移动 n 个像素)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-20
  • 1970-01-01
  • 2017-05-09
  • 1970-01-01
相关资源
最近更新 更多