如何从AffineTransform派生的形状对象中“获取”特定点 [英] How to 'Get' a Specific Point From A Shape Object Derived from AffineTransform

查看:164
本文介绍了如何从AffineTransform派生的形状对象中“获取”特定点的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

作为一个自我项目,我正试图制作游戏小行星。



目前,我一直试图弄清楚如何制作它,以便从我的船上发射的激光从提示出现船。到目前为止,我已经尝试过使用 Shape 对象的 .getBounds2D()。getX()方法,但是因为 getBounds2D()在多边形周围绘制一个矩形,激光最终出现在我的Polygon 船周围的假想盒子的角落里code>。



  package mcve.game; 

import javax.swing。*;
import java.awt.event。*;
import java.awt.geom。*;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.GraphicsConfiguration;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;

公共类MovementExample实现ActionListener {
public static void main(String [] args){
SwingUtilities.invokeLater(MovementExample :: new);
}

final int fps = 60;
final int period = 1000 / fps;

最终的JFrame框架;
最终GamePanel面板;
final控制控件;
final船舶;

final List< Bullet> bullets = new ArrayList<>();

MovementExample(){
frame = new JFrame(Movement Example);

尺寸大小= getMaximumWindowSize(frame);
size.width / = 2;
size.height / = 2;
frame.setPreferredSize(size);

panel = new GamePanel();
frame.setContentPane(panel);

frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);

controls = new Controls();

ship = new Ship(panel.getWidth()/ 2,
panel.getHeight()/ 2);

new Timer(句号,此).start();
}

@Override
public void actionPerformed(ActionEvent e){
double secondsElapsed = 1.0 / fps;
ship.update(secondsElapsed);

bullets.forEach(b - > b.update(secondsElapsed));
Rectangle bounds = panel.getBounds();
bullets.removeIf(b - >!bounds.contains(b.locX,b.locY));

panel.repaint();
}

类GamePanel扩展JPanel {
GamePanel(){
setBackground(Color.WHITE);
}

@Override
protected void paintComponent(Graphics g){
super.paintComponent(g);
Graphics2D g2 =(Graphics2D)g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

if(ship!= null){
ship.draw(g2);
}
bullets.forEach(b - > b.draw(g2));

g2.dispose();
}
}

抽象类AbstractGameObject {
double maxSpeed;
double rotationAngle;
double locX;
double locY;
double velX;
double velY;

AbstractGameObject(double initialX,double initialY){
locX = initialX;
locY = initialY;
}

abstract void update(double secondsElapsed);
abstract void draw(Graphics2D g2);
}

