java - 根据设计分辨率生成当前屏幕分辨率的正确坐标

标签 java swing math resolution graphics2d

我有一个我无法完全理解的问题,因此我正在努力解决它。

基本上我正忙于为 Java Swing 编写一个小型游戏引擎,这个引擎的关键组件之一是能够将设计分辨率与屏幕分辨率分开。这意味着如果我设计一个分辨率为 400 (w) x 300 (h) 的游戏,并且我将一个对象放置在设计分辨率的中心,那么用户可以指定他们想要玩游戏的实际分辨率,例如800 (w) x 600 (h),对象仍将以当前分辨率正确放置在屏幕中心。

这是我遇到问题的地方,当设计分辨率和当前分辨率相同时,即设计分辨率为 400 x 300,当前分辨率为 400 x 300,对象似乎正确放置在屏幕中央启动并且子弹正确地位于玩家的中心,无论玩家移动时的位置如何:

enter image description here

然而,当设计分辨率和当前屏幕分辨率不同时,即设计分辨率为 400 x 300 而当前分辨率为 800 x 600 时,对象不再正确放置在屏幕中心,对于玩家来说子弹也不会居中:

enter image description here

我有一个方法可以为所有可见对象(红色引用点、 Sprite /玩家和子弹)生成中心生成点,这个方法是一个简单方便的方法来帮助生成一个中心容器内 Sprite 或另一个 Sprite 的基于坐标:

public static Point2D getCenterSpawnPoint(int parentWidth, int parentHeight, int childWidth, int childHeight, double childXOffset, double childYOffset) {
    double spawnX = ((parentWidth - childWidth) / 2) + childXOffset;
    double spawnY = ((parentHeight - childHeight) / 2) + childYOffset;
    return new Point2D.Double((int) spawnX, (int) spawnY);
}

Sprite 和子弹使用屏幕坐标渲染:

    public int getScreenX() {
        //return (int) (imageScaler.getWidthScaleFactor() * this.getX());
        return (int) ((double) this.getX() / DESIGN_SCREEN_SIZE.width * CURRENT_SCREEN_SIZE.width);
    }

    public int getScreenY() {
        //return (int) (imageScaler.getHeightScaleFactor() * this.getY());
        return (int) ((double) this.getY() / DESIGN_SCREEN_SIZE.height * CURRENT_SCREEN_SIZE.height);
    }

我不确定我哪里出错了,但基本上我想看到的是我的第一个 GIF 中的相同行为,无论游戏当前的屏幕尺寸如何,红色引用点似乎位置正确而且它是简单地绘制到 JPanel 并绕过 getScreen... 调用:

// lets draw a centered dot based on the panels dimensions for a reference
int dotSize = 10;
g2d.setColor(Color.red);
Point2D centeredReferencePoint = getCenterSpawnPoint(getWidth(), getHeight(), dotSize, dotSize, 0, 0);
g2d.fillOval((int) centeredReferencePoint.getX(), (int) centeredReferencePoint.getY(), dotSize, dotSize);

这是 minaml 可重现的例子:

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.*;

public class ResolutionIndependentLocationIssue {

    /**
     * uncommenting this and commenting the line below will result in the bullet
     * spawning correctly at the center of the sprite/player
     */
    private static final Dimension CURRENT_SCREEN_SIZE = new Dimension(800, 600);
    //private static final Dimension CURRENT_SCREEN_SIZE = new Dimension(400, 300);
    private static final Dimension DESIGN_SCREEN_SIZE = new Dimension(400, 300);
    
    private Scene scene;
    private Sprite player;

