Java Arena
Step 3 of 8

Engines on: moving with vectors

The submarine turns. Now it needs to move: forward in the direction its nose points, faster while Up is held, and on by itself when Up is let go, the way things drift in water (and fly in space, in the MOOC's game).

Point2D

A position is two numbers, an x and a y, and so is a movement: "2 pixels right and 1 up in every frame". JavaFX has a class for such pairs, Point2D (from javafx.geometry). A Point2D is immutable, like a String: its methods return a new point and leave the old one as it was.

import javafx.geometry.Point2D;

public class Main {
    public static void main(String[] args) {
        Point2D place = new Point2D(100, 50);
        Point2D velocity = new Point2D(3, -4);

        place.add(velocity);
        System.out.println(place);

        place = place.add(velocity);
        System.out.println(place);
        System.out.println(place.getX() + " and " + place.getY());

        System.out.println(velocity.magnitude());
        System.out.println(velocity.multiply(2));
    }
}
Output
Point2D [x = 100.0, y = 50.0]
Point2D [x = 103.0, y = 46.0]
103.0 and 46.0
5.0
Point2D [x = 6.0, y = -8.0]
  • add(other) returns a new point with the x values added and the y values added. The first place.add(velocity) changes nothing: its result is thrown away. To keep it, assign it: place = place.add(velocity). There's also add(x, y) with two numbers.
  • getX() and getY() return the two numbers, as doubles.
  • magnitude() is the length of the arrow from (0, 0) to the point: the speed, for a velocity. (3, -4) is 5 long, by Pythagoras.
  • multiply(factor) returns the point with both numbers multiplied.

Point2D is a plain class: as here, a program can use it without a window.

A velocity, added every frame

A velocity says how far something moves in one frame: (1.5, -0.5) is 1.5 pixels right and half a pixel up per frame. In each frame, the game adds the velocity to the position:

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
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 {
    private Point2D velocity = new Point2D(1.5, -0.5);

    @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(100);
        sub.setTranslateY(160);
        pane.getChildren().addAll(new Rectangle(360, 240, Color.MIDNIGHTBLUE), sub);

        new AnimationTimer() {
            @Override
            public void handle(long now) {
                sub.setTranslateX(sub.getTranslateX() + velocity.getX());
                sub.setTranslateY(sub.getTranslateY() + velocity.getY());
            }
        }.start();

        stage.setScene(new Scene(pane));
        stage.setTitle("Drifting");
        stage.show();
    }

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

After: Run 80 frames

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

The window as text
Window "Drifting"
  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 (220, 120), fill #ffd700
frames run: 80

After 80 frames the submarine has moved 80 times 1.5 = 120 pixels right and 40 up, to (220, 120). It drifts sideways, though: it points right but moves up as well. Its velocity should come from where it points.

Speeding up where the nose points

A submarine turned by an angle a points in the direction (cos a, sin a): one step of length 1 that way goes Math.cos(a) across and Math.sin(a) down. At 0 degrees that's (1, 0), right; at 90 degrees (0, 1), down, since angles grow clockwise on the screen; at 180 degrees (-1, 0), left; and at -90 degrees (0, -1), up. Java's Math.cos and Math.sin take the angle in radians, not degrees, and Math.toRadians(degrees) converts it.

So to accelerate, a game adds a small step in that direction to the velocity:

double angle = Math.toRadians(sub.getRotate());
velocity = velocity.add(Math.cos(angle) * 0.125, Math.sin(angle) * 0.125);

Each frame with Up held adds 0.125 pixels a frame to the speed, in the direction the submarine points. Let go of Up, and the velocity stays: the submarine keeps going. Turning changes only where the next push goes, so a turned submarine curves slowly into its new direction, as in the MOOC's game.

import java.util.HashMap;
import java.util.Map;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
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<>();
    private Point2D velocity = new Point2D(0, 0);

    @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(60);
        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);
                }
                if (held.getOrDefault(KeyCode.UP, false)) {
                    double angle = Math.toRadians(sub.getRotate());
                    velocity = velocity.add(Math.cos(angle) * 0.125, Math.sin(angle) * 0.125);
                }
                sub.setTranslateX(sub.getTranslateX() + velocity.getX());
                sub.setTranslateY(sub.getTranslateY() + velocity.getY());
            }
        }.start();

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

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

After: Hold Up, run 12 frames, let go of Up, run 20 frames, hold Right, run 15 frames, let go of Right, hold Up, run 10 frames, let go of Up, then run 40 frames

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

The window as text
Window "Engines"
  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 (225.6875, 169.2551948402401), rotate 60, fill #ffd700
frames run: 97

The submarine speeds up to the right for 12 frames (to 1.5 pixels a frame), coasts for 20, turns 60 degrees clockwise, and pushes for 10 frames that way: the velocity becomes the old one plus the new pushes, so it heads down and to the right, but less steeply than its nose. Let it run longer and it leaves the window: step 6 brings it back from the other side.

The numbers in the window as text get long, such as the y of at (225.6875, 169.2551948402401) above: cos and sin of most angles have endless decimals. That's fine, since the same calculation in the same order always gives exactly the same number. It's also why the tasks say exactly how to calculate: Math.toRadians and the order in which things are added. (Math.cos(Math.toRadians(90)) isn't exactly 0 either, but a tiny number of about 0.00000000000000006, because the angle in radians can't be stored exactly.)

Your task

Challenge 1 of 3

The movement without a window. The class Motion keeps a position (x, y) and a velocity (vx, vy), all doubles that start at the given position and a velocity of (0, 0); its toString is written for you and rounds to two decimals. Write its two methods:

  • push(double degrees, double amount) adds to the velocity a step of length amount in the direction degrees: Math.cos of the angle in radians times amount to vx, and Math.sin of it times amount to vy.
  • step() adds the velocity to the position: vx to x and vy to y.

The first test runs this:

Motion m = new Motion(10, 20);
m.push(0, 2);
m.step();
m.step();
System.out.println(m);
m.push(90, 1);
m.step();
System.out.println(m);
at (14.0, 20.0), moving (2.0, 0.0)
at (16.0, 21.0), moving (2.0, 1.0)

Challenge 2

Give the submarine its engine. The starter code turns it with Left and Right (4 degrees a frame). Add a field velocity, a Point2D that starts at (0, 0), and in every frame, after the turning:

  1. if Up is held, add to the velocity a push of 0.125 in the direction of the submarine's rotate: Math.cos and Math.sin of Math.toRadians(sub.getRotate()), each times 0.125, added with velocity.add(...)
  2. then move the submarine by its velocity: add its x to the translate x and its y to the translate y

The test holds Up for 16 frames and then runs 24 frames more: the submarine reaches 2 pixels a frame and ends at (245, 120).

Challenge 3

What does this program print?