JavaFX是否可以直接学FXGL?

现在上班无聊,对桌面开发和游戏感兴趣了。

刚开始学JavaFX画桌面,看到FXGL游戏框架也可以做出很好看的桌面效果,在想是不是可以直接学这个游戏框架。本身是做javaWeb后端的,只想问是不是可以直接学?感谢大佬们多多解惑。

本来不太想写的,有点犯懒,但是另外一个回答让我看不下去了

什么底层是opengl,所以性能比fxgl好得多这种话都能说出来

fxgl底层不仅是opengl,而且在mac上还是metal,如果你的工具在mac上用opengl的话,直接崩了,这就是为什么我把fxgl games那个samples项目给直接bump到17.3的原因,因为低版本的javafx依赖opengl,而opengl在高版本的mac中直接不让用了

如果从底层渲染管道上看,fxgl应该要比libgdx以及其他在mac上用openl的要强得多

感觉像是一个培训机构的人,用javafx弄了点ui就在那边忽悠人,目的是忽悠你去其他培训机构会的技术栈上去

然后正面回答你的问题


先说一下利益相关:我是fxgl的Coordinator,然后常年会贡献一些代码给作者,自从fxgl的embedded模式之后,我就积极参与了该项目的研发,大部分需求都是我自己做游戏过程中遇到的困难,直接反馈给作者,然后修复,有些源码是我写的

现在我们的研发方向主要是animation增强的部分,fxgl的animation还不够强,尤其是比较难应用在一些其他素材领域,比如3d模型的animation

这是我基于fxgl做的一层封装,提供了额外的,比如reduce函数,比如游戏的navigation等功能

https://github.com/chengenzhao/fxcity​github.com/chengenzhao/fxcity

然后正面回答你的问题

javafx能否跳过,然后直接学fxgl

可以,不仅可以,我一直鼓励java ui的新手用这种方式来学习

因为javafx有一些web历史的遗留设计,比如fxml,显然有点跟不上时代

因为新生的flutter,swiftui等,都不再使用xml这种落后的ui布局方式,而是直接用语法增强后的高级语言,比如swift,dart等语法,来直接编写ui,还有安卓上的kotlin也是这种方式

我们通过观察安卓和ios上ui设计的发展,我们可以得出一个结论,就是markup language是落后的设计,只要应用程序自己的开发语言语法能跟得上,就可以这么干

java这些年的语法在逐步增强,添加了一些语法特性,是完全可以胜任直接编写ui这个任务的

而且写起来会比较通俗易懂,很多布局,其实就是一行代码搞定的事

但是呢,javafx在设计之初,那时候,可能是受到了web的影响,认为,应该分成几块

比如ui用xml写,然后加上样式单css,然后再搞出点脚本来简化java的语法

这三块时至今日看,除了css有点用以外,其他两个都是错误

最早的脚本叫做fxscript,已经废弃不用,官方建议你用java代码继续编写

然后fxml,这个是可选的,javafx有好几个模块都是可选的

fxml,webview和swing,这三个其实对于fxgl来说,没什么用,我一般是不用的

那不用xml怎么写ui呢?

简单,直接用java,其实fxml在真正load的时候,javafx会帮你把xml代码翻译成java代码,然后执行

而你可以跳过这一步

xml非常不直观,很多时候你用一行java代码就能解决的事,经过这种复杂的xml封装之后,就变得特别难看,而且啰嗦,ide支持还差,经常跳不出来,好像idea要终极版(收费的)才支持fxml的自动补全,word天

整个ui,我感觉你没有必要这么搞

绝大多数ui,都是2d的平面布局,我建议你搞懂这几个容器:pane,hbox,vbox,stackpane

就能轻松应付几乎所有的布局了

比如pane,可以让你设置每一个放入其中的javafx组件的x,y,width和height

x和y是layout x和layout y,width和height各个组件有不同名字,多数叫做preferred width和preferred height

然后这些都有属性,你要学会binding,会binding之后,布局就很少有什么能难得住你的了

我一般一个ui一个页面就100行代码最多了,就能写完

比如这个界面,我就用了150行左右代码将其完成

