我想让我的对象流动!键盘延迟 - Java

I want my object to flow! The Keyboard Delay - Java

我不知道应该使用哪个组件在我的 JPanel 上创建移动方块。所以我决定用另一个JPanel来做一个对象。我的第一个目标是移动物体,但你知道游戏,当你按下键盘上的按钮时,它的表现并不像打字,而是像在游戏中一样。我的意思是当你打字时角色的移动会很延迟。如何在不更改计算机键盘延迟设置的情况下修复延迟?如果您知道创建移动方块的更好方法,IDK 也可以告诉我。

这是源代码:

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;


public class Frame extends JFrame implements KeyListener {

    private static final long serialVersionUID = 1L;
    private static int width = 800;
    private static int height = width / 16 * 9;

    static //// For Action
    Panel panel = new Panel();
    Square sq = new Square();
    private int x = 0;
    private int y = 0;

    // Constructor
    Frame() {
        this.setSize(width, height);
        this.setLocationByPlatform(true);
        this.setLayout(null);
        this.setResizable(false);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setFocusable(true);
        this.setName("Window");
        this.addKeyListener(this);

        // Action is starting
        sq.setLocation(x, y);
        panel.add(sq);
        this.add(panel);

    }

    // KeyListener==
    @Override
    public void keyTyped(KeyEvent e) {
        // TODO Auto-generated method stub

    }

      //My problem is here, or I'm wrong!?

    @Override
    public void keyPressed(KeyEvent e) {
            switch (e.getKeyCode()) {
            case KeyEvent.VK_W:
                sq.setLocation(x, y - 5);
                y-=5;
                this.repaint();
                System.out.println(e.getKeyChar());
                break;
            case KeyEvent.VK_S:
                sq.setLocation(x, y + 5);
                y+=5;
                this.repaint();
                System.out.println(e.getKeyChar());
                break;
            case KeyEvent.VK_A:
                sq.setLocation(x - 5, y);
                x-=5;
                this.repaint();
                System.out.println(e.getKeyChar());
                break;
            case KeyEvent.VK_D:
                sq.setLocation(x + 5, y);
                x+=5;
                this.repaint();
                System.out.println(e.getKeyChar());
                break;
            
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        
    }

    // Main
    public static void main(String arguments[]) {
        Frame frame = new Frame();
        frame.add(panel);
        frame.setVisible(true);
    }
}



这是我的 Square class,我不确定它是否扩展了 JPanel:

import java.awt.Color;
import javax.swing.JPanel;

public class Square extends JPanel {
    
    private static final long serialVersionUID = 1L;
    private int width = 70;
    private int height = 70;
    
    Square(){
        setSize(width, height);
        setBackground(Color.white);
    }
}

最后是我的面板 Class:

public class Panel extends JPanel{

    private static final long serialVersionUID = 1L;
    private static int width = 800;
    private static int height = width / 16 * 9;

