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:
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:
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:
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?