fxcity的demo项目
  @Override
  public void initUI(GameScene gameScene, XInput input) {
    var bgStops = List.of(new Stop(0, Color.web("bfd1df")), new Stop(0.3, Color.web("3a74a6")), new Stop(1, Color.web("010425")));
    var bgRadialGradient = new RadialGradient(0, 0, 0.28, 0.33, 0.5, true, CycleMethod.NO_CYCLE, bgStops);

    var rect = new Rectangle();
    rect.setWidth(FXGL.getAppWidth());
    rect.setHeight(FXGL.getAppHeight());

    FillTransition ft = new FillTransition(Duration.millis(3000), rect, Color.TRANSPARENT, Color.web("000", 0.5));
    ft.setCycleCount(-1);
    ft.setAutoReverse(true);

    ft.play();

    var d = new Text("D   ");
    d.setUnderline(true);
    d.setFont(FXGL.getAssetLoader().loadFont("Ewert-Regular.ttf").newFont(FXGL.getAppHeight() / 3.0));
    d.setFill(Color.LIGHTGRAY.brighter());
    d.setStrokeWidth(3);
    d.setStroke(Color.web("3d75b0"));
    d.setStrokeLineJoin(StrokeLineJoin.ROUND);

    var stops = List.of(new Stop(0, Color.WHITE), new Stop(0.4, Color.web("3978ed")), new Stop(1, Color.web("030534")));
    var radialGradient = new RadialGradient(0, 0, 0.28, 0.33, 0.5, true, CycleMethod.NO_CYCLE, stops);

    var logo = SVG.newSVG(this.getLogoString(), 841.897, 562.099, radialGradient);
    var label = new Label();
    label.setDisable(true);
    label.setOpacity(1);
    label.setGraphic(logo);
    logo.setPrefHeight(FXGL.getAppHeight() / 3.0);
    logo.setPrefWidth(logo.getPrefHeight() / 562.099 * 841.897);
    label.translateXProperty().bind(label.widthProperty().map(v -> -v.doubleValue()));
    label.translateYProperty().bind(label.heightProperty().map(v -> -v.doubleValue() / 3));

    var emo = new Text("emo");
    emo.underlineProperty().bind(d.underlineProperty());
    emo.setFont(FXGL.getAssetLoader().loadFont("Girassol-Regular.ttf").newFont(FXGL.getAppHeight() / 3.0));
    emo.fillProperty().bind(d.fillProperty());
    emo.strokeWidthProperty().bind(d.strokeWidthProperty());
    emo.strokeProperty().bind(d.strokeProperty());
    emo.strokeLineCapProperty().bind(d.strokeLineCapProperty());
    emo.strokeLineJoinProperty().bind(d.strokeLineJoinProperty());
    emo.translateXProperty().bind(label.translateXProperty().map(v -> v.doubleValue() * 1.2));
    emo.translateYProperty().bind(d.translateYProperty());

    var textflow = new TextFlow();
    textflow.getChildren().addAll(d, label, emo);

    textflow.setTextAlignment(TextAlignment.CENTER);
    textflow.setMinWidth(Region.USE_PREF_SIZE);//no wrap

    DropShadow dropShadow = new DropShadow(50, Color.web("bfd1df"));
    textflow.setEffect(dropShadow);

    textflow.translateXProperty().bind(XBindings.reduce(d.layoutBoundsProperty(), label.widthProperty().map(Number::doubleValue), emo.layoutBoundsProperty(),
      (xLayout, labelWidth, trikeLayout) -> FXGL.getAppCenter().getX() - (xLayout.getWidth() + trikeLayout.getWidth() - labelWidth * .2) / 2));
    textflow.translateYProperty().bind(textflow.layoutBoundsProperty().map(layout -> FXGL.getAppCenter().getY() - layout.getHeight() * .8));

    //menu
    var gridpane = new GridPane();

    var platformGame = new Text("Platform Game");
    platformGame.setFont(FXGL.getAssetLoader().loadFont("Lato-Bold.ttf").newFont(50));
    platformGame.setFill(Color.WHITE);
    platformGame.setEffect(new Bloom());

    var rogueLikeGame = new Text("Rogue Like Game");
    rogueLikeGame.fontProperty().bind(platformGame.fontProperty());
    rogueLikeGame.fillProperty().bind(platformGame.fillProperty());
    rogueLikeGame.effectProperty().bind(platformGame.effectProperty());

    var dialogScene = new Text("Dialog Scene");
    dialogScene.fontProperty().bind(platformGame.fontProperty());
    dialogScene.fillProperty().bind(platformGame.fillProperty());
    dialogScene.effectProperty().bind(platformGame.effectProperty());

    gridpane.add(platformGame, 1, 0);
    gridpane.add(rogueLikeGame, 1, 1);
    gridpane.add(dialogScene, 1, 2);

    platformGame.setOnMouseEntered(_1 -> {
      if (!fingers.get(0).isVisible()) {
        FXGL.play("finger.wav");
        fingers.forEach(finger -> finger.setVisible(false));
        fingers.get(0).setVisible(true);
      }
    });
    rogueLikeGame.setOnMouseEntered(_1 -> {
      if (!fingers.get(1).isVisible()) {
        FXGL.play("finger.wav");
        fingers.forEach(finger -> finger.setVisible(false));
        fingers.get(1).setVisible(true);
      }
    });
    dialogScene.setOnMouseEntered(_1 -> {
      if (!fingers.get(2).isVisible()) {
        FXGL.play("finger.wav");
        fingers.forEach(finger -> finger.setVisible(false));
        fingers.get(2).setVisible(true);
      }
    });
    platformGame.setOnMouseClicked(_1 -> FXGL.getInput().mockKeyPress(KeyCode.ENTER));
    rogueLikeGame.setOnMouseClicked(_1 -> FXGL.getInput().mockKeyPress(KeyCode.ENTER));
    dialogScene.setOnMouseClicked(_1 -> FXGL.getInput().mockKeyPress(KeyCode.ENTER));

    var svg = generateFinger(platformGame.boundsInLocalProperty().map(b -> b.getHeight() * 0.8));
    fingers.add(svg);
    gridpane.add(svg, 0, 0);

    svg = generateFinger(platformGame.boundsInLocalProperty().map(b -> b.getHeight() * 0.8));
    fingers.add(svg);
    svg.setVisible(false);
    gridpane.add(svg, 0, 1);

    svg = generateFinger(platformGame.boundsInLocalProperty().map(b -> b.getHeight() * 0.8));
    fingers.add(svg);
    svg.setVisible(false);
    gridpane.add(svg, 0, 2);

    gridpane.setHgap(20);
    gridpane.setVgap(20);

    gridpane.setTranslateX(FXGL.getAppCenter().getX() - gridpane.getBoundsInLocal().getWidth() * 2 / 3);
    gridpane.setTranslateY(FXGL.getAppCenter().getY() * 1.2);

    var pane = gameScene.getContentRoot();
    pane.setPrefWidth(FXGL.getAppWidth());
    pane.setPrefHeight(FXGL.getAppHeight());

    gameScene.setBackgroundColor(bgRadialGradient);

    gameScene.addUINodes(rect, textflow, gridpane);
  }

