如何在两个键之间快速交替时避免键绑定延迟

How to avoid the key binding delay when alternating quickly between two keys

这是我的问题:当我在两个键之间快速切换时,它会在两个键之间阻塞半秒(例如,有两个键,一个左键和一个右键,用于移动游戏角色,首先我点击在左键上,然后我想快速切换到右键,即使我切换的时间很短,它也会让我的游戏角色停止约 0.5 秒,直到它向右移动)。 此 short video

中的示例

我实际上找不到任何方法来正确使用游戏中的键盘输入来解决这个奇怪的问题。 经过数小时的研究,我终于找到了自己的东西是这个主题:here,正如作者所说,在我必须对项目进行所有更改之后,无法知道它是否会起作用执行该 JInput 库。

这是使用键盘输入的代码部分:

class MainClient {
private static String clientName;
private static GameClient gameClient;
private static List<Object> allSolidObjects = new ArrayList<>();
private static boolean stopRight = false, stopLeft = false;
private static GameFrame gameFrame;
private static Character character;
private static CharacterView characterView;
private static final String MOVE_LEFT = "move left", MOVE_RIGHT = "move right", MOVE_STOP = "move stop";
private static final int FPS = 60;

public static void main(String[] args) {    
        SwingUtilities.invokeLater(() -> {
            character = new Character();
            characterView = new CharacterView(
                    character.getRelativeX(),
                    character.getRelativeY(),
                    Character.getRelativeWidth(),
                    Character.getRelativeHeight());

            gameFrame.getGamePanel().setCharacterView(characterView);

            final InputMap IM = gameFrame.getGamePanel().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
            final ActionMap AM = gameFrame.getGamePanel().getActionMap();
            MovementState movementState = new MovementState();

            IM.put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 0, true), MOVE_STOP);
            IM.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), MOVE_STOP);
            IM.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), MOVE_STOP);
            IM.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), MOVE_STOP);
            AM.put(MOVE_STOP, new MoveXAction(movementState, 0f));

            IM.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), MOVE_RIGHT);
            IM.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), MOVE_RIGHT);
            AM.put(MOVE_RIGHT, new MoveXAction(movementState, Character.getRelativeSpeed()));

            IM.put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 0, false), MOVE_LEFT);
            IM.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), MOVE_LEFT);
            AM.put(MOVE_LEFT, new MoveXAction(movementState, -Character.getRelativeSpeed()));
            Timer timer = new Timer(1000/FPS, e -> {
                if (movementState.xDirection < 0) {
                    stopRight = false;
                    if (!stopLeft) {
                        character.setRelativeX(character.getRelativeX() + movementState.xDirection);
                        for (Object object : allSolidObjects) {
                            if (CollisionDetection.isCollisionBetween(character, object)) {
                                stopLeft = true;
                            }
                        }
                    }
                } else if (movementState.xDirection > 0) {
                    stopLeft = false;
                    if (!stopRight) {
                        character.setRelativeX(character.getRelativeX() + movementState.xDirection);
                        for (Object object : allSolidObjects) {
                            if (CollisionDetection.isCollisionBetween(character, object)) {
                                stopRight = true;
                            }
                        }
                    }
                }

                characterView.setRelativeX(character.getRelativeX());
                characterView.setRelativeY(character.getRelativeY());
                gameFrame.getGamePanel().setCharacterView(characterView);

                gameFrame.getGamePanel().repaint();
            });
            timer.start();

        });
    }
}

// Not important method
private static void launchGameClient() {}

static class MoveXAction extends AbstractAction {
    private final MovementState movementState;
    private final float value;

    MoveXAction(MovementState movementState, float value) {
        this.movementState = movementState;
        this.value = value;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        this.movementState.xDirection = this.value;
    }
}

static class MovementState {
    float xDirection;
}
}

如果你想自己测试看看问题所在,完整的项目在GitHub: here

所以,有没有人知道如何解决这个问题,也许就是这样,一个无法解决的OS问题,但即使是这样,请留下答案并告诉^^.

