Графика Canvas с двойной буферизацией не работает

Я попытался сделать графику с двойной буферизацией для своего холста, но она всегда исчезает сразу после рендеринга, а иногда даже не рендерится. Вот код:

package initilizer;
import java.awt.AWTException;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import input.Keyboard;

public class Main extends Canvas{

    static int width = 800;
    static int height = 600;
    int cx = width/2;
    int cy = height/2;
    boolean initilized = false;

    double FOV = 0.5 * Math.PI;

    Camera cam = new Camera(1.0, 5.0, 3.0);
    Camera cam1 = new Camera(10.0, 50.0, 30.0);
    long lastFpsCheck = System.currentTimeMillis();

    public static JFrame frame = new JFrame("3D Engine");

    Robot robot;

    static Keyboard keyboard = new Keyboard();

    Image img;


    public static void main(String[] args) {
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Canvas canvas = new Main();
        canvas.setSize(width, height);
        canvas.addKeyListener(keyboard);
        canvas.setFocusable(true);
        canvas.setBackground(Color.black);
        frame.add(canvas);
        frame.pack();
        frame.setVisible(true);
        BufferedImage cursorImg = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);

        // Create a new blank cursor.
        Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
            cursorImg, new Point(0, 0), "blank cursor");

        // Set the blank cursor to the JFrame.
        canvas.setCursor(blankCursor);
    }

    void init() {
        try {
            robot = new Robot();
        } catch (AWTException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    double[] rotate2D(double[] pos,double[] rot) {
        double x = pos[0];
        double y = pos[1];

        double s = rot[0];
        double c = rot[1];

        double[] result = {(x * c) - (y * s), (y * c) + (x * s)};

        return result;
    }


    public void paint(Graphics MainGraphics) {
        Point startMousePos = MouseInfo.getPointerInfo().getLocation();
        double startMouseX = startMousePos.getX();
        double startMouseY = startMousePos.getY();

        if(img == null)
        {
            img = createImage(width, height);
        }
        Graphics g = img.getGraphics();;

        // First run initialization
        if (initilized == false) {
            initilized = true;
            init();
        }

        // Storing start time for FPS Counting
        long startTime = System.currentTimeMillis();

        // Clearing Last Frame
        //g.clearRect(0, 0, width, height);

        // Drawing Crosshair
        g.setColor(Color.white);
        g.fillRect(cx - 8, cy - 1, 16, 2);
        g.fillRect(cx - 1, cy - 8, 2, 16);


        // Drawing Debugger Menu
        g.drawString("HI WASSUp", 0, 16);





        g.dispose();


        if (frame.isFocused() == true) {
            robot.mouseMove(cx, cy);
            Point endMousePos = MouseInfo.getPointerInfo().getLocation();
            double endMouseX = endMousePos.getX();
            double endMouseY = endMousePos.getY();
            double[] rel = {startMouseX - endMouseX, startMouseY - endMouseY};
            cam.mouseMotion(rel);
        }




        // Limiting FPS
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // Calculating FPS
        long endTime = System.currentTimeMillis();
        double delta_time = (endTime - startTime);
        if ((lastFpsCheck + 1000) < endTime) {
            lastFpsCheck = endTime;
            frame.setTitle("3D Engine - FPS: " + (int) (1000/delta_time));
        }

        // Controlling camera movement
        if (keyboard.getW() == true) {
            cam.update(delta_time, "W");
        }
        if (keyboard.getA() == true) {
            cam.update(delta_time, "A");
        }
        if (keyboard.getS() == true) {
            cam.update(delta_time, "S");
        }
        if (keyboard.getD() == true) {
            cam.update(delta_time, "D");
        }
        if (keyboard.getE() == true) {
            cam.update(delta_time, "E");
        }
        if (keyboard.getQ() == true) {
            cam.update(delta_time, "Q");
        }

        // Draw rendered frame
        MainGraphics.drawImage(img, 0,0, null);

        // Draw next frame
        repaint();
    }

}

Недавно я опубликовал вопрос об этом коде. Если хотите, вы можете проверить клавиатуру java из этого последнего сообщения, но, пожалуйста, помогите мне с этим. Я новичок в программировании на Java (хотя у меня все еще есть некоторый опыт программирования). Спасибо


person rokoblox    schedule 08.12.2019    source источник
comment
Хорошо, давайте начнем с того, что JPanel по умолчанию имеет двойную буферизацию, поэтому вам действительно следует начать с нее. Вы хотите использовать Canvas, когда хотите взять под контроль процесс рисования, что означает, что вы действительно хотите использовать BufferStrategy вместо этого, но я бы посмотрел на JavaDocs в качестве примера лучше   -  person MadProgrammer    schedule 08.12.2019
comment
Не делайте Thread.sleep(1000); в методе paint, ничего не будет отображаться до тех пор, пока ПОСЛЕ того, как метод paint не вернется. Вы хотите отделить проход обновления от прохода рисования. Живопись не делает ничего другого с помощью красок. Все, что вы принимаете, должно быть сделано как часть вашего прохода обновления из вашего основного цикла, который должен выполняться (в данном случае) вне потока диспетчеризации событий, чтобы предотвратить возможные проблемы, но который затем вызывает кучу других проблем   -  person MadProgrammer    schedule 08.12.2019
comment
Я использовал Thread.sleep(1000); чтобы ограничить только FPS, было 1000/60, но я изменил его на это, потому что думал, что проблема может быть в скорости рендеринга   -  person rokoblox    schedule 08.12.2019
comment
Кроме того, когда я пытался использовать JPanel, он вообще не отображался. Не могли бы вы прислать мне код JPanel в качестве ответа?   -  person rokoblox    schedule 08.12.2019
comment
Неважно, я мог бы решить эту проблему, не используя метод paint() для рисования графики, в любом случае спасибо за помощь.   -  person rokoblox    schedule 08.12.2019


Ответы (1)


Ответ на ваш вопрос сложен.

  1. Java Swing JPanel (или JComponent) по умолчанию имеет двойную буферизацию
  2. В Swing уже есть механизм рисования, который вы не контролируете, поэтому вам нужно работать в рамках его функциональности.
  3. Единственная реальная причина, по которой вы бы использовали java.awt.Canvas, это если вы хотите получить полный контроль над процессом рисования.

Первое, что я бы посоветовал вам сделать, это взглянуть на Выполнение пользовательского рисования и Рисование в AWT и Swing, чтобы улучшить представление о том, как рисование работает в Swing/AWT. Это даст вам лучшее понимание API и того, хотите ли вы работать с ним или определить свой собственный.

Некоторые другие проблемные области:

  • Не делайте Thread.sleep(1000); в методе paint, ничего не будет отображаться до тех пор, пока ПОСЛЕ возврата не будет выполнен метод paint. Вы хотите отделить «проход обновления» от «прохода краски». Живопись не делает ничего другого с помощью красок. Все, что вы принимаете, должно быть сделано как часть вашего «прохода обновления» из вашего «основного цикла», который должен выполняться (в данном случае) вне потока диспетчеризации событий, чтобы предотвратить возможные проблемы, но который затем вызывает кучу других вопросов
  • MouseInfo.getPointerInfo().getLocation() это НЕ то, как вы должны отслеживать положение мыши. Swing уже предоставляет ряд механизмов для отслеживания событий мыши. См. Как написать прослушиватель мыши и Как написать прослушиватель движения мыши для получения более подробной информации.
  • Исходя из всего, меня также беспокоит то, как вы отслеживаете ввод с клавиатуры, и я настоятельно рекомендую вам взглянуть на Как использовать привязки клавиш для наиболее часто рекомендуемого метода управления вводом с клавиатуры пользователем.

В следующем примере просто используется JPanel в качестве основной поверхности рендеринга. Он использует преимущества ранее существовавшего процесса рисования и использует Swing Timer в качестве механизма «основного цикла», который отвечает за обработку пользовательского ввода и обновление состояния перед планированием нового прохода рисования.

Помните, что Swing НЕ является потокобезопасным, и вам не следует обновлять пользовательский интерфейс или что-либо, от чего пользовательский интерфейс может зависеть, вне контекста потока диспетчеризации событий.

import java.awt.Dimension;
import java.awt.EventQueue;
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.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import java.util.StringJoiner;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;

public class Test {

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

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                Main main = new Main();
                frame.add(main);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
                main.start();
            }
        });
    }

    // Decouple the input from the implementation
    enum Input {
        UP, DOWN, LEFT, RIGHT
    }

    public class Main extends JPanel {

        boolean initilized = false;

        double FOV = 0.5 * Math.PI;

        private Instant lastFpsCheck = Instant.now();
        private Point mousePosition;
        private Timer timer;

        private Set<Input> input = new HashSet<>();

        public Main() {
            MouseAdapter mouseHandler = new MouseAdapter() {
                @Override
                public void mouseMoved(MouseEvent e) {
                    // This is within the components coordinate space
                    mousePosition = e.getPoint();
                }

                @Override
                public void mouseEntered(MouseEvent e) {
                    mousePosition = e.getPoint();
                }

                @Override
                public void mouseExited(MouseEvent e) {
                    mousePosition = null;
                }
            };

            addMouseMotionListener(mouseHandler);
            addMouseListener(mouseHandler);

            InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap actionMap = getActionMap();

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), "Pressed.up");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "Released.up");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "Pressed.down");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "Released.down");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "Pressed.left");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "Released.left");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "Pressed.right");
            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "Released.right");

            actionMap.put("Pressed.up", new InputAction(input, Input.UP, true));
            actionMap.put("Released.up", new InputAction(input, Input.UP, false));
            actionMap.put("Pressed.down", new InputAction(input, Input.DOWN, true));
            actionMap.put("Released.down", new InputAction(input, Input.DOWN, false));
            actionMap.put("Pressed.left", new InputAction(input, Input.LEFT, true));
            actionMap.put("Released.left", new InputAction(input, Input.LEFT, false));
            actionMap.put("Pressed.right", new InputAction(input, Input.RIGHT, true));
            actionMap.put("Released.right", new InputAction(input, Input.RIGHT, false));

            timer = new Timer(15, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent evt) {
                    update();
                }
            });
        }

        public void start() {
            startTime = Instant.now();
            timer.start();
        }

        public void stop() {
            timer.stop();
        }

        // The start time of a given cycle
        private Instant startTime;
        // The estimated number of frames per second
        private double fps = 0;
        // The number of acutal updates performed
        // within a given cycle
        private int updates = 0;

        protected void update() {

            if (startTime == null) {
                startTime = Instant.now();
            }

            if (input.contains(Input.UP)) {
                //cam.update(delta_time, "W");
            }
            if (input.contains(Input.LEFT)) {
                //cam.update(delta_time, "A");
            }
            if (input.contains(Input.DOWN)) {
                //cam.update(delta_time, "S");
            }
            if (input.contains(Input.RIGHT)) {
                //cam.update(delta_time, "D");
            }
            // Don't know what these do, so you will need to devices
            // your own action
            //if (input.contains(Input.UP)) {
            //cam.update(delta_time, "E");
            //}
            //if (input.contains(Input.UP)) {
            //cam.update(delta_time, "Q");
            //}

            Instant endTime = Instant.now();
            Duration deltaTime = Duration.between(startTime, endTime);
            if (lastFpsCheck.plusSeconds(1).isBefore(endTime)) {
                System.out.println(deltaTime.toMillis());
                lastFpsCheck = endTime;
                fps = updates;
                updates = 0;
                startTime = Instant.now();
            }

            updates++;
            repaint();
        }

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

        double[] rotate2D(double[] pos, double[] rot) {
            double x = pos[0];
            double y = pos[1];

            double s = rot[0];
            double c = rot[1];

            double[] result = {(x * c) - (y * s), (y * c) + (x * s)};

            return result;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            //Point startMousePos = MouseInfo.getPointerInfo().getLocation();
            //double startMouseX = startMousePos.getX();
            //double startMouseY = startMousePos.getY();

            Graphics2D g2d = (Graphics2D) g.create();

            // Drawing Debugger Menu
            g2d.drawString("HI WASSUp", 0, 20);

            if (mousePosition != null) {
                g2d.drawString(mousePosition.x + "x" + mousePosition.y, 0, 40);
                // Your old code is broken, because MouseInfo.getPointerInfo 
                // doesn't give you the position of the mouse from within
                // the components coordinate space, but in the screen space
                // instead
                //robot.mouseMove(cx, cy);
                //Point endMousePos = MouseInfo.getPointerInfo().getLocation();
                //double endMouseX = endMousePos.getX();
                //double endMouseY = endMousePos.getY();
                //double[] rel = {startMouseX - endMouseX, startMouseY - endMouseY};
                //cam.mouseMotion(rel);
            }

            g2d.drawString(Double.toString(fps), 0, 60);

            StringJoiner sj = new StringJoiner(", ");
            for (Input item : input) {
                switch (item) {
                    case DOWN:
                        sj.add("down");
                        break;
                    case UP:
                        sj.add("up");
                        break;
                    case LEFT:
                        sj.add("left");
                        break;
                    case RIGHT:
                        sj.add("right");
                        break;
                }
            }

            g2d.drawString(sj.toString(), 0, 80);
            g2d.dispose();
        }

        public class InputAction extends AbstractAction {

            private final Set<Input> input;
            private final Input direction;
            private final boolean add;

            public InputAction(Set<Input> input, Input direction, boolean add) {
                this.input = input;
                this.direction = direction;
                this.add = add;
            }

            @Override
            public void actionPerformed(ActionEvent evt) {
                if (add) {
                    input.add(direction);
                } else {
                    input.remove(direction);
                }
            }

        }

    }
}

