问题描述
基本上我正忙于为 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 的基于坐标:
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,0);
g2d.filloval((int) centeredReferencePoint.getX(),(int) centeredReferencePoint.getY(),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("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);
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,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,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,"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,"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,"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,"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,"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,"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,"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,width,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,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(),0);
g2d.filloval((int) centeredReferencePoint.getX(),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.getWidth(),this.getHeight(),null);
} else {
g2d.drawImage(this.image,this.getScreenX(),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(),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)
之类的东西时,期望它出现在左上角,我得到了这个:
这是有道理的,因为我假设我们现在通过位于精灵中心的坐标进行定位,但是我将如何修复他的 setPosition
左上角和中心都可以工作,我想我可能需要修复 getCenterSpawnPoint
?
解决方法
在您的方法 getScreenX
和 getScreenY
中,您忽略了 getX
和 getY
包括精灵的宽度和高度。例如。 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)
能够做到这一点。不是这种情况。按照你写的方式,getX
和 getY
给出的坐标包括精灵的宽度和高度,记得吗?像 getScreenX
和 getScreenY
这样的方法,在我的修复中,考虑到这一点,并用于在正确的位置渲染精灵。这意味着坐标 (0,0) 描述了中心在 (0 + 50,0 + 50) 的位置,其中 50 只是 100 / 2,即精灵的宽度和高度除以 2。
要将精灵放置在左上角,使用setPosition
方法设置位置时需要考虑精灵的宽度和高度:在我们的示例中,精灵为100x100大,您需要通过(0 - 100 / 2,0 - 100 / 2),所以调用看起来像这样:player.setPosition(-50,-50)
。您当然可以通过使用 playerImage.getWidth()
等等来使其动态化。
建议:
我建议您让x
和y
的Sprite
相对于相应精灵的中心。这将对代码进行一些必要的更改,但它也会简化其他事情并使它们更直观。例如。 player.setPosition(0,0)
的问题不存在,它实际上会将精灵放在左上角,这正是您直觉所期望的。这也将简化 getScreenX
和 getScreenY
。仅在 render
方法中考虑由精灵的宽度和高度引起的偏移。这应该足够了。