    public ResolutionIndependentLocationIssue() {
        try {
            createAndShowUI();
        } catch (IOException ex) {
            Logger.getLogger(ResolutionIndependentLocationIssue.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(ResolutionIndependentLocationIssue::new);
    }

    private void createAndShowUI() throws MalformedURLException, IOException {
        JFrame frame = new JFrame("Resolution Issue");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        BufferedImage bulletImage = resize(ImageIO.read(new URL("/image/JlSEL.png")), 20, 20);
        BufferedImage playerImage = resize(ImageIO.read(new URL("https://icons.iconarchive.com/icons/icons8/windows-8/512/Programming-Java-Duke-Logo-icon.png")), 100, 100);
        player = new Sprite(playerImage);
        player.setBulletImage(bulletImage);

        System.out.println();

        // center player according to our design resolution
        Point2D spawnPoint = getCenterSpawnPoint(DESIGN_SCREEN_SIZE.width, DESIGN_SCREEN_SIZE.height, playerImage.getWidth(), playerImage.getHeight(), 0, 0);
        player.setPosition((int) spawnPoint.getX(), (int) spawnPoint.getY());

        System.out.println("ResolutionScalingIssue#createAndShowUI() - Player spawn point (always expressed in design resolution co-ordinates): X: " + spawnPoint.getX() + " Y: " + spawnPoint.getY());
        System.out.println("ResolutionScalingIssue#createAndShowUI() - Player Design Resolution X: " + player.getX() + " Y: " + player.getY());
        System.out.println("ResolutionScalingIssue#createAndShowUI() - Player Screen X: " + player.getScreenX() + " Screen Y: " + player.getScreenY());
        System.out.println("ResolutionScalingIssue#createAndShowUI() - Player Width: " + playerImage.getWidth() + " Height: " + playerImage.getHeight());
        System.out.println();

        this.scene = new Scene();
        this.scene.add(player);

        this.addKeyBindings();

        frame.add(this.scene);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        Thread gameLoop = new Thread(() -> {
            while (true) {
                this.scene.update();
                this.scene.repaint();

                try {
                    Thread.sleep(15);
                } catch (InterruptedException ex) {
                }
            }
        });
        gameLoop.start();
    }

    private void addKeyBindings() {
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "A pressed");
        this.scene.getActionMap().put("A pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.LEFT = true;
            }
        });
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "A released");
        this.scene.getActionMap().put("A released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.LEFT = false;
            }
        });
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "D pressed");
        this.scene.getActionMap().put("D pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.RIGHT = true;
            }
        });
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "D released");
        this.scene.getActionMap().put("D released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.RIGHT = false;
            }
        });
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), "W pressed");
        this.scene.getActionMap().put("W pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.UP = true;
            }
        });
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "W released");
        this.scene.getActionMap().put("W released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.UP = false;
            }
        });
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "S pressed");
        this.scene.getActionMap().put("S pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.DOWN = true;
            }
        });
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "S released");
        this.scene.getActionMap().put("S released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.DOWN = false;
            }
        });
        this.scene.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false), "Space pressed");
        this.scene.getActionMap().put("Space pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player.shoot();
            }
        });
    }

    public static BufferedImage resize(BufferedImage image, int width, int height) {
        BufferedImage bi = new BufferedImage(width, height, BufferedImage.TRANSLUCENT);
        Graphics2D g2d = (Graphics2D) bi.createGraphics();
        g2d.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
        g2d.drawImage(image, 0, 0, width, height, null);
        g2d.dispose();
        return bi;
    }

    /**
     * Used to calculate the center based spawning point, to ensure calculations
     * are the same for the player spawning on the screen and bullet spawning
     * from the player
     *
     * @return
     */
    public static Point2D getCenterSpawnPoint(int parentWidth, int parentHeight, int childWidth, int childHeight, double childXOffset, double childYOffset) {
        double spawnX = ((parentWidth - childWidth) / 2) + childXOffset;
        double spawnY = ((parentHeight - childHeight) / 2) + childYOffset;
        return new Point2D.Double((int) spawnX, (int) spawnY);
    }

    public class Scene extends JPanel {

        private final ArrayList<Sprite> sprites;

        public Scene() {
            this.sprites = new ArrayList<>();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2d = (Graphics2D) g;

            sprites.forEach((sprite) -> {
                sprite.render(g2d);
            });

            // lets draw a centered dot based on the panels dimensions for a reference
            int dotSize = 10;
            g2d.setColor(Color.red);
            Point2D centeredReferencePoint = getCenterSpawnPoint(getWidth(), getHeight(), dotSize, dotSize, 0, 0);
            g2d.fillOval((int) centeredReferencePoint.getX(), (int) centeredReferencePoint.getY(), dotSize, dotSize);
        }

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

        @Override
        public boolean getIgnoreRepaint() {
            return true;
        }

        public void add(Sprite sprite) {
            sprite.setScence(this);
            this.sprites.add(sprite);
        }

        private void update() {
            sprites.forEach((sprite) -> {
                sprite.update();
            });
        }
    }

    public class Sprite {

        protected int x;
        protected int y;
        protected int speed = 5;
        protected final BufferedImage image;

        public boolean UP, DOWN, LEFT, RIGHT;
        private boolean isFlippedX = false;
        private Scene scene;
        private BufferedImage bulletImage;

        public Sprite(BufferedImage image) {
            this.image = image;
        }

        public void render(Graphics2D g2d) {
            // sprite is drawn based on the position of the current screen relative to our design screen size
            g2d.setColor(Color.red);
            g2d.drawRect(this.getScreenX(), this.getScreenY(), this.getWidth(), this.getHeight());

            if (this.isFlippedX) {
                // flip horizontally
                g2d.drawImage(this.image, this.getScreenX() + this.image.getWidth(), this.getScreenY(), -this.getWidth(), this.getHeight(), null);
            } else {
                g2d.drawImage(this.image, this.getScreenX(), this.getScreenY(), null);
            }
        }

        public void update() {
            if (LEFT) {
                setFlippedX(true);
                this.x -= this.speed;
            }
            if (RIGHT) {
                setFlippedX(false);
                this.x += this.speed;
            }
            if (UP) {
                this.y -= this.speed;
            }
            if (DOWN) {
                this.y += this.speed;
            }
        }

        public void setFlippedX(boolean isFlippedX) {
            this.isFlippedX = isFlippedX;
        }

        /**
         *
         * @return The current screen x co-ordindate of the sprite relative to
         * the design resolution
         */
        public int getScreenX() {
            //return (int) (imageScaler.getWidthScaleFactor() * this.getX());
            return (int) ((double) this.getX() / DESIGN_SCREEN_SIZE.width * CURRENT_SCREEN_SIZE.width);
        }

        /**
         *
         * @return The current screen y co-ordindate of the sprite relative to
         * the design resolution
         */
        public int getScreenY() {
            //return (int) (imageScaler.getHeightScaleFactor() * this.getY());
            return (int) ((double) this.getY() / DESIGN_SCREEN_SIZE.height * CURRENT_SCREEN_SIZE.height);
        }

        /**
         *
         * @return The design resolution x co-ordindate
         */
        public int getX() {
            return this.x;
        }

        /**
         *
         * @return The design resolution y co-ordindate
         */
        public int getY() {
            return this.y;
        }

        public int getWidth() {
            return this.image.getWidth();
        }

        public int getHeight() {
            return this.image.getHeight();
        }

        public void setPosition(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public void setBulletImage(BufferedImage bulletImage) {
            this.bulletImage = bulletImage;
        }

        public void shoot() {
            System.out.println("Sprite#shoot() - Player Design Resolution X: " + this.getX() + " Y: " + this.getY());
            System.out.println("Sprite#shoot() - Player Width: " + this.getWidth() + " Height: " + this.getHeight());

            /**
             * center the bullet according to the players design x and y
             * co-ordinates, this is necessary as x and y should the design
             * co-ordinates and render method will call getScreenX and
             * getScreenY to calculate the current screen resolution
             * co-ordinates
             *
             */
            Point2D spawnPoint = getCenterSpawnPoint(this.getWidth(), this.getHeight(), bulletImage.getWidth(), bulletImage.getHeight(), this.getX(), this.getY());
            Bullet bullet = new Bullet((int) spawnPoint.getX(), (int) spawnPoint.getY(), this.bulletImage);

            System.out.println("Sprite#shoot() - Bullet spawn point (always expressed in design resolution co-ordinates): X: " + spawnPoint.getX() + " Y: " + spawnPoint.getY());
            System.out.println("Sprite#shoot() - Bullet spawn: X: " + bullet.getX() + " Y: " + bullet.getY());
            System.out.println("Sprite#shoot() - Bullet spawn: Screen X: " + bullet.getScreenX() + " Screen Y: " + bullet.getScreenY());
            System.out.println();

            //bullet.LEFT = this.isFlippedX;
            //bullet.RIGHT = !this.isFlippedX;
            this.scene.add(bullet);
        }

        public void setScence(Scene scene) {
            this.scene = scene;
        }

    }

    public class Bullet extends Sprite {

        public Bullet(int x, int y, BufferedImage image) {
            super(image);
            this.x = x;
            this.y = y;
            this.speed = 10;
        }
    }

}

