美文网首页Flutter圈子FlutterFlutter
Flutter游戏:简单规则与结束页

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

作者: 何小有 | 来源:发表于2019-08-10 19:30 被阅读20次

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

    简单游戏规则

    在创建的失败页面之前,要设置游戏失败的条件,目前就先设置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();
      }
    

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

    简单规则与结束页

    相关文章

      网友评论

        本文标题:Flutter游戏:简单规则与结束页

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