Generating correct co-ordinates of current screen resolution based off of design resolution

Viewed 245

I have a problem that I haven't been able to completely understand and thus I am struggling to fix it.

Basically I am busy writing a small game engine for Java Swing, and one of the key components of this engine is the ability to separate design resolution from screen resolution. Meaning if I design a game on a resolution of 400 (w) x 300 (h), and I position an object at the center of the design resolution, then a user can specify the actual resolution they want to play the game at for example 800 (w) x 600 (h) and the object will still be placed correctly at the center of the screen in the current resolution.

This is where I am having trouble, when the design resolution and the current resolution are the same i.e. design resolution 400 x 300 and current resolution is 400 x 300, the object seems to be placed correctly at the center of the screen on start up and the bullet correctly at the center of the player regardless of the players position when moved:

enter image description here

However when the design resolution and current screen resolution are not the same i.e. design resolution 400 x 300 and current resolution is 800 x 600 the object is no longer correctly placed at center of the screen and neither is the bullet centered for the player:

enter image description here

I have a method to generate the center spawn point for all visible objects (the red reference dot, the sprite/player and the bullet) this method is a simple convenience method to help generate a center based coordinate for a Sprite within a container or another 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);
}

The Sprite and bullet render using screen coordinates:

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

I am unsure of where I am going wrong, but essentially what Id want to see is the same behavior in my first GIF regardless of the current screen size the game is in, the red reference dot seems to position correctly and it is simply drawn to the JPanel and bypasses the getScreen... calls:

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

Here is the minaml reproducible example:

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://i.stack.imgur.com/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;
        }
    }

}

Any help would be greatly appreciated!

UPDATE:

When using the solution by @akuzminykh all seems to work fine, however, now when I set the players position to something like player.setPosition(0,0), expecting it to appear in the top left corner, I get this instead:

enter image description here

which makes sense as I assume we are now positioning via the coordinate being at the center of the sprite, but how would I fix his so both setPosition for the top left corner and center would work, I think I might need to fix the getCenterSpawnPoint?

1 Answers

In your methods getScreenX and getScreenY you are ignoring that getX and getY include the width and height of the sprite. E.g. getX doesn't give you the center position of the sprite in the x-axis, but the position minus half of the sprite's width. When you scale this like you do in getScreenX, then you also scale the offset in x for the sprite. To solve this, simply add the offset initially, do the scaling and subtract the offset finally.

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

Or more mathematically:

If we take your example with 400x300 in "design" resolution, 800x600 being the "current" resolution and the sprite being 100x100 big: The position of the sprite is (150, 100), which makes sense: (400 / 2 - 100 / 2, 300 / 2 - 100 / 2). Now the formula you've used to bring it in "current" resolution (only for x because I'm lazy): 150 / 400 * 800 = 300. Hm, but half of 800 is 400 and the position should be 400 - 100 / 2? Exactly, the offset 100 / 2 for the sprite got scaled as well, from 50 to 100, which results in .. 400 - 100 = 300.

Therefore, add the offset back initially, so you scale the center. Then it's: (150 + 50) / 400 * 800 = 400. Don't forget to finally subtract the offset: 400 - 50 = 350. Now you have the correct position in the x-axis.

Re: UPDATE:

When you want to put the sprite in the top left corner, you might expect player.setPosition(0, 0) to do the trick. This is not the case. The way you've written it, the coordinates given by getX and getY include the width and height of the sprite, remember? Methods like getScreenX and getScreenY, with my fix, consider that and are used to render the sprite at the correct position. That means the coordinates (0, 0) describe the position of the center to be at (0 + 50, 0 + 50), where 50 is just 100 / 2, the width and height of the sprite divided by two.

To place the sprite in the top left corner, you need to consider the sprite's width and height when setting its position using the method setPosition: In our example, where the sprite is 100x100 big, you need to pass (0 - 100 / 2, 0 - 100 / 2), so the call looks like this: player.setPosition(-50, -50). You can of course make it dynamic by using playerImage.getWidth() and so on and so on.


Suggestion:

I suggest you to let x and y of Sprite to be relative to the center of the corresponding sprite. This will make some changes to the code necessary but it will also simplify other things and make them more intuitive. E.g. the problem with player.setPosition(0, 0) won't exist, it will actually put the sprite at the top left corner, exactly what you'd intuitively expect. This will also simplify getScreenX and getScreenY. Consider the offsets caused by the sprite's width and height just in the render method. This should be enough.

Related