Flutter圈子Flutter中文社区

Flutter游戏:启动时的欢迎页

2019-07-22  本文已影响52人  何小有

本篇文章的内容需要在完成以下内容代码的基础上进行哦!

加载更多资源

首先下载接下来要用到的游戏资源文件,因为之前已经下载过一部分,所以下面讲一下这次添加了哪些内容。

了解了这些资源是干什么的以后,开始将以下代码添加到pubspec.yaml文件中的assets部分。

flutter:
  uses-material-design: true

  assets:
    ...
    - assets/images/bg/lose-splash.png
    - assets/images/branding/title.png
    - assets/images/ui/dialog-credits.png
    - assets/images/ui/dialog-help.png
    - assets/images/ui/icon-credits.png
    - assets/images/ui/icon-help.png
    - assets/images/ui/start-button.png

打开main.dart文件,并将刚才在pubspec.yaml文件中添加的资源传递给Flame.images.loadAll调用的字符串(String)列表中。

  Flame.images.loadAll(<String>[
    ...
    'bg/lose-splash.png',
    'branding/title.png',
    'ui/dialog-credits.png',
    'ui/dialog-help.png',
    'ui/icon-credits.png',
    'ui/icon-help.png',
    'ui/start-button.png',
  ]);

这样就完成了游戏资源的预加载部分。

准备游戏页面

一个好的游戏应该有一个欢迎页面和一个游戏页面,在长时间的游戏以后,胜利或失败时也要一个给出结果的页面,而且当玩家点击开始按钮时,应该从欢迎页面切换到游戏页面。所以,我们的游戏应该有下面3个页面。

对于上面的3个页面,都显示同一个背景,并且蚊子也是可见的,这会使玩家感觉游戏页面是主页面,而欢迎页面才是真正的主页面,欢迎真正的引导玩家进入游戏,最后游戏结束后的失败页面是玩家失败后,休息一下再重新开始的页面。

为此呢,我们需要记录当前页面,这里可以使用整数执行此操作,并将页面从0到2编号,或者还可以将页面以字符串来记录。

在Dart语言中,有一种枚举(enum)的数据类型,正好可以用来处理这个情况,我们将在查看玩家所处的页面或告诉游戏更改页面时枚举页面。新建一个lib/view.dart文件,并添加下面的代码。

enum View {
  home,
  playing,
  lost,
}

现在需要为游戏添加一个实例变量,它将为我们保存当前页面的值。因此呢,要在使用它之前先导入页面View枚举,然后再添加实例变量。我们将它命名为活动页面(activeView),类型设置为View枚举类型。

打开hit-game.dart文件,并提交下面的代码。

...
import 'package:hello_flame/view.dart';

class HitGame extends Game {
  ...
  View activeView = View.home;

这样我们就准备好处理每个页面了。

实现欢迎页面

在前面已经简单说了欢迎页面,而在代码中,页面只是另一个类似组件的逻辑,可以拥有自己的子组件。它也可以是虚拟的,无论玩家真正看到什么页面,它总是可见的。

如果是欢迎页面的话,我们将在定义页面时使用组件类,就像其他组件一样,我们只需从游戏循环中调用其实例的渲染(render)和更新(update)方法。

首先在lib目录下创建一个views文件夹,并在该目录下创建views/home-view.dart文件,并编写下面的代码。

import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';

class HomeView {
  final HitGame game;
  Rect titleRect;
  Sprite titleSprite;

  HomeView(this.game) {}

  void render(Canvas c) {}

  void update(double t) {}
}

上面代码中,先是导入将使用的类和定义的文件,然后我们定义一个名为HomeView的类,它有3个实例变量,其中一个是final,需要在创建这个类的实例时传递并赋值。同时该类有1个构造函数和另外2个方法,分别是游戏循环的渲染(render)和更新(update)方法。

在构造函数中,将初始化titleRecttitleSprite变量,以便它们可以在渲染(render)方法中使用。

