Teil 6: Breakout – ein erweiterer Spieleklassiker im Klonprogramm

FXGL Tutorial: Ein simpler Breakout-Klon mit JavaFX

Almas Baimagambetov

© iChzigo/Shutterstock.com

Willkommen zum sechsten Teil unseres FXGL-11-Tutorials! In diesem Abschnitt werden wir gemeinsam einen Klon des Spieleklassikers Breakout erstellen. Wie immer gilt, dass dieses Tutorial auf FXGL 11.0+ und Java 11+ ausgelegt ist.

In diesem Teil des Tutorials werden wir einen modifizierten Klon des Spieleklassikers Breakout erstellen. Es handelt sich hierbei um eine kleine Erweiterung unseres Pong-Spiels, daher solltet ihr euch versichern, dass ihr den darin verwendeten Code verstanden habt. Zudem sollte sich FXGL 11 (in diesem Fall Version 11.3) bereits auf eurem System befinden, das entweder per Maven, Gradle oder als Uber-JAR geladen werden kann.

FXGL Tutorial: JavaFX-Spiele selbst gestalten

Der Einfachheit halber ist der hier verwendete Code ganz bewusst monolithisch und repetitiv gehalten. Im Gegensatz zu unserem Pong-Klon, werden wir diesmal häufig verwendete FXGL-Konzepte einführen. Der Fokus liegt dementsprechend eher auf den Konzepten als auf dem Spiel. Am Ende gibt es wie immer den kompletten Quellcode.

Das Spiel wird dann so aussehen:

Quelle: GitHub

Quelle: GitHub

Importe

Wir erstellen die Datei BreakoutApp.java, in die wir all die folgenden Objekte importieren und für den Rest des Tutorials ignorieren können. Eine Anmerkung: Mit Hilfe des letzten (statischen) Imports können wir getInput() anstatt FXGL.getInput() schreiben, was den Code kürzer und prägnanter macht.

import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.core.math.FXGLMath;
import com.almasb.fxgl.entity.Entity;
import com.almasb.fxgl.entity.EntityFactory;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.entity.Spawns;
import com.almasb.fxgl.input.UserAction;
import javafx.geometry.Point2D;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

import static com.almasb.fxgl.dsl.FXGL.*;

Code

Dieser Teil des Artikels soll ausschließlich den für dieses Tutorial spezifischen Code erläutern, da der restliche Code zuvor im Pong-Beispiel erklärt wurde.

Jetzt fügen wir einen enum-Typ hinzu:

private enum BreakoutType {
    BRICK, BALL
}

Die meisten FXGL-Spiele besitzen einen ähnlichen Aufzählungstyp, um alle Entitätstypen im Spiel zu definieren. Normalerweise setzt die Physik-Engine diese ein, um die Kollisionen richtig behandeln zu können. Eine große Änderung in diesem Tutorial betrifft die Art und Weise, wie wir die Entitätskonstruktion spezifizieren. Bislang setzten wir Methoden wie spawnSomething() ein. Eine empfohlene Vorgehensweise ist der Einsatz einer Factory, der eine Entität via implements EntityFactory implementiert.

// remove the "static" modifier if the class is in a separate file than BreakoutApp.java
public static class BreakoutFactory implements EntityFactory {

    @Spawns("bat")
    public Entity newBat(SpawnData data) {
        return entityBuilder()
                .from(data)
                .viewWithBBox(new Rectangle(PADDLE_WIDTH, PADDLE_HEIGHT))
                .build();
    }

    @Spawns("ball")
    public Entity newBall(SpawnData data) {
        return entityBuilder()
                .from(data)
                .type(BreakoutType.BALL)
                .viewWithBBox(new Rectangle(BALL_SIZE, BALL_SIZE, Color.BLUE))
                .collidable()
                .with("velocity", new Point2D(BALL_SPEED, BALL_SPEED))
                .build();
    }

    @Spawns("brick")
    public Entity newBrick(SpawnData data) {
        return entityBuilder()
                .from(data)
                .type(BreakoutType.BRICK)
                .viewWithBBox(new Rectangle(BRICK_WIDTH, BRICK_HEIGHT, Color.RED))
                .collidable()
                .build();
    }
}

