Flutter游戏:简单规则与结束页

本篇文章的内容需要在完成以下内容代码的基础上进行哦!

简单游戏规则

在创建的失败页面之前,要设置游戏失败的条件,目前就先设置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),这是因为渲染图形不是控制器目的。现在还要编辑最后一部分,调用producestart方法。打开components/start-button.dart并将以下代码放在onTapDown处理程序中。

  void onTapDown() {
    game.activeView = View.playing;
    game.produce.start();
  }

接下来运行游戏,就可以看到蚊子会按照我们设置的规则来出现,而且也有了一个失败页面,可以正常开始游戏、输掉游戏,然后再重新开始游戏。

简单规则与结束页
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容