可以找到的一种解决方案是使用javax.swing.Timer,它将不断移动角色并将角色的速度作为其属性。因此,每当Timer 触发其动作侦听器时,其中之一会将当前速度添加到游戏对象(即角色)的位置。当你按下一个键时,动作监听器只会调整动画的速度Timer(根据按下的键)然后[重新]启动Timer。当您释放所有与动作相关的按键时,相应的动作监听器将停止Timer。
一般建议:
- 确保您没有屏蔽EDT。
- 正如问题的 cmets 中已经建议的那样,从长远来看,使用 Key Bindings 以获得更大的灵活性(而不是
KeyListeners)。
下面有两种变体,用于字符表示:
变体 1:自定义绘画
您可以通过覆盖paintComponent 方法,将游戏世界的每个对象绘制在世界的JComponent(例如JPanel)内。
遵循示例代码...
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Objects;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class CustomPaintedCharacters {
//This is the customly painted object representing an entity in the World:
public static class GameObject {
private final World world;
private final Icon how;
private final Point2D.Double where; //Important: use a Point2D.Double... Not a plain Point, because of precision.
public GameObject(final World world, final Point2D where, final Icon how) {
this.world = Objects.requireNonNull(world);
this.how = Objects.requireNonNull(how);
this.where = copyToDouble(where);
}
public void setPosition(final Point2D where) {
this.where.setLocation(where);
}
public Point2D getPosition() {
return copyToDouble(where);
}
public World getWorld() {
return world;
}
public Icon getIcon() {
return how;
}
public Rectangle2D getBounds() {
return new Rectangle2D.Double(where.getX(), where.getY(), how.getIconWidth(), how.getIconHeight());
}
public final void paint(final Graphics2D g) {
g.translate(where.getX(), where.getY()); /*Use as much of the precision as possible...
ie instead of calling 'how.paintIcon' with rounded coordinates converted to ints, we can
translate to where we want to by using doubles...*/
how.paintIcon(world, g, 0, 0);
}
}
public static interface RectangleIcon extends Icon {
@Override
default void paintIcon(final Component comp, final Graphics g, final int x, final int y) {
g.fillRect(x, y, getIconWidth(), getIconHeight());
}
}
public static class DefaultRectangleIcon implements RectangleIcon {
private final Color c;
private final int w, h;
public DefaultRectangleIcon(final Color c, final int width, final int height) {
this.c = Objects.requireNonNull(c);
w = width;
h = height;
}
@Override
public void paintIcon(final Component comp, final Graphics g, final int x, final int y) {
g.setColor(c);
RectangleIcon.super.paintIcon(comp, g, x, y);
}
@Override
public int getIconWidth() {
return w;
}
@Override
public int getIconHeight() {
return h;
}
}
public static class World extends JPanel {
private final ArrayList<GameObject> gameObjects;
public World() {
this.gameObjects = new ArrayList<>();
}
public GameObject createGameObject(final Point2D where, final Icon how) {
final GameObject go = new GameObject(this, where, how);
gameObjects.add(go);
return go;
}
@Override
protected void paintComponent(final Graphics g) {
super.paintComponent(g); //Don't forget this!
gameObjects.forEach(go -> { //Paint every object in the world...
final Graphics2D graphics = (Graphics2D) g.create();
go.paint(graphics);
graphics.dispose();
});
}
@Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet())
return super.getPreferredSize();
final Rectangle2D bounds = new Rectangle2D.Double(); //Important: use a Rectangle2D.Double... Not a plain Rectangle, because of precision.
gameObjects.forEach(go -> bounds.add(go.getBounds()));
return new Dimension((int) Math.ceil(bounds.getX() + bounds.getWidth()), (int) Math.ceil(bounds.getY() + bounds.getHeight()));
}
}
public static class Animation extends Timer {
private final Point2D.Double lvel; //Important: use a Point2D.Double... Not a plain Point, because of precision.
public Animation(final int delay, final GameObject go) {
super(delay, null);
Objects.requireNonNull(go);
lvel = new Point2D.Double();
super.setRepeats(true);
super.setCoalesce(true);
super.addActionListener(e -> {
final Point2D pos = go.getPosition();
go.setPosition(new Point2D.Double(pos.getX() + lvel.getX(), pos.getY() + lvel.getY()));
go.getWorld().repaint();
});
}
public void setLinearVelocity(final Point2D lvel) {
this.lvel.setLocation(lvel);
}
public Point2D getLinearVelocity() {
return copyToDouble(lvel);
}
}
/*Adds two points using a limit. As you have probably understood yourself, and as I have tested
it, the keys pressed with key bindings are keeping to invoke events repeatedly which would mean
for example that if we add to the velocity of a GameObject again and again (after holding the
same direction-button for a while) then the velocity would add up to a number greater than
intended so we must ensure this will not happen by forcing each coordinate to not exceed the
range of [-limit, limit].*/
private static Point2D addPoints(final Point2D p1, final Point2D p2, final double limit) {
final double limitAbs = Math.abs(limit); //We take the absolute value, in case of missusing the method.
return new Point2D.Double(Math.max(Math.min(p1.getX() + p2.getX(), limitAbs), -limitAbs), Math.max(Math.min(p1.getY() + p2.getY(), limitAbs), -limitAbs));
}
/*This method solely exists to ensure that any given Point2D we give (even plain Points) are
converted to Point2D.Double instances because we are working with doubles, not ints. For
example imagine if we were to update a plain Point with a Point2D.Double; we would lose precision
(in the location or velocity of the object) which is highly not wanted, because it could mean
that after a while, the precision would add up to distances that the character would be supposed
to have covered, but it wouldn't. I should note though that doubles are also not lossless, but
they are supposed to be better in this particular scenario. The lossless solution would probably
be to use BigIntegers of pixels but this would be a bit more complex for such a short
demonstration of another subject (so we settle for it).*/
private static Point2D.Double copyToDouble(final Point2D pt) {
return new Point2D.Double(pt.getX(), pt.getY());
}
private static AbstractAction restartAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
//When we press a key (eg UP) we want to activate the timer once (and after changing velocity):
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
animation.setLinearVelocity(addPoints(animation.getLinearVelocity(), acc, max));
if (!animation.isRunning())
animation.restart();
}
};
}
private static AbstractAction stopAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
/*When we release a key (eg UP) we want to undo the movement of the character in the corresponding
direction (up) and possibly even stop the animation (if both velocity coordinates are zero):*/
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (animation.isRunning()) {
//Decrement the velocity:
final Point2D newlvel = addPoints(animation.getLinearVelocity(), acc, max);
animation.setLinearVelocity(newlvel);
//If both velocities are zero, we stop the timer, to speed up EDT:
if (newlvel.getX() == 0 && newlvel.getY() == 0)
animation.stop();
}
}
};
}
private static void installAction(final InputMap inmap, final ActionMap actmap, final Animation animation, final String onPressName, final Point2D off, final double max) {
//One key binding for key press:
final KeyStroke onPressStroke = KeyStroke.getKeyStroke(onPressName); //By default binds to the key-press event.
inmap.put(onPressStroke, onPressName + " press");
actmap.put(onPressName + " press", restartAnimationAction(animation, off, max));
//One key binding for key release:
final KeyStroke onReleaseStroke = KeyStroke.getKeyStroke(onPressStroke.getKeyCode(), onPressStroke.getModifiers(), true); //Convert the key-stroke of key-press event to key-release event.
inmap.put(onReleaseStroke, onPressName + " release");
actmap.put(onPressName + " release", stopAnimationAction(animation, new Point2D.Double(-off.getX(), -off.getY()), max));
}
public static Animation installAnimation(final GameObject go, final int delayMS, final double stepOffset) {
final World world = go.getWorld();
final InputMap in = world.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
final ActionMap act = world.getActionMap();
final Animation anim = new Animation(delayMS, go);
/*The Strings used in each invocation of 'installAction' are very important as they will
define the KeyStroke obtained by 'KeyStroke.getKeyStroke' method calls inside 'installAction'.
So you shouldn't need to change those for this particular demonstration.*/
installAction(in, act, anim, "LEFT", new Point2D.Double(-stepOffset, 0), stepOffset);
installAction(in, act, anim, "RIGHT", new Point2D.Double(stepOffset, 0), stepOffset);
installAction(in, act, anim, "UP", new Point2D.Double(0, -stepOffset), stepOffset);
installAction(in, act, anim, "DOWN", new Point2D.Double(0, stepOffset), stepOffset);
return anim;
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(() -> { //Make sure to 'invokeLater' EDT related code.
final World world = new World();
final GameObject worldCharacter = world.createGameObject(new Point(200, 200), new DefaultRectangleIcon(Color.CYAN.darker(), 100, 50));
final JFrame frame = new JFrame("Move the character");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(world);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
installAnimation(worldCharacter, 10, 2);
});
}
}
变体 2:扩展 JComponent
您还可以为任何游戏的对象表示(例如角色)扩展JComponent。
在此解决方案中,您可以使用添加到角色的其他JComponents,以这种方式更改角色的外观,而无需绘制paintComponent 中的所有内容。
例如,如果您有角色的外部图像,您可以:
- 使用
ImageIO.read 加载图像(这将为您提供BufferedImage,即完全加载到内存中的非动画图像)。
- 通过提供
BufferedImage 构造一个ImageIcon(这将为您提供Icon 接口的实现,适用于下一步)。
- 使用
JLabel 作为字符并为其提供在第2 步中创建的ImageIcon。
但不仅如此,因为通过将JComponent 作为字符,您可以将其他JComponents 添加到它,因为它已经是Container,因此有更多选择。
最好使用LayoutManager 将字符(JComponent)定位在父级(另一个JComponent)内。您可以使用JComponent 游戏对象的setLocation 方法将它们定位在世界中。据我所知,非常不鼓励使用 null 作为世界布局,所以我从头开始创建了一个小的 LayoutManager,作为关于如何进行的建议。
遵循JLabel的示例代码作为没有图像的字符,只是为了演示概念......
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.geom.Point2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Objects;
import java.util.function.BiFunction;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class CharacterOfJComponent {
public static class ManualLayout implements LayoutManager, Serializable {
public static void setOperatedSize(final Dimension input,
final BiFunction<Integer, Integer, Integer> operator,
final Dimension inputOutput) {
inputOutput.setSize(operator.apply(input.width, inputOutput.width), operator.apply(input.height, inputOutput.height));
}
protected Dimension getGoodEnoughSize(final Component comp,
final Dimension defaultSize) {
final Dimension dim = new Dimension(defaultSize);
if (comp != null) { // && comp.isVisible()) {
/*Start with default size, and then listen to max and min
(if both max and min are set, we prefer the min one):*/
if (comp.isMaximumSizeSet())
setOperatedSize(comp.getMaximumSize(), Math::min, dim);
if (comp.isMinimumSizeSet())
setOperatedSize(comp.getMinimumSize(), Math::max, dim);
}
return dim;
}
protected Dimension getLayoutComponentSize(final Component comp) {
return getGoodEnoughSize(comp, (comp.getWidth() <= 0 && comp.getHeight() <= 0)? comp.getPreferredSize(): comp.getSize());
}
@Override
public void addLayoutComponent(final String name,
final Component comp) {
}
@Override
public void removeLayoutComponent(final Component comp) {
}
@Override
public Dimension preferredLayoutSize(final Container parent) {
return minimumLayoutSize(parent); //Preferred and minimum coincide for simplicity.
}
@Override
public Dimension minimumLayoutSize(final Container parent) {
final Component[] comps = parent.getComponents();
if (comps == null || comps.length <= 0)
return new Dimension();
final Rectangle totalBounds = new Rectangle(comps[0].getLocation(), getLayoutComponentSize(comps[0]));
for (int i = 1; i < comps.length; ++i)
totalBounds.add(new Rectangle(comps[i].getLocation(), getLayoutComponentSize(comps[i])));
final Insets parins = parent.getInsets();
final int addw, addh;
if (parins == null)
addw = addh = 0;
else {
addw = (parins.left + parins.right);
addh = (parins.top + parins.bottom);
}
return new Dimension(Math.max(0, totalBounds.x + totalBounds.width) + addw, Math.max(0, totalBounds.y + totalBounds.height) + addh);
}
@Override
public void layoutContainer(final Container parent) {
for (final Component comp: parent.getComponents())
comp.setSize(getLayoutComponentSize(comp)); //Just set the size. The locations are taken care by the class's client supposedly.
}
}
public static class GameObject extends JLabel {
private final Point2D.Double highPrecisionLocation;
public GameObject() {
highPrecisionLocation = copyToDouble(super.getLocation());
}
public void setHighPrecisionLocation(final Point2D highPrecisionLocation) {
this.highPrecisionLocation.setLocation(highPrecisionLocation);
final Insets parentInsets = getParent().getInsets();
setLocation((int) Math.round(highPrecisionLocation.getX()) + parentInsets.left + parentInsets.right,
(int) Math.round(highPrecisionLocation.getY()) + parentInsets.top + parentInsets.bottom);
}
public Point2D getHighPrecisionLocation() {
return copyToDouble(highPrecisionLocation);
}
@Override
public World getParent() {
return (World) super.getParent();
}
}
public static class World extends JPanel {
public World() {
super(new ManualLayout());
}
public ArrayList<GameObject> getGameObjects() {
final ArrayList<GameObject> gos = new ArrayList<>();
for (final Component child: getComponents())
if (child instanceof GameObject)
gos.add((GameObject) child);
return gos;
}
}
/*Adds two points using a limit. As you have probably understood yourself, and as I have tested
it, the keys pressed with key bindings are keeping to invoke events repeatedly which would mean
for example that if we add to the velocity of a GameObject again and again (after holding the
same direction-button for a while) then the velocity would add up to a number greater than
intended so we must ensure this will not happen by forcing each coordinate to not exceed the
range of [-limit, limit].*/
private static Point2D addPoints(final Point2D p1, final Point2D p2, final double limit) {
final double limitAbs = Math.abs(limit); //We take the absolute value, in case of missusing the method.
return new Point2D.Double(Math.max(Math.min(p1.getX() + p2.getX(), limitAbs), -limitAbs), Math.max(Math.min(p1.getY() + p2.getY(), limitAbs), -limitAbs));
}
/*This method solely exists to ensure that any given Point2D we give (even plain Points) are
converted to Point2D.Double instances because we are working with doubles, not ints. For
example imagine if we were to update a plain Point with a Point2D.Double; we would lose precision
(in the location or velocity of the object) which is highly not wanted, because it could mean
that after a while, the precision would add up to distances that the character would be supposed
to have covered, but it wouldn't. I should note though that doubles are also not lossless, but
they are supposed to be better in this particular scenario. The lossless solution would probably
be to use BigIntegers of pixels but this would be a bit more complex for such a short
demonstration of another subject (so we settle for it).*/
private static Point2D.Double copyToDouble(final Point2D pt) {
return new Point2D.Double(pt.getX(), pt.getY());
}
public static class Animation extends Timer {
private final Point2D.Double lvel; //Important: use a Point2D.Double... Not a plain Point, because of precision.
public Animation(final int delay, final GameObject go) {
super(delay, null);
Objects.requireNonNull(go);
lvel = new Point2D.Double();
super.setRepeats(true);
super.setCoalesce(true);
super.addActionListener(e -> {
final Point2D pos = go.getHighPrecisionLocation();
go.setHighPrecisionLocation(new Point2D.Double(pos.getX() + lvel.getX(), pos.getY() + lvel.getY()));
go.getParent().revalidate(); //Layout has changed.
go.getParent().repaint();
});
}
public void setLinearVelocity(final Point2D lvel) {
this.lvel.setLocation(lvel);
}
public Point2D getLinearVelocity() {
return copyToDouble(lvel);
}
}
private static AbstractAction restartAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
//When we press a key (eg UP) we want to activate the timer once (and after changing velocity):
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
animation.setLinearVelocity(addPoints(animation.getLinearVelocity(), acc, max));
if (!animation.isRunning())
animation.restart();
}
};
}
private static AbstractAction stopAnimationAction(final Animation animation, final Point2D acceleration, final double max) {
final Point2D acc = copyToDouble(acceleration); //Deffensive copy.
/*When we release a key (eg UP) we want to undo the movement of the character in the corresponding
direction (up) and possibly even stop the animation (if both velocity coordinates are zero):*/
return new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
if (animation.isRunning()) {
//Decrement the velocity:
final Point2D newlvel = addPoints(animation.getLinearVelocity(), acc, max);
animation.setLinearVelocity(newlvel);
//If both velocities are zero, we stop the timer, to speed up EDT:
if (newlvel.getX() == 0 && newlvel.getY() == 0)
animation.stop();
}
}
};
}
private static void installAction(final InputMap inmap, final ActionMap actmap, final Animation animation, final String onPressName, final Point2D off, final double max) {
//One key binding for key press:
final KeyStroke onPressStroke = KeyStroke.getKeyStroke(onPressName); //By default binds to the key-press event.
inmap.put(onPressStroke, onPressName + " press");
actmap.put(onPressName + " press", restartAnimationAction(animation, off, max));
//One key binding for key release:
final KeyStroke onReleaseStroke = KeyStroke.getKeyStroke(onPressStroke.getKeyCode(), onPressStroke.getModifiers(), true); //Convert the key-stroke of key-press event to key-release event.
inmap.put(onReleaseStroke, onPressName + " release");
actmap.put(onPressName + " release", stopAnimationAction(animation, new Point2D.Double(-off.getX(), -off.getY()), max));
}
public static Animation installAnimation(final GameObject go, final int delayMS, final double stepOffset) {
final World world = go.getParent();
final InputMap in = world.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
final ActionMap act = world.getActionMap();
final Animation anim = new Animation(delayMS, go);
/*The Strings used in each invocation of 'installAction' are very important as they will
define the KeyStroke obtained by 'KeyStroke.getKeyStroke' method calls inside 'installAction'.
So you shouldn't need to change those for this particular demonstration.*/
installAction(in, act, anim, "LEFT", new Point2D.Double(-stepOffset, 0), stepOffset);
installAction(in, act, anim, "RIGHT", new Point2D.Double(stepOffset, 0), stepOffset);
installAction(in, act, anim, "UP", new Point2D.Double(0, -stepOffset), stepOffset);
installAction(in, act, anim, "DOWN", new Point2D.Double(0, stepOffset), stepOffset);
return anim;
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(() -> { //Make sure to 'invokeLater' EDT related code.
final World world = new World();
final GameObject worldCharacter = new GameObject();
worldCharacter.setText("Character");
worldCharacter.setBorder(BorderFactory.createLineBorder(Color.CYAN.darker(), 2));
world.add(worldCharacter);
worldCharacter.setHighPrecisionLocation(new Point(200, 200));
final JFrame frame = new JFrame("Move the character");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(world);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
installAnimation(worldCharacter, 10, 2);
});
}
}
如您所见,有一个名为ManualLayout 的自定义LayoutManager。它尊重布局Components 的最小、首选和最大尺寸。然后这个类的客户端负责相应地使用setLocation方法来适应父母的插图和孩子的位置。
从长远来看,由于您可能希望在世界中放置多个可能重叠的对象,那么您可以使用JLayeredPane 作为World。
补充说明
- 您可以混合使用这些变体。
- 不要忘记在自定义绘画变体中添加
super.paintComponent调用覆盖paintComponent。
- 如果您希望能够暂停和恢复模拟,您可以创建一个动作监听器来设置启用或禁用其他键盘动作。
- 仅在 JDK/JRE 8 中测试过,但我认为它也应该在其他版本中工作。至少这个概念应该有效。