Dies ist üblicherweise die einzige Klasse, die für das Erstellen der Entitäten im Spiel zuständig ist. Konzentrieren wir uns nun auf die Methode, mit der wir einen Backstein (brick) erscheinen lassen können. Die Annotation @Spawns("brick") informiert FXGL darüber, welchen informellen Entitätsnamen wir benutzen, wenn wir diese Entität erscheinen lassen. Jetzt können wir zum Beispiel spawn("brick") benutzen, um eine Entität vom Typ BRICK erscheinen zu lassen. Die Methodensignatur muss zu public Entity anyMethodName(SpawnData data) passen, damit zusätzliche Argumente durch das SpawnData-Objekt übergeben werden können.

Der Aufruf von .from(data) erlaubt es FXGL, X und Y automatisch zu setzen, welche dazu benutzt wurden, um die Entität erscheinen zu lassen. Zum Beispiel wird spawn("brick", 100, 150) eine Entität des Typs BRICKS bei (100, 150) erscheinen lassen. Indem wir .type(BreakoutType.BRICK) benutzen, können wir unserer Entität einen Typ zuordnen. Zum Schluss benutzen wir .collidable(), um die Entität als kollidierbar zu kennzeichnen. Andere Methoden für das Erscheinen von Entitäten sind im gleichen Format gehalten.

Die Kollisionsbehandlung ist der finale Beitrag dieses Tutorials und sollte keine Schwierigkeiten bereiten:

@Override
protected void initPhysics() {
    onCollisionBegin(BreakoutType.BALL, BreakoutType.BRICK, (ball, brick) -> {
        brick.removeFromWorld();
        Point2D velocity = ball.getObject("velocity");

        if (FXGLMath.randomBoolean()) {
            ball.setProperty("velocity", new Point2D(-velocity.getX(), velocity.getY()));
        } else {
            ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
        }
    });
}

Die Methode onCollisionBegin() kann verwendet werden, um Kollisionen während des ersten Frames ihres Auftretens zu behandeln. Es gibt ähnliche Methoden (etwa onCollision() und onCollisionEnd()), die während bzw. am Ende von Kollisionen ausgelöst werden. Wir geben in einer festen Reihenfolge an, welche Typen mit welchen anderen Typen kollidieren (BreakoutType.BALL, BreakoutType.BRICK) und wir erhalten einen Callback ((ball, brick) -> {}), wenn eine Kollision auftritt, wobei die Reihenfolge der übergebenen Entitäten, die Reihenfolge der von uns angegebenen Typen ist.

Und das war es auch schon – unser Breakout-Klon ist komplett. Der vollständige Quellcode ist, wie immer, unten aufgeführt:

import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.core.math.FXGLMath;
import com.almasb.fxgl.entity.Entity;
import com.almasb.fxgl.entity.EntityFactory;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.entity.Spawns;
import com.almasb.fxgl.input.UserAction;
import javafx.geometry.Point2D;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

import static com.almasb.fxgl.dsl.FXGL.*;

/**
 * @author Almas Baimagambetov (almaslvl@gmail.com)
 */
public class BreakoutApp extends GameApplication {

    private static final int PADDLE_WIDTH = 30;
    private static final int PADDLE_HEIGHT = 100;
    private static final int BRICK_WIDTH = 50;
    private static final int BRICK_HEIGHT = 25;
    private static final int BALL_SIZE = 20;

    private static final int PADDLE_SPEED = 5;
    private static final int BALL_SPEED = 5;

    private Entity paddle1;
    private Entity paddle2;
    private Entity ball;

    @Override
    protected void initSettings(GameSettings settings) {
        settings.setTitle("Breakout");
    }

    @Override
    protected void initInput() {
        getInput().addAction(new UserAction("Up 1") {
            @Override
            protected void onAction() {
                paddle1.translateY(-PADDLE_SPEED);
            }
        }, KeyCode.W);

        getInput().addAction(new UserAction("Down 1") {
            @Override
            protected void onAction() {
                paddle1.translateY(PADDLE_SPEED);
            }
        }, KeyCode.S);

        getInput().addAction(new UserAction("Up 2") {
            @Override
            protected void onAction() {
                paddle2.translateY(-PADDLE_SPEED);
            }
        }, KeyCode.UP);

        getInput().addAction(new UserAction("Down 2") {
            @Override
            protected void onAction() {
                paddle2.translateY(PADDLE_SPEED);
            }
        }, KeyCode.DOWN);
    }