class Ship extends AbstractGameObject {
Polygon shape;
double rotationRate;

发货(double initialX,double initialY){
super(initialX,initialY);
maxSpeed = 128; // pixels / second
rotationAngle = Math.PI * 3/2;
rotationRate =(2 * Math.PI)/ 2; // radians / second

int xPoints [] = {0,-20,0,20};
int yPoints [] = {0,60,40,60};
shape = new Polygon(xPoints,yPoints,4);
}

Point2D.Double getTip(){
Point2D.Double center = getCenter();
//提示位于(0,0)并且它已经在x轴原点上居中
//,因此从
//提示到中心的距离只是中心.Y。
double distance = center.y;
//然后找到小费的位置,相对
//到中心。
double tipX = distance * Math.cos(rotationAngle);
double tipY = distance * Math.sin(rotationAngle);
//现在找到中心的实际位置。
center.x + = locX;
center.y + = locY;
//并将小费的实际位置(相对
//)返回到中心的实际位置。
返回新的Point2D.Double(tipX + center.x,tipY + center.y);
}

Point2D.Double getCenter(){
//返回船舶的中心点,
//相对于(0,0)。
Point2D.Double center = new Point2D.Double();
for(int i = 0; i< shape.npoints; ++ i){
center.x + = shape.xpoints [i];
center.y + = shape.ypoints [i];
}
center.x / = shape.npoints;
center.y / = shape.npoints;
返回中心;
}

@Override
void update(double secondsElapsed){
//在此处查看我的答案:https://stackoverflow.com/a/43692434/2891664
//讨论为什么这种逻辑就是这样的。
倍速= 0;
if(controls.isUpHeld()){
speed + = maxSpeed;
}
if(controls.isDownHeld()){
speed - = maxSpeed;
}
velX = speed * Math.cos(rotationAngle);
velY = speed * Math.sin(rotationAngle);
locX + = secondsElapsed * velX;
locY + = secondsElapsed * velY;

double rotation = 0;
if(controls.isLeftHeld()){
rotation - = rotationRate;
}
if(controls.isRightHeld()){
rotation + = rotationRate;
}
rotationAngle + = secondsElapsed * rotation;
//限制角度以便它永远不会例如得到这么多b $ b //大到它失去了精度。
if(rotationAngle> 2 * Math.PI){
rotationAngle - = 2 * Math.PI;
}

if(controls.isFireHeld()){
Point2D.Double tipLoc = getTip();
Bullet bullet = new Bullet(tipLoc.x,tipLoc.y,rotationAngle);
bullets.add(bullet);
}
}

@Override
void draw(Graphics2D g2){
Graphics2D copy =(Graphics2D)g2.create();
copy.setColor(Color.RED);

//转换到船的位置。
copy.translate(locX,locY);
//围绕中心旋转船只。
Point2D.Double center = getCenter();
// PI / 2偏移是必要的,因为
//多边形点是用船舶
//已经垂直定义的,即以-PI / 2的角度定义的。
copy.rotate(rotationAngle +(Math.PI / 2),center.x,center.y);

copy.fill(shape);
}
}

class Bullet扩展AbstractGameObject {
Ellipse2D.Double shape = new Ellipse2D.Double();

Bullet(double initialX,double initialY,double initialRotation){
super(initialX,initialY);
maxSpeed = 512;
rotationAngle = initialRotation;
velX = maxSpeed * Math.cos(rotationAngle);
velY = maxSpeed * Math.sin(rotationAngle);

double radius = 3;
shape.setFrame(-radius,-radius,2 * radius,2 * radius);
}

@Override
void update(double secondsElapsed){
locX + = secondsElapsed * velX;
locY + = secondsElapsed * velY;
}

@Override
void draw(Graphics2D g2){
Graphics2D copy =(Graphics2D)g2.create();
copy.setColor(Color.BLACK);
copy.translate(locX,locY);
copy.fill(shape);
}
}

//请参阅https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html
class Controls {
final Set< Integer> keysHeld = new HashSet<>();

Controls(){
bind(KeyEvent.VK_A,left);
bind(KeyEvent.VK_D,right);
bind(KeyEvent.VK_W,up);
bind(KeyEvent.VK_S,down);
bind(KeyEvent.VK_SPACE,fire);
}

boolean isLeftHeld(){return keysHeld.contains(KeyEvent.VK_A); }
boolean isRightHeld(){return keysHeld.contains(KeyEvent.VK_D); }
boolean isUpHeld(){return keysHeld.contains(KeyEvent.VK_W); }
boolean isDownHeld(){return keysHeld.contains(KeyEvent.VK_S); }
boolean isFireHeld(){return keysHeld.contains(KeyEvent.VK_SPACE); }

void bind(int keyCode,String name){
bind(keyCode,name,true);
bind(keyCode,name,false);
}

void bind(int keyCode,String name,boolean isOnRelease){
KeyStroke stroke = KeyStroke.getKeyStroke(keyCode,0,isOnRelease);
name + = isOnRelease? .released:。press;
panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(stroke,name);
panel.getActionMap()
.put(name,new AbstractAction(){
@Override
public void actionPerformed(ActionEvent e){
if(isOnRelease){
keysHeld.remove(keyCode);
} else {
keysHeld.add(keyCode);
}
}
});
}
}

