欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

记一次JavaFXGL框架的学习心得与体验

最编程 2024-07-23 21:43:26
...

实体类型

大多数游戏都有不同类型的实体,例如玩家、子弹、敌人等。

该类型可能是一个枚举,例如可能如下所示:

 public enum EntityType {
     PLAYER, BULLET, ENEMY
 }

枚举类的名称可以取成你想要的,但应该遵守规范。

你可以用MyGameNameType代替EntityTyp,这取决于什么更适合你。例如,一个太空入侵者的克隆体SpaceInvadersType枚举的名称。

实体工厂

在你的游戏中为不同集合类型创建实体时,建议分别使用一个单独的类来负责。例如,你可以创建一个只制造敌人的类,然后创建一个只制造能量的类,再创建一个专门制造背景和装饰实体的类。这种创建实体的类称为实体工厂。

 public class MyBlockFactory implements EntityFactory {
 ​
     @Spawns("block")
     public Entity newBlock(SpawnData data) {
         return FXGL.entityBuilder(data)
                 .viewFromNode(new Rectangle(70, 70))
                 .build();
     }
 }

您可以向游戏世界添加工厂,如下所示:getGameWorld().addEntityFactory(new MyBlockFactory());。然后您可以使用getGameWorld().spawn("block");生成一个实体,它又调用用@Spawns("block").

实体组件(FXGL 11)

实体

任何你能想到的游戏物体(玩家、硬币、电源、墙壁、灰尘、粒子、武器等等)。是一个实体。一个实体本身只不过是一个普通的对象。组件允许我们将实体塑造成我们喜欢的任何形状。

组件作为数据

在我们的上下文中,组件是实体的一部分。严格地说,组件是一种数据结构,包括修改和查询数据的方法。这类似于如何将实例字段添加到类定义中:

 class Car {
     int moveSpeed;
     Color color;
 }

Car类包含一个实例字段moveSpeed,暗示它可以移动。现在想象我们也有一个玩家角色,它也可以移动。我们可以加上moveSpeedPlayer对象。然而,还有许多其他可移动的游戏对象。所以也许我们应该用继承来代替,有一个抽象类MovableGameObject,或者更好的接口Movable。这很好,但是如果我们想要一个有时能移动,有时不能移动的物体,例如,玩家控制的汽车可以移动,但它自己不能移动。我们不能只是移除一个接口,然后在需要的时候再把它装回去。这就是组件的用武之地。你可能听说过“组成高于继承”。嗯,这是它的极端版本。组件允许我们在运行时“附加”字段,这多酷啊?考虑上面的片段,用组件重写。

 Entity entity = new Entity();
 entity.addComponent(new MoveSpeedComponent()); // 为实体对象添加新的移动组件
 entity.addComponent(new ColorComponent());  // 为实体对象添加新的颜色组件
 ​
 // some time later we can make it immovable
 entity.removeComponent(MoveSpeedComponent.class);  //移除移动组件
 ​
 // or change a component's value like a normal field
 entity.getComponentOptional(ColorComponent.class).ifPresent(color -> {
     color.setValue(Color.RED);
 });

此外,这种方法隔离了每个组件。这意味着我们可以将它附加到任何的实体并自行修改它。

请注意,最好在实体创建期间添加您知道实体将拥有的任何组件。然后,您可以根据需要启用/禁用组件。举个例子,CollidableComponent表示一个实体可能会与某物发生碰撞。假设我们不希望我们的实体在创建时是可聚合的。与其在实体变得可碰撞时添加组件,不如在创建时添加组件并禁用它。通过这样做,我们可以避免使用getComponentOptional()和使用getComponent().

组件作为行为

我们讨论了添加组件就像添加字段一样。嗯,添加一个组件也类似于添加一个方法。组件允许我们让一个实体做一些事情,本质上定义了实体的行为。假设我们想让一个实体成为升降机,这样它就可以把玩家载到山顶。

 entity.addComponent(new LiftComponent());

A ComponentonUpdate()可以实现以提供功能的方法。这就是lift组件可能实现的方式(省略不相关的代码):

 public class LiftComponent extends Component {
     @Override
     public void onUpdate(double tpf) {
         if (timer.elapsed(duration)) {  //如果 (计时器.过去时间(持续时间))
             goingUp = !goingUp;
             timer.capture();
         }
 ​
         entity.translateY(goingUp ? -speed * tpf : speed * tpf);
     }
 }

某些组件将具有需要手动触发的方法。例如,一个非常简单的播放器组件:

 public class PlayerComponent extends Component {
 ​
     // note that this component is injected automatically
     private TransformComponent position;
 ​
     private double speed = 0;
 ​
     @Override
     public void onUpdate(double tpf) {
         speed = tpf * 60;
     }
 ​
     public void up() {
         position.translateY(-5 * speed);
     }
 ​
     public void down() {
         position.translateY(5 * speed);
     }
 ​
     public void left() {
         position.translateX(-5 * speed);
     }
 ​
     public void right() {
         position.translateX(5 * speed);
     }
 }

