在本文中,我们将复刻经典的Pong游戏。要完成本教程,你首先需要获取FXGL要么通过Maven / Gradle,要么作为uber-jar。确保你使用FXGL 11 (例如11.3
)。
本教程大部分是独立的,但是完成以前的基本教程将对一般理解非常有帮助。完整的源代码可在本页末尾找到。请注意,为简单起见,这里使用的代码是故意单一的和重复的。
与Pong教程不同,这里将向你介绍常用的FXGL概念。因此,重点是在这些概念上,而不是在游戏上。
游戏将如下所示:
引入包
创建文件PongApp.java
让我们import以下这些内容,然后在本教程的其余部分中忘记它们。
注意: 最后一行import (static) 允许我们写入getInput()
而不是FXGL.getInput()
,这使得代码简洁。
import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.entity.Entity;
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 javafx.scene.text.Text;
import java.util.Map;
import static com.almasb.fxgl.dsl.FXGL.*;
复制代码
代码
本节将介绍每个方法,并解释代码的主要部分。
默认情况下,FXGL将游戏尺寸设置为800x600,这对我们的游戏是合适的。
你可以通过settings.setXXX()改变这些和其他各种设置。现在,我们只需设置标题并添加入口点--main()。
public class PongApp extends GameApplication {
@Override
protected void initSettings(GameSettings settings) {
settings.setTitle("Pong");
}
public static void main(String[] args) {
launch(args);
}
}
复制代码
接下来,我们将定义一些常数,这些常数是不言自明的。
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;
复制代码
我们有三个游戏对象,分别是两个球拍和一个球。FXGL中的游戏对象称为Entity
。因此,让我们定义我们的Entity:
private Entity paddle1;
private Entity paddle2;
private Entity ball;
复制代码
接下来,我们将initInput。与某些框架不同,无需手动查询输入状态。在FXGL中,我们通过定义动作 (游戏应该做什么) 并将它们绑定到输入触发器 onAction(当按下某物时) 来处理输入。例如:
@Override
protected void initInput() {
getInput().addAction(new UserAction("Up 1") {
@Override
protected void onAction() {
paddle1.translateY(-PADDLE_SPEED);
}
}, KeyCode.W);
// ...
}
复制代码
上面代码的意思是,当W被按下时,通过-PADDLE_SPEED在Y轴上移动paddle
,这基本上意味着向上移动球拍。
其余的输入代码如下:
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);
复制代码
现在我们将添加游戏变量来保持玩家1和玩家2的得分。我们可以直接使用int score1;
来创建游戏变量。
但是,FXGL提供了一个强大的属性概念,它建立在JavaFX属性的基础上。澄清一下,FXGL中的每个变量在内部都被存储为JavaFX属性,因此它是可观察和可绑定的。我们声明变量的方式如下:
@Override
protected void initGameVars(Map<String, Object> vars) {
vars.put("score1", 0);
vars.put("score2", 0);
}
复制代码
FXGL会根据默认值来推断每个变量的类型。在这种情况下,0是int类型的,所以score1将被分配为int类型。我们以后会看到这些变量与原始的Java类型相比有多么强大。
我们现在考虑创建我们的实体。如果你完成了以前的教程,这应该是很简单的。
@Override
protected void initGame() {
paddle1 = spawnBat(0, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
paddle2 = spawnBat(getAppWidth() - PADDLE_WIDTH, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
ball = spawnBall(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
}
private Entity spawnBat(double x, double y) {
return entityBuilder()
.at(x, y)
.viewWithBBox(new Rectangle(PADDLE_WIDTH, PADDLE_HEIGHT))
.buildAndAttach();
}
private Entity spawnBall(double x, double y) {
return entityBuilder()
.at(x, y)
.viewWithBBox(new Rectangle(BALL_SIZE, BALL_SIZE))
.with("velocity", new Point2D(BALL_SPEED, BALL_SPEED))
.buildAndAttach();
}
复制代码
我们调用 entityBuilder()
方法来:
-
用给定的 x, y 坐标创建新实体
-
使用我们提供的视图
-
从视图生成边界框
-
将创建的实体添加到游戏世界。
-
(在以下情况下
ball
) 我们还添加了一个新的实体属性,命名为velocity
的Point2D类型
接下来,我们设计我们的用户界面,它由两个Text
对象组成。重要的是,我们将这些对象的文本属性与我们之前创建的两个变量绑定。这是FXGL变量所提供的强大功能之一。更具体地说,当score1
被更新时,textScore1
UI对象的文本将被自动更新。
@Override
protected void initUI() {
Text textScore1 = getUIFactoryService().newText("", Color.BLACK, 22);
Text textScore2 = getUIFactoryService().newText("", Color.BLACK, 22);
textScore1.setTranslateX(10);
textScore1.setTranslateY(50);
textScore2.setTranslateX(getAppWidth() - 30);
textScore2.setTranslateY(50);
textScore1.textProperty().bind(getWorldProperties().intProperty("score1").asString());
textScore2.textProperty().bind(getWorldProperties().intProperty("score2").asString());
getGameScene().addUINodes(textScore1, textScore2);
}
复制代码
这个游戏的最后一块是更新勾选。通常情况下,FXGL游戏会在每一帧上使用Component
来为实体提供功能。所以更新代码可能根本就不需要。在这种情况下,作为一个简单的例子,我们将使用传统的更新方法,见下文。
@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) {
getWorldProperties().increment("score2", +1);
resetBall();
}
if (ball.getRightX() >= getAppWidth()) {
getWorldProperties().increment("score1", +1);
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()));
}
}
复制代码
我们使用住球的 "velocity"属性,用它来平移(移动)每一帧的球。然后,我们对球在游戏窗口和球拍上的位置做各种检查。如果球击中了窗口的顶部或底部,那么我们就在Y轴上进行反转。同样,如果球击中了一个球拍,那么我们就在X轴上倒转。最后,如果球没有打中球拍,而是打到了屏幕的一侧,那么对面的球拍就会得分,球就会被重置。重置的方法如下。
private void resetBall() {
ball.setPosition(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
ball.setProperty("velocity", new Point2D(BALL_SPEED, BALL_SPEED));
}
复制代码
大功告成 ! 你现在有了一个简单的Pong游戏。可以在下面获得完整的源代码。
完整源代码
import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.entity.Entity;
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 javafx.scene.text.Text;
import java.util.Map;
import static com.almasb.fxgl.dsl.FXGL.*;
public class PongApp extends GameApplication {
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;
private Entity paddle1;
private Entity paddle2;
private Entity ball;
@Override
protected void initSettings(GameSettings settings) {
settings.setTitle("Pong");
}
@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 initGameVars(Map<String, Object> vars) {
vars.put("score1", 0);
vars.put("score2", 0);
}
@Override
protected void initGame() {
paddle1 = spawnBat(0, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
paddle2 = spawnBat(getAppWidth() - PADDLE_WIDTH, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
ball = spawnBall(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
}
@Override
protected void initUI() {
Text textScore1 = getUIFactoryService().newText("", Color.BLACK, 22);
Text textScore2 = getUIFactoryService().newText("", Color.BLACK, 22);
textScore1.setTranslateX(10);
textScore1.setTranslateY(50);
textScore2.setTranslateX(getAppWidth() - 30);
textScore2.setTranslateY(50);
textScore1.textProperty().bind(getWorldProperties().intProperty("score1").asString());
textScore2.textProperty().bind(getWorldProperties().intProperty("score2").asString());
getGameScene().addUINodes(textScore1, textScore2);
}
@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) {
getWorldProperties().increment("score2", +1);
resetBall();
}
if (ball.getRightX() >= getAppWidth()) {
getWorldProperties().increment("score1", +1);
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 Entity spawnBat(double x, double y) {
return entityBuilder()
.at(x, y)
.viewWithBBox(new Rectangle(PADDLE_WIDTH, PADDLE_HEIGHT))
.buildAndAttach();
}
private Entity spawnBall(double x, double y) {
return entityBuilder()
.at(x, y)
.viewWithBBox(new Rectangle(BALL_SIZE, BALL_SIZE))
.with("velocity", new Point2D(BALL_SPEED, BALL_SPEED))
.buildAndAttach();
}
private void resetBall() {
ball.setPosition(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
ball.setProperty("velocity", new Point2D(BALL_SPEED, BALL_SPEED));
}
public static void main(String[] args) {
launch(args);
}
}
复制代码
。