//这将返回JFrame所在的
//显示的可用大小,如下所述:
// http://docs.oracle.com/javase/8/docs/api/java/awt/GraphicsEnvironment.html#getMaximumWindowBounds--
static Dimension getMaximumWindowSize(JFrame frame){
GraphicsConfiguration config = frame。 getGraphicsConfiguration();
Dimension size = config.getBounds()。getSize();
Insets insets = Toolkit.getDefaultToolkit()。getScreenInsets(config);
size.width - = insets.left + insets.right;
size.height - = insets.top + insets.bottom;
返回大小;
}
}

船的尖端还有其他方式可以计算,但我在MCVE中的方式是:


  1. 获取船的中心点,相对于(0,0)

  2. 获取从中心点到尖端的距离。提示位于(0,0)所以这只是中心的y坐标。

  3. 然后计算(x,y)小费相对于中心的位置。这与上图中的速度和速度非常相似,但斜边是中心与船尖之间的距离。

  4. 将中心翻译成相对的

  5. 将尖端的位置(相对于中心)翻译为相对于船舶的位置。

它也可以用 AffineTransform 完成,类似于你在问题代码中所做的,但你' d在每次更新时设置它。这样的事情:

  AffineTransform transform = new AffineTransform(); 

@Override
void update(double secondsElapsed){
...
//清除之前的翻译和轮换。
transform.setToIdentity();
//设置为当前。
transform.translate(locX,locY);
Point2D.Double center = getCenter();
transform.rotate(rotationAngle +(Math.PI / 2),center.x,center.y);

if(controls.isFireHeld()){
Point2D.Double tip = new Point2D.Double(0,0);
transform.transform(tip,tip);
Bullet bullet = new Bullet(tip.x,tip.y,rotationAngle);
bullets.add(bullet);
}
}

您仍然可以使用转换来进行计算但是你不会因为运动的变换而产生任何陌生感。 (在问题的代码中,例如,船舶只沿着y轴移动。明显的侧向移动是由于一系列旋转连接造成的。)


As a self-project, I'm trying to make the game 'Asteroids'.

Currently, I'm stuck on trying to figure out how to make it so the lasers fired from my ship appear from the tip of the ship. So far, I've tried experimenting with using the Shape object's .getBounds2D().getX() methods, but because getBounds2D() draws a rectangle around the polygon, the lasers end up appearing from the corner of the imaginary 'box' around my Polygon ship.

Here's a gif of what I have so far.

Is there a way to 'get' a specific point from a Shape object; where, in this case, that specific point is the tip of the ship.

Main Class:

public class AsteroidGame implements ActionListener, KeyListener{

    public static AsteroidGame game;
    public Renderer renderer;

    public boolean keyDown = false;
    public int playerAngle = 0;

    public boolean left = false;
    public boolean right = false;
    public boolean go = false;
    public boolean back = false;
    public boolean still = true;
    public double angle = 0;
    public int turnRight = 5;
    public int turnLeft = -5;

    public Shape transformed;

    public ArrayList<Laser> lasers;
    public ArrayList<Shape> transformedLasers;

    public final int WIDTH = 1400;
    public final int HEIGHT = 800;

    public Ship ship;
    public Rectangle shipHead;
    public Shape shipHeadTrans;
    public Point headPoint;


    public AffineTransform transform = new AffineTransform();
    public AffineTransform lasTransform = new AffineTransform();
    public AffineTransform headTransform = new AffineTransform();

    public AsteroidGame(){
        JFrame jframe = new JFrame();
        Timer timer = new Timer(20, this);
        renderer = new Renderer();

        jframe.add(renderer);
        jframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jframe.setSize(WIDTH, HEIGHT);
        jframe.setVisible(true);
        jframe.addKeyListener(this);
        jframe.setResizable(false);

        int xPoints[] = {800, 780, 800, 820};
        int yPoints[] = {400, 460, 440, 460}; 

        //(800, 400) is the initial location of the 'tip' of the ship'.
        headPoint = new Point(800, 400);

        lasers = new ArrayList<Laser>();
        transformedLasers = new ArrayList<Shape>();

        ship = new Ship(xPoints, yPoints, 4, 0);
        transformed = transform.createTransformedShape(ship);

        shipHead = new Rectangle(headPoint);
        shipHeadTrans = transform.createTransformedShape(shipHead);
        //shipHeadTrans.getBounds2D().

        timer.start();

    }

