我有一个我无法完全理解的问题,因此我正在努力解决它。
基本上我正忙于为 Java Swing 编写一个小型游戏引擎,这个引擎的关键组件之一是能够将设计分辨率与屏幕分辨率分开。这意味着如果我设计一个分辨率为 400 (w) x 300 (h) 的游戏,并且我将一个对象放置在设计分辨率的中心,那么用户可以指定他们想要玩游戏的实际分辨率,例如800 (w) x 600 (h),对象仍将以当前分辨率正确放置在屏幕中心。
这是我遇到问题的地方,当设计分辨率和当前分辨率相同时,即设计分辨率为 400 x 300,当前分辨率为 400 x 300,对象似乎正确放置在屏幕中央启动并且子弹正确地位于玩家的中心,无论玩家移动时的位置如何:
然而,当设计分辨率和当前屏幕分辨率不同时,即设计分辨率为 400 x 300 而当前分辨率为 800 x 600 时,对象不再正确放置在屏幕中心,对于玩家来说子弹也不会居中:
我有一个方法可以为所有可见对象(红色引用点、 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)
时,期望它出现在左上角,我得到这个:
这是有道理的,因为我假设我们现在通过位于 Sprite 中心的坐标进行定位,但是我将如何修复他的坐标以便左上角和中心的 setPosition
都可以工作,我想我可能需要修复 getCenterSpawnPoint
?
最佳答案
在您的方法 getScreenX
和 getScreenY
中,您忽略了 getX
和 getY
包括的宽度和高度 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)
来实现这一点。不是这种情况。按照您编写的方式,getX
和getY
给出的坐标包括 Sprite 的宽度和高度,还记得吗?像 getScreenX
和 getScreenY
这样的方法,在我的修复中,考虑到这一点并用于在正确的位置渲染 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()
等使其动态化。
建议:
我建议你让Sprite
的x
和y
相对于相应 Sprite 的中心。这将对代码进行一些必要的更改,但它也会简化其他事情并使它们更直观。例如。 player.setPosition(0, 0)
的问题将不存在,它实际上会将 Sprite 放在左上角,这正是您直觉所期望的。这也将简化 getScreenX
和 getScreenY
。考虑在 render
方法中由 Sprite 的宽度和高度引起的偏移。这应该足够了。
关于java - 根据设计分辨率生成当前屏幕分辨率的正确坐标,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65973411/