因此,在输入处理部分,我们可以将键绑定到方法up, down等。然后,用户将能够通过其组件来移动玩家实体。在添加组件的过程中会检查所需的组件,因此确保首先添加所有必需的组件非常重要。

输入(FXGL 11)

有多种方法允许您处理用户输入。

注意:我们只讨论FXGL输入处理。但是,如果出于任何原因需要,您可以获取对底层JavaFX场景对象的引用:getGameScene().getRoot().getScene()并使用JavaFX处理。

FXGL输入处理模型

任何输入触发器(键盘、鼠标或虚拟的)都由FXGL在内部捕获,并由开发人员指定的UserAction处理。用户操作有三种状态:

  • 开始(1次刻度)

  • 动作(一个或多个刻度)

  • 结束(1个刻度)

这些状态对应于用户按压、保持和释放触发器。这个三态系统允许我们提供一个简单的高级API来处理任何类型的输入。考虑一个击打高尔夫球的用户动作:

 UserAction hitBall = new UserAction("Hit") {
     @Override
     protected void onActionBegin() {
         // action just started (key has just been pressed), play swinging animation
     }
 ​
     @Override
     protected void onAction() {
         // action continues (key is held), increase swing power
     }
 ​
     @Override
     protected void onActionEnd() {
         // action finished (key is released), play hitting animation based on swing power
     }
 };

注册用户操作

既然我们已经创建了一个操作hitBall,我们可以要求输入服务注册它:

 @Override
 protected void initInput() {
     Input input = getInput();
 ​
     input.addAction(hitBall, KeyCode.F);
 }

注意:重要的是,所有操作都要在initInput()中注册,因为这是在FXGL初始化例程的早期调用的,所以菜单可以正确显示所有现有的命令。

的第二个参数addAction()是导火索。触发器可以是按键或鼠标按钮。我们说“动作”必然会“触发”,所以在这种情况下hitBall动作被绑定到F钥匙。

使用触发器监听器

您还可以监听一般的触发事件。开始-行动-结束模型同上。

 getInput().addTriggerListener(new TriggerListener() {
     @Override
     protected void onActionBegin(Trigger trigger) {
         System.out.println("Begin: " + trigger);
     }
 ​
     @Override
     protected void onAction(Trigger trigger) {
         System.out.println("On: " + trigger);
     }
 ​
     @Override
     protected void onActionEnd(Trigger trigger) {
         System.out.println("End: " + trigger);
     }
 });

使用上面的监听器,你也可以检查在这个框架中哪个键/按钮被按下了。例如,在onAction()您可以添加:

 if (trigger.isKey()) {
     var keyTrigger = (KeyTrigger) trigger;
 ​
     // key is being pressed now
     var key = keyTrigger.getKey();
 ​
 } else {
     var mouseTrigger = (MouseTrigger) trigger;
 ​
     // btn is being pressed now
     var btn = mouseTrigger.getButton();
 }

查询鼠标状态

您可以随时检查光标位置:

 Point2D cursorPointInWorld = input.getMouseXYWorld();
 Point2D cursorPointInUI = input.getMouseXYUI();

如你所见,我们不仅可以在游戏世界(场景)中查询光标位置,还可以在UI叠加中查询光标位置。对于屏幕不动的游戏,坐标是一样的。然而,想象一下像马里奥这样的平台游戏,你水平移动,世界(摄像机)也跟着你移动。那么世界中的光标可能是x = 3400,y = 450,而UI坐标中的光标将是x = 400,y = 450。当你的游戏世界层需要与UI层交互时,这非常有用。比方说,你捡起一枚硬币,然后硬币慢慢地向计算你收集的硬币的UI对象移动,随后与该对象合并以创建一个流畅的动画。

模仿用户输入

因为FXGL有自己的输入处理机制层,所以您也可以模拟输入。这在教程级别或电影动作中非常有用,也就是说,你需要用播放器做一些事情,并且有代码来做,但是你不想让用户去做。只需拨打:

 // behaves exactly the same as if the user pressed 'W' on the keyboard
 input.mockKeyPress(KeyCode.W);
 ​
 // behaves exactly the same as if the user released 'W' on the keyboard
 input.mockKeyRelease(KeyCode.W);

可以用类似的方式模仿鼠标事件。假设输入绑定,也就是你注册的动作,会相应地被触发。

重新绑定系统操作

您可以将屏幕截图操作(或任何其他操作)重新绑定到您自己的触发器。在…里initInput():

 getInput().rebind(getInput().getActionByName("Screenshot"), KeyCode.F11);

输入修饰符(CTRL、ALT、SHIFT)

一个例子:

 input.addAction(action, keyCode, InputModifier.CTRL);

只有当修饰符按键被按下。

按键序列(组合)

         Input input = FXGL.getInput();
 ​
         var sequence = new InputSequence(KeyCode.Q, KeyCode.W, KeyCode.E, KeyCode.R);
 ​
         // the action fires only when the sequence above (Q, W, E, R) is complete
         // useful for input combos
         input.addAction(new UserAction("Print Line") {
             @Override
             protected void onActionBegin() {
                 System.out.println("Action Begin");
             }
 ​
             @Override
             protected void onAction() {
                 System.out.println("On Action");
             }
 ​
             @Override
             protected void onActionEnd() {
                 System.out.println("Action End");
             }
         }, sequence);

要记住的事情

  • 动作必须有唯一且有意义的名称。

  • 一个动作<->一个触发器策略,即一个动作绑定到一个触发器,并且一个触发器只能绑定一个动作。

Assets

游戏世界(FXGL 11)

游戏世界负责添加、更新和删除实体。它还提供了通过各种标准查询实体的方法。游戏世界中的实体被认为active(这可以检查:entity.isActive()).一旦一个实体从世界上被删除,它就不再可用并被清除。游戏世界实例可以通过调用FXGL.getGameWorld().

添加和删除实体

要将实体添加到游戏世界中,使其成为游戏的一部分,只需调用:

 GameWorld world = getGameWorld();
 Entity e = ...
 world.addEntity(e);

你可以用类似的方法移除游戏世界中存在的实体:

 world.removeEntity(e);

每个实体都知道它所依附的世界。所以,代替上面的,你可以调用一个更方便的版本:

 e.removeFromWorld();

上面的两个调用在语义上是等价的。

问题

下面的代码片段允许您从世界上请求特定的实体。每个查询对实体组件都有一定的先决条件。例如,如果您基于以下内容进行查询TypeComponent,那么没有该组件的所有实体将被自动从搜索中过滤掉。一些查询返回实体列表,其他的-Optional<Entity>表示这种实体可能不存在。

按类型

示例:我们有一个枚举EntityType它包含游戏中实体的有效类型。

 List<Entity> enemies = world.getEntitiesByType(EntityType.ENEMY);

按ID

示例:您可能允许实体的副本。这对于RPG类型的游戏尤其有用,在这类游戏中你会有很多副本。因此,也许我们在游戏世界中放置了多个伪造品,每个都有自己独特的IDComponent。虽然名称相同-“Forge”,但其数字id是不同的。

 Optional<Entity> forge123 = world.getEntityByID("Forge", 123);

按单个

示例:假设我们知道只有一个“玩家”类型的实体。

 Entity player = world.getSingleton(EntityType.PLAYER);

随机地

例子:假设我们知道一个随机的敌人。

 Optional<Entity> enemy = world.getRandom(EntityType.ENEMY);

注意:返回类型是Optional因为我们可能根本没有敌人。

按组件

示例:您想要具有特定组件的实体。

 List<Entity> entityAbove = world.getEntitiesByComponent(MyComponent.class);

按范围

示例:您想要特定选择框中的实体。有助于选择多个实体,查看爆炸物是否应该摧毁一定范围内的物体,或者查看玩家是否可以与某个物体互动。

 List<Entity> entitiesNearby = world.getEntitiesInRange(new Rectangle2D(50, 50, 100, 100));

按过滤器

示例:您有自己的实体规格说明,您希望这些规格说明不属于上述任何类别。

 List<Entity> items = world.getEntitiesFiltered(e -> e.getInt("hp") == 10);

游戏世界属性全局可观察变量(FXGL 11)

您可以在应用程序类中声明游戏变量,如下所示:

     @Override
     protected void initGameVars(Map<String, Object> vars) {
         // this creates an observable integer variable with value 3
         vars.put("lives", 3);
     }
 PropertyMap state = FXGL.getWorldProperties();

有五种类型的已用值:

  • boolean

  • int

  • double

  • String

  • Object(任何不是来自上面的都属于这一类)

每个值都有一个与之关联的名称。可以通过以下方式访问原始值:

 int lives = state.getInt("lives");

每个值都由JavaFX备份Property。这意味着您可以监听更改,也可以绑定到自动更新的属性。属性值可以通过以下方式访问:

 IntegerProperty livesProperty = state.intProperty("lives");

最后,您可以使用以下命令编辑这些值:

 state.setValue("lives", 5);

物理世界 (FXGL 11)

FXGL,像许多其他2D游戏框架一样,使用jbox2d来处理物理。我们只介绍如何在FXGL中使用jbox2d,因为该库有自己的文档。