    public void repaint(Graphics g){

        g.setColor(Color.BLACK);
        g.fillRect(0, 0, WIDTH, HEIGHT);

        Graphics2D g2d = (Graphics2D)g;

        //drawing the ship
        g2d.setColor(Color.WHITE);
        g2d.draw(transformed);

        //drawing lasers
        g2d.setColor(Color.RED);
        for (int i = 0; i < transformedLasers.size(); i++){
            System.out.println(i);
            g2d.draw(transformedLasers.get(i));
        }



    }



    public void actionPerformed(ActionEvent arg0) {
        // TODO Auto-generated method stub

        /*The for if and else if statements are just to send the ship
         * to the other side of the canvas if it ever leaves the screen
         */
        if (transformed.getBounds2D().getMinX() > WIDTH){
            double tempAng = ship.getAng();
            double diff = 90-tempAng;

            transform.rotate(Math.toRadians(diff), ship.getCenterX(), ship.getCenterY());
            transform.translate(0,WIDTH);
            transform.rotate(Math.toRadians(-diff), ship.getCenterX(), ship.getCenterY());

        }

        else if (transformed.getBounds2D().getX() < 0){
            double tempAng = ship.getAng();
            double diff = 90-tempAng;
            transform.rotate(Math.toRadians(diff), ship.getCenterX(), ship.getCenterY());
            transform.translate(0,-WIDTH);
            transform.rotate(Math.toRadians(-diff), ship.getCenterX(), ship.getCenterY());
        }

        else if (transformed.getBounds2D().getY() > HEIGHT){
            double tempAng = ship.getAng();
            double diff = 180-tempAng;
            transform.rotate(Math.toRadians(diff), ship.getCenterX(), ship.getCenterY());
            transform.translate(0,HEIGHT);
            transform.rotate(Math.toRadians(-diff), ship.getCenterX(), ship.getCenterY());
        }

        else if (transformed.getBounds2D().getY() < 0){
            double tempAng = ship.getAng();
            double diff = 180-tempAng;
            transform.rotate(Math.toRadians(diff), ship.getCenterX(), ship.getCenterY());
            transform.translate(0,-HEIGHT);
            transform.rotate(Math.toRadians(-diff), ship.getCenterX(), ship.getCenterY());
        }


        if (right){
            ship.right();
            //rotating the ship
            transform.rotate(Math.toRadians(turnRight), ship.getCenterX(), ship.getCenterY());
            //rotating the 'tip' of the ship.
            headTransform.rotate(Math.toRadians(turnRight), ship.getCenterX(), ship.getCenterY());
        }

        else if (left){
            ship.left(); 
            //rotating the ship
            transform.rotate(Math.toRadians(turnLeft), ship.getCenterX(), ship.getCenterY());
            //rotating the 'tip' of the ship
            headTransform.rotate(Math.toRadians(turnLeft), ship.getCenterX(), ship.getCenterY());
        }
        if (go){
            ship.go();
        }
        else if (back){
            ship.reverse();
        }

        //moving and shaping each individual laser that had been shot
        for (int i = 0; i < transformedLasers.size(); i++){
            lasers.get(i).move();

            lasTransform = new AffineTransform();
            lasTransform.rotate(Math.toRadians(lasers.get(i).getAng()), transformed.getBounds2D().getX(), transformed.getBounds2D().getY());
            transformedLasers.set(i, lasTransform.createTransformedShape(lasers.get(i)));

        }

        //moving the ship
        ship.move();

        //moving the 'tip'
        shipHead.y -= ship.getSpeed();

        transformed = transform.createTransformedShape(ship);
        shipHeadTrans = headTransform.createTransformedShape(shipHead);


        renderer.repaint();

    }

    //defining a new laser
    public void fireLaser(){
        Laser tempLaser = new Laser((int)transformed.getBounds2D().getX(), (int)transformed.getBounds2D().getY(), 5, 10, ship.getAng());
        lasers.add(tempLaser);

        lasTransform = new AffineTransform();
        lasTransform.rotate(Math.toRadians(ship.getAng()), transformed.getBounds2D().getX(), transformed.getBounds2D().getY());
        transformedLasers.add(lasTransform.createTransformedShape(tempLaser));

    }

