使用Java FXGL构建太空游侠游戏

时间:2020-02-23 14:41:53  来源:igfitidea点击:

在这个简单的FXGL教程中,我们将开发一个名为" Space Ranger"的游戏。

什么是FXGL?

FXGL是基于顶级JavaFX构建的游戏引擎,JavaFX是用于台式机,移动和嵌入式系统的GUI工具包。

为什么在JavaFX上使用FXGL?

JavaFX本身提供了通用的呈现和UI功能。
最重要的是,FXGL带来了现实世界的游戏开发技术和工具,使开发跨平台游戏变得容易。

如何下载FXGL JAR?

FXGL可以作为Maven或者Gradle依赖项下载。
例如,Maven坐标如下,可与Java 11+一起使用。

<dependency>
  <groupId>com.github.almasb</groupId>
  <artifactId>fxgl</artifactId>
  <version>11.8</version>
</dependency>

如果您有任何困难,可以在本教程的结尾找到完整的源代码链接。

太空游侠游戏

我们的游戏思路比较简单。
我们有一堆试图进入我们基地的敌人,而我们只有一个基地保护者-玩家。
鉴于这是一个入门教程,因此我们将不使用任何资产,例如图像,声音和其他外部资源。
完成后,游戏将如下所示:

让我们开始吧!

所需进口

首先,让我们处理所有的导入操作,以便我们专注于本教程的代码方面。
为了简单起见,所有代码都在一个文件中,但是您可能希望将每个类放在自己的文件中。

创建一个文件SpaceRangerApp.java并放置以下导入:

import com.almasb.fxgl.animation.Interpolators;
import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.core.math.FXGLMath;
import com.almasb.fxgl.dsl.components.ProjectileComponent;
import com.almasb.fxgl.entity.Entity;
import com.almasb.fxgl.entity.EntityFactory;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.entity.Spawns;
import javafx.geometry.Point2D;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;

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

就是这样,我们的第一步已经完成。
请注意,最后一次静态导入简化了对FXGL的许多调用,这是推荐的方法。

游戏代码

现在,我们将开始编写一些代码。
与任何其他应用程序一样,我们从指定程序的入口点开始。
我们还将为FXGL提供一些基本设置,例如宽度,高度和游戏标题。

public class SpaceRangerApp extends GameApplication {

  @Override
  protected void initSettings(GameSettings settings) {
      settings.setTitle("Space Ranger");
      settings.setWidth(800);
      settings.setHeight(600);
  }

  public static void main(String[] args) {
      launch(args);
  }
}

游戏对象/实体

每个游戏引擎都有自己的用于游戏中对象的术语。
在使用实体组件模型的FXGL中,游戏对象称为实体。
每个实体通常具有一种类型,例如,玩家,敌人,弹丸等。
在我们的游戏中,我们将恰好描述了以下类型:

public enum EntityType {
  PLAYER, ENEMY, PROJECTILE
}

FXGL需要知道如何构造这些类型。
为此,我们创建了一家工厂。
如前所述,我们将所有类保留在同一文件中。
此时,如果您愿意,可以将" SpaceRangerFactory"类移动到其自己的文件中(如果这样做,只需删除" static"关键字)。

public class SpaceRangerApp extends GameApplication {

  public static class SpaceRangerFactory implements EntityFactory {

  }
}

有一个简化的流程来定义实体。
在" SpaceRangerFactory"内部,我们定义了一个新方法,该方法接受一个SpawnData对象,返回一个Entity对象,并具有一个Spawns注释:

@Spawns("player")
public Entity newPlayer(SpawnData data) {
  var top = new Rectangle(60, 20, Color.BLUE);
  top.setStroke(Color.GRAY);

  var body = new Rectangle(25, 60, Color.BLUE);
  body.setStroke(Color.GRAY);

  var bot = new Rectangle(60, 20, Color.BLUE);
  bot.setStroke(Color.GRAY);
  bot.setTranslateY(40);

  return entityBuilder()
          .type(EntityType.PLAYER)
          .from(data)
          .view(body)
          .view(top)
          .view(bot)
          .build();
}

从注释中可以很容易地看出,该方法产生了一个播放器对象(实体)。
现在,我们将详细考虑该方法。

首先,我们构造播放器视图的三个部分,它们是标准JavaFX矩形。
请注意,我们使用的是" var"语法。
使用" entityBuilder()",我们指定类型,用于设置玩家实体位置和视图的数据。
这种流畅的API使我们能够以简洁的方式构建实体。

我们的下一步是在游戏中定义弹丸:

@Spawns("projectile")
public Entity newProjectile(SpawnData data) {
  var view = new Rectangle(30, 3, Color.LIGHTBLUE);
  view.setStroke(Color.WHITE);
  view.setArcWidth(15);
  view.setArcHeight(10);

  return entityBuilder()
          .type(EntityType.PROJECTILE)
          .from(data)
          .viewWithBBox(view)
          .collidable()
          .zIndex(-5)
          .with(new ProjectileComponent(new Point2D(1, 0), 760))
          .build();
}

