美文网首页FlutterFlutter圈子Flutter
Flutter游戏:垃圾里会生蚊子

Flutter游戏:垃圾里会生蚊子

作者: 何小有 | 来源:发表于2019-07-11 17:10 被阅读19次

加载游戏资源

在开始下面的内容之前,最好的话是先把《开始用Flutter做游戏吧》过一遍,然后再完成《Flutter游戏:万有引力定律》里的游戏,因为下面的内容是在该游戏的基础上开发的。

首先下载这个游戏要用到的游戏资源文件,然后在项目目录下建立assets/images目录,在该目录下再分别建立bgflies目录,用于存放背景图片和组件图片。

资源文件就位后,在pubspec.yaml文件里添加对这些资源文件的引用。

flutter:

  uses-material-design: true

  assets:
    - assets/images/bg/backyard.png
    - assets/images/flies/agile-fly-1.png
    - assets/images/flies/agile-fly-2.png
    - assets/images/flies/agile-fly-dead.png
    - assets/images/flies/drooler-fly-1.png
    - assets/images/flies/drooler-fly-2.png
    - assets/images/flies/drooler-fly-dead.png
    - assets/images/flies/mosquito-fly-1.png
    - assets/images/flies/mosquito-fly-2.png
    - assets/images/flies/mosquito-fly-dead.png
    - assets/images/flies/hungry-fly-1.png
    - assets/images/flies/hungry-fly-2.png
    - assets/images/flies/hungry-fly-dead.png
    - assets/images/flies/macho-fly-1.png
    - assets/images/flies/macho-fly-2.png
    - assets/images/flies/macho-fly-dead.png

下面我们要在游戏开始时加载所有资源,这会花费几毫秒的时间,但是又有谁会注意到这几毫秒内的黑屏显示呢。打开main.dart文件,在顶部添加代码以导入flame/flame.dart包,然后就可以预加载游戏资源了。

...

import 'package:flame/flame.dart';

void main() async {
  Util flameUtil = Util();
  await flameUtil.fullScreen();
  await flameUtil.setOrientation(DeviceOrientation.portraitUp);

  Flame.images.loadAll(<String>[
    'bg/backyard.png',
    'flies/agile-fly-1.png',
    'flies/agile-fly-2.png',
    'flies/agile-fly-dead.png',
    'flies/drooler-fly-1.png',
    'flies/drooler-fly-2.png',
    'flies/drooler-fly-dead.png',
    'flies/mosquito-fly-1.png',
    'flies/mosquito-fly-2.png',
    'flies/mosquito-fly-dead.png',
    'flies/hungry-fly-1.png',
    'flies/hungry-fly-2.png',
    'flies/hungry-fly-dead.png',
    'flies/macho-fly-1.png',
    'flies/macho-fly-2.png',
    'flies/macho-fly-dead.png',
  ]);

  ...
}

上面的代码中,使用一个String列表作为参数传递给imagesloadAll方法,该方法用于预加载String列表指向的图像文件。这些图像将缓存在Flame的静态变量中,以便以后可以重复使用。

设置游戏背景

现在游戏的背景是一个灰蓝纯色背景,看起来还不错,但是我们接下来还是要改变它。我们预加载的资源中有一个bg/backyard.png,这是一个高度很高的垂直图片,因为我们的游戏目前只关心宽度,不管手机的纵横比如何,这张背景图都可以覆盖整个屏幕。

接下来,创建一个组件文件components/backyard.dart,将背景逻辑分开来,该文件声明了一个Backyard类,有一个构造函数和另外渲染(render)、更新(update)方法。

这个Backyard类还有一个最终(final)的HitGame实例变量,它将作为包含该组件的游戏实例及其属性的链接,可以参考components/fly.dart中的实现。另一个实例变量是一个名为bgSprite的精灵(Sprite),它会保存我们稍后将绘制在屏幕上的精灵(Sprite)数据。

import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';

class Backyard {
  final HitGame game;
  Sprite bgSprite;

  Backyard(this.game) {
    bgSprite = Sprite('bg/backyard.png');
  }

  void render(Canvas c) {}

  void update(double t) {}
}

在构造函数中,通过创建一个新的精灵(Sprite)并传递要使用的资源文件名来初始化bgSprite变量。文件bg/backyard.png已经在main.dart中被预加载,因此无需任何加载时间即可使用。

