1

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

rokoblox
  • 84
  • 9
  • Ok, let's start with `JPanel` is double buffered by default, so you really should start there. You want to use `Canvas` when you want to take control of the paint process, which means you really want to use a [`BufferStrategy`](https://docs.oracle.com/javase/tutorial/extra/fullscreen/bufferstrategy.html) instead, but I would look at the [JavaDocs](https://docs.oracle.com/javase/10/docs/api/java/awt/image/BufferStrategy.html) as the example is better – MadProgrammer Dec 08 '19 at 01:24
  • Don't do `Thread.sleep(1000);` in the `paint` method, nothing will be rendered until AFTER the `paint` method returns. You want to seperate the "update pass" from the "paint pass". Painting does nothing else by paints. All you decision making should be done as part of your "update pass" from your "main loop", which should be executed (in this case) off the Event Dispatching Thread, so as to prevent possible issues, but which then raises a bunch of other issues – MadProgrammer Dec 08 '19 at 01:26
  • 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 – rokoblox Dec 08 '19 at 11:20
  • Also when I tried using JPanel, It didn't render at all, Could you please send me the JPanel code as an answer? – rokoblox Dec 08 '19 at 11:21
  • Nevermind, I could solve this by not using paint() method to draw graphics, Thanks for the help anyways – rokoblox Dec 08 '19 at 12:38

1 Answers1

2

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:

  • Don't do Thread.sleep(1000); in the paint method, nothing will be rendered until AFTER the paint method returns. You want to seperate the "update pass" from the "paint pass". Painting does nothing else by paints. All you decision making should be done as part of your "update pass" from your "main loop", which should be executed (in this case) off the Event Dispatching Thread, so as to prevent possible issues, but which then raises a bunch of other issues
  • MouseInfo.getPointerInfo().getLocation() is NOT how you should be tracking the position of the mouse. Swing already provides a number of mechanisms for tracking the mouse events. See How to write a Mouse Listener and How to Write a Mouse-Motion Listener for more details.
  • Based on every thing, I'd also be concerned about how you're tracking keyboard input and would highly recommend that you take a look at How to Use Key Bindings for the most commonly recommended method for managing key board input from the user.

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)

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366