Как использовать ключевые привязки вместо ключевых прослушивателей

Я использую KeyListener в своем коде (игра или как-то иначе), так как мои экранные объекты реагируют на пользователя ключ ввод. Вот мой код:

public class MyGame extends JFrame {

    static int up = KeyEvent.VK_UP;
    static int right = KeyEvent.VK_RIGHT;
    static int down = KeyEvent.VK_DOWN;
    static int left = KeyEvent.VK_LEFT;
    static int fire = KeyEvent.VK_Q;

    public MyGame() {

//      Do all the layout management and what not...
        JLabel obj1 = new JLabel();
        JLabel obj2 = new JLabel();
        obj1.addKeyListener(new MyKeyListener());
        obj2.addKeyListener(new MyKeyListener());
        add(obj1);
        add(obj2);
//      Do other GUI things...
    }

    static void move(int direction, Object source) {

        // do something
    }

    static void fire(Object source) {

        // do something
    }

    static void rebindKey(int newKey, String oldKey) {

//      Depends on your GUI implementation.
//      Detecting the new key by a KeyListener is the way to go this time.
        if (oldKey.equals("up"))
            up = newKey;
        if (oldKey.equals("down"))
            down = newKey;
//      ...
    }

    public static void main(String[] args) {

        new MyGame();
    }

    private static class MyKeyListener extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {

            Object source = e.getSource();
            int action = e.getExtendedKeyCode();

/* Will not work if you want to allow rebinding keys since case variables must be constants.
            switch (action) {
                case up:
                    move(1, source);
                case right:
                    move(2, source);
                case down:
                    move(3, source);
                case left:
                    move(4, source);
                case fire:
                    fire(source);
                ...
            }
*/
            if (action == up)
                move(1, source);
            else if (action == right)
                move(2, source);
            else if (action == down)
                move(3, source);
            else if (action == left)
                move(4, source);
            else if (action == fire)
                fire(source);
        }
    }
}

У меня проблемы с отзывчивостью:

  • Мне нужно щелкнуть по объекту, чтобы он работал.
  • Ответ, который я получаю для нажатия одного из ключей, не так, как я хотел, чтобы он работал - слишком отзывчивый или слишком не реагирующий.

Почему это происходит и как я могу это исправить?

Ответы

Ответ 1

Этот ответ объясняет и демонстрирует, как использовать привязки клавиш вместо ключевых слушателей для образовательной цели. Это не

  • Как написать игру на Java.
  • Как должно выглядеть хорошее написание кода (например, видимость).
  • Самый эффективный (с точки зрения производительности или кода) способ реализации привязок ключей.

Это

  • То, что я опубликую в качестве ответа любому, у кого возникнут проблемы с ключевыми слушателями.

Ответ; Прочитайте руководство Swing по привязкам клавиш.

Я не хочу читать руководства, скажите, почему я хотел бы использовать привязки клавиш вместо красивого кода, который у меня уже есть!

В учебнике Swing объясняется, что

  • Связывание клавиш не требует щелчка по компоненту (для его фокусировки):
    • Удаляет неожиданное поведение с точки зрения пользователя.
    • Если у вас есть 2 объекта, они не могут перемещаться одновременно, так как только один из объектов может иметь фокус в данный момент времени (даже если вы привязываете их к разным клавишам).
  • Связывание клавиш проще в обслуживании и управлении:
    • Отключение, повторное связывание, переназначение действий пользователя намного проще.
    • Код легче читать.

Хорошо, вы убедили меня попробовать. Как это работает?

tutorial содержит хороший раздел. Ключевые привязки включают в себя 2 объекта InputMap и ActionMap. InputMap отображает вход пользователя в имя действия, ActionMap сопоставляет имя действия с Action. Когда пользователь нажимает клавишу, карта ввода ищет ключ и находит имя действия, затем в карте действий выполняется поиск имени действия и выполняется действие.

Выглядит громоздко. Почему бы не связать пользовательский ввод напрямую с действием и избавиться от имени действия? Тогда вам понадобится только одна карта, а не две.

Хороший вопрос! Вы увидите, что это одна из тех вещей, которые делают привязки клавиш более управляемыми (отключить, переустановить и т.д.).

Я хочу, чтобы вы дали мне полный рабочий код этого.

Нет (учебник Swing имеет рабочие примеры).

Ты сосешь! Я тебя ненавижу!

Вот как сделать одно ключевое связывание:

myComponent.getInputMap().put("userInput", "myAction");
myComponent.getActionMap().put("myAction", action);

Обратите внимание, что есть 3 InputMap, реагирующие на разные состояния фокусировки:

myComponent.getInputMap(JComponent.WHEN_FOCUSED);
myComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
  • WHEN_FOCUSED, который также используется при отсутствии аргументов, используется, когда компонент имеет фокус. Это похоже на случай прослушивания ключа.
  • WHEN_ANCESTOR_OF_FOCUSED_COMPONENT используется, когда сфокусированный компонент находится внутри компонента, который зарегистрирован для получения действия. Если у вас есть много членов экипажа внутри космического корабля, и вы хотите, чтобы космический корабль продолжал получать вход, в то время как любой из членов экипажа сосредоточен, используйте это.
  • WHEN_IN_FOCUSED_WINDOW используется, когда компонент, который зарегистрирован для получения действия, находится внутри сфокусированного компонента. Если у вас много танков в сфокусированном окне, и вы хотите, чтобы все они получали вход одновременно, используйте это.

