Flutter圈子Flutter DeveloperFlutter中文社区

Flutter游戏:蚊子飞来飞去

2019-07-15  本文已影响9人  何小有

本文紧接上文《Flutter游戏:垃圾里会生蚊子》中完成的代码内容,建议先完成前面的代码呦。

更多蚊子种类

现在我们可以为蚊子添加更多种类,即为Fly类添加更多子类,这一步应该很快就可以完成,因为它们与components/mosquito-fly.dart文件基本相同,唯一的区别就是引用的图像文件名不一样。

创建一个新子类文件components/drooler-fly.dart,声明一个DroolerFly类,表示这是一只懒惰的蚊子。

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

class DroolerFly extends Fly {
  DroolerFly(HitGame game, double x, double y) : super(game, x, y) {
    flyingSprite = List<Sprite>();
    flyingSprite.add(Sprite('flies/drooler-fly-1.png'));
    flyingSprite.add(Sprite('flies/drooler-fly-2.png'));
    deadSprite = Sprite('flies/drooler-fly-dead.png');
  }
}

创建一个新子类文件components/agile-fly.dart,声明一个AgileFly类,表示这是一只敏捷的蚊子。

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

class AgileFly extends Fly {
  AgileFly(HitGame game, double x, double y) : super(game, x, y) {
    flyingSprite = List<Sprite>();
    flyingSprite.add(Sprite('flies/agile-fly-1.png'));
    flyingSprite.add(Sprite('flies/agile-fly-2.png'));
    deadSprite = Sprite('flies/agile-fly-dead.png');
  }
}

创建一个新子类文件components/macho-fly.dart,声明一个MachoFly类,表示这是一只猛男的蚊子。

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

class MachoFly extends Fly {
  MachoFly(HitGame game, double x, double y) : super(game, x, y) {
    flyingSprite = List<Sprite>();
    flyingSprite.add(Sprite('flies/macho-fly-1.png'));
    flyingSprite.add(Sprite('flies/macho-fly-2.png'));
    deadSprite = Sprite('flies/macho-fly-dead.png');
  }
}

创建一个新子类文件components/hungry-fly.dart,声明一个HungryFly类,表示这是一只饥饿的蚊子。

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

class HungryFly extends Fly {
  HungryFly(HitGame game, double x, double y) : super(game, x, y) {
    flyingSprite = List<Sprite>();
    flyingSprite.add(Sprite('flies/hungry-fly-1.png'));
    flyingSprite.add(Sprite('flies/hungry-fly-2.png'));
    deadSprite = Sprite('flies/hungry-fly-dead.png');
  }
}

随机蚊子种类

现在我们有5种不同的蚊子种类,现在需要在每次产生蚊子时,它都会在这5种之间随机化。在hit-game.dart文件中,导入我们刚刚创建的所有Fly子类文件,然后在spawnFly方法添加、删除以下代码。

...
import 'package:hello_flame/components/agile-fly.dart';
import 'package:hello_flame/components/drooler-fly.dart';
import 'package:hello_flame/components/hungry-fly.dart';
import 'package:hello_flame/components/macho-fly.dart';

class HitGame extends Game {
  ...

  void produceFly() {
    double x = rnd.nextDouble() * (screenSize.width - tileSize);
    double y = rnd.nextDouble() * (screenSize.height - tileSize);
    // 删除内容
    // enemy.add(MosquitoFly(this, x, y));
    switch (rnd.nextInt(5)) {
      case 0:
        enemy.add(MosquitoFly(this, x, y));
        break;
      case 1:
        enemy.add(DroolerFly(this, x, y));
        break;
      case 2:
        enemy.add(AgileFly(this, x, y));
        break;
      case 3:
        enemy.add(MachoFly(this, x, y));
        break;
      case 4:
        enemy.add(HungryFly(this, x, y));
        break;
    }
  }

  ...
}

上面的代码中,首先使用了nextInt方法从rnd中获得一个随机整数,参数为5表明我们需要从0~4范围中随机选择。然后把得到的随机值传递给switch代码块,switch再根据传递给它的值执行不同的代码,生产不同种类的蚊子。

现在我们再运行游戏,应该会看到每次产生的蚊子都是不同种类的,它是随机选择的,所以也不排除随机几次都是一样的情况。

蚊子扇动翅膀

到现在为止,我们仅仅是一个可以玩的游戏,具有好看的图像和足够的变化,以保持玩家的娱乐性,但是这个游戏还不完善,游戏体验非常生硬,比如说,蚊子没有动翅膀,它们是用魔法保持在空中的,正常来讲,它们应该要扇动翅膀以提供足够的上升力来推动整个身体向上。

