【问题标题】:I want my object to flow! The Keyboard Delay - Java我希望我的对象流动!键盘延迟 - Java
【发布时间】:2020-11-15 16:02:50
【问题描述】:

我不知道应该使用哪个组件在我的 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 类,我不确定它是否扩展了 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);
    }
}

最后是我的面板类:

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);
    }
}

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

【问题讨论】:

  • 你应该添加 SWING 标签。
  • 请问您为什么故意将扩展拼写为“ExTeNdInG”?这样的事情让我们更难阅读,因此更难为您提供帮助。
  • 1) Frame extends JFrame 不要掩盖在其他包中重复的类名。使用更具描述性的名称,例如 MovingSquareFrame。但是.. 2)不要在没有充分理由的情况下扩展类(例如更改现有方法)。据我所知,这里没有案例。 3)我会用Panel的自定义绘画来解决这个问题(另见第一个提示重新命名)。 4) 对于 Swing,我们通常使用key bindings 而不是较低级别的KeyListener。 5) setLayout(null); 是代码使用错误方法的危险信号。
  • 参见Motion Using the Keyboard 中的Keyboard Animation 示例。它演示了使用Key BindingsSwing Timer 来控制对象的移动。

标签: java swing keyboard keylistener keyevent


【解决方案1】:

为了使您的程序更快,您可以尝试创建一个 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) {
                }
            }
        }
    }
}

【讨论】:

  • 谢谢伙计,这个想法真的很棒,但你知道延迟是键盘的问题。当你开始按下一个按钮时,它会等待你一段时间来得到你想要做的事情。如果您按下该按钮足够长的时间,它就会认为您正在一遍又一遍地写同一个字母。它不会立即开始写同一封信。所以我的问题是键盘,延迟。实际上,您可以通过计算机的设置来减少延迟,但我不想这样做。我知道有一种方法可以毫不拖延地获取按钮事件。但再次感谢您的想法。
【解决方案2】:

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

一般建议:

  1. 确保您没有屏蔽EDT
  2. 正如问题的 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 中的所有内容。

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

  1. 使用ImageIO.read 加载图像(这将为您提供BufferedImage,即完全加载到内存中的非动画图像)。
  2. 通过提供BufferedImage 构造一个ImageIcon(这将为您提供Icon 接口的实现,适用于下一步)。
  3. 使用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

补充说明

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

【讨论】:

    猜你喜欢
    • 2017-04-15
    • 2017-08-10
    • 2021-08-30
    • 2023-01-27
    • 2012-12-27
    • 2013-07-02
    • 2022-08-17
    • 2014-08-21
    • 2011-02-08
    相关资源
    最近更新 更多