Java Arena
Step 5 of 8

Bumping into jellyfish

A jellyfish that drifts through the submarine should end the dive. So the game needs to know when two shapes overlap, and then the sea needs more than two jellyfish.

Shape.intersect

The class Shape has a static method for this: Shape.intersect(a, b) returns a new shape, the area that a and b share, with each one where its translate and rotate put it. Shapes that don't overlap share nothing, and then the box around that empty area, getBoundsInLocal(), has the width -1. So two shapes overlap when that width isn't -1:

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.scene.shape.Shape;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        Pane pane = new Pane();
        pane.setPrefSize(360, 240);
        pane.getChildren().add(new Rectangle(360, 240, Color.MIDNIGHTBLUE));

        Polygon sub = new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6);
        sub.setFill(Color.GOLD);
        sub.setTranslateX(180);
        sub.setTranslateY(100);
        Polygon near = new Polygon(-10, 0, -6, -8, 0, -10, 6, -8, 10, 0, 7, 10, 3, 2, 0, 10, -3, 2, -7, 10);
        near.setFill(Color.ORCHID);
        near.setTranslateX(192);
        near.setTranslateY(108);
        Polygon far = new Polygon(-10, 0, -6, -8, 0, -10, 6, -8, 10, 0, 7, 10, 3, 2, 0, 10, -3, 2, -7, 10);
        far.setFill(Color.ORCHID);
        far.setTranslateX(240);
        far.setTranslateY(60);
        Rectangle rock = new Rectangle(20, 200, 100, 40);
        rock.setFill(Color.DIMGRAY);
        Rectangle reef = new Rectangle(120, 180, 60, 60);
        reef.setFill(Color.CORAL);
        pane.getChildren().addAll(sub, near, far, rock, reef);

        Shape shared = Shape.intersect(sub, near);
        System.out.println("sub and near: " + shared.getBoundsInLocal().getWidth());
        System.out.println("sub and far: " + Shape.intersect(sub, far).getBoundsInLocal().getWidth());
        System.out.println("rock and reef: " + Shape.intersect(rock, reef).getBoundsInLocal().getWidth());

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

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

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

The window as text
Window "Overlaps"
  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, 100), fill #ffd700
    Polygon (-10, 0) (-6, -8) (0, -10) (6, -8) (10, 0) (7, 10) (3, 2) (0, 10) (-3, 2) (-7, 10), at (192, 108), fill #da70d6
    Polygon (-10, 0) (-6, -8) (0, -10) (6, -8) (10, 0) (7, 10) (3, 2) (0, 10) (-3, 2) (-7, 10), at (240, 60), fill #da70d6
    Rectangle 100 x 40 at (20, 200), fill #696969
    Rectangle 60 x 60 at (120, 180), fill #ff7f50
Output
sub and near: 12.0
sub and far: -1.0
rock and reef: -1.0
  • The submarine and the near jellyfish overlap: their shared area is 12 pixels wide.
  • The far jellyfish shares nothing with the submarine: -1.
  • The rock and the reef touch: the rock ends at x 120, where the reef starts. Touching isn't overlapping, since they share a line but no area, so that's -1 too.

The shapes must be in the same pane, as they are in a game. intersect works for polygons, circles and rectangles, turned and moved: the jellyfish's tentacles and the gaps between them count exactly, not only the box around them.

So the class Mover gets one more method:

public boolean collides(Mover other) {
    Shape overlap = Shape.intersect(shape, other.getShape());
    return overlap.getBoundsInLocal().getWidth() != -1;
}

Circles, with a ruler

For two circles there's a simpler test, which needs no JavaFX: they overlap when the distance between their centers is less than the sum of their radii. If it's exactly equal, they only touch. The distance between (x1, y1) and (x2, y2) is, by Pythagoras, the square root of dx² + dy², where dx and dy are the differences of the x and y values: Math.sqrt(dx * dx + dy * dy), as Point2D's distance method computes it. The first challenge below uses this rule.

Many jellyfish, from a seeded Random

A sea with a handful of jellyfish in random places is more fun than two placed by hand. Module 46 made random numbers with Random, and said why a seed matters: new Random(8) gives the same numbers on every run. Here that's not a choice. The page runs your program again from the start for every new key or frames line, so an unseeded Random would put the jellyfish somewhere else on every run, and the game would jump around under your eyes. (The check also compares your window with the model solution's, which needs the same jellyfish.) So every game in these lessons gives its Random a seed.

Stopping the game

When the submarine bumps into a jellyfish, the game is over: the submarine turns red, and the timer stops. stop() is AnimationTimer's method, and inside handle it can be called as just stop(), since handle belongs to the timer. A stopped timer's handle isn't called again, so nothing moves in the frames after it, though they still run and count.

Main.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
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.Rectangle;
import javafx.stage.Stage;

public class Main extends Application {
    private Map<KeyCode, Boolean> held = new HashMap<>();
    private Random random = new Random(8);
    private int frame = 0;

    @Override
    public void start(Stage stage) {
        Pane pane = new Pane();
        pane.setPrefSize(360, 240);
        pane.getChildren().add(new Rectangle(360, 240, Color.MIDNIGHTBLUE));

        Sub sub = new Sub(180, 200);
        pane.getChildren().add(sub.getShape());
        List<Jellyfish> jellyfish = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Jellyfish j = new Jellyfish(random.nextInt(360), random.nextInt(100), random.nextInt(360));
            jellyfish.add(j);
            pane.getChildren().add(j.getShape());
        }

        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) {
                frame++;
                if (held.getOrDefault(KeyCode.LEFT, false)) {
                    sub.turn(-4);
                }
                if (held.getOrDefault(KeyCode.RIGHT, false)) {
                    sub.turn(4);
                }
                if (held.getOrDefault(KeyCode.UP, false)) {
                    sub.accelerate(0.125);
                }
                sub.move();
                jellyfish.forEach(j -> j.move());
                for (Jellyfish j : jellyfish) {
                    if (sub.collides(j)) {
                        sub.getShape().setFill(Color.RED);
                        System.out.println("Bump after " + frame + " frames");
                        stop();
                        break;
                    }
                }
            }
        }.start();

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

    public static void main(String[] args) {
        launch(Main.class);
    }
}
Mover.java
import javafx.geometry.Point2D;
import javafx.scene.shape.Shape;