    @Override
    protected void initGame() {
        getGameWorld().addEntityFactory(new BreakoutFactory());

        paddle1 = spawn("bat", 0, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
        paddle2 = spawn("bat", getAppWidth() - PADDLE_WIDTH, getAppHeight() / 2 - PADDLE_HEIGHT / 2);

        ball = spawn("ball", getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);

        for (int i = 0; i < 10; i++) {
            spawn("brick", getAppWidth() / 2 - 200 - BRICK_WIDTH, 100 + i*(BRICK_HEIGHT + 20));
            spawn("brick", getAppWidth() / 2 + 200, 100 + i*(BRICK_HEIGHT + 20));
        }
    }

    @Override
    protected void initPhysics() {
        onCollisionCollectible(BreakoutType.BALL, BreakoutType.BRICK, (brick) -> {
            Point2D velocity = ball.getObject("velocity");

            if (FXGLMath.randomBoolean()) {
                ball.setProperty("velocity", new Point2D(-velocity.getX(), velocity.getY()));
            } else {
                ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
            }
        });
    }

    @Override
    protected void onUpdate(double tpf) {
        Point2D velocity = ball.getObject("velocity");
        ball.translate(velocity);

        if (ball.getX() == paddle1.getRightX()
                && ball.getY() < paddle1.getBottomY()
                && ball.getBottomY() > paddle1.getY()) {
            ball.setProperty("velocity", new Point2D(-velocity.getX(), velocity.getY()));
        }

        if (ball.getRightX() == paddle2.getX()
                && ball.getY() < paddle2.getBottomY()
                && ball.getBottomY() > paddle2.getY()) {
            ball.setProperty("velocity", new Point2D(-velocity.getX(), velocity.getY()));
        }

        if (ball.getX() <= 0) {
            resetBall();
        }

        if (ball.getRightX() >= getAppWidth()) {
            resetBall();
        }

        if (ball.getY() <= 0) {
            ball.setY(0);
            ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
        }

        if (ball.getBottomY() >= getAppHeight()) {
            ball.setY(getAppHeight() - BALL_SIZE);
            ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
        }
    }

    private void resetBall() {
        ball.setPosition(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
        ball.setProperty("velocity", new Point2D(BALL_SPEED, BALL_SPEED));
    }

    private enum BreakoutType {
        BRICK, BALL
    }

    public static class BreakoutFactory implements EntityFactory {

        @Spawns("bat")
        public Entity newBat(SpawnData data) {
            return entityBuilder()
                    .from(data)
                    .viewWithBBox(new Rectangle(PADDLE_WIDTH, PADDLE_HEIGHT))
                    .build();
        }

        @Spawns("ball")
        public Entity newBall(SpawnData data) {
            return entityBuilder()
                    .from(data)
                    .type(BreakoutType.BALL)
                    .viewWithBBox(new Rectangle(BALL_SIZE, BALL_SIZE, Color.BLUE))
                    .collidable()
                    .with("velocity", new Point2D(BALL_SPEED, BALL_SPEED))
                    .build();
        }

        @Spawns("brick")
        public Entity newBrick(SpawnData data) {
            return entityBuilder()
                    .from(data)
                    .type(BreakoutType.BRICK)
                    .viewWithBBox(new Rectangle(BRICK_WIDTH, BRICK_HEIGHT, Color.RED))
                    .collidable()
                    .build();
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Wer mehr über die Kernfunktionen wissen oder mit vorgefertigten Spielen experimentieren will, findet dazu alles auf GitHub.

Verwandte Themen:

Geschrieben von
Almas Baimagambetov
Almas Baimagambetov
Almas is engaged with research in the field of Automated Diagram Generation. He also teaches Computer Science. In his spare time, he enjoys playing video games as well as designing and developing them.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: