Getting Started with FXGL Game Development

Avatar
Frank Delporte

Frank Delporte (@frankdelporte) is a Java developer, blogger, author of "Getting started with Java on Raspberry Pi", and contributor to Pi4J. Frank blogs about his experiments with Java, sometimes combined with electronic components, on the Raspberry Pi.

What is FXGL?

FXGL is a JavaFX Game Library (Engine) for Java and Kotlin, created by Almas Baimagambetov.

Why FXGL?

  • No installation or setup required
  • "Out of the box": Java 8-14, Win/Mac/Linux/Android 5.0+/iOS 11.0+/Web
  • Simple and clean API, a higher level than other engines
  • Superset of JavaFX: no need to learn new UI API
  • Real-world game development techniques: Entity-Component, interpolated animations, particles, and many more
  • Games are easily packaged into a single executable .jar or native images

Good for:

  • Any 2D genre (side-scroller / platformer / arcade / RPG)
  • Complex UI controls
  • Hobby / academic / commercial projects
  • Teaching / learning / improving game development skills
  • Fast prototyping of game ideas

About this tutorial

In this tutorial, we will make a very simple clone of Geometry Wars using FXGL. You can find the finished project of this post on my GitHub as a Maven project.

To complete this tutorial you first need to get FXGL either via Maven / Gradle, or as an uber-jar. Ensure you use FXGL 11, the current latest version is 11.11. This tutorial is standalone and does not require previous FXGL knowledge.

Dependencies

I started with an empty Maven project and extended the pom.xml file with this dependency:

<dependency>
    <groupId>com.github.almasb</groupId>
    <artifactId>fxgl</artifactId>
    <version>11.11</version>
</dependency>

Code

The code is split into two classes

  • GeoWarsApp.java which is the main application
  • GeoWarsFactory.java which defines how the graphical elements look like and how they behave

GeoWarsApp minimal code

Create a file GeoWarsApp.java and let's import all of these and forget about them for the rest of the tutorial. Note: the static imports allow us to write for instance getAppHeight() instead of FXGL.getAppHeight(), which makes the code cleaner.

import static com.almasb.fxgl.dsl.FXGL.getAppHeight;
import static com.almasb.fxgl.dsl.FXGL.getAppWidth;
import static com.almasb.fxgl.dsl.FXGL.getGameController;
import static com.almasb.fxgl.dsl.FXGL.getGameWorld;
import static com.almasb.fxgl.dsl.FXGL.onBtnDown;
import static com.almasb.fxgl.dsl.FXGL.onCollisionBegin;
import static com.almasb.fxgl.dsl.FXGL.onKey;
import static com.almasb.fxgl.dsl.FXGL.run;
import static com.almasb.fxgl.dsl.FXGL.showMessage;
import static com.almasb.fxgl.dsl.FXGL.spawn;

import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.entity.Entity;
import com.almasb.fxgl.entity.EntityFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.util.Duration;

Minimal code in the game class

This section will go through each method and explain the major parts of the code. By default FXGL sets the game size to 800x600, which works for our game. You can change these and various other settings, for example settings.setWidth(1280). For now, we will just set the title and add the entry point - main().

public class GeoWarsApp extends GameApplication {

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

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

Defining the game entities

The next step is to think about the types of game objects, which are called entities, that we will have in the game. In this simple example we will have the player, bullets and enemies. To mark these types, we will create an enum:

public enum EntityType {
    PLAYER, BULLET, ENEMY
}

GeoWarsFactory

In this factory we create the element types. In FXGL, there is one place where all entities are created and it is called an entity factory. To create your own factory, we create a second class with the listed imports, again some are static to keep the code clean:

import static com.almasb.fxgl.dsl.FXGL.entityBuilder;
import static com.almasb.fxgl.dsl.FXGL.getAppHeight;
import static com.almasb.fxgl.dsl.FXGL.getAppWidth;
import static com.almasb.fxgl.dsl.FXGL.getInput;
import static com.almasb.fxgl.dsl.FXGLForKtKt.getGameWorld;

import be.webtechie.GeoWarsApp.EntityType;
import com.almasb.fxgl.dsl.components.OffscreenCleanComponent;
import com.almasb.fxgl.dsl.components.ProjectileComponent;
import com.almasb.fxgl.dsl.components.RandomMoveComponent;
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 javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;

public class GeoWarsFactory implements EntityFactory {

}

Define the player entity

The factory needs to know how to create each entity. So, for each type we specified above in the EntityType enum, starting with player first, we must provide the following details:

public class GeoWarsFactory implements EntityFactory {

    @Spawns("player")
    public Entity newPlayer(SpawnData data) {
        return entityBuilder()
                .from(data)
                .type(EntityType.PLAYER)
                .viewWithBBox(new Rectangle(30, 30, Color.BLUE))
                .collidable()
                .build();
    }
}

First, the method signature is of importance. It has an annotation that we can later use to spawn a player. Next, let's have a look at how we define the player.

  • We use entityBuilder() to help us do so.
  • .from(data) sets up typical properties such as the position, which is obtained from SpawnData data.
  • We also set the type of the entity via .type(EntityType.PLAYER).
  • The next line .viewWithBBox(new Rectangle(30, 30, Color.BLUE)) has two purposes: a) it provides the rectangle view for the player and b) it generates a bounding box for collisions from the view.
  • Lastly, we mark the entity as .collidable() and build it.