    Panel() {
        this.setSize(width, height);
        this.setBackground(Color.DARK_GRAY);
        this.setLayout(null);
    }
}

我需要你们的帮助。感谢大家的回复。

为了让你的程序更快,你可以尝试创建一个 Thread 包含一个 while 循环,它使用 boolean 变量不断检查是否按下了一个键。

这样,您还可以通过使用 Thread.sleep(time);

在特定时间停止 Thread 来设置程序的灵敏度

演示代码:

package test;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class Frame extends JFrame implements KeyListener {

    private static final long serialVersionUID = 1L;
    private static int width = 800;
    private static int height = width / 16 * 9;

    static Panel panel = new Panel();
    Square sq = new Square();
    private int x = 0;
    private int y = 0;

    int key;
    //Sensitivity should not be greater than 1000.
    int sensitivity = 100;
    boolean keyPressed = false;

    public Frame() {
        this.setSize(width, height);
        this.setLocationByPlatform(true);
        this.setLayout(null);
        this.setResizable(false);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setFocusable(true);
        this.setName("Window");
        this.addKeyListener(this);

        // Action is starting
        sq.setLocation(x, y);
        panel.add(sq);
        add(panel);
        setVisible(true);
        new Thread(new MoveSquare()).start();
    }

    // KeyListener==
    @Override
    public void keyTyped(KeyEvent e) {
        // TODO Auto-generated method stub

    }

    //My problem is here, or I'm wrong!?
    @Override
    public void keyPressed(KeyEvent e) {
        key = e.getKeyCode();
        keyPressed = true;
    }

    @Override
    public void keyReleased(KeyEvent e) {
        keyPressed = false;
    }

    // Main
    public static void main(String arguments[]) {
        new Frame();

    }

    protected class MoveSquare implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    if (keyPressed) {
                        try {
                            int s = 1;
                            switch (key) {
                                case KeyEvent.VK_W:
                                    sq.setLocation(x, y - s);
                                    y -= s;
                                    Frame.this.repaint();

                                    break;
                                case KeyEvent.VK_S:
                                    sq.setLocation(x, y + s);
                                    y += s;
                                    Frame.this.repaint();

                                    break;
                                case KeyEvent.VK_A:
                                    sq.setLocation(x - s, y);
                                    x -= s;
                                    Frame.this.repaint();

                                    break;
                                case KeyEvent.VK_D:
                                    sq.setLocation(x + s, y);
                                    x += s;
                                    Frame.this.repaint();

                                    break;

                            }
                        } catch (Exception ex) {
                        }
                    }
                    Thread.sleep(1000 / sensitivity);
                } catch (Exception ex) {
                }
            }
        }
    }
}

一个可以找到的解决方案是使用 javax.swing.Timer 来不断移动角色并将角色的速度作为它的 属性。因此,每当 Timer 触发其动作侦听器时,其中一个侦听器会将当前速度添加到游戏 object(即角色)的位置。当您按下一个键时,动作侦听器将调整动画的速度 Timer(根据按下的键),然后 [重新] 启动 Timer。当您释放所有 motion-related 键时,相应的动作侦听器将停止 Timer.

一般建议:

  1. 确保您没有阻止 EDT
  2. 正如问题评论中已经建议的那样,在长 运行(而不是 KeyListeners)中使用 Key Bindings 以获得更大的灵活性。

字符表示有两种变体:

变体 1:自定义绘画

您可以通过重写 paintComponent 方法,让游戏世界的每个 object 都绘制在世界 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

您还可以为任何游戏的 object 表示(例如角色)扩展 JComponent

在此解决方案中,您可以使用添加到角色的其他 JComponent,以这种方式改变角色的外观,而无需在 paintComponent.

中绘制所有内容

例如,如果您有角色的外部图像,您可以:

  1. ImageIO.read 加载图像(这会给你一个 BufferedImage,即 non-animated 图像完全加载到内存中)。
  2. 通过提供 BufferedImage 构建一个 ImageIcon(这将为您提供 Icon 接口的实现,适用于下一步)。
  3. 使用 JLabel 作为字符并为其提供在步骤 2 中创建的 ImageIcon

但不只是这样,因为,通过将 JComponent 作为字符,您可以向其添加其他 JComponent,因为它已经是 Container,因此具有更多选择。

这里最好使用 LayoutManager 将字符(JComponent)定位在 parent(另一个 JComponent)中。您可以使用 JComponent 游戏 object 的 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。它尊重布局 Component 的最小、首选和最大尺寸。然后这个 class 的客户端负责使用 setLocation 方法相应地适应 parent 的插图和 child 的位置。

在长的运行中,由于你可能想在世界上放多个object,它们可能会重叠,那么你可以用一个JLayeredPane作为[=48] =].

补充说明

  1. 您可以混合使用这些变体。
  2. 不要忘记在 自定义绘画 变体中添加 super.paintComponent 覆盖 paintComponent 调用。
  3. 如果您希望能够暂停和恢复模拟,您可以创建一个动作侦听器来设置启用或禁用其他键盘动作。
  4. 仅在 JDK/JRE 8 中测试过,但我认为它应该也适用于其他版本。至少这个概念应该可行。