  HomeView(this.game) {
    titleRect = Rect.fromLTWH(
      game.tileSize,
      (game.screenSize.height / 2) - (game.tileSize * 4),
      game.tileSize * 7,
      game.tileSize * 4,
    );
    titleSprite = Sprite('branding/title.png');
  }

上面的2行代码中,第1行为titleRect变量赋值,第2行为titleSprite变量赋值。其中,titleRect变量的值是一个矩形(Rect)的定义,4行代码对应于工厂构造函数.fromLTWH所需的参数。

因为我们确定要在一个7×4块的矩形内显示标题图片,所以会将game.tileSize * 7game.tileSize * 4传递给最后2个参数,这2个参数对应于矩形的宽度和高度。

对于左侧(left)参数,从9个图块大小的屏幕宽度中,减去标题图像7个图块大小的矩形宽度,可以得到2个额外空间的图块。为了使标题图像居中,这里将这2个额外的图块分布到左右两侧,使图像偏移1个图块。所以,这里就传入了game.tileSize * 1或简单地传递game.tileSize

顶部(top)参数有所不同,因为我们不想让标题图像位于屏幕的中心位置。要计算屏幕的中心位置,只需将屏幕高度除以2即可,从中减去4个图块大小的标题图像高度,就可以得到中心化的适当偏移位置。

目前已经初始化了titleRecttitleSprite,可以开始编写用于显示实际图像的代码了,在渲染(render)方法中,添加下面的代码。

  void render(Canvas c) {
    titleSprite.renderRect(c, titleRect);
  }

现在打开hit-game.dart文件,并导入HomeView类文件,然后添加一个HomeView类型的homeView实例变量。接下来,还需要在确定屏幕大小后初始化该实例变量,因此在调用resize之后,将在initialize方法中添加以下代码。

...
import 'package:hello_flame/views/home-view.dart';

class HitGame extends Game {
  ...
  View activeView = View.home;
  HomeView homeView;

  ...

  void initialize() async {
    ...
    homeView = HomeView(this);
    produceFly();
  }

最后还要在屏幕上使用渲染HomeView的渲染(render),因此,在游戏类的渲染(render)方法中,在最后调用HomeView实例的渲染(render)方法,使其最后渲染。

因为渲染的顺序与显示顺序相同,因为我们想要的是背景优先,然后是蚊子,最后才是标题图像,这将确保标题图片位于屏幕所有内容之上。

  void render(Canvas canvas) {
    background.render(canvas);
    enemy.forEach((Fly fly) => fly.render(canvas));

    if (activeView == View.home) homeView.render(canvas);
  }

上面的代码中,先判断活动视图(activeView)当前是否为主视图,如果是,就渲染HomeView实例,如果不是,则渲染(render)方法将跳过此行代码,因此不会呈现HomeView实例。

现在运行游戏,可以看到像下图所展示的效果。

添加标题图片之后

但是呢,当还在游戏页面内时,玩家仍然可以点击并干掉蚊子,不过没关系,这对游戏没有任何影响。接下来,玩家要开始游戏了,所以必须有一个开始按钮。首先,创建另一个组件,命名为StartButton,放在新建的components/start-button.dart文件中。

import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';

class StartButton {
  final HitGame game;
  Rect rect;
  Sprite sprite;

  StartButton(this.game) {}

  void render(Canvas c) {}

  void update(double t) {}

  void onTapDown() {}
}

这个类定义与其他组件类大致相同,不同的是它有一个onTapDown处理程序,我们将在这里编写“开始”游戏的代码。首先在构造函数中初始化rectsprite变量。

  StartButton(this.game) {
    rect = Rect.fromLTWH(
      game.tileSize * 1.5,
      (game.screenSize.height * .75) - (game.tileSize * 1.5),
      game.tileSize * 6,
      game.tileSize * 3,
    );
    sprite = Sprite('ui/start-button.png');
  }

上面的代码与HomeView类构造函数中,标题图像的初始化基本相同。不同的是,除了6×3个图块大小外,还有左侧(left)和顶部(top)偏移。

开始按钮的宽度为6个图块,这意味着在屏幕的9个图块宽度上有3个额外的图块,这样的话,每侧都有1.5个图块,所以这里将game.tileSize * 1.5提供给左侧(left)参数。对于顶部(top)参数,这样就可以让按钮的垂直位置恰好位于屏幕高度的四分之三即0.75的位置。

在初始化rectsprite变量之后,需要渲染图像,所以要下面代码添加到render函数中。

