现在上班无聊,对桌面开发和游戏感兴趣了。
刚开始学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/fxcitygithub.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;先后折腾过:
- gnome下的gtk3+(c),还有gtk+的动态绑定(py和php);
- swing;jdk1.8内置的javafx,jdk17+的openjfx(graalvm aot native);
- js栈的electron,
- rust的tauri
- 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移植复刻,你可以说我抄! 哈哈哈)