Flutter游戏:蚊子飞来飞去
本文紧接上文《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
子类都有自己的大小与尺寸。
而且因为我们不需要在这里进行初始化,所以也不再需要使用x
和y
参数,也可以删除。现在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
调用,这样它就不会传递x
和y
值,也不会因为刚刚在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.dart
、components/drooler-fly.dart
、components/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.tileSize
的2.025
倍,所以我们需要回到跳转到hit-game.dart
文件中,并修改produceFly
方法中x
和y
的最大值。
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
大于或等于toTarget
的distance
属性,则意味着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