本篇文章的内容需要在完成以下内容代码的基础上进行哦!
简单游戏规则
在创建的失败页面之前,要设置游戏失败的条件,目前就先设置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();
}
接下来运行游戏,就可以看到蚊子会按照我们设置的规则来出现,而且也有了一个失败页面,可以正常开始游戏、输掉游戏,然后再重新开始游戏。
简单规则与结束页















网友评论