本篇文章的内容需要在完成以下内容代码的基础上进行哦!
简单游戏规则
在创建的失败页面之前,要设置游戏失败的条件,目前就先设置1个条件,就是是如果玩家点击屏幕而且没打中蚊子。要检查点击是否命中蚊子还是没有命中,就需要创建另一个布尔(bool
)变量,这个变量将在坚持是否命中之前定义。
打开hit-game.dart
并在循环遍历蚊子之前,将didHitAFly
变量声明放在onTapDown
处理程序中。同时在循环过滤蚊子时,添加一个if
块中,检查是否点击蚊子。然后在forEach
循环之后,再检查当前是否处于游戏页面中,以及是否点击了蚊子。
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;
}
}
if (!isHandled) {
bool didHitAFly = false;
enemy.forEach((Fly fly) {
if (fly.flyRect.contains(d.globalPosition)) {
fly.onTapDown();
isHandled = true;
didHitAFly = true;
}
});
if (activeView == View.playing && !didHitAFly) {
activeView = View.lost;
}
}
}
上面代码中,先是在if
语句里检查两件事,一是如果处于游戏页面中,二是如果没有命中蚊子。如果满足这2个条件,就将activeView
设置为View.lost
值,该值对应于失败页面。
现在运行游戏,就会发现,如果玩家有一次没有命中蚊子,开始按钮会出现在屏幕上。还记得之前的渲染(render
)方法不,只有在欢迎页面或失败页面时,才会显示开始按钮。所以,现在通过有没有显示标题图片,就可以判断在哪个页面。
实现失败页面
失败页面和欢迎页面基本相同,唯一的区别只是显示的图像不是标题图片而是其他。新创建一个views/lost-view.dart
文件,并输入下面的代码。
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';
class LostView {
final HitGame game;
Rect rect;
Sprite sprite;
LostView(this.game) {
rect = Rect.fromLTWH(
game.tileSize,
(game.screenSize.height / 2) - (game.tileSize * 5),
game.tileSize * 7,
game.tileSize * 5,
);
sprite = Sprite('bg/lose-splash.png');
}
void render(Canvas c) {
sprite.renderRect(c, rect);
}
void update(double t) {}
}
上面的代码基本和欢迎页面相同,不同的是精灵(Sprite
)加载的图像文件的文件名,还有图像高度是5
个图块。然后和欢迎页面一样,需要打开游戏类hit-game.dart
文件,创建LostView
类的实例,然后渲染它。
在hit-game.dart
文件中导入views/lost-view.dart
文件,再创建一个lostView
实例变量,并实例化LostView
对象,然后将其分配给initialize
方法中的lostView
变量,最后,在渲染(render
)方法中渲染它。
...
import 'package:hello_flame/views/lost-view.dart';
class HitGame extends Game {
...
LostView lostView;
...
void initialize() async {
...
lostView = LostView(this);
produceFly();
}
...
void render(Canvas canvas) {
...
if (activeView == View.home) homeView.render(canvas);
if (activeView == View.lost) lostView.render(canvas);
if (activeView == View.home || activeView == View.lost) {
startButton.render(canvas);
}
}
...
}
上面的代码中,基本是和之前将欢迎页面添加到游戏类中一样的。现在运行游戏,点击开始按钮,然后单击屏幕上没有蚊子位置,应该就会在屏幕上看到游戏失败页面了。
游戏的控制器
在前面的部分中,提到过目前游戏有几个错误,一是蚊子产生的方式,它有两个方面,在技术方面,当使用forEach
函数循环列表(List
)时,代码不应该修改列表,向其中添加项目或从中删除项目。
在检查是否命中蚊子,使用forEach
循环遍历所有蚊子时,如果玩家击中一只蚊子,我们的代码会产生另一只蚊子。因为当我们添加一个蚊子时,就已在forEach
循环中,这就出现了并发修改列表的情况,导致一种很奇怪的错误。
这并不是游戏中的错误,这是什么时候产生蚊子本身的逻辑问题,蚊子应该根据时间而不是基于玩家手速产生。所以呢,现在需要创建一个生产控制器,而且控制器是一个不可见的组件。
新建一个lib/controllers
文件夹,然后在此文件夹中新建一个controllers/producer.dart
文件,并编写下面的代码。
import 'package:hello_flame/hit-game.dart';
class FlyProducer {
final HitGame game;
FlyProducer(this.game) {}
void start() {}
void killAll() {}
void update(double t) {}
}
上面的代码,还是熟悉的组件结构,唯一的区别是没有渲染(render
)方法,因为这个组件是一个控制器,在屏幕上没有图形表示。与其他组件和页面一样,在最终(final
)变量game
中保留对HitGame
实例的引用,并且将此变量的值作为构造函数的参数。
首先编写killAll
方法,因为下面会访问Fly
类,所以需要先导入它。然后循环游戏enemy
列表中的所有蚊子,并将true
值分配到其isDead
属性,从而有效的干掉所有蚊子。先添加几个最终(final
)实例变量到类中,然后紧接着上面再添加两个变量。
然后开始编写start
方法的内容,每次玩家点击开始按钮时都会调用此方法。,。
class FlyProducer {
final HitGame game;
final int maxProducerInterval = 3000;
final int minProducerInterval = 250;
final int intervalChange = 3;
final int maxProducerOnScreen = 7;
int currentInterval;
int nextProducer;
FlyProducer(this.game) {}
void start() {
killAll();
currentInterval = maxProducerInterval;
nextProducer = DateTime.now().millisecondsSinceEpoch + currentInterval;
}
上面代码中,从第1个常量maxProducerInterval
开始,这个实例变量控制何时产生蚊子的上限,游戏开始时,currentInterval
设置为maxProducerInterval
的值,这是设置成3000
毫秒。第2个常量minProducerInterval
与此完全相反,每次产生蚊子时的时间下限,使currentInterval
变量最多也只会减少250
毫秒。
第3个常量intervalChange
是每次生成蚊子时,从实例变量currentInterval
中减少的量。因此,从3000
毫秒开始,每生成一个蚊子,产生的速度就越来越快,直到它下降到250
毫秒为止。
第4个常量maxProducerOnScreen
的作用是,即使游戏在产生蚊子的过程中达到最快的速度,只要当前屏幕上已经有7
只蚊子活着并飞来飞去,就不会再产生蚊子。
第5个实例变量currentInterval
存储的内容是下一个生成蚊子时,从当前添加的时间量。最后一个变量nextProducer
是为下一次生产蚊子的实际时间。
在start
方法中,首先通过调用killAll()
方法来杀死所有蚊子,然后将currentInterval
重置为最大值(maxProducerInterval
)并在下一行使用此值,最后使用DateTime.now().millisecondsSinceEpoch
和添加的currentInterval
值来安排下一次的生产。
接下来在构造函数中,添加以下代码。
FlyProducer(this.game) {
start();
game.produceFly();
}
上面代码中,第1行将安排在创建此控制器的实例3
秒后生成一个蚊子,第2行只产生一只蚊子。按照这个顺序完成是因为假如先产生一个Fly
对象,start()
将调用killAll()
并且只会杀死所生成的第一个Fly
对象。
接下来在更新(update
)方法中,将会添加大量的生产逻辑,将以下代码块添加到更新(update
)方法中。
void update(double t) {
int nowTimestamp = DateTime.now().millisecondsSinceEpoch;
int livingFly = 0;
game.enemy.forEach((Fly fly) {
if (!fly.isDead) livingFly += 1;
});
if (nowTimestamp >= nextProducer && livingFly < maxProducerOnScreen) {
game.produceFly();
if (currentInterval > minProducerInterval) {
currentInterval -= intervalChange;
currentInterval -= (currentInterval * .02).toInt();
}
nextProducer = nowTimestamp + currentInterval;
}
}
上面的代码中,第1行代码存储当前时间,单位是毫秒。下一个代码块计算列表(game.enemy
)中还有效的蚊子数量,代码只是循环遍历列表,如果蚊子没有死,就向livingFly
添加1
。
接下来是一个更大的代码块,进入if
块判断当前时间是否已经过了nextProducer
值,以及幸存蚊子的数量是否小于maxProducerOnScreen
常量。如果满足条件,就会产生一只蚊子。之后,只有当currentInterval
高于最小间隔(minProducerInterval
)时,才会将currentInterval
的值减去intervalChange
常量中的值,再加上currentInterval
值的2%
。
最后1行代码仍然在上面的代码快内,使用当前时间安排下一个生产时间,并添加currentInterval
的值。
到这里为止,我们的controllers/producer.dart
里面应该有以下代码。
import 'package:hello_flame/hit-game.dart';
import 'package:hello_flame/components/fly.dart';
class FlyProducer {
final HitGame game;
final int maxProducerInterval = 3000;
final int minProducerInterval = 250;
final int intervalChange = 3;
final int maxProducerOnScreen = 7;
int currentInterval;
int nextProducer;
FlyProducer(this.game) {
start();
game.produceFly();
}
void start() {
killAll();
currentInterval = maxProducerInterval;
nextProducer = DateTime.now().millisecondsSinceEpoch + currentInterval;
}
void killAll() {
game.enemy.forEach((Fly fly) => fly.isDead = true);
}
void update(double t) {
int nowTimestamp = DateTime.now().millisecondsSinceEpoch;
int livingFly = 0;
game.enemy.forEach((Fly fly) {
if (!fly.isDead) livingFly += 1;
});
if (nowTimestamp >= nextProducer && livingFly < maxProducerOnScreen) {
game.produceFly();
if (currentInterval > minProducerInterval) {
currentInterval -= intervalChange;
currentInterval -= (currentInterval * .02).toInt();
}
nextProducer = nowTimestamp + currentInterval;
}
}
}
集成游戏控制
要将我们在上面创建的生产控制器集成到游戏类中,第一步是在hit-game.dart
中,删除initialize
方法中的以下行删除之前对produceFly
方法的调用。
void initialize() async {
enemy = List<Fly>();
rnd = Random();
resize(await Flame.util.initialDimensions());
background = Backyard(this);
homeView = HomeView(this);
startButton = StartButton(this);
lostView = LostView(this);
// 删除内容
// produceFly();
}
然后在components/fly.dart
中删除onTapDown
处理程序中使用的game. produceFly()
方法。
void onTapDown() {
isDead = true;
// 删除内容
// game.produceFly();
}
再回到hit-game.dart
中,创建一个生产控制器的实例并将其存储在一个实例变量中。首先导入类,然后创建一个实例变量produce
,再到initialize
方法中创建实例并将其存储到实例变量中,最后在更新方法内调用produceFly
类的更新方法。
...
import 'package:hello_flame/controllers/producer.dart';
class HitGame extends Game {
...
FlyProducer produce;
...
void initialize() async {
...
background = Backyard(this);
produce = FlyProducer(this);
homeView = HomeView(this);
...
}
void update(double t) {
produce.update(t);
enemy.forEach((Fly fly) => fly.update(t));
enemy.removeWhere((Fly fly) => fly.isOffScreen);
}
...
在游戏循环中使用组件和控制器之间的区别在于,控制器调用的主要方法是更新(update
),这是因为渲染图形不是控制器目的。现在还要编辑最后一部分,调用produce
的start
方法。打开components/start-button.dart
并将以下代码放在onTapDown
处理程序中。
void onTapDown() {
game.activeView = View.playing;
game.produce.start();
}
接下来运行游戏,就可以看到蚊子会按照我们设置的规则来出现,而且也有了一个失败页面,可以正常开始游戏、输掉游戏,然后再重新开始游戏。