如有任何帮助,我们将不胜感激!

更新:

当使用@akuzminykh 的解决方案时,一切似乎都工作正常,但是,现在当我将玩家位置设置为 player.setPosition(0,0) 时,期望它出现在左上角,我得到这个:

enter image description here

这是有道理的,因为我假设我们现在通过位于 Sprite 中心的坐标进行定位,但是我将如何修复他的坐标以便左上角和中心的 setPosition 都可以工作,我想我可能需要修复 getCenterSpawnPoint

最佳答案

在您的方法 getScreenXgetScreenY 中,您忽略了 getXgetY 包括的宽度和高度 Sprite 。例如。 getX 不会为您提供 Sprite 在 x 轴上的中心位置,而是该位置减去 Sprite 宽度的一半。当您像在 getScreenX 中那样缩放它时,您还缩放了 Sprite 在 x 中的偏移量。要解决这个问题,只需先添加偏移量,然后进行缩放,最后减去偏移量即可。

/**
 *
 * @return The current screen x co-ordindate of the sprite relative to
 * the design resolution
 */
public int getScreenX() {
    //return (int) (imageScaler.getWidthScaleFactor() * this.getX());
    //return (int) ((double) this.getX() / DESIGN_SCREEN_SIZE.width * CURRENT_SCREEN_SIZE.width);
    double halfWidth = this.getWidth() / 2.0;
    double xCenterDesign = this.getX() + halfWidth;
    double xCenterCurrent = xCenterDesign / DESIGN_SCREEN_SIZE.width * CURRENT_SCREEN_SIZE.width;
    return (int) (xCenterCurrent - halfWidth);
}

