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

3
我有一个问题,我还没有完全理解,所以我很难解决它。
基本上,我正在编写一个小型的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

我有一种方法可以为所有可见对象(红色参考点、精灵/玩家和子弹)生成中心生成点。 这种方法是一种简单的便捷方法,用于帮助在容器或其他精灵中为精灵生成基于中心的坐标:
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 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);

这是最小可重现的示例:

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("https://istack.dev59.com/JlSEL.webp")), 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

我认为这是有道理的,因为我假设我们现在是通过坐标位于精灵中心来定位的,但如何修复使得setPosition对于左上角和中心点都起作用,我想我可能需要修复getCenterSpawnPoint


3
感谢您在提问时付出了这么多的努力。当问题像这样详细,且包含如此好的 MREs 时,回答起来其实很有趣。 - akuzminykh
1个回答

4
在你的方法getScreenXgetScreenY中,你忽略了getXgetY包括精灵的宽度和高度。例如,getX并不给出精灵在x轴上的中心位置,而是位置减去精灵宽度的一半。当你像在getScreenX中那样进行缩放时,也会缩放精灵在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的“当前”分辨率为例,并且精灵大小为100x100:精灵的位置是(150,100),这是有意义的:(400/2-100/2,300/2-100/2)。现在你用来将它带入“当前”分辨率的公式(因为我懒只针对x):150/400*800=300。嗯,但是800的一半是400,位置应该是400-100/2吗?没错,精灵的偏移量100/2也被缩放了,从50变成了100,结果是... 400-100 = 300。
因此,在最初添加偏移量时,以便缩放中心。然后就是:(150 + 50)/ 400 * 800 = 400。不要忘记最后减去偏移量:400-50 = 350。现在你有了x轴上的正确位置。
关于更新:
当你想把精灵放在左上角时,你可能期望player.setPosition(0,0)能够解决问题。但情况并非如此。根据你的写法,getXgetY给出的坐标包括精灵的宽度和高度,记得吗?使用我的修正方法的方法,如getScreenXgetScreenY考虑到这一点,并用于以正确的位置呈现精灵。这意味着坐标(0,0)描述中心处于(0 + 50,0 + 50)的位置,其中50只是100/2,即精灵宽度和高度除以二。
要将精灵放置在左上角,必须在使用setPosition方法设置其位置时考虑精灵的宽度和高度:在我们的示例中,精灵大小为100x100,因此需要传递(0-100 / 2,0-100 / 2),因此调用看起来像这样:player.setPosition(-50,-50)。当然,您可以通过使用 playerImage.getWidth()等使其动态化。

我建议将Spritexy设为相对于相应精灵的中心点。这会使得一些代码需要做出改变,但也会简化其他事情并使它们更加直观易懂。例如:player.setPosition(0, 0) 的问题将不复存在,实际上它将把精灵放在左上角,正是您预期的位置。这也会简化 getScreenXgetScreenY。请考虑在render方法中由精灵宽度和高度引起的偏移量。以上就足够了。


谢谢,这非常完美,而且运行得很好!请查看我的问题更新,当我尝试像 player.setPosition(0,0) 这样的东西时,我期望玩家会出现在左上角,现在它确实是这样的,但距离左上角有相当大的空间。所以我猜想也许应该在 getCenterSpawnPoint 中修复 getScreen... 的问题? - David Kroukamp
@DavidKroukamp 这些方法都不需要更改。将精灵放置在左上角的getPosition参数为(-50, -50)。请参见编辑。 - akuzminykh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接