javagraphicsawtdouble-buffering

Canvas double buffered graphics not working


I tried to make double buffered graphics for my canvas but it always disappears right after rendering, and sometimes it doesn't even render, Here is the code:

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();
    }

}

I posted a question recently about this code, You could check keyboard java from that last post if you wanted to, But please help me with this I'm new to java programming (I still have some programming experience tho), Thank you


Solution

  • The answer to your question is complicated.

    1. Java Swing JPanel (or JComponent) are double buffered by default
    2. Swing already has a painting mechanism, which you don't control, so you need to work within it's functionality.
    3. The only real reason you would use a java.awt.Canvas is if you want to take complete control over the painting process

    The first thing I would suggest you do is take a look at Performing Custom Painting and Painting in AWT and Swing to get a better idea of how painting works in Swing/AWT. This will provide you a better understanding of the API and whether you want to work with it or define your own.

    Some other areas of concern:

    The following example simply uses a JPanel as it's primary rendering surface. It takes advantage of the pre-existing painting process and uses a Swing Timer as the "main loop" mechanism, which is responsible for processing the user input and update the state before scheduling a new paint pass.

    Remember, Swing is NOT thread safe and you should not update the UI or anything the UI might depend on, from outside the context of the Event Dispatching Thread.

    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);
                    }
                }
    
            }
    
        }
    }
    

    Now, because of the way Swing's paint process works, the FPS is at best a "guesstimate" and I'd personally no rely to heavy on it. I might consider setting the Timer to use a 5 millisecond delay instead and just go as fast as you can.

    Now, if you absolutely, positively must have, without question, complete control over the painting process, then you will need to start with a java.awt.Canvas and make use of the BufferStrategy API.

    This will give you complete control over the painting process. It's more complicated and will require you to take into consideration more edge cases, but will provide you with complete control over scheduling when a paint pass occurs and thus, better control over the FPS.

    I would recommend having a look at the JavaDocs as the example is better.

    I used the Thread.sleep(1000); to limit FPS only, It was 1000/60 but I changed it to this because I thought the problem may be in the speed of rendering

    This is, to be frank, is a naive approach and demonstrates a lack of understanding with how the painting process works - no offensive, you've got to start somewhere. But a better place to start would be by reading the available documentation, which I've provided above so you can gain a better understanding of how the API actually works and make better decisions about whether you want to use it (ie JPanel) or roll your own (ie Canvas)