Building a Space Rangers Game with Java FXGL

Filed Under: JavaFX
Fxgl Game Development

In this simple FXGL tutorial, we will develop a game called “Space Ranger”.

What is FXGL?

FXGL is a game engine built on top JavaFX, which is a GUI toolkit for desktop, mobile, and embedded systems.

Why use FXGL over JavaFX?

On its own, JavaFX provides general-purpose rendering and UI capabilities. On top of it, FXGL brings real-world game development techniques and tools, making it easy to develop cross-platform games.

How to Download FXGL JARs?

FXGL can be downloaded as a Maven or Gradle dependency. For example, Maven coordinates are as follows, which can be used with Java 11+.

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

If you get stuck at any point, you can find the full source code link at the end of this tutorial.

Space Rangers Game

Our game idea is relatively simple. We have a bunch of enemies who are trying to get to our base and we have a single base protector — the player. Given this is an introduction tutorial, we will not use any assets, such as images, sounds, and other external resources. When completed, the game will look like this:

Let us begin!

Required Imports

First, let’s take care of all the imports, so we can focus on the code aspect of the tutorial. For simplicity, all of the code will be in a single file, however you may wish to place each class in its own file.

Create a file SpaceRangerApp.java and place the following imports:

import com.almasb.fxgl.animation.Interpolators;
import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.core.math.FXGLMath;
import com.almasb.fxgl.dsl.components.ProjectileComponent;
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.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;

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

That’s it, our first step is done. Note that the last static import simplifies a lot of calls made to FXGL and is the recommended approach.

Game Code

We will now begin writing some code. As with any other application, we start by specifying the entry point to the program. We will also provide some basic settings to FXGL, such as width, height and the title of our game.

public class SpaceRangerApp extends GameApplication {

    @Override
    protected void initSettings(GameSettings settings) {
        settings.setTitle("Space Ranger");
        settings.setWidth(800);
        settings.setHeight(600);
    }

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

Game Objects / Entities

Each game engine has its own terminology for in-game objects. In FXGL, which uses the Entity-Component model, game objects are called entities. Each entity typically has a type, for example, the player, an enemy, a projectile and so on. In our game we are going to have exactly these types just described:

public enum EntityType {
    PLAYER, ENEMY, PROJECTILE
}

FXGL needs to know how to construct these types. To do that we create a factory. As mentioned earlier we are keeping all classes in the same file. At this point, if you prefer, you can move the class “SpaceRangerFactory” into its own file (if you do, just remove the “static” keyword).

public class SpaceRangerApp extends GameApplication {

