Vraag Hoe Key Bindings te gebruiken in plaats van Key Listeners


ik gebruik KeyListeners in mijn code (game of anderszins) als de manier waarop mijn objecten op het scherm reageren op de invoer van de gebruikerssleutel. Hier is mijn code:

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

Ik heb problemen met de reactietijd:

  • Ik moet op het object klikken om het te laten werken.
  • Het antwoord dat ik krijg als ik op een van de toetsen druk, is niet hoe ik wilde dat het werkte - te reactief of te weinig reageerde.

Waarom gebeurt dit en hoe los ik dit op?


16
2018-03-30 06:57


oorsprong


antwoorden:


Dit antwoord legt uit en demonstreert hoe sleutelbindingen te gebruiken in plaats van sleutelluisteraars voor educatieve doeleinden. Het is niet

  • Hoe een spel in Java te schrijven.
  • Hoe goed schrijven van een code eruit zou moeten zien (bijvoorbeeld zichtbaarheid).
  • De meest efficiënte (prestatie- of codewijze) manier om sleutelbindingen te implementeren.

Het is

  • Wat ik zou posten als een antwoord voor iedereen die problemen heeft met belangrijke luisteraars.

Antwoord; Lees de Swing tutorial over belangrijke bindingen.

Ik wil geen handleidingen lezen, vertel me waarom ik de belangrijkste bindingen zou willen gebruiken in plaats van de mooie code die ik al heb!

Nou ja, de Swing tutorial verklaart dat

  • Voor toetscombinaties hoeft u niet op het onderdeel te klikken (om het focus te geven):
    • Verwijdert onverwacht gedrag van het gezichtspunt van de gebruiker.
    • Als u 2 objecten hebt, kunnen deze niet tegelijkertijd worden verplaatst, omdat slechts één van de objecten op een bepaald moment de focus kan hebben (zelfs als u ze aan verschillende toetsen bindt).
  • Sneltoetsen zijn gemakkelijker te onderhouden en te manipuleren:
    • Uitschakelen, opnieuw binden, opnieuw toewijzen van gebruikersacties is veel eenvoudiger.
    • De code is gemakkelijker te lezen.

OK, je overtuigde me om het uit te proberen. Hoe werkt het?

De zelfstudie heeft er een goed hoofdstuk over. Sleutelbindingen omvatten 2 objecten InputMap en ActionMap. InputMap wijst een gebruikersinvoer toe aan een actienaam, ActionMap wijst een actienaam toe aan een Action. Wanneer de gebruiker op een toets drukt, wordt de invoerkaart opgezocht naar de sleutel en wordt een actienaam gevonden, waarna de actiekaart wordt gezocht naar de actienaam en de actie uitvoert.

Ziet er omslachtig uit. Waarom de gebruikersinvoer niet rechtstreeks aan de actie binden en de actienaam verwijderen? Dan hebt u slechts één kaart nodig en geen twee.

Goede vraag! Je zult zien dat dit een van de dingen is die belangrijke bindingen hanteerbaarder maken (uitschakelen, opnieuw binden, enz.).

Ik wil dat je me een volledige code geeft.

Nee (de Swing tutorial heeft werkende voorbeelden).

Je bakt er niks van! Ik haat je!

Hier leest u hoe u een enkele sleutelbinding kunt maken:

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

Merk op dat er 3 zijn InputMaps die reageren op verschillende focusstatussen:

myComponent.getInputMap(JComponent.WHEN_FOCUSED);
myComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
  • WHEN_FOCUSED, die ook wordt gebruikt als er geen argument wordt opgegeven, wordt gebruikt als de component focus heeft. Dit is vergelijkbaar met de case met de belangrijkste luisteraar.
  • WHEN_ANCESTOR_OF_FOCUSED_COMPONENT wordt gebruikt wanneer een gefocusseerd onderdeel zich binnen een component bevindt die is geregistreerd om de actie te ontvangen. Als je veel bemanningsleden in een ruimteschip hebt en je wilt dat het ruimteschip input blijft ontvangen terwijl een van de bemanningsleden focus heeft, gebruik dit dan.
  • WHEN_IN_FOCUSED_WINDOWwordt gebruikt als een component die is geregistreerd om de actie te ontvangen zich in een gericht onderdeel bevindt. Als u veel tanks in een gericht venster hebt en u wilt dat ze allemaal tegelijkertijd invoer ontvangen, gebruik deze dan.

De code die in de vraag wordt weergegeven, ziet er ongeveer zo uit, ervan uitgaande dat beide objecten tegelijkertijd moeten worden beheerd:

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.
        }
    }
}

U ziet dat het scheiden van de invoerkaart van de actiekaart herbruikbare code en betere controle van bindingen mogelijk maken. Bovendien kunt u een actie ook rechtstreeks besturen als u de functionaliteit nodig hebt. Bijvoorbeeld:

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

Zie de Actie-tutorial voor meer informatie.

Ik zie dat je 1 actie gebruikte, verplaats, voor 4 toetsen (richtingen) en 1 actie, vuur, voor 1 sleutel. Waarom geef je niet elke toets zijn eigen actie, of geef je alle toetsen dezelfde actie en zoek je uit wat je moet doen in de actie (zoals in de verplaatsing)?

Goed punt. Technisch gezien kun je beide doen, maar je moet nadenken over wat logisch is en wat zorgt voor eenvoudig beheer en herbruikbare code. Hier veronderstelde ik dat bewegen gelijk is voor alle richtingen en schieten anders is, dus ik koos voor deze benadering.

Ik zie er veel van KeyStrokes gebruikt, wat zijn die? Zijn ze als een KeyEvent?

Ja, ze hebben een vergelijkbare functie, maar zijn meer geschikt om hier te gebruiken. Zie hun API voor informatie en hoe je ze kunt maken.


Vragen? Verbeteringen? Iets te melden? Laat een reactie achter. Heb je een beter antwoord? Post het.


47
2018-03-30 06:57



Let op: dit is niet een antwoord, alleen een opmerking met te veel code :-)

Krijgen van toetsaanslagen via getKeyStroke (String) is de juiste manier - maar moet het api-doc zorgvuldig lezen:

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_".

De laatste regel zou beter moeten zijn exacte naam, dat is het geval: voor de down-toets is de exacte sleutelcodenaam VK_DOWN, dus de parameter moet "DOWN" zijn (niet "Down" of een andere variatie van hoofdletters / kleine letters)

Niet helemaal intuïtief (lees: moest zelf een beetje graven) krijgt een KeyStroke naar een modificatietoets. Zelfs met de juiste spelling werkt het volgende niet:

KeyStroke control = getKeyStroke("CONTROL"); 

Dieper in de AWT-gebeurteniswachtrij wordt een keyEvent voor een enkele modificatietoets met zichzelf gemaakt als modifier. Als u wilt binden aan de besturingssleutel, hebt u de lijn nodig:

KeyStroke control = getKeyStroke("ctrl CONTROL"); 

6
2018-03-30 11:02