文件顶部的import语句导入了3个内容,dart:ui允许我们访问画布(Canvas)类,flame/sprite.dart允许我们使用精灵(Sprite)类,hello_flame/hit-game.dart使我们可以访问HitGame类。

那么在添加背景图以后,我们怎么定位游戏组件的位置勒?如果打开bg/backyard.png文件,可以看到它的大小为1080 x 2760像素,我们不用关注它的物理像素或逻辑像素,我们只要关心我们的背景有9个图块的宽度就好了。

1080(像素) ÷ 9(图块) = 120(每个图块的像素)

2760(像素) ÷ 120(每个图块的像素) = 23(图块)

如同上面的计算结果所示,我们当前使用的背景图像宽为9个图块、高为23个图块。

绘制游戏背景

现在我们可以开始绘制游戏背景了,将背景图像底部锚定在屏幕的底部,为此需要定义一个包含背景尺寸的矩形(Rect),这里需要正确计算大小,以便在渲染过程中保留背景图像的宽高比。

components/backyard.dart文件中添加一个名为bgRect的矩形(Rect)实例变量,并在构造函数内部,初始化这个矩形(Rect)。

class Backyard {
  ...

  Rect bgRect;

  Backyard(this.game) {
    bgSprite = Sprite('bg/backyard.png');
    
    bgRect = Rect.fromLTWH(
      0,
      game.screenSize.height - (game.tileSize * 23),
      game.tileSize * 9,
      game.tileSize * 23,
    );
  }

  ...
}

上面代码中,矩形(Rect)构造函数fromLTWH的4个参数分别对应于x坐标、y坐标、宽度和高度的值。我们以最大宽度绘制背景,因此,它的宽度从x开始延伸到game.tileSize * 9为止,我们也可以在这里使用game.screenSize.width,因为game.tileSize是等于game.screenSize.width ÷ 9的。

在前面的计算中,我们已知背景图像为9 x 23的图块大小,因此要绘制整个背景图像的话,只需要设置game.tileSize * 23的高度即可。最后,y坐标是一个负数,对应于屏幕大小和背景图像的差异。

如果设备屏幕的宽高比为9:16,则屏幕的高度为16 * 图块大小,如果从中减去23 * 图块大小,我们就可以得到-7 * 图块大小的值,这意味着背景图片是使用屏幕顶部边缘上方的7个图块大小的地方开始绘制的。

通过上面的计算,背景图像将始终锚定在设备屏幕的底部,最后,我们在调用此组件的渲染(render)方法时就绘制背景图像。

  void render(Canvas c) {
    bgSprite.renderRect(c, bgRect);
  }

到这里为止,我们的components/backyard.dart里面应该有以下代码。

import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';

class Backyard {
  final HitGame game;
  Sprite bgSprite;

  Rect bgRect;

  Backyard(this.game) {
    bgSprite = Sprite('bg/backyard.png');

    bgRect = Rect.fromLTWH(
      0,
      game.screenSize.height - (game.tileSize * 23),
      game.tileSize * 9,
      game.tileSize * 23,
    );
  }

  void render(Canvas c) {
    bgSprite.renderRect(c, bgRect);
  }

  void update(double t) {}
}

添加游戏背景

上面我们已经完成了背景组件,现在让我们把它添加到游戏逻辑中,打开hit-game.dart文件,导入hello_flame/components/backyard.dart,然后添加一个名为backgroundBackyard类型的新实例变量。

然后在initialize方法中,实例化一个新的Backyard对象,并将其分配给实例变量background,而且,必须在确定屏幕大小后再执行此操作,因为Backyard类的构造函数中使用到了屏幕大小和图块大小值。

还有就是,要像我们创建游戏组件Fly一样,使用关键字this传递当前的HitGame实例。

...
import 'package:hello_flame/components/backyard.dart';

class HitGame extends Game {
  ...
  Backyard background;

  ...

  void initialize() async {
    enemy = List<Fly>();
    rnd = Random();
    resize(await Flame.util.initialDimensions());

    background = Backyard(this);
    produceFly();
  }

  ...
}

然后在渲染(render)方法中,调用background的渲染(render)方法并将画布(Canvas)传递给它。同时,删除我们之前绘制的一个纯色矩形背景。

  void render(Canvas canvas) {
    // 删除以下内容
    // Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
    // Paint bgPaint = Paint();
    // bgPaint.color = Color(0xff576574);
    // canvas.drawRect(bgRect, bgPaint);
    background.render(canvas);

    enemy.forEach((Fly fly) => fly.render(canvas));
  }

