How to write a KeyListener for JavaFX

I want to write a little game where I can move a ball on a JavaFX Panel using the W, A, S, D keys.
I have a getPosX() and setPosX() but I don't know how to write a KeyListener which will e.g. calculate setPosX(getPosX()+1) if I press D.

What do I have to do?


Solution 1:

From a JavaRanch Forum post.

Key press and release handlers are added on the scene and update movement state variables recorded in the application. An animation timer hooks into the JavaFX pulse mechanism (which by default will be capped to fire an event 60 times a second) - so that is a kind of game "loop". In the timer the movement state variables are checked and their delta actions applied to the character position - which in effect moves the character around the screen in response to key presses.

zelda

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
 
/**
 * Hold down an arrow key to have your hero move around the screen.
 * Hold down the shift key to have the hero run.
 */
public class Runner extends Application {
 
    private static final double W = 600, H = 400;
 
    private static final String HERO_IMAGE_LOC =
            "http://icons.iconarchive.com/icons/raindropmemory/legendora/64/Hero-icon.png";
 
    private Image heroImage;
    private Node  hero;
 
    boolean running, goNorth, goSouth, goEast, goWest;
 
    @Override
    public void start(Stage stage) throws Exception {
        heroImage = new Image(HERO_IMAGE_LOC);
        hero = new ImageView(heroImage);
 
        Group dungeon = new Group(hero);
 
        moveHeroTo(W / 2, H / 2);
 
        Scene scene = new Scene(dungeon, W, H, Color.FORESTGREEN);
 
        scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                switch (event.getCode()) {
                    case UP:    goNorth = true; break;
                    case DOWN:  goSouth = true; break;
                    case LEFT:  goWest  = true; break;
                    case RIGHT: goEast  = true; break;
                    case SHIFT: running = true; break;
                }
            }
        });
 
        scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                switch (event.getCode()) {
                    case UP:    goNorth = false; break;
                    case DOWN:  goSouth = false; break;
                    case LEFT:  goWest  = false; break;
                    case RIGHT: goEast  = false; break;
                    case SHIFT: running = false; break;
                }
            }
        });
 
        stage.setScene(scene);
        stage.show();
 
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                int dx = 0, dy = 0;
 
                if (goNorth) dy -= 1;
                if (goSouth) dy += 1;
                if (goEast)  dx += 1;
                if (goWest)  dx -= 1;
                if (running) { dx *= 3; dy *= 3; }
 
                moveHeroBy(dx, dy);
            }
        };
        timer.start();
    }
 
    private void moveHeroBy(int dx, int dy) {
        if (dx == 0 && dy == 0) return;
 
        final double cx = hero.getBoundsInLocal().getWidth()  / 2;
        final double cy = hero.getBoundsInLocal().getHeight() / 2;
 
        double x = cx + hero.getLayoutX() + dx;
        double y = cy + hero.getLayoutY() + dy;
 
        moveHeroTo(x, y);
    }
 
    private void moveHeroTo(double x, double y) {
        final double cx = hero.getBoundsInLocal().getWidth()  / 2;
        final double cy = hero.getBoundsInLocal().getHeight() / 2;
 
        if (x - cx >= 0 &&
            x + cx <= W &&
            y - cy >= 0 &&
            y + cy <= H) {
            hero.relocate(x - cx, y - cy);
        }
    }
 
    public static void main(String[] args) { launch(args); }
}

Please ignore the rest of this answer if the information already provided is sufficient for your purposes.

While the above solution is sufficient to answer this question, if interested, a more sophisticated input handler (with a more general and separated, input and update handling logic), can be found in this demo breakout game:

  • Breakout input handler.

Example generic input handler from the sample breakout game:

class InputHandler implements EventHandler<KeyEvent> {
    final private Set<KeyCode> activeKeys = new HashSet<>();

    @Override
    public void handle(KeyEvent event) {
        if (KeyEvent.KEY_PRESSED.equals(event.getEventType())) {
            activeKeys.add(event.getCode());
        } else if (KeyEvent.KEY_RELEASED.equals(event.getEventType())) {
            activeKeys.remove(event.getCode());
        }
    }

    public Set<KeyCode> getActiveKeys() {
        return Collections.unmodifiableSet(activeKeys);
    }
}

While an ObservableSet with an appropriate set change listener could be used for the set of active keys, I have used an accessor which returns an unmodifiable set of keys which were active at a snapshot in time, because that is what I was interested in here rather than observing changes to the set of active keys in real-time.

Example generic input handler usage:

Scene gameScene = createGameScene();

// register the input handler to the game scene.
InputHandler inputHandler = new InputHandler();
gameScene.setOnKeyPressed(inputHandler);
gameScene.setOnKeyReleased(inputHandler);

gameLoop = createGameLoop();

// . . .

private AnimationTimer createGameLoop() {
    return new AnimationTimer() {
        public void handle(long now) {
            update(now, inputHandler.getActiveKeys());
            if (gameState.isGameOver()) {
                this.stop();
            }
        }
    };
}

public void update(long now, Set<KeyCode> activeKeys) {
    applyInputToPaddle(activeKeys);
    // . . . rest of logic to update game state and view.
}

// The paddle is sprite implementation with
// an in-built velocity setting that is used to
// update its position for each frame.
//
// on user input, The paddle velocity changes 
// to point in the correct predefined direction.
private void applyInputToPaddle(Set<KeyCode> activeKeys) {
    Point2D paddleVelocity = Point2D.ZERO;

    if (activeKeys.contains(KeyCode.LEFT)) {
        paddleVelocity = paddleVelocity.add(paddleLeftVelocity);
    }

    if (activeKeys.contains(KeyCode.RIGHT)) {
        paddleVelocity = paddleVelocity.add(paddleRightVelocity);
    }

    gameState.getPaddle().setVelocity(paddleVelocity);
}

Solution 2:

Scene myScene = new Scene();

KeyCombination cntrlZ = new KeyCodeCombination(KeyCode.Z, KeyCodeCombination.CONTROL_DOWN);
myScene.setOnKeyPressed(new EventHandler<KeyEvent>(){
    @Override
    public void handle(KeyEvent event) {
        if(contrlZ.match(event)){
           //Do something
        }
    }
});