This issue recommends 2D and 3D game engines developed using JavaFX/Kotlin——FXGL。
FXGL is a JavafX-based 2D and 3D game engine for developing any type of 2D game, including side-scrolling, platform, arcade, RPG, etc. It also provides plenty of game examples to help us master game development skills.
FXGL features:
- No installation required, out of the box, support Java 8-15, Win/Mac/Linux/Android 8+/iOS 11.0+/Web
- Simple and clean API, more efficient than other engines
- JavaFX’s superset: No need to learn new UI apis
- Real world game development techniques: physical components, interpolating animations, particles, and more
- Games are easily packaged into a single executable.jar or native image
rely on
Maven
<dependency>
<groupId>com.github.almasb</groupId>
<artifactId>fxgl</artifactId>
<version>11.17</version>
</dependency>
Gradle
compile 'com.github.almasb:fxgl:11.17'
Simple example: a table tennis game
By default, FXGL sets the game size to 800×600, which works for our game. You can change these settings and various other Settings by settings.setxxx (). For now, we’ll just set the title and add the entry point -main ().
public class PongApp extends GameApplication {
@Override
protected void initSettings(GameSettings settings) {
settings.setTitle("Pong");
}
public static void main(String[] args) {
launch(args);
}
}
Next, we will define some self-explanatory constants.
private static final int PADDLE_WIDTH = 30;
private static final int PADDLE_HEIGHT = 100;
private static final int BALL_SIZE = 20;
private static final int PADDLE_SPEED = 5;
private static final int BALL_SPEED = 5;
We have three game objects, specifically two OARS and a ball. A game object in FXGL is called an Entity. So, let’s define our entity.
private Entity paddle1;
private Entity paddle2;
private Entity ball;
Next, we’ll look at the input. Unlike some frameworks, there is no need to manually query the input status. In FXGL, we process input by defining actions (what the game should do) and binding them to input triggers (when something is pressed). For example:
@Override
protected void initInput() {
getInput().addAction(new UserAction("Up 1") {
@Override
protected void onAction() {
paddle1.translateY(-PADDLE_SPEED);
}
}, KeyCode.W);
// ...
}
The above means that when W is pressed, paddle moves on the Y-axis -PADDLE_SPEED, which actually means moving the paddle up. Remaining input:
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);
We will now add game variables to record the scores of player 1 and Player 2. We can use int score1; However, FXGL provides a powerful attribute concept that builds on JavaFX attributes. To clarify, every variable in FXGL is stored internally as a JavaFX property and is therefore observable and bindable. We declare variables as follows:
@Override
protected void initGameVars(Map<String, Object> vars) {
vars.put("score1", 0);
vars.put("score2", 0);
}
FXGL will infer the type of each variable based on the default value. In this case 0 is type int, so score1 will be assigned type int. We’ll see later how powerful these variables are compared to the original Java types.
@Override
protected void initGame() {
paddle1 = spawnBat(0, getHeight() / 2 - PADDLE_HEIGHT / 2);
paddle2 = spawnBat(getWidth() - PADDLE_WIDTH, getHeight() / 2 - PADDLE_HEIGHT / 2);
ball = spawnBall(getWidth() / 2 - BALL_SIZE / 2, getHeight() / 2 - BALL_SIZE / 2);
}
private Entity spawnBat(double x, double y) {
return Entities.builder()
.at(x, y)
.viewFromNodeWithBBox(new Rectangle(PADDLE_WIDTH, PADDLE_HEIGHT))
.buildAndAttach();
}
private Entity spawnBall(double x, double y) {
return Entities.builder()
.at(x, y)
.viewFromNodeWithBBox(new Rectangle(BALL_SIZE, BALL_SIZE))
.with("velocity", new Point2D(BALL_SPEED, BALL_SPEED))
.buildAndAttach();
}
We require Entities builders to:
- Create a new entity at given x, y
- Use the views we provide
- Generates a bounding box from the view
- Adds the created entity to the game world.
- Added a new entity attribute of type Point2D called “Speed”
After that, we designed a UI consisting of two Text objects. Importantly, we bind the text properties of these objects to the two variables we created earlier. This is one of the powerful features provided by FXGL variables. More specifically, when score1 updates, the text of the textScore1UI object is automatically updated.
@Override
protected void initUI() {
Text textScore1 = getUIFactory().newText("", Color.BLACK, 22);
Text textScore2 = getUIFactory().newText("", Color.BLACK, 22);
textScore1.setTranslateX(10);
textScore1.setTranslateY(50);
textScore2.setTranslateX(getWidth() - 30);
textScore2.setTranslateY(50);
textScore1.textProperty().bind(getGameState().intProperty("score1").asString());
textScore2.textProperty().bind(getGameState().intProperty("score2").asString());
getGameScene().addUINodes(textScore1, textScore2);
}
The last part of this game is to update the ticking sound. Typically, FXGL games will use Components to provide functionality to entities on each frame. So there is no need to update the code at all. In this case, as a simple example, we will use the traditional update method as follows:
@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) {
getGameState().increment("score2", +1);
resetBall();
}
if (ball.getRightX() >= getWidth()) {
getGameState().increment("score1", +1);
resetBall();
}
if (ball.getY() <= 0) {
ball.setY(0);
ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
}
if (ball.getBottomY() >= getHeight()) {
ball.setY(getHeight() - BALL_SIZE);
ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
}
}
We take the “speed” attribute of the ball and use it to pan (move) the ball on each frame. We then perform various checks on the position of the ball in relation to the game window and the racket. If the ball hits the top or bottom of the window, then we reverse on the Y-axis. Similarly, if the ball hits the paddle, then we reverse the X-axis. Finally, if the ball misses the racket and hits the side of the screen, the reverse slap is scored and the ball is reset. The reset method is as follows:
private void resetBall() {
ball.setPosition(getWidth() / 2 - BALL_SIZE / 2, getHeight() / 2 - BALL_SIZE / 2);
ball.setProperty("velocity", new Point2D(BALL_SPEED, BALL_SPEED));
}
After running, the game will look like this:
More examples
In addition, FXGL also provides a large number of full game examples, including Bomber, Flying Bird, Mario, Space Ranger, Pac-Man, etc., if you want to learn how to develop a 2D game, you can refer to these examples.
Project address:
https://github.com/AlmasB/FXGLGames