public abstract class Mover {
    private Shape shape;
    private Point2D velocity;

    public Mover(Shape shape, double x, double y) {
        this.shape = shape;
        this.shape.setTranslateX(x);
        this.shape.setTranslateY(y);
        this.velocity = new Point2D(0, 0);
    }

    public Shape getShape() {
        return shape;
    }

    public void turn(double degrees) {
        shape.setRotate(shape.getRotate() + degrees);
    }

    public void accelerate(double amount) {
        double angle = Math.toRadians(shape.getRotate());
        velocity = velocity.add(Math.cos(angle) * amount, Math.sin(angle) * amount);
    }

    public void move() {
        shape.setTranslateX(shape.getTranslateX() + velocity.getX());
        shape.setTranslateY(shape.getTranslateY() + velocity.getY());
    }

    public boolean collides(Mover other) {
        Shape overlap = Shape.intersect(shape, other.getShape());
        return overlap.getBoundsInLocal().getWidth() != -1;
    }
}
Sub.java
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;

public class Sub extends Mover {

    public Sub(double x, double y) {
        super(new Polygon(-15, -6, 9, -6, 15, 0, 9, 6, -15, 6), x, y);
        getShape().setFill(Color.GOLD);
    }
}
Jellyfish.java
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;

public class Jellyfish extends Mover {

    public Jellyfish(double x, double y, double heading) {
        super(new Polygon(-10, 0, -6, -8, 0, -10, 6, -8, 10, 0, 7, 10, 3, 2, 0, 10, -3, 2, -7, 10), x, y);
        getShape().setFill(Color.ORCHID);
        turn(heading);
        accelerate(0.5);
    }

    @Override
    public void move() {
        super.move();
        turn(1);
    }
}
The window

After: Hold Left, run 22 frames, let go of Left, hold Up, run 10 frames, let go of Up, then run 150 frames

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

The window as text
Window "Jellyfish"
  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 (183.9043811935918, 88.19315122723808), rotate -88, fill #ff0000
    Polygon (-10, 0) (-6, -8) (0, -10) (6, -8) (10, 0) (7, 10) (3, 2) (0, 10) (-3, 2) (-7, 10), at (301.1188496747079, 66.07159430468215), rotate 126, fill #da70d6
    Polygon (-10, 0) (-6, -8) (0, -10) (6, -8) (10, 0) (7, 10) (3, 2) (0, 10) (-3, 2) (-7, 10), at (7.223336435126232, 93.72718241812248), rotate 274, fill #da70d6
    Polygon (-10, 0) (-6, -8) (0, -10) (6, -8) (10, 0) (7, 10) (3, 2) (0, 10) (-3, 2) (-7, 10), at (-6.5676767951966255, 38.06842191749874), rotate 289, fill #da70d6
    Polygon (-10, 0) (-6, -8) (0, -10) (6, -8) (10, 0) (7, 10) (3, 2) (0, 10) (-3, 2) (-7, 10), at (182.88115032529208, 64.07159430468215), rotate 286, fill #da70d6
    Polygon (-10, 0) (-6, -8) (0, -10) (6, -8) (10, 0) (7, 10) (3, 2) (0, 10) (-3, 2) (-7, 10), at (12.813210422927078, 87.73531732552561), rotate 264, fill #da70d6
frames run: 182
Output
Bump after 116 frames

The field frame counts the frames, as step 2 said a game should. The submarine turns 88 degrees to the left, points almost straight up, speeds up for 10 frames, and after 116 frames in all it meets a jellyfish. break leaves the loop over the jellyfish, so the bump is printed once even if two jellyfish touch the submarine in the same frame. The jellyfish drift out of the window: step 6 brings them back.

Your task

Challenge 1 of 3

Write the method overlaps(x1, y1, r1, x2, y2, r2): it returns true when the circle with its center at (x1, y1) and the radius r1 overlaps the circle at (x2, y2) with the radius r2, that is, when the distance between the centers is less than r1 + r2. Circles that only touch don't overlap. The first test runs main:

true
false

Challenge 2

Make the bump count. Two files need a change:

  1. In Mover.java, collides returns false for now. Make it return whether this Mover's shape and the other's overlap, with Shape.intersect.
  2. In Main.java, in handle, after everything has moved, go through the jellyfish in the list's order. For the first one that the submarine collides with: fill the submarine with Color.RED, print Bump!, stop the timer, and leave the loop.

The submarine starts at (60, 120), a jellyfish at (150, 120) drifts toward it, and another at (300, 40) drifts down. The test runs 150 frames.

Bump!

Challenge 3

A swarm from a seeded Random. Add to Main a field random, a new Random(14). The starter code has an empty list of jellyfish, which the game loop moves. Where the comment is, make five jellyfish with a loop. For each one, take three numbers from random, in this order: its x, random.nextInt(360), its y, random.nextInt(80), and its heading, random.nextInt(360). Make the jellyfish with them, add it to the list and add its shape to the pane.

The first test shows the window before any frame runs.