    public static class SpaceRangerFactory implements EntityFactory {

    }
}

There is a streamlined process for defining entities. Inside “SpaceRangerFactory”, we define a new method that accepts a SpawnData object, returns an Entity object and has a Spawns annotation:

@Spawns("player")
public Entity newPlayer(SpawnData data) {
    var top = new Rectangle(60, 20, Color.BLUE);
    top.setStroke(Color.GRAY);

    var body = new Rectangle(25, 60, Color.BLUE);
    body.setStroke(Color.GRAY);

    var bot = new Rectangle(60, 20, Color.BLUE);
    bot.setStroke(Color.GRAY);
    bot.setTranslateY(40);

    return entityBuilder()
            .type(EntityType.PLAYER)
            .from(data)
            .view(body)
            .view(top)
            .view(bot)
            .build();
}

As readily seen from the annotation, this method spawns a player object (entity). We will now consider the method in detail.

First, we construct the three parts of the player view, which are standard JavaFX rectangles. Note that we are using the “var” syntax. Using the “entityBuilder()”, we specify the type, the data from which to set the position and views of the player entity. This fluent API allows us to build entities in a concise way.

Our next step is to define the projectiles in our game:

@Spawns("projectile")
public Entity newProjectile(SpawnData data) {
    var view = new Rectangle(30, 3, Color.LIGHTBLUE);
    view.setStroke(Color.WHITE);
    view.setArcWidth(15);
    view.setArcHeight(10);

    return entityBuilder()
            .type(EntityType.PROJECTILE)
            .from(data)
            .viewWithBBox(view)
            .collidable()
            .zIndex(-5)
            .with(new ProjectileComponent(new Point2D(1, 0), 760))
            .build();
}

The view is, again, a JavaFX rectangle with rounded corners. This time we call “viewWithBBox()”, rather than just “view()”. The former method automatically generates a bounding box based on the view. Neat!

Next, using “collidable()”, we mark our projectile as an entity that can collide with other entities. We will come back to collisions later on. We set the z-index to a negative value so that it gets drawn before the player (by default each entity has z-index of 0).

Finally, we add a ProjectileComponent, with a speed equal to 760 and a directional vector (1, 0), which means 1 in the X-axis and 0 in the Y-axis, which in turn means to move to the right.

Our last entity to define is of type enemy:

@Spawns("enemy")
public Entity newEnemy(SpawnData data) {
    var view = new Rectangle(80, 20, Color.RED);
    view.setStroke(Color.GRAY);
    view.setStrokeWidth(0.5);

    animationBuilder()
            .interpolator(Interpolators.SMOOTH.EASE_OUT())
            .duration(Duration.seconds(0.5))
            .repeatInfinitely()
            .animate(view.fillProperty())
            .from(Color.RED)
            .to(Color.DARKRED)
            .buildAndPlay();

    return entityBuilder()
            .type(EntityType.ENEMY)
            .from(data)
            .viewWithBBox(view)
            .collidable()
            .with(new ProjectileComponent(new Point2D(-1, 0), FXGLMath.random(50, 150)))
            .build();
}

We already covered the fluent API methods, such as “type()” and “collidable()”, so we will focus on the animation instead.

As you can see, the animation builder also follows a similar fluent API convention. It allows us to set various animation settings, such as duration and how many times to repeat.

We can observe that the animation operates on the “fillProperty()” on the rectangular view, which we are using to represent the enemy. In particular, the fill is animated from RED to DARKRED every 0.5 seconds. Feel free to adjust the animation settings to see what works best for your game.

Our factory class is now complete and we will start bringing our code together.

Input

All of the FXGL input is typically handled inside the “initInput” method, as follows:

@Override
protected void initInput() {
    onKey(KeyCode.W, () -> getGameWorld().getSingleton(EntityType.PLAYER).translateY(-5));
    onKey(KeyCode.S, () -> getGameWorld().getSingleton(EntityType.PLAYER).translateY(5));

    onBtnDown(MouseButton.PRIMARY, () -> {
        double y = getGameWorld().getSingleton(EntityType.PLAYER).getY();
        spawn("projectile", 0, y + 10);
        spawn("projectile", 0, y + 50);
    });
}

The first two calls set up our player movement. More specifically, W and S keys are going to move the player up and down respectively. Our last call sets up the player action, which is shooting. When the primary mouse button is pressed, we spawn our projectiles, which we defined earlier. The two last arguments to the “spawn” function are the x and y values of where to spawn the projectile.

Game Logic

Before we can start the game, we need to initialize some game logic, which we can do as follows:

@Override
protected void initGame() {
    getGameScene().setBackgroundColor(Color.BLACK);

    getGameWorld().addEntityFactory(new SpaceRangerFactory());

    spawn("player", 0, getAppHeight() / 2 - 30);

    run(() -> {
        double x = getAppWidth();
        double y = FXGLMath.random(0, getAppHeight() - 20);

        spawn("enemy", x, y);
    }, Duration.seconds(0.25));
}

We set the game scene background to color black (you may choose a different color to suit your needs).

Next, we add our entity factory — FXGL needs to know how to spawn our entities.

Thereafter, we spawn our player on the left side of the screen since the x value is 0.

Lastly, we set up a timer action that runs every 0.25 seconds. The action is to spawn an enemy at a random Y location.

Physics

Our physics code is trivial since there are not many things that will be colliding in our game.

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

As can be seen above, the only two entity types we care about with respect to collisions are projectile and enemy. We set up a handler that is called when these two types collide, giving us the references to specific entities that collided. Note the order in which the types are defined. This is the order in which entity references are passed in. We want to remove both from the world when they collide.

Update

There are not many things in our game loop. We only want to know when an enemy has reached our base, i.e. the enemy’s X value is less than 0.

@Override
protected void onUpdate(double tpf) {
    var enemiesThatReachedBase = getGameWorld().getEntitiesFiltered(e -> e.isType(EntityType.ENEMY) && e.getX() < 0);

    if (!enemiesThatReachedBase.isEmpty()) {
        showMessage("Game Over!", () -> getGameController().startNewGame());
    }
}

To achieve this, we query the game world to give us all entities whose type is enemy and whose X value is less than 0. If the list is not empty then we lost the game, so we show the appropriate message and restart.

Conclusion

That is the end of this tutorial. You should now be able to run the game in your IDE. The FXGL game engine is fully open-source and the source is available at https://github.com/AlmasB/FXGL.

The full source code of this tutorial is available at https://github.com/AlmasB/FXGLGames/tree/master/SpaceRanger.

    Leave a Reply

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

    close
    Generic selectors
    Exact matches only
    Search in title
    Search in content
    Search in posts
    Search in pages