物理世界 (FXGL 11) ·AlmasB/FXGL Wiki ·GitHub

视窗

FXGL 中的视口承担摄像机的责任。您可以按如下方式获取对视口的引用:

 Viewport viewport = getGameScene().getViewport();

您可以通过调用以下内容手动更改视口的 和 值:x``y

 viewport.setX(...);
 viewport.setY(...);

最有用的功能之一是绑定视口以跟随实体:

 Entity player = ...;
 ​
 // distX and distY is how far the viewport origin should be from the entity
 viewport.bindToEntity(player, distX, distY);

当视口跟随玩家时,它可能会偏离关卡边界。您可以将边界设置为视口可以"徘徊"的距离:

 viewport.setBounds(minX, minY, maxX, maxY);

计时器操作 (FXGL 11)

在游戏中,您通常希望在一段时间或某个时间间隔后运行操作。例如,可以使用以下内容将某些操作安排在 1 秒后仅运行一次。该操作将在 JavaFX 线程上运行。

 getGameTimer().runOnceAfter(() -> {
     // code to run once after 1 second
 }, Duration.seconds(1));

请注意,以上内容等效于FXGL DSL:

 import static com.almasb.fxgl.dsl.FXGL.*;
 ​
 runOnce(() -> {
     // code to run once after 1 second
 }, Duration.seconds(1))

类似的 DSL 函数可用于其他计时器操作。如果您希望某些内容持续运行,则可以使用:

 getGameTimer().runAtInterval(() -> {
     // code to run every 300 ms
 }, Duration.millis(300));

请注意,这些操作由状态计时器控制,这意味着如果您的游戏已暂停,则计时器也会暂停。每个状态 (, 等) 都有自己的计时器,因此您可以选择所需的计时器。GAME``GAME_MENU``MAIN_MENU

计划操作后,将返回对该操作的引用。

 TimerAction timerAction = getGameTimer().runAtInterval(() -> {
     // ...
 }, Duration.seconds(0.5));

您可以在需要时暂停和恢复计时器操作。

timerAction.pause();
timerAction.resume();

最后,如果您不再需要该操作,可以使其过期。

timerAction.expire();

数学函数

数学函数 (FXGL 11) ·AlmasB/FXGL Wiki ·GitHub

保存和加载 (FXGL 11)

import com.almasb.fxgl.app.GameApplication; import com.almasb.fxgl.app.GameSettings; import com.almasb.fxgl.app.MenuItem; import com.almasb.fxgl.core.serialization.Bundle; import com.almasb.fxgl.profile.DataFile; import com.almasb.fxgl.profile.SaveLoadHandler; import javafx.scene.input.KeyCode; import javafx.scene.paint.Color; import javafx.util.Duration;

import java.util.EnumSet; import java.util.Map;

import static com.almasb.fxgl.dsl.FXGL.*;

public class SaveLoadSample extends GameApplication { @Override protected void initSettings(GameSettings settings) { settings.setMainMenuEnabled(true); settings.setEnabledMenuItems(EnumSet.allOf(MenuItem.class)); }

 @Override
 protected void initGameVars(Map<String, Object> vars) {
     vars.put("time", 0.0);
 }
 ​
 @Override
 protected void onPreInit() {
     getSaveLoadService().addHandler(new SaveLoadHandler() {
         @Override
         public void onSave(DataFile data) {
             // create a new bundle to store your data
             var bundle = new Bundle("gameData");
 ​
             // store some data
             double time = getd("time");
             bundle.put("time", time);
 ​
             // give the bundle to data file
             data.putBundle(bundle);
         }
 ​
         @Override
         public void onLoad(DataFile data) {
             // get your previously saved bundle
             var bundle = data.getBundle("gameData");
 ​
             // retrieve some data
             double time = bundle.get("time");
 ​
             // update your game with saved data
             set("time", time);
         }
     });
 }
 ​
 @Override
 protected void initInput() {
     onKeyDown(KeyCode.F, "Save", () -> {
         getSaveLoadService().saveAndWriteTask("save1.sav").run();
     });
 ​
     onKeyDown(KeyCode.G, "Load", () -> {
         getSaveLoadService().readAndLoadTask("save1.sav").run();
     });
 }
 ​
 @Override
 protected void initGame() {
     run(() -> inc("time", 1.0), Duration.seconds(1.0));
 }
 ​
 @Override
 protected void initUI() {
     var text = getUIFactoryService().newText("", Color.BLACK, 18.0);
     text.textProperty().bind(getdp("time").asString());
 ​
     addUINode(text, 100, 100);
 }
 ​
 public static void main(String[] args) {
     launch(args);
 }

}

。。。。。。。。