记一次JavaFXGL框架的学习心得与体验
实体类型
大多数游戏都有不同类型的实体,例如玩家、子弹、敌人等。
该类型可能是一个枚举,例如可能如下所示:
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
,暗示它可以移动。现在想象我们也有一个玩家角色,它也可以移动。我们可以加上moveSpeed
到Player
对象。然而,还有许多其他可移动的游戏对象。所以也许我们应该用继承来代替,有一个抽象类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 Component
有onUpdate()
可以实现以提供功能的方法。这就是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); }
}
。。。。。。。。
上一篇: 懒人必备!GitHub 当下热门看点速览