当我们现在运行游戏时,应该可以看到游戏背景在不同手机上会有不同的高度。

游戏背景组件

改变游戏组件

目前,文件的预加载资源中,有五种不同的蚊子素材,我们现在就重点看下它们的视觉差异。具体可以通过子类来实现,就是创建一个类作为子类扩展现有的父类。

我们的蚊子素材的大小相对于实例变量flyRect的矩形来说,会占用更大的图块大小,要解释清楚的话,可能要借助于下面的示例图。

蚊子素材占用的图块大小.png

在上面的示例图中,精灵(sprite)将被绘制在蓝色图块内,我们就称它为精灵(sprite)矩形。但是点击需要发生在红色图块内,我们就称它为命中矩形,而在代码中它被命名为flyRect

在开始创建第一个子类之前,我们要先准备好一个可以进行扩展的父类。打开components/fly.dart文件,Fly类将具有所有蚊子种类共享的常用方法和变量。首先,删除画矩形(drawRect)方法,因为我们不需要绘制矩形了,再清空渲染(render)方法。

然后,删除所有对flyPaint的引用,因为该对象仅用于绘制矩形,从实例变量、onTapDown处理方法和构造函数中都删除它。但是我们仍然会使用flyRect作为命中矩形,所以让它留在文件中。

class Fly {
  final HitGame game;
  Rect flyRect;
  // 删除内容
  // Paint flyPaint;
  bool isDead = false;
  bool isOffScreen = false;

  Fly(this.game, double x, double y) {
    flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
    // 删除内容
    // flyPaint = Paint();
    // flyPaint.color = Color(0xff6ab04c);
  }

  void render(Canvas c) {
    // 删除内容
    // c.drawRect(flyRect, flyPaint);
  }

  ...

  void onTapDown() {
    isDead = true;
    // 删除内容
    // flyPaint.color = Color(0xffff4757);
    game.produceFly();
  }
}

对于Fly类或其子类的每个实例,都需要准备和存储2组精灵(sprite),1组将由2个精灵(sprite)组成,这些精灵将1个接1个地显示,以绘制出“飞行”的动画效果,我们需要一个List

另一组将只有1个精灵(sprite)将在蚊子死亡时显示,这里需要另一个实例变量来存储将为“飞行”动画显示的精灵(sprite)。

在文件顶部导入flame/sprite.dart,并在实例变量部分中添加下面代码。

...
import 'package:flame/sprite.dart';

class Fly {
  ...
  List<Sprite> flyingSprite;
  Sprite deadSprite;
  double flyingSpriteIndex = 0;

但是这些精灵(sprite)变量不会在Fly类中初始化,因为每个子类都会使用不同的精灵(sprite),在渲染(render)方法中,我们会根据实例的状态(死或生)来渲染精灵(sprite)。

  void render(Canvas c) {
    if (isDead) {
      deadSprite.renderRect(c, flyRect.inflate(2));
    } else {
      flyingSprite[flyingSpriteIndex.toInt()].renderRect(c, flyRect.inflate(2));
    }
  }

在上面的代码中,渲染(render)方法通过检查isDead变量来决定显示哪个精灵(sprite),如果当前实例已死,则渲染deadSprite,如果没有,则渲染flyingSprite列表中的第0个下标项。

对于flyingSpriteIndex.toInt()来说,List的精灵(sprite)项由整数索引访问,而flyingSpriteIndex是双精度(double)类型的,所以需要先转换为整型(int)。那么为啥它是双精度(double)类型的呢,因为我们将使用更新(update)方法中的时间增量(t)来递增它。

最后一部分的.inflate(2)只是创建了一个被调用的矩形的副本,但是从中心开始按乘数膨胀,这里我们把乘数设置为2,因为从蚊子素材的大小来看,精灵(sprite)矩形的大小约是命中矩形的2倍。

到这里为止,我们的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;

  Fly(this.game, double x, double y) {
    flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
  }

  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;
      }
    }
  }

  void onTapDown() {
    isDead = true;
    game.produceFly();
  }
}

创建组件子类