代码轻松易读,光看每一句代码的英语就能猜出来要干啥,源码在这里:https://github.com/chengenzhao/fxcity-demo/blob/main/src/main/java/com/example/fxcitydemo/xgamescenes/Index.java

值得注意的是,我这里都没用binding,因为fxgl的ui设计,做了一个非常棒的做法

就是它允许你自定义game width和height,然后你可以设置加入fxgl的组件的大小,这个大小,就是game width和height这个坐标尺度下的尺寸

比如你可以设置game height为1000,然后你设置,加入其中的一个按钮的高度是100

那么无论这个软件的高度如何缩放,这个按钮的高度,始终是软件高度的100/1000 = 0.1 也就是十分之一

这样就不需要做binding了,如果用binding写的话

那就是,伪码,有些不重要的方法我就不写了,比如h.double value

button.preferredHeightProperty().bind(button.getScene().heightProperty().map(h -> h/10));

对比fxgl里面

button.setPreferredHeight(100);

可以明显感觉到,fxgl的ui写起来简单多了,可读性也要强多了,代码不仅短,而且直观,看懂这种ui布局代码毫无难度


然后解释一下javafx跟fxgl的关系

fxgl是javafx的超集,之所以选择在javafx上做后续研发,很重要一点

我们不想重复造轮子

做游戏的人,应该没有几个人会对gui感兴趣,但是游戏中,你又不得不面对gui

那怎么办?最好的方式就是在gui基础上添加比如物理世界,游戏世界等功能

然后游戏引擎做游戏中该做的事,比如精灵,然后遇到gui,丢给gui去做

这一点上,苹果的swiftui和sprite kit,Google的flutter和flame,java的javafx和fxgl,都是同样的逻辑

同样,我们也不想浪费太多时间在编程语言上,比如gc的迭代更新,比如模块剪裁runtime等功能,这些都交给java sdk也就是jdk去做,fxgl不干这事

也就是说,fxgl对于gui(javafx)和sdk(jdk)的部分,它只用,但是不重新造相关的轮子,节省我们的精力,可以focus在我们真正感兴趣的部分上

fxgl最棒的地方就在于此,可以复用我们对于java以及javafx的经验

这一点上,libgdx就不行了,libgdx的gui就是他们自己画的,而且libgdx至今还没有做模块化处理,所以像jlink等工具,它就用不了,而且他们也没有对mac上的metal做适配,所以现在理论上你在mac上用不了libgdx,除非它做了适配

那fxgl是怎么适配的呢?

嘿嘿,前面说了,fxgl依赖javafx和java,因为java做了适配啊,所以javafx和fxgl就不需要自己去适配了,用java的project lanai就行了,只要升级一下,就能在mac用上metal渲染管道