错误是你的逻辑。如果你按下一个按钮,然后另一个(你有两个活动键),然后释放一个,你最终会得到一个“none”状态,然后你必须等待在重复状态开始之前是初始按下状态

所以,例如...

  • 向左 (MOVE_LEFT)
  • (MOVE_RIGHT)
  • 释放离开 (MOVE_NONE)
  • 初始按下和重复事件发生之间的延迟

一个更好的主意是设置一个状态,其中 "none" 是缺少 "active" 状态而不是状态。

为此,我使用 Direction enum...

public static enum Direction {
    LEFT(-1), RIGHT(1);

    private int delta;

    private Direction(int delta) {
        this.delta = delta;
    }

    public int getDelta() {
        return delta;
    }

}

我已经预先播种了 delta,但你不需要 "have" 这样做,但它确实展示了一个可能简单的实现

然后我使用 Set 来管理状态...

private Set<Direction> movement = new TreeSet<>();

当按下一个键时,相应的 enum 被添加,当释放时它被删除。

这是通过 "press" 和 "release" Action...

完成的
static public class PressAction extends AbstractAction {

    private final Set<Direction> movement;
    private final Direction value;

    public PressAction(Set<Direction> movementState, Direction value) {
        this.movement = movementState;
        this.value = value;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        movement.add(value);
    }
}
static public class ReleaseAction extends AbstractAction {

    private final Set<Direction> movement;
    private final Direction value;

    public ReleaseAction(Set<Direction> movementState, Direction value) {
        this.movement = movementState;
        this.value = value;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        movement.remove(value);
    }
}

这意味着,只要 enum 存在于 Set 中,就应该应用它的增量。

这样做的一个很好的副作用是,如果您同时按住左右键,增量会相互抵消

可运行示例...

所以,我把你剥离出来 "example" 变成了一个演示基本原理的可运行示例。

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.time.Duration;
import java.time.Instant;
import java.util.Set;
import java.util.TreeSet;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public static enum Direction {
        LEFT(-1), RIGHT(1);

        private int delta;

        private Direction(int delta) {
            this.delta = delta;
        }

        public int getDelta() {
            return delta;
        }

    }

    public static class TestPane extends JPanel {

        private int xPos = 95;

        private Set<Direction> movement = new TreeSet<>();

        public TestPane() {
            final InputMap im = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
            final ActionMap am = getActionMap();

            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 0, true), "Release.left");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "Release.left");

            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "Release.right");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "Release.right");

            am.put("Release.left", new ReleaseAction(movement, Direction.LEFT));
            am.put("Release.right", new ReleaseAction(movement, Direction.RIGHT));

            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "Press.right");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "Press.right");
            am.put("Press.right", new PressAction(movement, Direction.RIGHT));

            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 0, false), "Press.left");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "Press.left");
            am.put("Press.left", new PressAction(movement, Direction.LEFT));

            Timer timer = new Timer(5, e -> {
                for (Direction dir :  movement) {
                    xPos += dir.getDelta();
                }
                repaint();
            });
            timer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setColor(Color.RED);
            g2d.fillRect(xPos, 95, 10, 10);
            g2d.dispose();
        }

    }

    private static final Instant ANCHOR = Instant.now();
    private static Instant switchAd = Instant.now();

    protected static long switchDelay() {
        return Duration.between(switchAd, Instant.now()).toMillis();
    }

    protected static long tick() {
        return Duration.between(ANCHOR, Instant.now()).toMillis();
    }

    static public class PressAction extends AbstractAction {

        private final Set<Direction> movement;
        private final Direction value;

        public PressAction(Set<Direction> movementState, Direction value) {
            this.movement = movementState;
            this.value = value;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            movement.add(value);
        }
    }
    static public class ReleaseAction extends AbstractAction {

        private final Set<Direction> movement;
        private final Direction value;

        public ReleaseAction(Set<Direction> movementState, Direction value) {
            this.movement = movementState;
            this.value = value;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            movement.remove(value);
        }
    }

}