Rotate a moving shape around its center

2019-03-05 12:22发布

问题:

I'm making a 2D game in Java where the player guides a polygon through obstacles. The polygon moves up and down and the game world scrolls left and right. I need the polygon to rotate around its center, but since it is constantly being translated the point it is being rotated around moves. Trying to translate it back to the original center, rotate it, and translate it back doesn't work. How do I get the center of the shape?

Here are my motion calculations on a 2ms timer:

@Override
public void actionPerformed(ActionEvent e) {

    double theta = angleRad+90;
    if (up == true) {
        if (accelerating == false) {
            time2 = 0;
            moveX0 = moveX;
            moveY0 = moveY;
            accelerating = true;
        }
        time1++;
        double t = time1/500;
        if (accCount % 10 == 0) {
            DronePilot.velocity++;
        }
        moveX = moveX0 + velX*Math.cos(theta)*t;
        moveY = moveY0 + velY*Math.sin(theta)*t-(1/2d)*g*Math.pow(t, 2);
        velX = (DronePilot.velocity)*Math.cos(theta);
        velY = (DronePilot.velocity)*Math.sin(theta)-g*(t);
        accCount++;
    } else if (up == false){
        if (accelerating == true) {
            time1 = 0;
            moveX0 = moveX;
            moveY0 = moveY;
            accelerating = false;
        }
        time2++;
        double t = time2/500;
        moveX = moveX0 + velX*Math.cos(theta)*t;
        moveY = moveY0 + velY*Math.sin(theta)*t-(1/2d)*g*Math.pow(t, 2);
        accCount = 0;
    } if (left == true) {
        angleCount++;
        if (angleCount % 2 == 0) {
            angleDeg++;
        }
        angleRad = Math.toRadians(angleDeg);
    } else if (right == true) {
        angleCount--;
        if (angleCount % 2 == 0) {
            angleDeg--;
        }
        angleRad = Math.toRadians(angleDeg);
    }
    repaint();
}
}

And here is my paintComponent method:

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2D = (Graphics2D)g;

    Graphics g2 = g.create();
    Graphics2D copy = (Graphics2D)g2;




    copy.rotate(-angleRad, xPos, yPos);

    copy.translate(0, -moveY);

    g2D.translate(-moveX, 0);

    copy.draw(player.shape);

    for (Rectangle2D.Double r: DronePilot.rocksFloorArray) {
        g2D.draw(r);
    }
    for (Rectangle2D.Double r: DronePilot.rocksCeilArray) {
        g2D.draw(r);
    }
    for (Rectangle2D.Double r: DronePilot.roomsArray) {
        g2D.draw(r);
    }
}

where (xPos, yPos) are the center of the screen.

回答1:

Transformations are (normally) compounding

public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2D = (Graphics2D)g;

    Graphics g2 = g.create();
    Graphics2D copy = (Graphics2D)g2;

    copy.rotate(-angleRad, xPos, yPos);
    copy.translate(0, -moveY);

    g2D.translate(-moveX, 0);

    copy.draw(player.shape);
    for (Rectangle2D.Double r: DronePilot.rocksFloorArray) {
        g2D.draw(r);
    }
    for (Rectangle2D.Double r: DronePilot.rocksCeilArray) {
        g2D.draw(r);
    }
    for (Rectangle2D.Double r: DronePilot.roomsArray) {
        g2D.draw(r);
    }
}

In your above code, you are translating both the original Graphics context and the copy. In this context, copy won't be affected by the original and the original won't be affected by the copy, BUT, the original context is a shared resource and since you don't reset the translation, you will continue to get a translated context each time (compounding).

As a general rule of thumb, do ALL transformations on a copy and dispose of it when you're done.

For example...