这就是不重复造轮子的好处

然后怎么在fxgl中用javafx呢?

简单,fxgl提供了一个dsl,也就是方言,这个方言可以用java的static方法调用

你只需要

FXGL.addUINode(node);

就可以了

然后值得注意的是,fxgl的dsl只提供了单个game scene的游戏场景

我基于这个设计,做了一层封装,可以在多个game scene之间切换

fxgl里面的game scene的关系是

每一个game scene,包含有两层,分别是game world和ui层

gameworld包括你的entity,component什么都在这里,比如游戏中的战斗单位,怪物,game world有一个camera摄像头跟随,你可以通过调整camera来调整viewport

ui层则不会随着camera的改变而改变,所以一般你ui的部分,都放在这一层

每一个游戏都有一个主game scene

然后你还可以在主game scene上添加game sub scene

然后每一个game sub scene里面又有一个game scene,然后这个game scene里面又有一个game world和ui层

所以如果你想做一些复杂多层game scene的游戏,你就需要这些东西

不过如果是简单的,就没有必要了

所以综上所述,你完全可以用fxgl取代javafx,不仅写起来更简单,而且功能也更多


最后说一下scene builder,我已经给sb的作者们提出建议,要求他们直接生成java源码,而不需要经过fxml这一步,有点脱裤子放p的感觉

他们表示认同,但是嘛,鬼佬干活的效率……,是吧

我后来在跟gurrit还有fxgl作者还有frank(azul的advocator)的meetup的时候,就提出来,fxml

现在所有人的普遍看法是:fxml你如果觉得不舒服,你就别用,你可以不用

这一步你可以绕开,但是如果你的代码是用scene builder生成的

那么需要一种持久化的工具,这种持久化的工具要让普通用户也能看懂

所以会用fxml,生成,并保存到硬盘上去

但如果你不用scene builder,比如用的是fxgl这些,那么你不需要浪费时间在xml上

javafx还支持fxml是历史原因,类似的模块还有跟swing对接的javafx-swing和跟webview对接的javafx-webview,这三个模块都是可选的,你可以不用

当然可能别人会用,但那是别人的事,反正你觉得不爽就可以不用,林北就没用

如果你只是想写点小玩意儿,小游戏啥的,可以直接学fxgl;顺带就补习了openjfx的api和控件;如果你是对桌面感兴趣,那我劝你还是openjfx起步开始学习;你也说了,fxgl让你眼前一亮的是好看的效果,openjfx本身配合css也是可以做到的,而且自己亲力亲为,理解更透彻不是更好么?再说了,fxgl准确说是利用gui元素对游戏常规开发的一次包装,让开发人员更快的出一些常规类别游戏;不过还是给你分享一下心得吧:我也是javaweb,和你一样,业务写多了,也想整点gui;先后折腾过:

  1. gnome下的gtk3+(c),还有gtk+的动态绑定(py和php);
  2. swing;jdk1.8内置的javafx,jdk17+的openjfx(graalvm aot native);
  3. js栈的electron,
  4. rust的tauri
  5. python的tcl,tk绑定:tkinter,以及和qt的绑定PyQt(5,6)

最后跟你分享下心得吧:

1. 如果你不想离开java,想围绕java,那swing和javafx都可以玩玩,这二者的优势是可以发挥你java熟悉,深入javaweb触碰不到的知识领域;缺点:界面的表达缺乏表达力(相对于h5 的flex布局而言)

2.如果你注重功能和外部硬件的调用:建议你考虑c#或者cpp的QT;更可控(这二者我没深入使用过,就不发表优劣心得言论了)

3.如果你注重效果,美观:那我建议你使用前端js栈的;毕竟当下得益于h5的标准;很多界面效果,用h5+css3都可以很容易的做到,还有布局方面,强烈推荐flex布局;真的很灵活,缺点嘛:electron的打包体积比较大,毕竟一个浏览器内核在里头,rust的tauri打包体积倒是不大(没有浏览器内核,仅一个webkit),但是后端语言需要你去学习rust

4.最后,如果是想日常开发个小工具啥的:比如给财务写个excel导入导出啥的,他们用windows系统,可以考虑下py的tk,几行代码就出活儿了,打包体积也小,win下大约才10M;linux才28M

最后再分享几个我使用javafx开发的工具吧;截图这个是一个纯本地利用javafx+sqlite 存储和查询的密码本;方便自己记录各种公司,系统等等的账号密码;不联网,支持和mobile端的数据交换同步;

其他的因为某些原因就不做过多分享啦(navicat的个人javafx移植复刻,你可以说我抄! 哈哈哈)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值