Теперь, из-за того, как работает процесс рисования Swing, FPS в лучшем случае является «приблизительным», и я лично не стал бы полагаться на него слишком сильно. Я мог бы вместо этого установить Timer для использования задержки в 5 миллисекунд и просто двигаться так быстро, как вы можете.

Теперь, если вам абсолютно необходимо иметь полный контроль над процессом рисования, вам нужно будет начать с java.awt.Canvas и использовать BufferStrategy API.

Это даст вам полный контроль над процессом рисования. Это сложнее и потребует от вас учитывать больше пограничных случаев, но предоставит вам полный контроль над планированием, когда происходит проход краски, и, таким образом, лучший контроль над FPS.

Я бы рекомендовал взглянуть на JavaDocs как пример лучше.

Я использовал Thread.sleep(1000); чтобы ограничить только FPS, было 1000/60, но я изменил его на это, потому что думал, что проблема может быть в скорости рендеринга

Это, прямо скажем, наивный подход и свидетельствует о непонимании того, как устроен процесс рисования – без обид, с чего-то надо начинать. Но лучше начать с прочтения доступной документации, которую я предоставил выше, чтобы вы могли лучше понять, как на самом деле работает API, и принять более взвешенное решение о том, хотите ли вы использовать его (например, JPanel) или отказаться от него. свой собственный (т.е. Canvas)

person MadProgrammer    schedule 08.12.2019
comment
Спасибо за ваш ответ, я уже понял, как использовать BufferStrategy и использовал его в своем холсте. - person rokoblox; 09.12.2019