    public static void main(String[] args){
        game = new AsteroidGame();
    }

    @Override
    public void keyPressed(KeyEvent e) {
        // TODO Auto-generated method stub
        if (e.getKeyCode() == KeyEvent.VK_RIGHT){
            right = true;
            keyDown = true;
        }else if (e.getKeyCode() == KeyEvent.VK_LEFT){
            left = true;
            keyDown = true;
        }

        else if (e.getKeyCode() == KeyEvent.VK_UP){
            go = true;
        }
        else if (e.getKeyCode() == KeyEvent.VK_DOWN){
            back = true;
        }

        //fire laser
        if (e.getKeyCode() == KeyEvent.VK_SPACE){
            fireLaser();
        }

    }

    @Override
    public void keyReleased(KeyEvent e) {
        // TODO Auto-generated method stub
        if (e.getKeyCode() == KeyEvent.VK_RIGHT){
            right = false;
        }
        if (e.getKeyCode() == KeyEvent.VK_LEFT){
            left = false;
        }
        if (e.getKeyCode() == KeyEvent.VK_UP){
            go = false;
        }
        if (e.getKeyCode() == KeyEvent.VK_DOWN){
            back = false;
        }
        still = true;
        keyDown = false;
    }

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

    }

Ship Class (I don't think it's relevant though)

package asteroidGame;

import java.awt.Polygon;
import java.util.Arrays;

public class Ship extends Polygon{

    /**
     * 
     */
    private double currSpeed = 0;

    private static final long serialVersionUID = 1L;
    public double angle;
    public int[] midX;
    public int[] midY;

    public Ship(int[] x, int[] y, int points, double angle){
        super(x, y, points);
        midX = x;
        midY = y;

        this.angle= angle;
    }


    public void right(){
        angle += 5;
    }
    public void left(){
        angle -= 5;
    }

    public void move(){
        for (int i = 0; i < super.ypoints.length; i++){
            super.ypoints[i] -= currSpeed;
            //System.out.println(super.ypoints[i]);
            //System.out.println(super.xpoints[i]);
        }
        //System.out.println(Arrays.toString(super.ypoints));



    }
    public double getSpeed(){
        return currSpeed;
    }


    public void reverse(){
        if (currSpeed  > -15) currSpeed -= 0.2;
    }

    public void go(){
        if (currSpeed < 25) currSpeed += 0.5;

    }

    public int getCenterX(){
        return super.xpoints[2];
    }
    public int getCenterY(){
        return super.ypoints[2];
    }

    public double getAng(){
        return angle;
    }
    public void test(){
        for (int x = 0; x < super.ypoints.length; x++){
            super.ypoints[x] += 1000;
        }
    }

    /*
    public void decrement(){
        if(currSpeed == 0){}

        else if (currSpeed > 0 && currSpeed < 15){
            currSpeed -= 0.05;

        }
        else if (currSpeed < 0 && currSpeed > -15){
            currSpeed += 0.05;

        }
        System.out.println("losing speed");
    }
    */
}

