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

news2025/1/4 7:11:57

在本文中,我们将复刻经典的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() 方法来:

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

  2. 使用我们提供的视图

  3. 从视图生成边界框

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

  5. (在以下情况下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);
    }
}
复制代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/25475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

刷题日记【第十三篇】-笔试必刷题【数根+星际密码+跳台阶扩展问题+快到碗里来】

刷题日记【第十三篇】-笔试必刷题【数根星际密码跳台阶扩展问题快到碗里来】 1.方法区在JVM中也是一个非常重要的区域&#xff0c;它与堆一样&#xff0c;是被【线程共享】的区域。 下面有关JVM内存&#xff0c;说法错误的是&#xff1f;(c) A.程序计数器是一个比较小的内存区…

GO语言之Goroutine和channel

1&#xff0c;goroutine-看一个需求 需求&#xff1a;要求统计1-90000000000的数字中&#xff0c;哪些是素数哦&#xff1f; 分析思路&#xff1a; 1&#xff09;传统的方法&#xff0c;就是使用一个循环&#xff0c;循环的判断各个数是不是素数。 2&#xff09;使用并发或…

Metabase学习教程:提问-5

多级聚合 如何使用查询生成器对多个部分提出问题。 许多分析问题只需四个步骤即可回答&#xff1a; 连接需要的表得到需要的信息。过滤数据使其仅包含期望的记录。分组和聚合这些数据&#xff0c;创造你所需要的价值。可视化结果&#xff0c;方便直观的理解数据告诉了你什么…

SpringBoot 集成JWT实现登录认证

如果文章对你有帮助欢迎【关注❤️❤️❤️点赞&#x1f44d;&#x1f44d;&#x1f44d;收藏⭐⭐⭐】一键三连&#xff01;一起努力&#xff01; 一、JWT简介 JSON Web Token&#xff08;JWT&#xff09;是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递…