Extend with the bullet entity

Same approach now for the bullet...

public class GeoWarsFactory implements EntityFactory {

    // ...

    @Spawns("bullet")
    public Entity newBullet(SpawnData data) {
        Entity player = getGameWorld().getSingleton(EntityType.PLAYER);
        Point2D direction = getInput().getMousePositionWorld()
            .subtract(player.getCenter());

        return entityBuilder()
                .from(data)
                .type(EntityType.BULLET)
                .viewWithBBox(new Rectangle(10, 2, Color.BLACK))
                .collidable()
                .with(new ProjectileComponent(direction, 1000))
                .with(new OffscreenCleanComponent())
                .build();
    }
}

First we get the player instance. The second line computes the direction in which the bullet will travel when created, starting from the players center. Excluding the API we already covered above, .with(new ProjectileComponent(direction, 1000)) and .with(new OffscreenCleanComponent()) attach components to our bullet entity. A component can contain data and behaviour and brings new functionality to an entity. For example, ProjectileComponent moves the entity every frame along direction with the given speed. OffscreenCleanComponent, as the name implies, removes the entity from the game if it is beyond the screen bounds.

Finally, we have the enemy entity

And the enemy is the last one we defined in the EntityType enum.

public class GeoWarsFactory implements EntityFactory {

    // ...

    @Spawns("enemy")
    public Entity newEnemy(SpawnData data) {
        Circle circle = new Circle(20, 20, 20, Color.RED);
        circle.setStroke(Color.BROWN);
        circle.setStrokeWidth(2.0);

        return entityBuilder()
                .from(data)
                .type(EntityType.ENEMY)
                .viewWithBBox(circle)
                .collidable()
                .with(new RandomMoveComponent(
                    new Rectangle2D(0, 0, 
                    getAppWidth(), getAppHeight()), 50))
                .build();
    }
}

For our enemy, we will use a circle as the view. Most methods have already been covered above, so we will focus on RandomMoveComponent. This component, using the provided rectangular bounds, moves the entity randomly within these bounds. This simple behaviour is sufficient for our little game.

We are done with the factory class now!

GeoWarsApp further extended

Now FXGL knows how to create the entities, let's go back to the application class GeoWarsApp to add input and logic.

First we add a reference to the factory and our player, which we will spawn later.

public class GeoWarsApp extends GameApplication {

    private final GeoWarsFactory geoWarsFactory = new GeoWarsFactory();
    private Entity player;

    // .. other code
}

Using this reference, we can start adding some user actions, known as input handling:

@Override
protected void initInput() {
    onKey(KeyCode.W, () -> player.translateY(-5));
    onKey(KeyCode.S, () -> player.translateY(5));
    onKey(KeyCode.A, () -> player.translateX(-5));
    onKey(KeyCode.D, () -> player.translateX(5));
    onBtnDown(MouseButton.PRIMARY, () -> 
        spawn("bullet", player.getCenter()));
}

The code above should be self-explanatory and makes use of Java 8+ lambda notation. If you want to be able to shoot while the mouse button is being pressed, rather than just on a single press, you can change onBtnDown to onBtn.

There are two other things to add to our game: initialization logic and physics. Don't worry, whilst it may sound complex, it isn't. Initialization logic:

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

    player = spawn("player", getAppWidth() / 2 - 15, getAppHeight() / 2 - 15);
    geoWarsFactory.setPlayer(player);

    run(() -> spawn("enemy"), Duration.seconds(1.0));
}

First, we add our factory to the game world, so that we can use methods like spawn(). Next, we initialize our player reference by spawning the player entity in the center of the game. We also need to provide this player to our geoWarsFactory as it needs it to define the starting point of new bullets.

The last call sets up a timer that runs every second. Can you guess what happens every second? The answer is: spawn("enemy"), i.e. a new enemy entity is spawned. Since we don't provide any position, the enemy entities will spawn at (0,0).

The physics code is straightforward as we have already set up the most complex things:

@Override
protected void initPhysics() {
    onCollisionBegin(EntityType.BULLET, EntityType.ENEMY, (bullet, enemy) -> {
        bullet.removeFromWorld();
        enemy.removeFromWorld();
    });

    onCollisionBegin(EntityType.ENEMY, EntityType.PLAYER, (enemy, player) -> {
        showMessage("You Died!", () -> {
            getGameController().startNewGame();
        });
    });
}

We set up two collision handlers. The first one handles the collision between the bullet type and the enemy type. When such a collision occurs, we simply remove both entities from the game. In the second handler, we show an information dialog with text "You Died!" and restart the game.

Conclusion

Hit run in your IDE on the main-method and that is all! A first working JavaFX game created with FXGL.

You can get the finished sources from github.com/FDelporte/FXGLFirstTest.

Visit FXGL wiki for more tutorials. Pre-built games are available from itch.io to give an idea of what can be achieved with FXGL. More complex examples are available at FXGLGames.


Note: Used with permission and thanks — originally written by Almas Baimagambetov and published on webtechie.be.

Topics:

Don’t Forget to Share This Post!

Comments (0)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Related Articles

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard