• [翻译] 使用FXGL创建一个简单游戏 Pong (FXGL 11)


    在本文中,我们将复刻经典的Pong游戏。要完成本教程,你首先需要获取FXGL要么通过Maven / Gradle,要么作为uber-jar。确保你使用FXGL 11 (例如11.3)。

    本教程大部分是独立的,但是完成以前的基本教程将对一般理解非常有帮助。完整的源代码可在本页末尾找到。请注意,为简单起见,这里使用的代码是故意单一的和重复的。

    与Pong教程不同,这里将向你介绍常用的FXGL概念。因此,重点是在这些概念上,而不是在游戏上。

    游戏将如下所示:

    引入包

    创建文件PongApp.java让我们import以下这些内容,然后在本教程的其余部分中忘记它们。


    注意: 最后一行import (static) 允许我们写入getInput()而不是FXGL.getInput(),这使得代码简洁。

    1. import com.almasb.fxgl.app.GameApplication;
    2. import com.almasb.fxgl.app.GameSettings;
    3. import com.almasb.fxgl.entity.Entity;
    4. import com.almasb.fxgl.input.UserAction;
    5. import javafx.geometry.Point2D;
    6. import javafx.scene.input.KeyCode;
    7. import javafx.scene.paint.Color;
    8. import javafx.scene.shape.Rectangle;
    9. import javafx.scene.text.Text;
    10. import java.util.Map;
    11. import static com.almasb.fxgl.dsl.FXGL.*;
    12. 复制代码

    代码

    本节将介绍每个方法,并解释代码的主要部分。

    默认情况下,FXGL将游戏尺寸设置为800x600,这对我们的游戏是合适的。

    你可以通过settings.setXXX()改变这些和其他各种设置。现在,我们只需设置标题并添加入口点--main()。

    1. public class PongApp extends GameApplication {
    2. @Override
    3. protected void initSettings(GameSettings settings) {
    4. settings.setTitle("Pong");
    5. }
    6. public static void main(String[] args) {
    7. launch(args);
    8. }
    9. }
    10. 复制代码

    接下来,我们将定义一些常数,这些常数是不言自明的。

    1. private static final int PADDLE_WIDTH = 30;
    2. private static final int PADDLE_HEIGHT = 100;
    3. private static final int BALL_SIZE = 20;
    4. private static final int PADDLE_SPEED = 5;
    5. private static final int BALL_SPEED = 5;
    6. 复制代码

    我们有三个游戏对象,分别是两个球拍和一个球。FXGL中的游戏对象称为Entity。因此,让我们定义我们的Entity:

    1. private Entity paddle1;
    2. private Entity paddle2;
    3. private Entity ball;
    4. 复制代码

    接下来,我们将initInput。与某些框架不同,无需手动查询输入状态。在FXGL中,我们通过定义动作 (游戏应该做什么) 并将它们绑定到输入触发器 onAction(当按下某物时) 来处理输入。例如:

    1. @Override
    2. protected void initInput() {
    3. getInput().addAction(new UserAction("Up 1") {
    4. @Override
    5. protected void onAction() {
    6. paddle1.translateY(-PADDLE_SPEED);
    7. }
    8. }, KeyCode.W);
    9. // ...
    10. }
    11. 复制代码

    上面代码的意思是,当W被按下时,通过-PADDLE_SPEED在Y轴上移动paddle ,这基本上意味着向上移动球拍。

    其余的输入代码如下:

    1. getInput().addAction(new UserAction("Down 1") {
    2. @Override
    3. protected void onAction() {
    4. paddle1.translateY(PADDLE_SPEED);
    5. }
    6. }, KeyCode.S);
    7. getInput().addAction(new UserAction("Up 2") {
    8. @Override
    9. protected void onAction() {
    10. paddle2.translateY(-PADDLE_SPEED);
    11. }
    12. }, KeyCode.UP);
    13. getInput().addAction(new UserAction("Down 2") {
    14. @Override
    15. protected void onAction() {
    16. paddle2.translateY(PADDLE_SPEED);
    17. }
    18. }, KeyCode.DOWN);
    19. 复制代码

    现在我们将添加游戏变量来保持玩家1和玩家2的得分。我们可以直接使用int score1;来创建游戏变量。

    但是,FXGL提供了一个强大的属性概念,它建立在JavaFX属性的基础上。澄清一下,FXGL中的每个变量在内部都被存储为JavaFX属性,因此它是可观察和可绑定的。我们声明变量的方式如下:

    1. @Override
    2. protected void initGameVars(Map<String, Object> vars) {
    3. vars.put("score1", 0);
    4. vars.put("score2", 0);
    5. }
    6. 复制代码

    FXGL会根据默认值来推断每个变量的类型。在这种情况下,0是int类型的,所以score1将被分配为int类型。我们以后会看到这些变量与原始的Java类型相比有多么强大。

    我们现在考虑创建我们的实体。如果你完成了以前的教程,这应该是很简单的。

    1. @Override
    2. protected void initGame() {
    3. paddle1 = spawnBat(0, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
    4. paddle2 = spawnBat(getAppWidth() - PADDLE_WIDTH, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
    5. ball = spawnBall(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
    6. }
    7. private Entity spawnBat(double x, double y) {
    8. return entityBuilder()
    9. .at(x, y)
    10. .viewWithBBox(new Rectangle(PADDLE_WIDTH, PADDLE_HEIGHT))
    11. .buildAndAttach();
    12. }
    13. private Entity spawnBall(double x, double y) {
    14. return entityBuilder()
    15. .at(x, y)
    16. .viewWithBBox(new Rectangle(BALL_SIZE, BALL_SIZE))
    17. .with("velocity", new Point2D(BALL_SPEED, BALL_SPEED))
    18. .buildAndAttach();
    19. }
    20. 复制代码

    我们调用 entityBuilder() 方法来:

    1. 用给定的 x, y 坐标创建新实体

    2. 使用我们提供的视图

    3. 从视图生成边界框

    4. 将创建的实体添加到游戏世界。

    5. (在以下情况下ball) 我们还添加了一个新的实体属性,命名为velocity 的Point2D类型

    接下来,我们设计我们的用户界面,它由两个Text对象组成。重要的是,我们将这些对象的文本属性与我们之前创建的两个变量绑定。这是FXGL变量所提供的强大功能之一。更具体地说,当score1被更新时,textScore1 UI对象的文本将被自动更新。

    1. @Override
    2. protected void initUI() {
    3. Text textScore1 = getUIFactoryService().newText("", Color.BLACK, 22);
    4. Text textScore2 = getUIFactoryService().newText("", Color.BLACK, 22);
    5. textScore1.setTranslateX(10);
    6. textScore1.setTranslateY(50);
    7. textScore2.setTranslateX(getAppWidth() - 30);
    8. textScore2.setTranslateY(50);
    9. textScore1.textProperty().bind(getWorldProperties().intProperty("score1").asString());
    10. textScore2.textProperty().bind(getWorldProperties().intProperty("score2").asString());
    11. getGameScene().addUINodes(textScore1, textScore2);
    12. }
    13. 复制代码

    这个游戏的最后一块是更新勾选。通常情况下,FXGL游戏会在每一帧上使用Component来为实体提供功能。所以更新代码可能根本就不需要。在这种情况下,作为一个简单的例子,我们将使用传统的更新方法,见下文。

    1. @Override
    2. protected void onUpdate(double tpf) {
    3. Point2D velocity = ball.getObject("velocity");
    4. ball.translate(velocity);
    5. if (ball.getX() == paddle1.getRightX()
    6. && ball.getY() < paddle1.getBottomY()
    7. && ball.getBottomY() > paddle1.getY()) {
    8. ball.setProperty("velocity", new Point2D(-velocity.getX(), velocity.getY()));
    9. }
    10. if (ball.getRightX() == paddle2.getX()
    11. && ball.getY() < paddle2.getBottomY()
    12. && ball.getBottomY() > paddle2.getY()) {
    13. ball.setProperty("velocity", new Point2D(-velocity.getX(), velocity.getY()));
    14. }
    15. if (ball.getX() <= 0) {
    16. getWorldProperties().increment("score2", +1);
    17. resetBall();
    18. }
    19. if (ball.getRightX() >= getAppWidth()) {
    20. getWorldProperties().increment("score1", +1);
    21. resetBall();
    22. }
    23. if (ball.getY() <= 0) {
    24. ball.setY(0);
    25. ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
    26. }
    27. if (ball.getBottomY() >= getAppHeight()) {
    28. ball.setY(getAppHeight() - BALL_SIZE);
    29. ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
    30. }
    31. }
    32. 复制代码

    我们使用住球的 "velocity"属性,用它来平移(移动)每一帧的球。然后,我们对球在游戏窗口和球拍上的位置做各种检查。如果球击中了窗口的顶部或底部,那么我们就在Y轴上进行反转。同样,如果球击中了一个球拍,那么我们就在X轴上倒转。最后,如果球没有打中球拍,而是打到了屏幕的一侧,那么对面的球拍就会得分,球就会被重置。重置的方法如下。

    1. private void resetBall() {
    2. ball.setPosition(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
    3. ball.setProperty("velocity", new Point2D(BALL_SPEED, BALL_SPEED));
    4. }
    5. 复制代码

    大功告成 ! 你现在有了一个简单的Pong游戏。可以在下面获得完整的源代码。

    完整源代码

    1. import com.almasb.fxgl.app.GameApplication;
    2. import com.almasb.fxgl.app.GameSettings;
    3. import com.almasb.fxgl.entity.Entity;
    4. import com.almasb.fxgl.input.UserAction;
    5. import javafx.geometry.Point2D;
    6. import javafx.scene.input.KeyCode;
    7. import javafx.scene.paint.Color;
    8. import javafx.scene.shape.Rectangle;
    9. import javafx.scene.text.Text;
    10. import java.util.Map;
    11. import static com.almasb.fxgl.dsl.FXGL.*;
    12. public class PongApp extends GameApplication {
    13. private static final int PADDLE_WIDTH = 30;
    14. private static final int PADDLE_HEIGHT = 100;
    15. private static final int BALL_SIZE = 20;
    16. private static final int PADDLE_SPEED = 5;
    17. private static final int BALL_SPEED = 5;
    18. private Entity paddle1;
    19. private Entity paddle2;
    20. private Entity ball;
    21. @Override
    22. protected void initSettings(GameSettings settings) {
    23. settings.setTitle("Pong");
    24. }
    25. @Override
    26. protected void initInput() {
    27. getInput().addAction(new UserAction("Up 1") {
    28. @Override
    29. protected void onAction() {
    30. paddle1.translateY(-PADDLE_SPEED);
    31. }
    32. }, KeyCode.W);
    33. getInput().addAction(new UserAction("Down 1") {
    34. @Override
    35. protected void onAction() {
    36. paddle1.translateY(PADDLE_SPEED);
    37. }
    38. }, KeyCode.S);
    39. getInput().addAction(new UserAction("Up 2") {
    40. @Override
    41. protected void onAction() {
    42. paddle2.translateY(-PADDLE_SPEED);
    43. }
    44. }, KeyCode.UP);
    45. getInput().addAction(new UserAction("Down 2") {
    46. @Override
    47. protected void onAction() {
    48. paddle2.translateY(PADDLE_SPEED);
    49. }
    50. }, KeyCode.DOWN);
    51. }
    52. @Override
    53. protected void initGameVars(Map<String, Object> vars) {
    54. vars.put("score1", 0);
    55. vars.put("score2", 0);
    56. }
    57. @Override
    58. protected void initGame() {
    59. paddle1 = spawnBat(0, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
    60. paddle2 = spawnBat(getAppWidth() - PADDLE_WIDTH, getAppHeight() / 2 - PADDLE_HEIGHT / 2);
    61. ball = spawnBall(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
    62. }
    63. @Override
    64. protected void initUI() {
    65. Text textScore1 = getUIFactoryService().newText("", Color.BLACK, 22);
    66. Text textScore2 = getUIFactoryService().newText("", Color.BLACK, 22);
    67. textScore1.setTranslateX(10);
    68. textScore1.setTranslateY(50);
    69. textScore2.setTranslateX(getAppWidth() - 30);
    70. textScore2.setTranslateY(50);
    71. textScore1.textProperty().bind(getWorldProperties().intProperty("score1").asString());
    72. textScore2.textProperty().bind(getWorldProperties().intProperty("score2").asString());
    73. getGameScene().addUINodes(textScore1, textScore2);
    74. }
    75. @Override
    76. protected void onUpdate(double tpf) {
    77. Point2D velocity = ball.getObject("velocity");
    78. ball.translate(velocity);
    79. if (ball.getX() == paddle1.getRightX()
    80. && ball.getY() < paddle1.getBottomY()
    81. && ball.getBottomY() > paddle1.getY()) {
    82. ball.setProperty("velocity", new Point2D(-velocity.getX(), velocity.getY()));
    83. }
    84. if (ball.getRightX() == paddle2.getX()
    85. && ball.getY() < paddle2.getBottomY()
    86. && ball.getBottomY() > paddle2.getY()) {
    87. ball.setProperty("velocity", new Point2D(-velocity.getX(), velocity.getY()));
    88. }
    89. if (ball.getX() <= 0) {
    90. getWorldProperties().increment("score2", +1);
    91. resetBall();
    92. }
    93. if (ball.getRightX() >= getAppWidth()) {
    94. getWorldProperties().increment("score1", +1);
    95. resetBall();
    96. }
    97. if (ball.getY() <= 0) {
    98. ball.setY(0);
    99. ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
    100. }
    101. if (ball.getBottomY() >= getAppHeight()) {
    102. ball.setY(getAppHeight() - BALL_SIZE);
    103. ball.setProperty("velocity", new Point2D(velocity.getX(), -velocity.getY()));
    104. }
    105. }
    106. private Entity spawnBat(double x, double y) {
    107. return entityBuilder()
    108. .at(x, y)
    109. .viewWithBBox(new Rectangle(PADDLE_WIDTH, PADDLE_HEIGHT))
    110. .buildAndAttach();
    111. }
    112. private Entity spawnBall(double x, double y) {
    113. return entityBuilder()
    114. .at(x, y)
    115. .viewWithBBox(new Rectangle(BALL_SIZE, BALL_SIZE))
    116. .with("velocity", new Point2D(BALL_SPEED, BALL_SPEED))
    117. .buildAndAttach();
    118. }
    119. private void resetBall() {
    120. ball.setPosition(getAppWidth() / 2 - BALL_SIZE / 2, getAppHeight() / 2 - BALL_SIZE / 2);
    121. ball.setProperty("velocity", new Point2D(BALL_SPEED, BALL_SPEED));
    122. }
    123. public static void main(String[] args) {
    124. launch(args);
    125. }
    126. }
    127. 复制代码

  • 相关阅读:
    vscode连接服务器一直retry
    Vue3 + ElementPlus 前端实现分片上传
    redis持久化
    Java实习生常规技术面试题每日十题Java基础(七)
    【mockserver】linux服务器搭建 easy-mock,用于挡板或模拟接口返回服务器
    图 - 06 关键路径
    C 程序结构
    nacos未授权-CVE-2021-29441复现
    磁性聚苯乙烯纳米微球负载1-Me-AZADO/双硫腙接枝聚苯乙烯微球/氨基酸聚苯乙烯树脂微球的合成与表征
    vue 页面的css样式代码多,怎么把css样式代码做成独立的文件,然后引用进来;以及style的scoped标签介绍以及scoped穿透介绍
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/127977737