Laser Class (I don't think this is relevant either, but here ya go.)

package asteroidGame;

import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.Rectangle2D;

public class Laser extends Rectangle{

    private double angle;

    public Laser(int x, int y , int width, int height, double ang){
        super(x, y, width, height);
        angle = ang;
        Rectangle tst = new Rectangle(); 


    }

    public void move(){
        super.y -= 35;
    }

    public double getAng(){
        return angle;
    }

    public boolean intersects (Rectangle2D r){
        //if intersects
        if (super.intersects(r)){
            return true;
        }
        else{
            return false;
        }

    }


}

I was thinking of maybe turning the the Shape object transformed back into a Polygon to get the point, but I'm not sure how or if that would work.

解决方案

You can use AffineTransform.transform(Point2D, Point2D) to transform a single point on your polygon.

Things would be a lot simpler for you if instead of trying to move the ship by using a rotation transform you kept a single (x,y) location of where the ship is. You'd move the ship's location in move() instead of trying to translate the polygon. Then when you want to paint the ship you e.g. do:

// Optionally copying the Graphics so the
// transform doesn't affect later painting.
Graphics2D temp = (Graphics2D) g2d.create();
temp.translate(ship.locX, ship.locY);
temp.rotate(ship.angle);
temp.draw(ship);

To move a point based on speed you can do this to find the movement vector:

double velX = speed * Math.cos(angle);
double velY = speed * Math.sin(angle);
locX += timeElapsed * velX;
locY += timeElapsed * velY;

That is essentially a conversion from polar to Cartesian coordinates. The x and y velocities are the legs of a triangle whose hypotenuse is speed and whose known angle is angle:

             /|
            / |
           /  |
          /   |
   speed /    |
        /     |
       /      |velY
      / angle |
     /)_______|
         velX

There's an example of doing movement this way in an answer of mine here: https://stackoverflow.com/a/43692434/2891664.


For your comments:

Are you saying that, unlike my initial move function, just to make ship hold a single point, and thus I would only translate that instead?

More or less, yes. You'd still have a polygon to hold the ship's shape, but the points on the polygon would be relative to (0,0).

Suppose the following definitions:

  • Each (x,y) point on the polygon is pi. (In other words, one of p0, p1, p2 and p3.)
  • The (x,y) coordinates of the translation are T

Then, after translating the Graphics2D, each pi coordinate becomes pi+T on the panel. So if your polygon points are defined relative to (0,0) then translating to the ship's (locX,locY) will move the polygon to a location relative to (locX,locY).

It could be simplest then to define the point which is the tip of the polygon as being (0,0) so that after the translation the tip of the ship is the ship's location:

// Your original points:
int xPoints[] = {800, 780, 800, 820};
int yPoints[] = {400, 460, 440, 460}; 
// Become these points relative to (0,0):
int xPoints[] = {0, -20, 0, 20};
int yPoints[] = {0, 60, 40, 60};

And to e.g. start the ship in the same place, you would initialize its location to (800,400).


I was thinking about this again and realized the rotation is a little more complicated, because you probably don't want to rotate the ship around the tip. You probably want to rotate the ship around its center.

So, here's an MCVE demonstrating how to do all of this.

package mcve.game;

import javax.swing.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.GraphicsConfiguration;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;