我们预加载的资源中已经提供了蚊子动画所需要的所有帧图像,并且已经在每个Fly实例中准备了精灵(Sprite),所以,现在打开components/fly.dart文件,在更新(update)方法中,将else代码块放在if代码块的末尾。

  void update(double t) {
    if (isDead) {
      flyRect = flyRect.translate(0, game.tileSize * 12 * t);
      if (flyRect.top > game.screenSize.height) {
        isOffScreen = true;
      }
    } else {
      flyingSpriteIndex += 30 * t;
      if (flyingSpriteIndex >= 2) {
        flyingSpriteIndex -= 2;
      }
    }
  }

上面代码中,使用30乘于时间增量(t)并将其结果值添加到flyingSpriteIndex变量中,此变量在绘制期间会转换为int,其int值用于确定要显示的帧图像,第0个或第1个。

现在我们每秒实现15次扇动,即15个动画周期,由于每个周期都有2个动画帧,因此将每秒显示30帧。假设游戏以每秒60帧的速度运行,更新方法将以大约每16.6毫秒执行一次,这是时间增量(t)的值,但以秒为单位,flyingSpriteIndex的起始值为0

对于第1帧,30 x 0.0166被添加到flyingSpriteIndex上,flyingSpriteIndex的值现在是0.498,现在对这个值运行.toInt(),将得到0,显示第0个帧图像。

在第2帧上,另一个30 x 0.0166被添加到flyingSpriteIndex上,使其值为0.996,现在对这个值运行.toInt(),仍然会得到0,这显示了第0个帧图像。

然后在第3帧上,添加另一个30 x 0.0166,该值将变为1.494,在此值上运行.toInt()将返回1,显示第1个帧图像。

当我们到达第4帧时,添加另一个30 x 0.0166,该值将变为1.992.toInt()值仍为1,因此仍显示第1个帧图像。

当在第5帧时,再添加30 x 0.0166得到2.49。然后,我们有一个if代码块,如果它的值大于或等于2,则会重置flyingSpriteIndex变量,因为我们现在没有第2个帧图像。

现在的值为2.49,我们从值中减去2,使其仅为0.49,其中.toInt()值为0,再次显示第0个帧图像。这种情况在2帧之间以每秒15个周期一次又一次地循环。

根据计算,最终会得到一个单帧,其中帧图像将持续显示3帧。但是实际情况并不是这样的,因为在上面的计算中,我们没有使用精确值,1秒 ÷ 60帧/秒 = 0.016666...,是无限循环小数。如果乘以30始终给出0.5的值,而且,时间增量(t)并非总是0.016666...。就想上面的计算,我们使整个计算逻辑实现了每秒15个扇动。

现在运行游戏,就可以看到蚊子的翅膀开始扇动起来,终于不用靠魔法来飞行了。蚊子扇动翅膀.gif

蚊子扇动翅膀

规范蚊子大小

之前我们为所有的Fly都设置成一个图块的大小,但是现在我们有正常蚊子、下垂蚊子、敏捷蚊子、猛男蚊子、饥饿蚊子,很明显,如果它们都一样大小就不合理了。

打开components/fly.dart文件,删除原有的构造函数,我们要根据不同的蚊子种类去调整大小,所以不需要在Fly类中对flyRect进行初始化,而是由Fly类的子类进行初始化,这样每个Fly子类都有自己的大小与尺寸。

而且因为我们不需要在这里进行初始化,所以也不再需要使用xy参数,也可以删除。现在Fly类的构造函数代码如下。

class Fly {
  ...