/**
 *
 * @return The current screen y co-ordindate of the sprite relative to
 * the design resolution
 */
public int getScreenY() {
    //return (int) (imageScaler.getHeightScaleFactor() * this.getY());
    //return (int) ((double) this.getY() / DESIGN_SCREEN_SIZE.height * CURRENT_SCREEN_SIZE.height);
    double halfHeight = this.getHeight() / 2.0;
    double yCenterDesign = this.getY() + halfHeight;
    double yCenterCurrent = yCenterDesign / DESIGN_SCREEN_SIZE.height * CURRENT_SCREEN_SIZE.height;
    return (int) (yCenterCurrent - halfHeight);
}

或者更数学地说:

如果我们以“设计”分辨率为 400x300 的示例为例,“当前”分辨率为 800x600, Sprite 为 100x100 大: Sprite 的位置为 (150, 100),这是有道理的:(400/2 - 100/2、300/2 - 100/2)。现在你用来把它带到“当前”分辨率的公式(只适用于 x 因为我很懒):150/400 * 800 = 300。嗯,但是 800 的一半是 400,位置应该是 400 - 100/2?确切地说, Sprite 的偏移量 100/2 也进行了缩放,从 50 到 100,结果是 .. 400 - 100 = 300。

因此,最初添加偏移量,以便缩放中心。然后是:(150 + 50)/400 * 800 = 400。别忘了最后减去偏移量:400 - 50 = 350。现在你在 x 轴上的位置是正确的。

回复:更新:

当您想将 Sprite 放在左上角时,您可能希望 player.setPosition(0, 0) 来实现这一点。不是这种情况。按照您编写的方式,getXgetY 给出的坐标包括 Sprite 的宽度和高度,还记得吗?像 getScreenXgetScreenY 这样的方法,在我的修复中,考虑到这一点并用于在正确的位置渲染 Sprite 。这意味着坐标 (0, 0) 将中心位置描述为 (0 + 50, 0 + 50),其中 50 正好是 100/2, Sprite 的宽度和高度除以二。

要将 Sprite 放置在左上角,您需要在使用 setPosition 方法设置其位置时考虑 Sprite 的宽度和高度:在我们的示例中, Sprite 大小为 100x100,您需要传递 (0 - 100/2, 0 - 100/2),因此调用如下所示:player.setPosition(-50, -50)。当然,您可以使用 playerImage.getWidth() 等使其动态化。


建议:

我建议你让Spritexy相对于相应 Sprite 的中心。这将对代码进行一些必要的更改,但它也会简化其他事情并使它们更直观。例如。 player.setPosition(0, 0) 的问题将不存在,它实际上会将 Sprite 放在左上角,这正是您直觉所期望的。这也将简化 getScreenXgetScreenY。考虑在 render 方法中由 Sprite 的宽度和高度引起的偏移。这应该足够了。

关于java - 根据设计分辨率生成当前屏幕分辨率的正确坐标,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65973411/

相关文章:

java - 滚动条不适用于我的 JTextArea

java - CYK算法实现java

java - JFrame 的真正方法是什么,在 Linux 中最小化它

objective-c - 使用 CoreData 关系计算摘要

java - 如何在不将完整文件加载到内存的情况下将大文件插入 BLOB (Oracle)?

java - 拦截传出的 servlet http 请求

Java 图形绘制自定义圆形

java - 无法添加动态 jtextfields 并保存它们的值

math - Big O(logn) 是以 e 为底的对数吗?

javascript - 获取随机数并关注限制(抛物线)