本篇文章的内容需要在完成以下内容代码的基础上进行哦!
- 《开始用Flutter做游戏吧》
- 《Flutter游戏:万有引力定律》
- 《Flutter游戏:垃圾里会生蚊子》
- 《Flutter游戏:蚊子飞来飞去》
加载更多资源
首先下载接下来要用到的游戏资源文件,因为之前已经下载过一部分,所以下面讲一下这次添加了哪些内容。
- branding/title.png:游戏标题图片,建议7:4大小,即7x4图块
- ui/start-button.png:开始游戏按钮,建议2:1大小,即6x3图块
- bg/lose-splash.png:游戏结束图像,建议7:5大小,即7x5图块
- ui/dialog-credits.png:关于对话框,建议3:2大小,即12x8图块
- ui/dialog-help.png:帮助对话框,与关于对话框大小相同
- ui/icon-credits.png:关于图标,建议为1个图块大小的正方形
- ui/icon-help.png:帮助图标,与关于图标大小相同
了解了这些资源是干什么的以后,开始将以下代码添加到pubspec.yaml
文件中的assets
部分。
flutter:
uses-material-design: true
assets:
...
- assets/images/bg/lose-splash.png
- assets/images/branding/title.png
- assets/images/ui/dialog-credits.png
- assets/images/ui/dialog-help.png
- assets/images/ui/icon-credits.png
- assets/images/ui/icon-help.png
- assets/images/ui/start-button.png
打开main.dart
文件,并将刚才在pubspec.yaml
文件中添加的资源传递给Flame.images.loadAll
调用的字符串(String
)列表中。
Flame.images.loadAll([
...
'bg/lose-splash.png',
'branding/title.png',
'ui/dialog-credits.png',
'ui/dialog-help.png',
'ui/icon-credits.png',
'ui/icon-help.png',
'ui/start-button.png',
]);
这样就完成了游戏资源的预加载部分。
准备游戏页面
一个好的游戏应该有一个欢迎页面和一个游戏页面,在长时间的游戏以后,胜利或失败时也要一个给出结果的页面,而且当玩家点击开始按钮时,应该从欢迎页面切换到游戏页面。所以,我们的游戏应该有下面3个页面。
- 欢迎页面:首次打开游戏时,显示欢迎页面,或者叫主页面,该视图会在中间显示一个游戏标题,然后页面底部还会显示开始按钮。
- 游戏页面:这是玩家在玩游戏时所看到的页面,这个页面隐藏了游戏标题,并专注于蚊子的随机运动。
- 失败页面:当玩家失败时,会出现一个飞溅的页面,具体情况是当玩家失败时,屏幕中间会出现一个提示玩家游戏失败的飞溅图像,并带有开始按钮,玩家可以重新开始。
对于上面的3个页面,都显示同一个背景,并且蚊子也是可见的,这会使玩家感觉游戏页面是主页面,而欢迎页面才是真正的主页面,欢迎真正的引导玩家进入游戏,最后游戏结束后的失败页面是玩家失败后,休息一下再重新开始的页面。
为此呢,我们需要记录当前页面,这里可以使用整数执行此操作,并将页面从0到2编号,或者还可以将页面以字符串来记录。
在Dart语言中,有一种枚举(enum
)的数据类型,正好可以用来处理这个情况,我们将在查看玩家所处的页面或告诉游戏更改页面时枚举页面。新建一个lib/view.dart
文件,并添加下面的代码。
enum View {
home,
playing,
lost,
}
现在需要为游戏添加一个实例变量,它将为我们保存当前页面的值。因此呢,要在使用它之前先导入页面View
枚举,然后再添加实例变量。我们将它命名为活动页面(activeView
),类型设置为View
枚举类型。
打开hit-game.dart
文件,并提交下面的代码。
...
import 'package:hello_flame/view.dart';
class HitGame extends Game {
...
View activeView = View.home;
这样我们就准备好处理每个页面了。
实现欢迎页面
在前面已经简单说了欢迎页面,而在代码中,页面只是另一个类似组件的逻辑,可以拥有自己的子组件。它也可以是虚拟的,无论玩家真正看到什么页面,它总是可见的。
如果是欢迎页面的话,我们将在定义页面时使用组件类,就像其他组件一样,我们只需从游戏循环中调用其实例的渲染(render
)和更新(update
)方法。
首先在lib
目录下创建一个views
文件夹,并在该目录下创建views/home-view.dart
文件,并编写下面的代码。
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';
class HomeView {
final HitGame game;
Rect titleRect;
Sprite titleSprite;
HomeView(this.game) {}
void render(Canvas c) {}
void update(double t) {}
}
上面代码中,先是导入将使用的类和定义的文件,然后我们定义一个名为HomeView
的类,它有3个实例变量,其中一个是final
,需要在创建这个类的实例时传递并赋值。同时该类有1个构造函数和另外2个方法,分别是游戏循环的渲染(render
)和更新(update
)方法。
在构造函数中,将初始化titleRect
和titleSprite
变量,以便它们可以在渲染(render
)方法中使用。
HomeView(this.game) {
titleRect = Rect.fromLTWH(
game.tileSize,
(game.screenSize.height / 2) - (game.tileSize * 4),
game.tileSize * 7,
game.tileSize * 4,
);
titleSprite = Sprite('branding/title.png');
}
上面的2行代码中,第1行为titleRect
变量赋值,第2行为titleSprite
变量赋值。其中,titleRect
变量的值是一个矩形(Rect
)的定义,4行代码对应于工厂构造函数.fromLTWH
所需的参数。
因为我们确定要在一个7×4
块的矩形内显示标题图片,所以会将game.tileSize * 7
和game.tileSize * 4
传递给最后2个参数,这2个参数对应于矩形的宽度和高度。
对于左侧(left
)参数,从9
个图块大小的屏幕宽度中,减去标题图像7
个图块大小的矩形宽度,可以得到2
个额外空间的图块。为了使标题图像居中,这里将这2
个额外的图块分布到左右两侧,使图像偏移1
个图块。所以,这里就传入了game.tileSize * 1
或简单地传递game.tileSize
。
顶部(top
)参数有所不同,因为我们不想让标题图像位于屏幕的中心位置。要计算屏幕的中心位置,只需将屏幕高度除以2
即可,从中减去4
个图块大小的标题图像高度,就可以得到中心化的适当偏移位置。
目前已经初始化了titleRect
和titleSprite
,可以开始编写用于显示实际图像的代码了,在渲染(render
)方法中,添加下面的代码。
void render(Canvas c) {
titleSprite.renderRect(c, titleRect);
}
现在打开hit-game.dart
文件,并导入HomeView
类文件,然后添加一个HomeView
类型的homeView
实例变量。接下来,还需要在确定屏幕大小后初始化该实例变量,因此在调用resize
之后,将在initialize
方法中添加以下代码。
...
import 'package:hello_flame/views/home-view.dart';
class HitGame extends Game {
...
View activeView = View.home;
HomeView homeView;
...
void initialize() async {
...
homeView = HomeView(this);
produceFly();
}
最后还要在屏幕上使用渲染HomeView
的渲染(render
),因此,在游戏类的渲染(render
)方法中,在最后调用HomeView
实例的渲染(render
)方法,使其最后渲染。
因为渲染的顺序与显示顺序相同,因为我们想要的是背景优先,然后是蚊子,最后才是标题图像,这将确保标题图片位于屏幕所有内容之上。
void render(Canvas canvas) {
background.render(canvas);
enemy.forEach((Fly fly) => fly.render(canvas));
if (activeView == View.home) homeView.render(canvas);
}
上面的代码中,先判断活动视图(activeView
)当前是否为主视图,如果是,就渲染HomeView
实例,如果不是,则渲染(render
)方法将跳过此行代码,因此不会呈现HomeView
实例。
现在运行游戏,可以看到像下图所展示的效果。
但是呢,当还在游戏页面内时,玩家仍然可以点击并干掉蚊子,不过没关系,这对游戏没有任何影响。接下来,玩家要开始游戏了,所以必须有一个开始按钮。首先,创建另一个组件,命名为StartButton
,放在新建的components/start-button.dart
文件中。
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';
class StartButton {
final HitGame game;
Rect rect;
Sprite sprite;
StartButton(this.game) {}
void render(Canvas c) {}
void update(double t) {}
void onTapDown() {}
}
这个类定义与其他组件类大致相同,不同的是它有一个onTapDown
处理程序,我们将在这里编写“开始”游戏的代码。首先在构造函数中初始化rect
和sprite
变量。
StartButton(this.game) {
rect = Rect.fromLTWH(
game.tileSize * 1.5,
(game.screenSize.height * .75) - (game.tileSize * 1.5),
game.tileSize * 6,
game.tileSize * 3,
);
sprite = Sprite('ui/start-button.png');
}
上面的代码与HomeView
类构造函数中,标题图像的初始化基本相同。不同的是,除了6×3
个图块大小外,还有左侧(left
)和顶部(top
)偏移。
开始按钮的宽度为6
个图块,这意味着在屏幕的9
个图块宽度上有3
个额外的图块,这样的话,每侧都有1.5
个图块,所以这里将game.tileSize * 1.5
提供给左侧(left
)参数。对于顶部(top
)参数,这样就可以让按钮的垂直位置恰好位于屏幕高度的四分之三即0.75
的位置。
在初始化rect
和sprite
变量之后,需要渲染图像,所以要下面代码添加到render
函数中。
void render(Canvas c) {
sprite.renderRect(c, rect);
}
接下来需要在游戏类中添加一个StartButton
组件的实例,打开hit-game.dart
文件,导入依赖,然后将实例变量与其他实例变量一起添加。同时在确定屏幕大小后,使用StartButton
类的新实例初始化startButton
变量,并在render
方法中增加下面代码。
...
import 'package:hello_flame/components/start-button.dart';
class HitGame extends Game {
...
HomeView homeView;
StartButton startButton;
...
void initialize() async {
...
startButton = StartButton(this);
produceFly();
}
...
void render(Canvas canvas) {
...
if (activeView == View.home || activeView == View.lost) {
startButton.render(canvas);
}
}
上面代码中,我们添加的这4行代码,分别是导入StartButton
类、创建StartButton
类的实例并将其存储在实例变量中、最后展示StartButton
到屏幕上。
现在运行游戏,可以看到开始按钮会在欢迎页面和失败页面上呈现,这样玩家就可以从欢迎页面或失败页面重新开始游戏了。
处理开始按钮
在上一步中,我们让开始按钮显示在屏幕上,但是点击没有反应,不用急,接下来就开始处理开始按钮的响应逻辑。首先,需要确保点击不会穿过物体,例如在点击开始按钮(startButton
)时,同一位置的蚊子不应该接收到点击事件。
在游戏类的onTapDown
处理程序中创建一个isHandled
变量,用于保存当前是否已调用了点击处理程序,在onTapDown
处理程序的开始处创建它,并将初始值设置为false
。在检查点击是否命中矩形(Rect
)组件之前,先检查isHandled
是否仍为false
值,然后再调用组件的点击处理程序。
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;
}
}
enemy.forEach((Fly fly) {
if (fly.flyRect.contains(d.globalPosition)) {
fly.onTapDown();
}
});
}
上面代码中,首先对isHandled
进行检查,确保尚未处理点击事件,这个检查项与检查点击是否在开始按钮(startButton
)的rect
属性内是一起的。如果通过了这些条件的检查,再额外检查玩家是否当前处于欢迎页面或失败页面中。
只有满足所有条件,游戏才会调用开始按钮的onTapDown
处理程序,变量isHandled
也被设置为true
,让下面的代码知道已经处理了这次的点击。接下来要做的是,使用isHandled
检查把之前的蚊子点击处理程序包装起来。
void onTapDown(TapDownDetails d) {
...
if (!isHandled) {
enemy.forEach((Fly fly) {
if (fly.flyRect.contains(d.globalPosition)) {
fly.onTapDown();
isHandled = true;
}
});
}
}
上面的代码基本上与上面类似,首先包含了对isHandled
的检查,这使得代码块只会没有处理开始按钮的情况下运行,其次,如果至少有一个蚊子被点击,则将isHandled
变量设置为true
。
接下来,打开components/start-button.dart
文件,开始编写实际处理开始按钮点击的代码。在调用开始按钮的onTapHandler
时,需要将游戏的activeView
设置为View.playing
,所以这里先导入定义View
枚举的文件。然后在onTapDown
里面,将游戏的activeView
设置为所需的值。
...
import 'package:hello_flame/view.dart';
class StartButton {
...
void onTapDown() {
game.activeView = View.playing;
}
}
现在运行游戏,可以看到点击开始按钮以后,标题图片和开始按钮都不见了,进入了我们之前的游戏页面。