如何为JavaFX编写一个KeyListener

14
我想写一个小游戏,可以使用WASD键在JavaFX面板上移动球。
我的代码中有getPosX()setPosX(),但我不知道如何编写一个 KeyListener,以便在按下 D 键时计算 setPosX(getPosX()+1)
我该怎么做?

1
请查看这个答案 - Roland
3个回答

34

来自JavaRanch论坛帖子

在场景上添加按键按下和释放处理程序,并更新记录在应用程序中的移动状态变量。动画计时器钩入JavaFX脉冲机制(默认情况下将被限制为每秒触发60次事件)-因此这是一种游戏“循环”。在计时器中,检查移动状态变量并应用它们的增量操作到角色位置-这实际上是通过按键响应在屏幕上移动角色的效果。

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

关于过滤器、处理程序和焦点

为了接收按键事件,设置事件处理程序的对象必须是可聚焦的。在这个例子中,处理程序直接设置在场景上,但如果你要在面板而不是场景上设置处理程序,那么它需要是可聚焦的并且有焦点。

如果你想要一个全局拦截点来覆盖或拦截通过内置事件处理程序路由的事件(这些事件将被消耗,例如按钮和文本字段),你可以在场景上使用事件过滤器而不是处理程序。

为了更好地理解处理程序和过滤器之间的区别,请确保学习并理解JavaFX事件教程中所解释的事件捕获和冒泡阶段。

通用输入处理程序

如果已经提供的信息足够您的目的,请忽略此答案的其余部分。
虽然上述解决方案足以回答这个问题,但是如果您有兴趣,可以在这个演示打砖块游戏中找到更复杂的输入处理程序(具有更通用且分离的输入和更新处理逻辑): 打砖块游戏输入处理程序
示例来自样品打砖块游戏的通用输入处理程序:
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);
    }
}

虽然使用具有适当集合更改监听器的ObservableSet可以用于活动键集,但我已经使用了一个访问器,该访问器返回在某个时间点处处于活动状态的键的不可修改集合,因为这是我感兴趣的内容,而不是实时观察活动键集合的变化。

如果您想跟踪按键的顺序,可以使用队列、列表或TreeSet,而非集合(例如,使用TreeSet按键按下的时间排序,则最近按下的键将成为集合中的最后一个元素)。

示例通用输入处理程序用法:

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

当我加载一个fxml文件时,我该如何做呢?似乎不起作用,我是否需要使用@FXML注释来注册监听器? - Flying Swissman
2
@FlyingSwissman 这个例子实际上与FXML没有任何关系。在这个例子中,键事件处理程序被添加到场景中,即使应用程序基于FXML,这些处理程序也不会在FXML中定义。如果您继续对事件处理程序和FXML有疑问,请使用 mcve 提出新问题。 - jewelsea

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

0
使用JNativeHook: https://github.com/kwhat/jnativehook
<!-- https://mvnrepository.com/artifact/com.1stleg/jnativehook -->
<dependency>
  <groupId>com.1stleg</groupId>
  <artifactId>jnativehook</artifactId>
  <version>2.1.0</version>
</dependency>



    private void initKeyListener(Stage primaryStage){
    /* Note: JNativeHook does *NOT* operate on the event dispatching thread.
     * Because Swing components must be accessed on the event dispatching
     * thread, you *MUST* wrap access to Swing components using the
     * SwingUtilities.invokeLater() or EventQueue.invokeLater() methods.
     */
    try {
        GlobalScreen.registerNativeHook();
    } catch (NativeHookException e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
    }
    GlobalScreen.addNativeKeyListener(new NativeKeyListener() {
        public void nativeKeyPressed(NativeKeyEvent e) {
            if ( (e.getModifiers() & NativeKeyEvent.CTRL_MASK) != 0
                    && (e.getModifiers() & NativeKeyEvent.ALT_MASK) != 0
                    && (e.getKeyCode() == NativeKeyEvent.VC_B)){
                logger.debug("key :Hide");
                primaryStage.hide();
            }
            if ( (e.getModifiers() & NativeKeyEvent.CTRL_MASK) != 0
                    && (e.getModifiers() & NativeKeyEvent.SHIFT_MASK) != 0
                    && (e.getModifiers() & NativeKeyEvent.ALT_MASK) != 0
                    && (e.getKeyCode() == NativeKeyEvent.VC_B)){
                logger.debug("key :Show");
                primaryStage.show();
            }
            //System.out.println("Key Pressed: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
        }

        public void nativeKeyReleased(NativeKeyEvent e) {
            //System.out.println("Key Released: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
        }

        public void nativeKeyTyped(NativeKeyEvent e) {
            //System.out.println("Key Typed: " + NativeKeyEvent.getKeyText(e.getKeyCode()));
        }
    });
    /*
    GlobalScreen.addNativeMouseListener(new NativeMouseListener() {
        @Override
        public void nativeMouseReleased(NativeMouseEvent arg0) {
            // TODO Auto-generated method stub
            System.out.println("MouseReleased()");
        }

        @Override
        public void nativeMousePressed(NativeMouseEvent arg0) {
            // TODO Auto-generated method stub
            System.out.println("MousePressed()");
        }

        @Override
        public void nativeMouseClicked(NativeMouseEvent arg0) {
            // TODO Auto-generated method stub
            System.out.println("MouseClicked()");
        }
    });
    */
}

2
如果OP的需求可以完全通过核心JavaFX解决,为什么要使用第三方库呢? - kleopatra
此实现用于通过按键显示和隐藏舞台。 如果它被隐藏了,那么必须以这种方式监听。 - selami tastan
啊,我明白了...但这与问题有什么关系呢? - kleopatra

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接