public class MovementExample implements ActionListener {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(MovementExample::new);
    }

    final int fps    = 60;
    final int period = 1000 / fps;

    final JFrame    frame;
    final GamePanel panel;
    final Controls  controls;
    final Ship      ship;

    final List<Bullet> bullets = new ArrayList<>();

    MovementExample() {
        frame = new JFrame("Movement Example");

        Dimension size = getMaximumWindowSize(frame);
        size.width  /= 2;
        size.height /= 2;
        frame.setPreferredSize(size);

        panel = new GamePanel();
        frame.setContentPane(panel);

        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        controls = new Controls();

        ship = new Ship(panel.getWidth()  / 2,
                        panel.getHeight() / 2);

        new Timer(period, this).start();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        double secondsElapsed = 1.0 / fps;
        ship.update(secondsElapsed);

        bullets.forEach(b -> b.update(secondsElapsed));
        Rectangle bounds = panel.getBounds();
        bullets.removeIf(b -> !bounds.contains(b.locX, b.locY));

        panel.repaint();
    }

    class GamePanel extends JPanel {
        GamePanel() {
            setBackground(Color.WHITE);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2 = (Graphics2D) g.create();
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                RenderingHints.VALUE_ANTIALIAS_ON);

            if (ship != null) {
                ship.draw(g2);
            }
            bullets.forEach(b -> b.draw(g2));

            g2.dispose();
        }
    }

    abstract class AbstractGameObject {
        double maxSpeed;
        double rotationAngle;
        double locX;
        double locY;
        double velX;
        double velY;

        AbstractGameObject(double initialX, double initialY) {
            locX = initialX;
            locY = initialY;
        }

        abstract void update(double secondsElapsed);
        abstract void draw(Graphics2D g2);
    }

    class Ship extends AbstractGameObject {
        Polygon shape;
        double  rotationRate;

        Ship(double initialX, double initialY) {
            super(initialX, initialY);
            maxSpeed      = 128; // pixels/second
            rotationAngle = Math.PI * 3 / 2;
            rotationRate  = (2 * Math.PI) / 2; // radians/second

            int xPoints[] = {0, -20, 0, 20};
            int yPoints[] = {0, 60, 40, 60};
            shape = new Polygon(xPoints, yPoints, 4);
        }

        Point2D.Double getTip() {
            Point2D.Double center = getCenter();
            // The tip is at (0,0) and it's already centered
            // on the x-axis origin, so the distance from the
            // tip to the center is just center.y.
            double distance = center.y;
            // Then find the location of the tip, relative
            // to the center.
            double tipX = distance * Math.cos(rotationAngle);
            double tipY = distance * Math.sin(rotationAngle);
            // Now find the actual location of the center.
            center.x += locX;
            center.y += locY;
            // And return the actual location of the tip, relative
            // to the actual location of the center.
            return new Point2D.Double(tipX + center.x, tipY + center.y);
        }

        Point2D.Double getCenter() {
            // Returns the center point of the ship,
            // relative to (0,0).
            Point2D.Double center = new Point2D.Double();
            for (int i = 0; i < shape.npoints; ++i) {
                center.x += shape.xpoints[i];
                center.y += shape.ypoints[i];
            }
            center.x /= shape.npoints;
            center.y /= shape.npoints;
            return center;
        }

        @Override
        void update(double secondsElapsed) {
            // See my answer here: https://stackoverflow.com/a/43692434/2891664
            // for a discussion of why this logic is the way it is.
            double speed = 0;
            if (controls.isUpHeld()) {
                speed += maxSpeed;
            }
            if (controls.isDownHeld()) {
                speed -= maxSpeed;
            }
            velX  = speed * Math.cos(rotationAngle);
            velY  = speed * Math.sin(rotationAngle);
            locX += secondsElapsed * velX;
            locY += secondsElapsed * velY;

            double rotation = 0;
            if (controls.isLeftHeld()) {
                rotation -= rotationRate;
            }
            if (controls.isRightHeld()) {
                rotation += rotationRate;
            }
            rotationAngle += secondsElapsed * rotation;
            // Cap the angle so it can never e.g. get so
            // large that it loses precision.
            if (rotationAngle > 2 * Math.PI) {
                rotationAngle -= 2 * Math.PI;
            }

            if (controls.isFireHeld()) {
                Point2D.Double tipLoc = getTip();
                Bullet bullet = new Bullet(tipLoc.x, tipLoc.y, rotationAngle);
                bullets.add(bullet);
            }
        }

        @Override
        void draw(Graphics2D g2) {
            Graphics2D copy = (Graphics2D) g2.create();
            copy.setColor(Color.RED);

            // Translate to the ship's location.
            copy.translate(locX, locY);
            // Rotate the ship around its center.
            Point2D.Double center = getCenter();
            // The PI/2 offset is necessary because the
            // polygon points are defined with the ship
            // already vertical, i.e. at an angle of -PI/2.
            copy.rotate(rotationAngle + (Math.PI / 2), center.x, center.y);

            copy.fill(shape);
        }
    }

    class Bullet extends AbstractGameObject {
        Ellipse2D.Double shape = new Ellipse2D.Double();

        Bullet(double initialX, double initialY, double initialRotation) {
            super(initialX, initialY);
            maxSpeed      = 512;
            rotationAngle = initialRotation;
            velX          = maxSpeed * Math.cos(rotationAngle);
            velY          = maxSpeed * Math.sin(rotationAngle);

            double radius = 3;
            shape.setFrame(-radius, -radius, 2 * radius, 2 * radius);
        }

        @Override
        void update(double secondsElapsed) {
            locX += secondsElapsed * velX;
            locY += secondsElapsed * velY;
        }

        @Override
        void draw(Graphics2D g2) {
            Graphics2D copy = (Graphics2D) g2.create();
            copy.setColor(Color.BLACK);
            copy.translate(locX, locY);
            copy.fill(shape);
        }
    }

    // See https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html
    class Controls {
        final Set<Integer> keysHeld = new HashSet<>();

        Controls() {
            bind(KeyEvent.VK_A, "left");
            bind(KeyEvent.VK_D, "right");
            bind(KeyEvent.VK_W, "up");
            bind(KeyEvent.VK_S, "down");
            bind(KeyEvent.VK_SPACE, "fire");
        }

        boolean isLeftHeld()  { return keysHeld.contains(KeyEvent.VK_A); }
        boolean isRightHeld() { return keysHeld.contains(KeyEvent.VK_D); }
        boolean isUpHeld()    { return keysHeld.contains(KeyEvent.VK_W); }
        boolean isDownHeld()  { return keysHeld.contains(KeyEvent.VK_S); }
        boolean isFireHeld()  { return keysHeld.contains(KeyEvent.VK_SPACE); }

        void bind(int keyCode, String name) {
            bind(keyCode, name, true);
            bind(keyCode, name, false);
        }

        void bind(int keyCode, String name, boolean isOnRelease) {
            KeyStroke stroke = KeyStroke.getKeyStroke(keyCode, 0, isOnRelease);
            name += isOnRelease ? ".released" : ".pressed";
            panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
                 .put(stroke, name);
            panel.getActionMap()
                 .put(name, new AbstractAction() {
                     @Override
                     public void actionPerformed(ActionEvent e) {
                         if (isOnRelease) {
                             keysHeld.remove(keyCode);
                         } else {
                             keysHeld.add(keyCode);
                         }
                     }
                 });
        }
    }

    // This returns the usable size of the display which
    // the JFrame resides in, as described here:
    // http://docs.oracle.com/javase/8/docs/api/java/awt/GraphicsEnvironment.html#getMaximumWindowBounds--
    static Dimension getMaximumWindowSize(JFrame frame) {
        GraphicsConfiguration config = frame.getGraphicsConfiguration();
        Dimension size   = config.getBounds().getSize();
        Insets    insets = Toolkit.getDefaultToolkit().getScreenInsets(config);
        size.width  -= insets.left + insets.right;
        size.height -= insets.top  + insets.bottom;
        return size;
    }
}

