加载游戏资源
在开始下面的内容之前,最好的话是先把《开始用Flutter做游戏吧》过一遍,然后再完成《Flutter游戏:万有引力定律》里的游戏,因为下面的内容是在该游戏的基础上开发的。
首先下载这个游戏要用到的游戏资源文件,然后在项目目录下建立assets/images
目录,在该目录下再分别建立bg
和flies
目录,用于存放背景图片和组件图片。
资源文件就位后,在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
列表作为参数传递给images
的loadAll
方法,该方法用于预加载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
,然后添加一个名为background
的Backyard
类型的新实例变量。
然后在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));
}
当我们现在运行游戏时,应该可以看到游戏背景在不同手机上会有不同的高度。
![](https://img.haomeiwen.com/i6218810/3b965309ca77ccad.jpeg)
改变游戏组件
目前,文件的预加载资源中,有五种不同的蚊子素材,我们现在就重点看下它们的视觉差异。具体可以通过子类来实现,就是创建一个类作为子类扩展现有的父类。
我们的蚊子素材的大小相对于实例变量flyRect
的矩形来说,会占用更大的图块大小,要解释清楚的话,可能要借助于下面的示例图。
![](https://img.haomeiwen.com/i6218810/d1c0d42a5d45dc4a.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));
}
现在运行游戏,应该可以看到下面图片所示的效果。
网友评论