Graphics2D g2d = (Graphics2D)g.create();
AffineTransform at = AffineTransform.getTranslateInstance(playerPoint.x, playerPoint.y);
at.rotate(Math.toRadians(angle), player.getBounds2D().getCenterX(), player.getBounds2D().getCenterY());
g2d.setTransform(at);
g2d.setColor(Color.RED);
g2d.fill(player);
g2d.setColor(Color.BLACK);
g2d.draw(player);
g2d.dispose();

This basically translates the position of the object to the player's position and then rotates the object about the centre of the object

You could, also, apply one transformation, create a copy of that context and apply another transformation, which would become compounded (so you could translate one context, copy it, and then rotate the copy and the first translation would still applied to the copy)

This incredible simple example demonstrates two basic example...

  1. Using the Graphics context and a AffineTransform to translate and rotate the player object (about it's centre point)
  2. Using a Path2D to generate a transformed shape (this example makes two objects, but you could use a single AffineTransform translated and rotated and apply it once).

In both cases, they don't affect the original shape

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
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();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private Shape player;
        private Point playerPoint;
        private float angle;
        private float deltaZ = 1.0f;

        private int deltaX, deltaY;

        public TestPane() {
            player = new Rectangle(0, 0, 20, 20);
            playerPoint = new Point(80, 80);
            Random rnd = new Random();
            deltaX = 1;
            deltaY = -1;

            Timer timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    playerPoint.x += deltaX;
                    playerPoint.y += deltaY;

                    Shape rotatedPlayer = rotatedAndTranslatedPlayer();
                    Rectangle2D bounds = rotatedPlayer.getBounds2D();
                    if (bounds.getX() < 0.0) {
                        playerPoint.x = (int)(bounds.getX() * -1);
                        deltaX *= -1;
                    } else if (bounds.getX() + bounds.getWidth() >= getWidth()) {
                        playerPoint.x = getWidth() - (int)bounds.getWidth();
                        deltaX *= -1;
                    }
                    if (bounds.getY() < 0) {
                        playerPoint.y = 0;
                        deltaY *= -1;
                    } else if (bounds.getY() + bounds.getHeight() > getHeight()) {
                        playerPoint.y = getHeight() - (int)bounds.getHeight();
                        deltaY *= -1;
                    }
                    angle += deltaZ;
                    repaint();
                }
            });
            timer.start();
        }

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

        protected Shape rotatedAndTranslatedPlayer() {
            Path2D.Double rotated = new Path2D.Double(player,  AffineTransform.getRotateInstance(
                    Math.toRadians(angle),
                    player.getBounds2D().getCenterX(), 
                    player.getBounds2D().getCenterY()));
            return new Path2D.Double(rotated, AffineTransform.getTranslateInstance(playerPoint.x, playerPoint.y));            
        }

        // Simply paints the "area" that the player takes up when it's rotated and
        // translated
        protected void paintAutoTranslatedShape(Graphics2D g2d) {
            g2d.setColor(Color.DARK_GRAY);
            g2d.fill(rotatedAndTranslatedPlayer().getBounds2D());
        }

        // Uses a AffineTransform to translate and rotate the player
        protected void paintPlayer(Graphics2D g2d) {
            AffineTransform at = AffineTransform.getTranslateInstance(playerPoint.x, playerPoint.y);
            at.rotate(Math.toRadians(angle), player.getBounds2D().getCenterX(), player.getBounds2D().getCenterY());
            g2d.setTransform(at);
            g2d.setColor(Color.RED);
            g2d.fill(player);
            g2d.setColor(Color.BLACK);
            g2d.draw(player);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            paintAutoTranslatedShape(g2d);
            g2d.dispose();
            g2d = (Graphics2D) g.create();
            paintPlayer(g2d);
            g2d.dispose();
        }

    }
}


回答2:

In openGL we save transformation state using pushMatrix() and popMatrix(). Here their equivalents are Graphics.create() and Graphics.dispose()

Graphics g1 = g.create();
//do transformations
g1.dispose();

Graphics g2 = g.create();
//do other stuff
g2.dispose();