There are other ways the tip of the ship could be calculated, but the way I did it in the MCVE is this:

  1. Get the center point of the ship, relative to (0,0).
  2. Get the distance from the center point to the tip. The tip is at (0,0) so this is just the y-coordinate of the center.
  3. Then calculate the (x,y) location of the tip, relative to the center. This is done in a very similar way to the figure above for speed and velocity, but the hypotenuse is the distance between the center and the tip of the ship.
  4. Translate the center to be relative to the ship's location.
  5. Translate the location of the tip (which is relative to the center) to be relative to the ship's location.

It could also all be done with an AffineTransform, similar to what you are doing in the code in the question, but you'd set it on every update. Something like this:

AffineTransform transform = new AffineTransform();

@Override
void update(double secondsElapsed) {
    ...
    // Clear the previous translation and rotation.
    transform.setToIdentity();
    // Set to current.
    transform.translate(locX, locY);
    Point2D.Double center = getCenter();
    transform.rotate(rotationAngle + (Math.PI / 2), center.x, center.y);

    if (controls.isFireHeld()) {
        Point2D.Double tip = new Point2D.Double(0, 0);
        transform.transform(tip, tip);
        Bullet bullet = new Bullet(tip.x, tip.y, rotationAngle);
        bullets.add(bullet);
    }
}

You could still use a transform to do calculations that way, but you don't end up with any strangeness from depending on the transform for movement. (In the code in the question, the ship is e.g. only ever moved along the y-axis. The apparent sideways movement is due to the series of rotation concatenations.)

这篇关于如何从AffineTransform派生的形状对象中“获取”特定点的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