该视图再次是带有圆角的JavaFX矩形。
这次我们称" viewWithBBox()",而不仅仅是" view()"。
前一种方法会根据视图自动生成边界框。
整齐!

接下来,使用" collidable()",将射弹标记为可以与其他实体碰撞的实体。
稍后我们将回到冲突中。
我们将z-index设置为负值,以便在玩家之前绘制它(默认情况下,每个实体的z-index为0)。

最后,我们添加一个ProjectileComponent,其速度等于760,方向矢量为(1,0),这意味着X轴为1,Y轴为0,这又意味着向右移动。

我们最后定义的实体是敌人类型:

@Spawns("enemy")
public Entity newEnemy(SpawnData data) {
  var view = new Rectangle(80, 20, Color.RED);
  view.setStroke(Color.GRAY);
  view.setStrokeWidth(0.5);

  animationBuilder()
          .interpolator(Interpolators.SMOOTH.EASE_OUT())
          .duration(Duration.seconds(0.5))
          .repeatInfinitely()
          .animate(view.fillProperty())
          .from(Color.RED)
          .to(Color.DARKRED)
          .buildAndPlay();

  return entityBuilder()
          .type(EntityType.ENEMY)
          .from(data)
          .viewWithBBox(view)
          .collidable()
          .with(new ProjectileComponent(new Point2D(-1, 0), FXGLMath.random(50, 150)))
          .build();
}

我们已经介绍了流畅的API方法,例如" type()"和" collidable()",因此我们将重点放在动画上。

如您所见,动画构建器还遵循类似的流畅API约定。
它允许我们设置各种动画设置,例如持续时间和重复次数。

我们可以观察到动画在矩形视图的" fillProperty()"上运行,我们用它来表示敌人。
特别是,填充会每0.5秒从红色变为暗红色。
随意调整动画设置,以查看最适合您的游戏的动画。

我们的工厂程序现已完成,我们将开始将我们的代码整合在一起。

输入项

通常,所有FXGL输入都在" initInput"方法内部进行处理,如下所示:

@Override
protected void initInput() {
  onKey(KeyCode.W, () -> getGameWorld().getSingleton(EntityType.PLAYER).translateY(-5));
  onKey(KeyCode.S, () -> getGameWorld().getSingleton(EntityType.PLAYER).translateY(5));

  onBtnDown(MouseButton.PRIMARY, () -> {
      double y = getGameWorld().getSingleton(EntityType.PLAYER).getY();
      spawn("projectile", 0, y + 10);
      spawn("projectile", 0, y + 50);
  });
}

前两个呼叫设置了我们的玩家移动。
更具体地说,W和S键将分别向上和向下移动播放器。
我们的最后一个通话设置了玩家的动作,即射击。
当按下主鼠标按钮时,我们会生成我们先前定义的弹丸。
" spawn"函数的最后两个参数是生成弹丸的位置的x和y值。

游戏逻辑

在开始游戏之前,我们需要初始化一些游戏逻辑,我们可以执行以下操作:

@Override
protected void initGame() {
  getGameScene().setBackgroundColor(Color.BLACK);

  getGameWorld().addEntityFactory(new SpaceRangerFactory());

  spawn("player", 0, getAppHeight()/2 - 30);

  run(() -> {
      double x = getAppWidth();
      double y = FXGLMath.random(0, getAppHeight() - 20);

      spawn("enemy", x, y);
  }, Duration.seconds(0.25));
}

我们将游戏场景背景设置为黑色(您可以根据需要选择其他颜色)。

接下来,我们添加实体工厂-FXGL需要知道如何生成我们的实体。

此后,由于x值为0,我们在屏幕左侧生成了播放器。

最后,我们设置了一个计时器动作,该动作每0.25秒运行一次。
动作是在一个随机的Y位置生成一个敌人。

物理

我们的物理代码是微不足道的,因为在我们的游戏中不会发生很多冲突。

@Override
protected void initPhysics() {
  onCollisionBegin(EntityType.PROJECTILE, EntityType.ENEMY, (proj, enemy) -> {
      proj.removeFromWorld();
      enemy.removeFromWorld();
  });
}

从上面可以看出,我们关心的关于碰撞的仅有的两种实体类型是射弹和敌人。
我们设置了当这两种类型发生冲突时会调用的处理程序,为我们提供了对发生冲突的特定实体的引用。
请注意定义类型的顺序。
这是实体引用传递的顺序。
当它们相互碰撞时,我们希望将两者从世界中删除。

更新资料

我们的游戏循环中没有很多东西。
我们只想知道敌人何时到达我们的基地,即敌人的X值小于0。

@Override
protected void onUpdate(double tpf) {
  var enemiesThatReachedBase = getGameWorld().getEntitiesFiltered(e -> e.isType(EntityType.ENEMY) && e.getX() < 0);

  if (!enemiesThatReachedBase.isEmpty()) {
      showMessage("Game Over!", () -> getGameController().startNewGame());
  }
}

为此,我们查询游戏世界以提供所有类型为敌人且X值小于0的实体。
如果列表不为空,则我们输了游戏,因此我们显示适当的消息并重新启动。