【问题标题】:How to 'Get' a Specific Point From A Shape Object Derived from AffineTransform如何从 AffineTransform 派生的形状对象中“获取”特定点
【发布时间】:2017-06-05 14:22:14
【问题描述】:

作为一个自我项目,我正在尝试制作游戏“Asteroids”。

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

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

有没有办法从 Shape 对象中“获取”特定点?在这种情况下,该特定点是船的尖端。

主类:

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

    }

船级(不过我认为这无关紧要)

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");
    }
    */
}

激光课程(我认为这也无关紧要,但你去吧。)

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

    }


}

我在考虑是否可以将 Shape 对象 transformed 转回 Polygon 以理解这一点,但我不确定这将如何或是否可行。

【问题讨论】:

    标签: java shape affinetransform


    【解决方案1】:

    您可以使用AffineTransform.transform(Point2D, Point2D) 转换多边形上的单个点。

    如果不是尝试使用旋转变换来移动船,而是保留船所在位置的单个 (x,y) 位置,那么事情会简单得多。您将在move() 中移动船的位置,而不是尝试平移多边形。然后当你想画船时,例如做:

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

    要根据速度移动一个点,你可以这样做来找到移动向量:

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

    这本质上是从极坐标到笛卡尔坐标的转换。 x 和 y 速度是三角形的边,其斜边为speed,已知角度为angle

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

    在我的回答中有一个以这种方式进行移动的示例:https://stackoverflow.com/a/43692434/2891664


    对于您的 cmets:

    你是说,不像我最初的移动功能,只是为了让ship 保持一个点,因此我只会翻译它吗?

    或多或少,是的。你仍然有一个多边形来保持船的形状,但多边形上的点将相对于(0,0)

    假设有以下定义:

    • 多边形上的每个(x,y) 点都是p<sub>i</sub>。 (换句话说,p<sub>0</sub>p<sub>1</sub>p<sub>2</sub>p<sub>3</sub> 之一。)
    • 翻译的(x,y)坐标是T

    然后,翻译Graphics2D后,面板上的每个p<sub>i</sub>坐标变成p<sub>i</sub>+T。因此,如果您的多边形点是相对于(0,0) 定义的,那么平移到船的(locX,locY) 会将多边形移动到相对于(locX,locY) 的位置。

    最简单的方法是将多边形的尖端定义为(0,0),这样在平移后船的尖端就是船的位置:

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

    例如在同一个地方启动飞船,你可以将它的位置初始化为(800,400)


    我又在想这个问题,发现旋转有点复杂,因为你可能不想让船绕着尖端旋转。你可能想围绕它的中心旋转船。

    所以,这里有一个 MCVE 演示如何完成所有这些操作。

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

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

    1. 获取船的中心点,相对于(0,0)
    2. 获取从中心点到尖端的距离。提示位于(0,0),所以这只是中心的 y 坐标。
    3. 然后计算尖端相对于中心的(x,y) 位置。这与上图的速度和速度非常相似,但斜边是船中心和船头之间的距离。
    4. 将中心转换为相对于船的位置。
    5. 将尖端的位置(相对于中心)转换为相对于船的位置。

    也可以使用AffineTransform 完成,类似于您在问题中的代码中所做的,但您需要在每次更新时设置它。像这样的:

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

    您仍然可以使用变换来进行这种计算,但您不会因为依赖变换进行移动而感到奇怪。 (在问题的代码中,例如,船仅沿 y 轴移动。明显的侧向移动是由于一系列旋转串联造成的。)

    【讨论】:

    • 你是说,不像我最初的移动功能,只是为了让ship保持一个点,因此我只会翻译它吗?
    • 另外,我只是想知道如何找回船的“尖端”——我不一定需要翻译它。在画布周围移动和平移的实例是 Shape transformed,但没有任何方法可以让我获得船尖的特定坐标。一旦我得到“提示”,我就可以更改激光发射的位置和方式的坐标。
    • 我意识到旋转有问题,所以我添加了一个 MCVE,希望能有所帮助。
    • 好吧,我最终接受了您最初的建议/答案,并且效果很好,再次感谢。如果你想看看我最终做了什么,这里是我的 github 的链接:github.com/GoodJuan/Asteroids
    • 没问题!很高兴能提供帮助。
    猜你喜欢
    • 2023-03-09
    • 2010-10-16
    • 1970-01-01
    • 1970-01-01
    • 2012-07-27
    • 1970-01-01
    • 2018-05-26
    • 1970-01-01
    • 2022-08-15
    相关资源
    最近更新 更多