  void render(Canvas c) {
    sprite.renderRect(c, rect);
  }

接下来需要在游戏类中添加一个StartButton组件的实例,打开hit-game.dart文件,导入依赖,然后将实例变量与其他实例变量一起添加。同时在确定屏幕大小后,使用StartButton类的新实例初始化startButton变量,并在render方法中增加下面代码。

...
import 'package:hello_flame/components/start-button.dart';

class HitGame extends Game {
  ...
  HomeView homeView;
  StartButton startButton;

  ...

  void initialize() async {
    ...
    startButton = StartButton(this);
    produceFly();
  }

  ...

  void render(Canvas canvas) {
    ...
    if (activeView == View.home || activeView == View.lost) {
      startButton.render(canvas);
    }
  }

上面代码中,我们添加的这4行代码,分别是导入StartButton类、创建StartButton类的实例并将其存储在实例变量中、最后展示StartButton到屏幕上。

现在运行游戏,可以看到开始按钮会在欢迎页面和失败页面上呈现,这样玩家就可以从欢迎页面或失败页面重新开始游戏了。

添加开始按钮之后

处理开始按钮

在上一步中,我们让开始按钮显示在屏幕上,但是点击没有反应,不用急,接下来就开始处理开始按钮的响应逻辑。首先,需要确保点击不会穿过物体,例如在点击开始按钮(startButton)时,同一位置的蚊子不应该接收到点击事件。

在游戏类的onTapDown处理程序中创建一个isHandled变量,用于保存当前是否已调用了点击处理程序,在onTapDown处理程序的开始处创建它,并将初始值设置为false。在检查点击是否命中矩形(Rect)组件之前,先检查isHandled是否仍为false值,然后再调用组件的点击处理程序。

  void onTapDown(TapDownDetails d) {
    bool isHandled = false;

    if (!isHandled && startButton.rect.contains(d.globalPosition)) {
      if (activeView == View.home || activeView == View.lost) {
        startButton.onTapDown();
        isHandled = true;
      }
    }

    enemy.forEach((Fly fly) {
      if (fly.flyRect.contains(d.globalPosition)) {
        fly.onTapDown();
      }
    });
  }

上面代码中,首先对isHandled进行检查,确保尚未处理点击事件,这个检查项与检查点击是否在开始按钮(startButton)的rect属性内是一起的。如果通过了这些条件的检查,再额外检查玩家是否当前处于欢迎页面或失败页面中。

只有满足所有条件,游戏才会调用开始按钮的onTapDown处理程序,变量isHandled也被设置为true,让下面的代码知道已经处理了这次的点击。接下来要做的是,使用isHandled检查把之前的蚊子点击处理程序包装起来。

  void onTapDown(TapDownDetails d) {
    ...

    if (!isHandled) {
      enemy.forEach((Fly fly) {
        if (fly.flyRect.contains(d.globalPosition)) {
          fly.onTapDown();
          isHandled = true;
        }
      });
    }
  }

上面的代码基本上与上面类似,首先包含了对isHandled的检查,这使得代码块只会没有处理开始按钮的情况下运行,其次,如果至少有一个蚊子被点击,则将isHandled变量设置为true

接下来,打开components/start-button.dart文件,开始编写实际处理开始按钮点击的代码。在调用开始按钮的onTapHandler时,需要将游戏的activeView设置为View.playing,所以这里先导入定义View枚举的文件。然后在onTapDown里面,将游戏的activeView设置为所需的值。

...
import 'package:hello_flame/view.dart';

class StartButton {
  ...

  void onTapDown() {
    game.activeView = View.playing;
  }
}

现在运行游戏,可以看到点击开始按钮以后,标题图片和开始按钮都不见了,进入了我们之前的游戏页面。

添加欢迎之后
上一篇下一篇

猜你喜欢

热点阅读