  Fly(this.game);
  // 删除内容
  // Fly(this.game, double x, double y) {
  //   flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
  // }

然后打开components/mosquito-fly.dart文件,并在构造函数中编辑super调用,这样它就不会传递xy值,也不会因为刚刚在Fly构造函数中删除了这些而报异常。

然后在这个构造函数中,添加刚从Fly类中删除的flyRect初始化,同时还要导入dart:ui包以使用矩形(Rect)类。

...
import 'dart:ui';

class MosquitoFly extends Fly {
  MosquitoFly(HitGame game, double x, double y) : super(game) {
  // 删除内容
  // MosquitoFly(HitGame game, double x, double y) : super(game, x, y) {
    flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
    ...

接下来我们还要对所有的Fly子类进行相同的更改。

文件名 名称 尺寸
mosquito-fly 正常蚊子 1.0x
agile-fly 敏捷蚊子 1.0x
drooler-fly 懒惰蚊子 1.0x
hungry-fly 饥饿蚊子 1.1x
macho-fly 猛男蚊子 1.35x

正常蚊子、敏捷蚊子和懒惰蚊子将是相同的大小,但是现在需要使它们更大些。因此对于这些Fly子类,具体是components/mosquito-fly.dartcomponents/drooler-fly.dartcomponents/agile-fly.dart文件,要修改它们在构造函数中的flyRect初始化代码。

    // 删除内容
    // flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
    flyRect = Rect.fromLTWH(x, y, game.tileSize * 1.5, game.tileSize * 1.5);

加上这个以后,点击框不再与game.tileSize相同,它变大了1.5倍,这个现在是我们的基本大小了。精灵框也随之更改,因为它是点击框放大后的副本。

对于猛男蚊子(MachoFly)类,即components/macho-fly.dart文件,它的大小是其他蚊子的1.35倍。

1.5 x 1.35 = 2.025

将其flyRect初始化更改为如下代码。

    // 删除内容
    // flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
    flyRect = Rect.fromLTWH(x, y, game.tileSize * 2.025, game.tileSize * 2.025);

再对饥饿蚊子(HungryFly)类,即components/hungry-fly.dart文件,做同样的事情,但使用1.5 x 1.1 = 1.65作为我们的大小因子。

    // 删除内容
    // flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
    flyRect = Rect.fromLTWH(x, y, game.tileSize * 1.65, game.tileSize * 1.65);

现在最大的Fly子类是game.tileSize2.025倍,所以我们需要回到跳转到hit-game.dart文件中,并修改produceFly方法中xy的最大值。

  void produceFly() {
    // 删除内容
    // double x = rnd.nextDouble() * (screenSize.width - tileSize);
    // double y = rnd.nextDouble() * (screenSize.height - tileSize);
    double x = rnd.nextDouble() * (screenSize.width - (tileSize * 2.025));
    double y = rnd.nextDouble() * (screenSize.height - (tileSize * 2.025));

现在再次运行游戏,如下图所示,可以明显的发现蚊子变大了,并且它们的大小有了规范。

规范蚊子大小

蚊子飞来飞去

现在游戏中的蚊子就是不会动的,在一个位置等着玩家点击,实际上蚊子是不停的飞来飞去的,接下来我们就在游戏中实现蚊子的飞行。

首先添加一个名为speed的属性,这是蚊子的移动速度,大多数蚊子的速度相同,但也有些蚊子特殊一些。属性只是实例变量的另一个名称,在这个游戏中,区别在于如何定义和使用它,打开components/fly.dart文件,我们将通过定义一个getter来创建一个属性。

我们使用game.tileSize * 3的默认值,因此蚊子可以在2秒钟内在屏幕上突然出现。在开始在更新(update)方法中移动蚊子之前,需要计算其移动方向,然后为了更好的模拟飞行运动,还可以在更新(update)方法运行时做一个随机值,让蚊子看起来像是在随机抖动。

添加一个名为targetLocation的偏移(Offset)类型实例变量,偏移(Offset)类里有一些函数,可以用来计算方向、距离、缩放等。现在这个targetLocation实例变量就是一个蚊子在改变方向之前到达的目标点,然后再让我们使用可重用的方法来更改实例变量targetLocation的值。

class Fly {
  ...
  Offset targetLocation;

  double get speed => game.tileSize * 3;

  Fly(this.game);

  void setTargetLocation() {
    double x = game.rnd.nextDouble() *
        (game.screenSize.width - (game.tileSize * 2.025));
    double y = game.rnd.nextDouble() *
        (game.screenSize.height - (game.tileSize * 2.025));
    targetLocation = Offset(x, y);
  }

  ...
}

就像在hit-game.dart中的produceFly中一样,我们使用相同的最大规则初始化x变量、y变量、随机值,蚊子只能到达它可以在屏幕上出现的位置。然后在构造函数中,调用此方法,以便在创建Fly实例时创建一个非空值(null)的targetLocation实例变量。

  Fly(this.game) {
    setTargetLocation();
  }

现在我们让蚊子动起来,在更新(update)方法内部,判断当前Fly实例没有被点击,isDead不为true时,将Fly实例朝着它的目标方向移动,参考时间增量值(t),如果它到达目标位置,就调用setTargetLocation来随机化目标。

  void update(double t) {
    ...

      flyingSpriteIndex += 30 * t;
      if (flyingSpriteIndex >= 2) {
        flyingSpriteIndex -= 2;
      }

      double stepDistance = speed * t;
      Offset toTarget = targetLocation - Offset(flyRect.left, flyRect.top);
      if (stepDistance < toTarget.distance) {
        Offset stepToTarget =
            Offset.fromDirection(toTarget.direction, stepDistance);
        flyRect = flyRect.shift(stepToTarget);
      } else {
        flyRect = flyRect.shift(toTarget);
        setTargetLocation();
      }
  }

在上面的代码中,首先定义一个stepDistance变量,该变量将保存蚊子应该移动多少。如果速度(speed)是蚊子在1秒钟内可以移动的速度(speed),我们可以将它乘以时间差值(t),得出了在那个时候的蚊子应该移动的距离。

然后创建一个新的偏移(Offset)类,它表示从Fly实例当前位置到它的目标位置(targetLocation)的偏移,这里使用偏移(Offset)类的减法操作。

如果蚊子目前在(50, 50),而目标位置是(120, 70),则该toTarget将具有((120-50), (70-50))(70, 20)的值。然后我们再检查stepDistance是否小于toTarget偏移量中的.distance,如果为true则意味着蚊子仍然远离目标位置,那么继续移动Fly实例。

为了移动Fly实例,需要使用fromDirection构造函数创建一个新的偏移(Offset),该构造函数采用方向和可选距离,对于方向,只需要提供toTarget的方向属性,对于距离,距离默认为1,我们输入已经计算好的stepDistance值。

如果stepDistance大于或等于toTargetdistance属性,则意味着Fly实例非常靠近目标位置(targetLocation),此时可以肯定它已达到目标。所以只需使用toTarget中的值将蚊子移动到目标,这是从蚊子到targetLocation的实际距离。将蚊子捕捉到目标中,最后调用setTargetLocation()来为Fly实例提供一个新的目标。

到这里为止,我们的fly.dart里面应该有以下代码。

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

class Fly {
  final HitGame game;
  List<Sprite> flyingSprite;
  Sprite deadSprite;
  double flyingSpriteIndex = 0;
  Rect flyRect;
  bool isDead = false;
  bool isOffScreen = false;
  Offset targetLocation;

  double get speed => game.tileSize * 3;

  Fly(this.game) {
    setTargetLocation();
  }

  void setTargetLocation() {
    double x = game.rnd.nextDouble() *
        (game.screenSize.width - (game.tileSize * 2.025));
    double y = game.rnd.nextDouble() *
        (game.screenSize.height - (game.tileSize * 2.025));
    targetLocation = Offset(x, y);
  }

  void render(Canvas c) {
    if (isDead) {
      deadSprite.renderRect(c, flyRect.inflate(2));
    } else {
      flyingSprite[flyingSpriteIndex.toInt()].renderRect(c, flyRect.inflate(2));
    }
  }

  void update(double t) {
    if (isDead) {
      flyRect = flyRect.translate(0, game.tileSize * 12 * t);
      if (flyRect.top > game.screenSize.height) {
        isOffScreen = true;
      }
    } else {
      flyingSpriteIndex += 30 * t;
      if (flyingSpriteIndex >= 2) {
        flyingSpriteIndex -= 2;
      }

      double stepDistance = speed * t;
      Offset toTarget = targetLocation - Offset(flyRect.left, flyRect.top);
      if (stepDistance < toTarget.distance) {
        Offset stepToTarget =
            Offset.fromDirection(toTarget.direction, stepDistance);
        flyRect = flyRect.shift(stepToTarget);
      } else {
        flyRect = flyRect.shift(toTarget);
        setTargetLocation();
      }
    }
  }

  void onTapDown() {
    isDead = true;
    game.produceFly();
  }
}

控制蚊子速度

完成上面部分以后,蚊子都可以飞了,但是还没有为不同的蚊子设置一些差异化,下面就来实现差异化的代码。

对于AgileFly类,即敏捷的蚊子,文件在components/agile-fly.dart。因为它们很敏捷,所以我们覆盖speed属性并赋予速度因子为5,使它们更快一些。

class AgileFly extends Fly {
  double get speed => game.tileSize * 5;

而对于DroolerFly类,即懒惰的蚊子,文件在components/drooler-fly.dart。因为它们很懒惰,所以它们的移动速度只是正常蚊子飞行速度的一半。

class DroolerFly extends Fly {
  double get speed => game.tileSize * 1.5;

还有MachoFly类,即猛男蚊子,文件在components/macho-fly.dart。因为有巨大的肌肉而且很重,让它比正常蚊子慢一点。

class MachoFly extends Fly {
  double get speed => game.tileSize * 2.5;

现在我们再运行游戏,可以看到如下图所展示的效果,蚊子会在屏幕上飞来飞去,就像真的蚊子一样。蚊子飞来飞去.gif

蚊子飞来飞去.gif
上一篇下一篇

猜你喜欢

热点阅读