Код, представленный в вопросе, будет выглядеть примерно так, если предположить, что оба объекта должны контролироваться одновременно:

public class MyGame extends JFrame {

    private static final int IFW = JComponent.WHEN_IN_FOCUSED_WINDOW;
    private static final String MOVE_UP = "move up";
    private static final String MOVE_DOWN = "move down";
    private static final String FIRE = "move fire";

    static JLabel obj1 = new JLabel();
    static JLabel obj2 = new JLabel();

    public MyGame() {

//      Do all the layout management and what not...

        obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("UP"), MOVE_UP);
        obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("DOWN"), MOVE_DOWN);
//      ...
        obj1.getInputMap(IFW).put(KeyStroke.getKeyStroke("control CONTROL"), FIRE);
        obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("W"), MOVE_UP);
        obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("S"), MOVE_DOWN);
//      ...
        obj2.getInputMap(IFW).put(KeyStroke.getKeyStroke("T"), FIRE);

        obj1.getActionMap().put(MOVE_UP, new MoveAction(1, 1));
        obj1.getActionMap().put(MOVE_DOWN, new MoveAction(2, 1));
//      ...
        obj1.getActionMap().put(FIRE, new FireAction(1));
        obj2.getActionMap().put(MOVE_UP, new MoveAction(1, 2));
        obj2.getActionMap().put(MOVE_DOWN, new MoveAction(2, 2));
//      ...
        obj2.getActionMap().put(FIRE, new FireAction(2));

//      In practice you would probably create your own objects instead of the JLabels.
//      Then you can create a convenience method obj.inputMapPut(String ks, String a)
//      equivalent to obj.getInputMap(IFW).put(KeyStroke.getKeyStroke(ks), a);
//      and something similar for the action map.

        add(obj1);
        add(obj2);
//      Do other GUI things...
    }

    static void rebindKey(KeyEvent ke, String oldKey) {

//      Depends on your GUI implementation.
//      Detecting the new key by a KeyListener is the way to go this time.
        obj1.getInputMap(IFW).remove(KeyStroke.getKeyStroke(oldKey));
//      Removing can also be done by assigning the action name "none".
        obj1.getInputMap(IFW).put(KeyStroke.getKeyStrokeForEvent(ke),
                 obj1.getInputMap(IFW).get(KeyStroke.getKeyStroke(oldKey)));
//      You can drop the remove action if you want a secondary key for the action.
    }

    public static void main(String[] args) {

        new MyGame();
    }

    private class MoveAction extends AbstractAction {

        int direction;
        int player;

        MoveAction(int direction, int player) {

            this.direction = direction;
            this.player = player;
        }

        @Override
        public void actionPerformed(ActionEvent e) {

            // Same as the move method in the question code.
            // Player can be detected by e.getSource() instead and call its own move method.
        }
    }

    private class FireAction extends AbstractAction {

        int player;

        FireAction(int player) {

            this.player = player;
        }

        @Override
        public void actionPerformed(ActionEvent e) {

            // Same as the fire method in the question code.
            // Player can be detected by e.getSource() instead, and call its own fire method.
            // If so then remove the constructor.
        }
    }
}

Вы можете видеть, что разделение карты ввода с картой действий позволяет использовать многократный код и лучший контроль привязок. Кроме того, вы можете также управлять действием напрямую, если вам нужна функциональность. Например:

FireAction p1Fire = new FireAction(1);
p1Fire.setEnabled(false); // Disable the action (for both players in this case).

Дополнительную информацию см. в Учебном пособии.

Я вижу, что вы использовали 1 действие, движение, для 4 ключей (направлений) и 1 действие, огонь, для 1 клавиши. Почему бы не дать каждому ключу его собственное действие или дать всем клавишам одно и то же действие и разобраться, что делать внутри действия (например, в случае перемещения)?

Хорошая точка. Технически вы можете сделать то и другое, но вы должны думать, что имеет смысл и что позволяет легко управлять и использовать многоразовый код. Здесь я предположил, что перемещение аналогично для всех направлений, и стрельба различна, поэтому я выбрал этот подход.

Я вижу много используемых KeyStroke, что это такое? Являются ли они похожими на KeyEvent?

Да, они имеют аналогичную функцию, но более подходят для использования здесь. См. Их API для информации и о том, как их создать.


Вопросы? Улучшения? Предложения? Оставить комментарий. Получите лучший ответ? Отправьте его.

Ответ 2

Примечание: это не ответ, просто комментарий со слишком большим количеством кода: -)

Получение keyStrokes через getKeyStroke (String) является правильным способом, но требует тщательного чтения api doc:

modifiers := shift | control | ctrl | meta | alt | altGraph
typedID := typed <typedKey>
typedKey := string of length 1 giving Unicode character.
pressedReleasedID := (pressed | released) key
key := KeyEvent key code name, i.e. the name following "VK_".

Последняя строка должна быть лучше точное имя, это имеет значение: для клавиши "вниз" точное имя key code равно VK_DOWN, поэтому параметр должен быть "DOWN" (не "Down" или любой другой вариант букв верхнего/нижнего регистра)

Не совсем интуитивно понятный (читай: пришлось самому копать) получает ключ-ключ к ключу-модификатору. Даже при правильном написании, следующее не будет работать:

KeyStroke control = getKeyStroke("CONTROL"); 

Более подробно в очереди событий awt, keyEvent для одного ключа-модификатора создается самим как модификатор. Чтобы привязываться к управляющему ключу, вам нужен штрих:

KeyStroke control = getKeyStroke("ctrl CONTROL");