Flutter游戏:蚊子飞来飞去

作者: 何小有 | 来源:发表于2019-07-15 10:24 被阅读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

    相关文章

      网友评论

        本文标题:Flutter游戏:蚊子飞来飞去

        本文链接:https://www.haomeiwen.com/subject/zrpjkctx.html