基于粒子群优化算法的最佳方式设置无线传感器节点的位置,以减轻由于任何能量耗尽节点而产生的覆盖空洞(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

数据脱敏的安全管理

什么是数据脱敏 我们需要先说下什么是敏感数据&#xff0c; 敏感数据泛指个人信息&#xff08;姓名、电话、住址、健康信息、证件等数据&#xff09;、涉及需要保护的数据。 数据脱敏&#xff0c;是将敏感数据按照一定的规则对敏感数据进行变形&#xff0c;达到保护用户信息安…

提升80%上云集成效率, TA是如何做到的

摘要&#xff1a;基于华为云开天aPaaS&#xff0c;提升80%上云集成效率&#xff0c;降低50%集成成本没有充足资金&#xff0c;没有足够的项目规划和过渡时间&#xff0c;也没有经验丰富的IT团队支持&#xff0c;中小企业的上云路可谓是困难重重。如何帮助企业高效上云、实现降本…

【Globalmapper中文入门到精通系列实验图文教程】(附配套实验数据持续更新)

【Globalmapper中文版入门到精通系列实验图文教程】&#xff08;附配套实验数据持续更新&#xff09; 文章目录一、专栏简介二、文章目录三、数据目录四、传送门一、专栏简介 本专栏为GlobalMapper中文入门实战精品教程&#xff0c;内容主要涉及&#xff1a;Globalmapper23软件…

【Oracle】数据库账号频繁被锁问题解决

文中使用的Oracle版本为11g。 今天在测试环境中遇到了一个问题&#xff0c;如下图&#xff1a; 所有的数据库客户端访问Oracle11g都出现了上面的提示“ORA-28000: the account is locked”&#xff0c;一开始其实并不知道是什么原因引起的问题&#xff0c;到后面才发现是登录错…

Kotlin 开发Android app(九):Android两大布局LinearLayout和RelativeLayout

Kotlin 的基本特性就先写到这里&#xff0c;我们这个系列的定位是基础&#xff0c;也就是能用就好&#xff0c;够用就好&#xff0c;我们不会举太多的例子&#xff0c;但是这些都是最经常用到的特性。 从这节开始就是Kotlin和android 进行结合&#xff0c;使用Kotlin进行安卓应…

基于Spring Cloud的架构使用学习升级之路

引言 Spring Cloud全家桶用了挺长时间了&#xff0c;很长一段时间都是基于已有的架构进行需求研发。今年成为团队技术负责人&#xff0c;承担了新的项目&#xff0c;这是很好的一个机会&#xff0c;于是开启了项目架构升级之路。 架构&#xff0c;是团队项目的根基。在一个团…

数字信号处理-5-傅里叶分析

1 傅里叶系数 傅里叶级数用公式如下&#xff1a; a0、a1、a2、a3…b1、b2、b3…叫做傅里叶系数。cosnx 或 sinnx 中的 n 对应着频率&#xff0c;决定 sin、cos 大小的系数是 an、bn。 2 傅里叶变换 步骤1 求傅里叶系数 从原波形 F(x) 中求傅里叶系数中的 a0、a1、a2、a3……

IPD-产品需求管理过程(1)

一、产品需求管理模型 在确定客户需求时,要考虑影响用户购买标准的八类基本需求($APPEALS),并基于客户视角进行详细分解,形成有针对性的产品。 1.1、需求管理业务流程 二、需求收集流程 2.1、需求收集的来源 路标规划:通过市场管理流程分析,落实到路标规划中的需求…

python中StringIO和BytesIO

1. 类文件对象 最常见的io操作是将磁盘中的文件读到内存以及内存内容写入文I件。还有一种内存和内存之间的IO&#xff0c;叫类文件对象&#xff0c;python中的StringIO和BytesIO就是类文件对象&#xff0c;通俗解释即&#xff1a;像操作文件一样在内存中操作字符串和二进制内容…

基于FPGA的SD卡的数据读写实现(SD NAND FLASH)

文章目录 1、存储芯片分类 2、NOR Flash 与 NAND Flash的区别 3、什么是SD卡&#xff1f; 4、什么是SD NAND&#xff1f; 5、SD NAND的控制时序 6、FPGA实现SD NAND读写 6.1、设计思路 6.2、仿真结果 6.3、实验结果 1、存储芯片分类 目前市面上的存储芯片&#xff0…

如何使用腾讯云提供的WordPress应用镜像搭建博客网站系统!

之前也有写过搭建WordPress的教程&#xff0c;如何使用轻量应用服务器搭建WordPress个人博客使用的是宝塔面板一件搭建的方式&#xff0c;但是还是有一些麻烦&#xff0c;这里我们之间使用腾讯云提供的WordPress应用镜像搭建&#xff0c;感兴趣小伙伴可以参考以下&#xff01; …

嵌入式分享合集108

一、PLC串口通讯的基本知识 这几天弄plc都要神经了 尤其西门子的 太烦了 s7200cn s7200smart s1200 编程软件都不一样~~服 &#xff0c; 然后接线也很烦 好了 正题 电气作业人员在使用PLC的时候会接触到很多的通讯协议以及通讯接口 什么是串口通讯&#xff1f; 串口通讯的使…

阿里强势推出Spring源码进阶宝典:思维脑图+视频教程+笔记文档

这不是准备跳槽了&#xff0c;所以最近摸鱼比较多一些&#xff0c;老大默许了&#xff0c;我觉得我老大还是很好的。也在网上看了一些资料&#xff0c;但是&#xff0c;我发现很多讲解注解的时候&#xff0c;对于一些可以直接点击源码查看的内容讲解的占多数&#xff0c;但是授…

ThreadPoolExecutor 线程池参数详解,执行流程

线程池的使用: public static void main(String[] args) {ThreadFactory sThreadFactory new ThreadFactory() {private final AtomicInteger mCount new AtomicInteger(1);Overridepublic Thread newThread(Runnable r) {int andIncrement mCount.getAndIncrement();return…

成熟的汽车衡称重软件,应具备哪些品质

每台汽车都配电子计算机、打印机各一台&#xff0c;并配相应称重管理软件。制造厂商开发的最新软件应免费及时为买方升级。自动称重系统管理软件选用国内成熟产品&#xff0c;至少在国内有10套以上稳定运行业绩&#xff0c;需配一套容量为2KVA&#xff0c;220V的UPS电源至少满足…