Slow movement using paintComponent method

2019-01-29 14:10发布

问题:

I decided to re-write my game using Swing's painting technique paintComponent() method(someone on SO actually told me to use this method). I decided to use JPanel as the game's base instead of Canvas. My previous written game uses a Canvas but the game could not show up on my 64 bit desktop but could show up on my 32 bit labtop which is why this game had to be re-written.

Problem now is, while the ship's movement works, the drawing seems awfully slow(unless it is my laptop problem?) compare to what I did before which was using AWT's double buffering drawing technique. I spend a whole day but could not figure out what could possibly make the ship run faster.

   public class Ship extends JLabel implements KeyListener{

        private Image image;
        private boolean turnRight;
        private int x;
        private int y;
        private int speed = 5;
        private boolean turnLeft;

        public Ship(int x, int y)
        {
            this.x = x;
            this.y = y;

            try {
                image = ImageIO.read(new File("Ship/Ship.PNG"));
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            addKeyListener(this);

        }

        public Image getImage()
        {
            return image;
        }

        @Override
        public void keyPressed(KeyEvent e) {
            // TODO Auto-generated method stub

            if(e.getKeyCode() == KeyEvent.VK_RIGHT)
            {

                x += speed;
                setTurnRight(true);
                setTurnLeft(false);

            }
            else if(e.getKeyCode() == KeyEvent.VK_LEFT)
            {

                x -= speed;
                setTurnLeft(true);
                setTurnRight(false);
            }


            // redraw yourself
            repaint();

        }

        private void setTurnLeft(boolean turnLeft) {
            // TODO Auto-generated method stub
            this.turnLeft = turnLeft;
        }

        // swing custom painting 
        public void paintComponent(Graphics g)
        {
            if(x <= 0)
            {
                x = 0;

            }
            else if(x >= 610)
            {
                x = 610;
            }

            g.drawImage(getImage(), x, y, null);

        }

        public void setTurnRight(boolean turnRight)
        {
            this.turnRight = turnRight;
        }

        public boolean getTurnLeft()
        {
            return turnLeft;
        }

        public boolean getTurnRight()
        {
            return turnRight;
        }

        @Override
        public void keyReleased(KeyEvent e) {
            // TODO Auto-generated method stub

        }

        @Override
        public void keyTyped(KeyEvent e) {
            // TODO Auto-generated method stub

        }

    }

回答1:

Normally, I would create a concept of a renderable element. I would maintain a list of these elements and update them accordingly within my main loop.

At the very least, each would have a concept of location, direction and rotation (if required), they would also be capable of been painted.

Within my main component, I would simply loop through all these elements and "draw" them, offset the Graphics context to allow for there position within the game space.

But that's not what you are doing...

Remember, components have a sense of location and size already, you shouldn't be trying to re-implement this requirement, instead, you should be finding ways to take advantage of it...(ie, don't maintain a reference to the x/y values ;))

The following is a simple example. It uses a JPanel to render the main image. The main loop (in this case a javax.swing.Timer), tells the component that it should update it's movement as required.

The ship itself is responding to key events by changing the rotation value by a given, variable, delta. This allows you to control the speed of the spin as you need (I've deliberately set it low to start with, so play around with it)

What you should resist doing, is changing the frame rate ;)

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.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
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;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.LineBorder;

public class BattleShipGame {

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

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

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

    public class OceanPane extends JPanel {

        private BattleShip ship;

        public OceanPane() {
            setLayout(new GridBagLayout());
            ship = new BattleShip();
            add(ship);

            Timer timer = new Timer(40, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    ship.move();
                    revalidate();
                    repaint();
                }
            });
            timer.setRepeats(true);
            timer.setCoalesce(true);
            timer.start();
        }

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

    public static class BattleShip extends JPanel {

        protected static final int MAX_TURN_RATE = 5;

        private BufferedImage ship;
        private float angle;
        private float angleDelta;

        public BattleShip() {
            setOpaque(false);
            try {
                ship = ImageIO.read(new File("BattleShip.png"));
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            setFocusable(true);
            InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap am = getActionMap();

            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "leftTurn");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "rightTurn");

            am.put("leftTurn", new TurnAction(-0.1f));
            am.put("rightTurn", new TurnAction(0.1f));
        }

        public void move() {

            angle += angleDelta;

        }

        public void setAngle(float angle) {
            this.angle = angle;
        }

        public float getAngle() {
            return angle;
        }

        @Override
        public Dimension getPreferredSize() {
            Dimension size = new Dimension(0, 0);
            if (ship != null) {
                double rads = Math.toRadians(getAngle());
                double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads));
                int w = ship.getWidth();
                int h = ship.getHeight();
                size.width = (int) Math.floor(w * cos + h * sin);
                size.height = (int) Math.floor(h * cos + w * sin);
            }
            return size;
        }

        @Override
        public Dimension getMinimumSize() {
            return getPreferredSize();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (ship != null) {
                Graphics2D g2d = (Graphics2D) g.create();

                double rads = Math.toRadians(getAngle());
                double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads));
                int w = ship.getWidth();
                int h = ship.getHeight();
                int newWidth = (int) Math.floor(w * cos + h * sin);
                int newHeight = (int) Math.floor(h * cos + w * sin);

                AffineTransform at = new AffineTransform();
                at.translate((newWidth - w) / 2, (newHeight - h) / 2);
                at.rotate(Math.toRadians(getAngle()), w / 2, h / 2);

                g2d.drawImage(ship, at, this);
                g2d.dispose();
            }
        }

        protected class TurnAction extends AbstractAction {

            protected float delta;

            public TurnAction(float delta) {
                this.delta = delta;
            }

            @Override
            public void actionPerformed(ActionEvent e) {
                angleDelta += delta;
                if (angleDelta > MAX_TURN_RATE) {
                    angleDelta = MAX_TURN_RATE;
                } else if (angleDelta < (MAX_TURN_RATE * -1)) {
                    angleDelta = (MAX_TURN_RATE * -1);
                }
            }

        }
    }
}


回答2:

I would recommend having a class which extends JPanel, using a javax.swing.Timer in there, defining your 1000/fps and your ActionListener, in which you use a repaint() which uses a paintComponent that you will make that would call upon the draw method in your Ship, which is now called paintComponent.

So, as that explaination was terrible, here is some code:

public class Class_Name extends JPanel()
{
    Ship ship = new Ship(0,0);
    javax.swing.Timer timer = new javax.swing.Timer(1000/60, new ActionListener(){
        repaint();
    });

    public void paintComponent(Graphics g)
    {
        super.paintComponent();
        ship.draw(g);
    }
}

and the draw is, what is now called paintComponent.

If this didn't answer your question, please let me know.