Java Arena
Step 2 of 8

Steering with the keys

A game reacts to the keyboard, and it keeps moving while nobody touches it. This step adds both: key events, and a game loop that runs many times a second.

Key events

In part 13 a button had setOnAction. For the keyboard, the Scene has setOnKeyPressed and setOnKeyReleased, each taking a handler, usually a lambda. The handler gets a KeyEvent (from javafx.scene.input), and event.getCode() says which key it was, as a KeyCode: an enum with a constant for every key, such as KeyCode.LEFT, KeyCode.UP, KeyCode.SPACE and KeyCode.A.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        Pane pane = new Pane();
        pane.setPrefSize(360, 240);
        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);
        sub.setFill(Color.GOLD);
        sub.setTranslateX(180);
        sub.setTranslateY(120);
        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);

        Scene scene = new Scene(pane);
        scene.setOnKeyPressed(event -> System.out.println("pressed " + event.getCode()));
        scene.setOnKeyReleased(event -> System.out.println("released " + event.getCode()));

        stage.setScene(scene);
        stage.setTitle("Keys");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

After: Press and release Space, hold Left, let go of Left, then press and release A

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Keys"
  Pane 360 x 240
    Rectangle 360 x 240 at (0, 0), fill #191970
    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), fill #ffd700
Output
pressed SPACE
released SPACE
pressed LEFT
released LEFT
pressed A
released A

The key handlers go on the scene, not on a node: the scene gets the keys of its whole window. The event lines are the page's way of pressing keys: press key LEFT presses the left arrow key and holds it, release key LEFT lets go of it, and key SPACE presses and releases Space at once. The keys are named as KeyCode names them (LEFT, RIGHT, UP, DOWN, SPACE, A), and the task cards say them in words: "Hold Left", "let go of Left", "press and release Space".

Holding a key

A game needs to know which keys are held right now, not only when one goes down. So it keeps them in a map, Map<KeyCode, Boolean>: the pressed handler puts true for its key, the released handler false, and the rest of the game asks the map with held.getOrDefault(KeyCode.LEFT, false), which is false for a key that was never pressed.

This matters for one more reason. On a computer, holding a key down makes the system repeat its press, about 30 times a second after a short pause, so a handler that turned the submarine on each press would turn it in jerks. Here a held key isn't repeated at all: it gets one press. A game that keeps the held keys in a map and does its work in the game loop behaves the same either way, which is why the MOOC's game does it too.

The game loop: AnimationTimer

A game redraws its world many times a second. Each of those pictures is a frame, and JavaFX shows about 60 frames a second. An AnimationTimer (from javafx.animation) runs code once for every frame: its method handle is called once a frame, for as long as the timer runs. AnimationTimer is an abstract class, so a game makes a class of its own that extends it, usually right where it's needed:

new AnimationTimer() {
    @Override
    public void handle(long now) {
        // runs once every frame
    }
}.start();

new AnimationTimer() { ... } is an anonymous class: a class without a name that extends AnimationTimer, written and used in one place. Its body overrides handle, and .start() starts the new timer at once. Like a lambda (module 49), its code can use the local variables of start that never change, such as sub below, and the fields of Main.

handle gets a number, now: the time of the frame in nanoseconds. On a computer it comes from the computer's clock; here it's a made-up clock, so a game in these lessons never uses it. To know how many frames have passed, count them in a field.

On this page, frames run only when you ask for them: the event line frames 20 runs 20 frames, calling handle 20 times, and the page records each one. The submarine turns 4 degrees each frame while Left or Right is held:

import java.util.HashMap;
import java.util.Map;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class Main extends Application {
    private Map<KeyCode, Boolean> held = new HashMap<>();

    @Override
    public void start(Stage stage) {
        Pane pane = new Pane();
        pane.setPrefSize(360, 240);
        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);
        sub.setFill(Color.GOLD);
        sub.setTranslateX(180);
        sub.setTranslateY(120);
        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);

        Scene scene = new Scene(pane);
        scene.setOnKeyPressed(event -> held.put(event.getCode(), true));
        scene.setOnKeyReleased(event -> held.put(event.getCode(), false));

        new AnimationTimer() {
            @Override
            public void handle(long now) {
                if (held.getOrDefault(KeyCode.LEFT, false)) {
                    sub.setRotate(sub.getRotate() - 4);
                }
                if (held.getOrDefault(KeyCode.RIGHT, false)) {
                    sub.setRotate(sub.getRotate() + 4);
                }
            }
        }.start();

        stage.setScene(scene);
        stage.setTitle("Steering");
        stage.show();
    }

    public static void main(String[] args) {
        launch(Main.class);
    }
}
The window

After: Hold Right, run 20 frames, let go of Right, hold Left, run 5 frames, let go of Left, then run 10 frames

This window is wider than the screen: scroll it sideways, or read it as text below.

The window as text
Window "Steering"
  Pane 360 x 240
    Rectangle 360 x 240 at (0, 0), fill #191970
    Polygon (-15, -6) (9, -6) (15, 0) (9, 6) (-15, 6), at (180, 120), rotate 60, fill #ffd700
frames run: 35

Right is held for 20 frames (20 times 4 degrees, 80 clockwise), then Left for 5 (20 back), and then 10 frames run with no key held, which change nothing: the submarine ends at 60 degrees. The page shows those 35 frames as an animation. In the window as text, the last line counts them, frames run: 35; the challenges compare the window after the last event and that count, not the frames on the way.

Between the frames lines, nothing moves: a key line only runs the key handlers. On a computer with the real JavaFX, the frames run by themselves and you press the keys while they do.

Your task

Challenge 1 of 3

Fill in the blanks so that releasing a key puts false in the map, handle asks the map whether Left is held, and the timer starts. The test holds Left for 15 frames, so the submarine ends turned -60 degrees.

Challenge 2

The submarine dives and climbs. In the starter code, the submarine is at (180, 120). Keep the held keys in a Map<KeyCode, Boolean> and add a game loop: in every frame, while Up is held the submarine moves up 2 pixels (its translate y goes down by 2), but only if its y is more than 20; while Down is held it moves down 2 pixels, but only if its y is less than 220. Check Up first, then Down. It doesn't turn, and nothing is printed.

The test holds Up for 30 frames, so the submarine ends at (180, 60).

Challenge 3

The submarine's sonar charges while Space is held. Count the frames during which Space is held: in every frame, if Space is held, add 1 to a counter (a field). When Space is released, print Sonar charged for N frames with the counter, and set the counter back to 0. Other keys don't print anything. Keep the held keys in a Map<KeyCode, Boolean>.

The test holds Space for 12 frames, lets go, runs 5 more frames, and then presses and releases Space at once (no frame runs while it's held).

Sonar charged for 12 frames
Sonar charged for 0 frames