现在创建第一个蚊子种类,这是一个“正常”的种类,就将它命名为MosquitoFly,一只正常飞行的蚊子。在components文件夹下新建一个mosquito-fly.dart文件并打开它,创建基本的组件类,但这次我们扩展了Fly类。

import 'package:flame/sprite.dart';
import 'package:hello_flame/components/fly.dart';
import 'package:hello_flame/hit-game.dart';

class MosquitoFly extends Fly {
  MosquitoFly(HitGame game, double x, double y) : super(game, x, y) {
    flyingSprite = List<Sprite>();
    flyingSprite.add(Sprite('flies/mosquito-fly-1.png'));
    flyingSprite.add(Sprite('flies/mosquito-fly-2.png'));
    deadSprite = Sprite('flies/mosquito-fly-dead.png');
  }
}

上面的代码中,先导入该类所依赖的包和类,然后声明一个名为MosquitoFly的类,并使其扩展Fly类,从而有效地创建一个Fly子类,其可以访问和覆盖Fly类的变量和方法。

构造函数中调用super,这样在构造函数执行代码当前类代码之前,就会先运行父类的构造函数。构造函数只映射父类构造函数所需的参数,并在调用super期间转发它们。

在构造函数中,通过创建一个精灵(Sprite)列表的新实例来初始化此子类从Fly类继承的flyingSprite变量,然后我们在这个列表中添加两个精灵(Sprite),它们对应于飞行动画的2个帧。

然后我们将“正常”蚊子的掉落图加载到精灵(Sprite)中并将其分配给deadSprite

我们现在不会覆盖更新(update)和渲染(render)方法,因为目前没有针对这类蚊子的特定内容,所有蚊子都相同。

生产正常蚊子

现在回到hit-game.dart文件中,编辑produceFly方法以一个MosquitoFly而不是父类Fly。在文件顶部导入刚刚创建的子类,然后替换之前生成Fly的代码。

...
import 'package:hello_flame/components/mosquito-fly.dart';

class HitGame extends Game {
  ...

  void produceFly() {
    double x = rnd.nextDouble() * (screenSize.width - tileSize);
    double y = rnd.nextDouble() * (screenSize.height - tileSize);
    // 删除内容
    // enemy.add(Fly(this, x, y));
    enemy.add(MosquitoFly(this, x, y));
  }

现在运行游戏,应该可以看到下面图片所示的效果。

换上背景和素材后.PNG

相关文章

  • Flutter游戏:垃圾里会生蚊子

    加载游戏资源 在开始下面的内容之前,最好的话是先把《开始用Flutter做游戏吧》过一遍,然后再完成《Flutte...

  • Flutter游戏:蚊子飞来飞去

    本文紧接上文《Flutter游戏:垃圾里会生蚊子》中完成的代码内容,建议先完成前面的代码呦。 更多蚊子种类 现在我...

  • 蚊子👎

    这儿的蚊子不多,但是,它们也很疯狂呀! 就是在睡觉的时候,蚊子大军开始出发了,用他那天天在垃圾桶里...

  • 蚊子👎

    这儿的蚊子不多,但是,它们也很疯狂呀! 就是在睡觉的时候,蚊子大军开始出发了,用他那天天在垃圾桶里...

  • 2.1 Flutter源码调试

    一般情况下,通过flutter run 构建并运行flutter工程时,会生成Engine/Embedder的依赖...

  • 2021-03-20 为什么你的疾病会不断反复

    房间里面有苍蝇蚊子,你要怎样来处理,要找到垃圾源头,把垃圾清理走,才可以让苍蝇蚊子变少,离开房间。人身体的炎症也是...

  • Flutter 技能篇: Flutter 上的内存泄漏监控

    Flutter 上的内存泄漏监控 原文地址 1、前言 Flutter 所使用的 dart 语言具有垃圾回收机制,有...

  • Unity2D垃圾分类小游戏

    通过拖拽实例化的垃圾到制定的垃圾桶里,实现游戏逻辑。Unity实现对UI的拖拽。移动。

  • Flutter状态管理

    Flutter状态管理ScopedModel 垃圾Redux 咸鱼的 太大bloc 也还行StatefulWidg...

  • Flutter开发游戏初体验,喜大普奔

    今天来给大家带来一个更劲爆的知识点——Flutter开发游戏。是的,没错,Flutter也可以用来开发游戏了。有人...

网友评论

    本文标题:Flutter游戏:垃圾里会生蚊子

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