一. 游戏介绍

使用 Flutter & Flame 模仿微信飞机大战.

源码: https://github.com/Flame-CN/biubiubiu.git

预览: 飞机大战体验地址

二.创建项目

创建项目

1
flutter create biubiubiu

1. 在pubspec.yaml文件中添加依赖

1
2
flame: ^0.24.0
flame_scrolling_sprite: ^0.0.2

flame 游戏引擎
flame_scrolling_sprite 图片滚屏组件

2.添加资源

根目录创建assets 文件夹,将资源文件放入其中:

1
2
3
4
5
├─assets
│ ├─audio
│ ├─font
│ └─images

pubspec.yaml中配置资源文件:

1
2
3
4
5
6
7
#~~~
assets:
- assets/audio/
- assets/images/
- assets/images/ui/
#~~~

3.初始化项目

删除test文件夹下内容, 清空 ./lib/main.dart 内容并添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'package:flame/flame.dart';
import 'package:flutter/material.dart';

import 'biu_biu_game.dart';

void main() async{
WidgetsFlutterBinding.ensureInitialized();
//设置全屏
await Flame.util.fullScreen();
//设置屏幕方向只能竖屏显示
await Flame.util.setPortraitDownOnly();
//获取屏幕size
Size size = await Flame.util.initialDimensions();
//稍后创建BiuBiuGame类
runApp(BiuBiuGame(size).widget);

}

BiuBiuGame().widget就是一个Flutter的widget,因此你可以把它放在Fluuter中的任何地方.

3.创建Game类

创建文件 ./lib/biu_biu_game.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'dart:ui';

import 'package:flame/game.dart';

class BiuBiuGame extends BaseGame {
// 适配屏幕 精灵大小的基本单位
double tileSize;
BiuBiuGame(Size size) {
resize(size);
}

@override
void resize(Size size) {
// 屏幕可显示9个精灵
tileSize = size.width / 9;
super.resize(size);
}

@override
Color backgroundColor() => Color(0xffc3c8c9);
}

这里重写backgroundColor()方法 设置背景色为0xffc3c8c9,与随后要使用的背景图片基色一致. 如果不这样设置,图片滚动时图片连接处会有一条缝隙.

运行 app 可看到如下效果:

4.创建背景

在./lib创建文件夹component;然后在component文件夹下创建background.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'dart:ui';

import 'package:flame_scrolling_sprite/flame_scrolling_sprite.dart';

class Background extends ScrollingSpriteComponent {
Background(Size size, {double speed = 30, x = 0.0, y = 0.0})
: super(
x: x, //图片x方向偏移距离
y: y, //图片y方向偏移距离
scrollingSprite: ScrollingSprite(
spritePath: "background.png",
spriteWidth: 480,
spriteHeight: 700,
width: size.width,
height: size.height,
verticalSpeed: speed,
),
);
}

ScrollingSprite中用到的属性解释:

  • spritePath: 背景图片地址
  • spriteWidth: 背景图片的宽度
  • spriteHeight: 背景图片的高度
  • width: 滚动区域宽度
  • height: 滚动区域高度
  • verticalSpeed: 垂直滚动速度

./lib/biu_biu_game.dart中将 背景组件加入gmae中:

1
2
3
4
5
6
7
8
///...
BiuBiuGame(Size size) {
this.size = size;
//添加背景组件
add(Background(size));
}
///...

运行app可看到如下效果:

5.创建Player

创建./lib/component/player.dart 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import 'dart:ui';

import 'package:flame/animation.dart';
import 'package:flame/components/animation_component.dart';
import 'package:flame/components/component.dart';
import 'package:flame/components/mixins/has_game_ref.dart';
import 'package:flame/sprite.dart';

import '../biu_biu_game.dart';

class Player extends PositionComponent with HasGameRef<BiuBiuGame> {

Animation _live;
//player是否在移动
bool onMove = false;
//生命值
int life = 1;

Player({this.life = 1});

@override
void onMount() {
//player图片宽高为102*126 设置player 宽度为 1 tileSize
width = gameRef.tileSize;
height = 126 / 102 * gameRef.tileSize;
//设置player动画
_live = Animation.spriteList(
[
Sprite("me1.png"),
Sprite("me2.png"),
],
stepTime: 0.2,
);
}

@override
void render(Canvas c) {
prepareCanvas(c);
if (life >= 0) {
_live.getSprite().render(c,
width: width, height: height);
}
}

@override
void update(double t) {
super.update(t);
if (life >= 0) {
_live.update(t);
}
}
}

​ 创建Player 类 , Player 继承PositionComponent 类,用来记录Player的位置,Plaer mixin HasGameRef<BiuBiuGame>,这样当我们在 BiuBiuGame中调用add方法添加我们的Player时,BiuBiuGame会将BiuBiuGame的引用赋值给 HasGameRef中的gameRef.这样我们可以很方便的在 Player类中使用BiuBiuGame.

​ 因为我们需要根据BiuBiuGame 中的size 来计算Player的大小,所以需要在在onMount()方法中初始化Player.

我们的BiubiuGame继承了BaseGame ,当使用add方法添加 component组件时,会对mixinHasGameRef<T>Resizable等组件进行处理,然后调用 component组件的 onMount()方法.

BiuBiuGame中添加一个Player:

1
2
3
4
5
6
7
8
9
10
11
12
13
class BiuBiuGame extends BaseGame {
...
Player player;
...
BiuBiuGame(Size size) {
....
add(player = Player()
..anchor = Anchor.center//设置player的中心点
//设置player的位置在宽度的中心,高度的1/4处
..setByPosition(Position(size.width / 2, size.height * 0.75)));
....
}
}

篇幅原因会在已有的类中新添加内容时使用...包裹新添加的内容,详细代码参照源码.

运行游戏可以看到下面画面:

控制player的移动:

Player中添加void move(Offset offset)方法:

1
2
3
4
5
6
7
8
9
10
void move(Offset offset) {
x += offset.dx;
//限制x轴移动距离防止超出屏幕
x = max(0.0, x);
x = min(gameRef.size.width, x);
y += offset.dy;
//限制y轴移动距离防止超出屏幕
y = max(0.0, y);
y = min(gameRef.size.height, y);
}

BiuBiuGame中添加拖动的手势控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class BiuBiuGame extends BaseGame with PanDetector {
...
@override
void onPanStart(DragStartDetails details) {
//拖动的起始点如果在player上,改变player移动状态
if (player.toRect().contains(details.globalPosition)) {
player.onMove = true;
}
}
@override
void onPanUpdate(DragUpdateDetails details) {
if (player.onMove) {
//拖动更新 移动player
player.move(details.delta);
}
}
@override
void onPanEnd(DragEndDetails details) {
//拖动结束时
if (player.onMove) {
onPanCancel();
}
}
@override
void onPanCancel() {
if (player.onMove) {
player.onMove = false;
}
}
...
}

运行游戏我们可以控制我们的player了:

6. 发射子弹

创建./lib/component/bullet.dart文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import 'dart:ui';

import 'package:biubiubiu/biu_biu_game.dart';
import 'package:flame/anchor.dart';
import 'package:flame/components/component.dart';
import 'package:flame/components/mixins/has_game_ref.dart';
import 'package:flame/position.dart';
import 'package:flame/sprite.dart';
import 'package:flame/time.dart';
import 'package:flutter/cupertino.dart';

import 'player.dart';

class Bullet extends SpriteComponent {
//子弹的速度
double speed;
//子弹的伤害
double power;
//是否销毁
bool isDestroy = false;

Bullet({Position position, this.speed = 300.0, this.power = 1.0,String img="bullet1.png"}) {
setByPosition(position);
width = 5.0;
height = 11.0;
sprite = Sprite(img);
anchor = Anchor.center;
}

@override
void update(double dt) {
super.update(dt);
y -= speed * dt;
//子弹超出屏幕销毁
if (y < 0) {
isDestroy = true;
}
}

@override
bool destroy() => isDestroy;
}

./lib/component/bullet.dart中添加一个BulletFactory class 用来产生子弹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BulletFactory extends Component with HasGameRef<BiuBiuGame> {
Player player;
Timer _timer;
double limit;

BulletFactory({this.limit = 1});

@override
void onMount() {
_timer = Timer(limit, repeat: true, callback: () {
gameRef.addLater(Bullet(position: gameRef.player.toPosition()));
});
_timer.start();
}

@override
void render(Canvas c) {}

@override
void update(double t) {
_timer.update(t);
}
}

flame提供了Timer类来执行定时任务,Timer类接收三个参数:

  • limit:必填参数 任务间隔时间 单位 秒
  • repeat: 可选默认 false
  • callback 可选 需要执行的任务(回调函数)

给plaer装备武器系统–在./lib/component/player.dart中添加 BulletFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Player extends PositionComponent with HasGameRef<BiuBiuGame> {
...
BulletFactory _bulletFactory;
...

@override
void onMount() {
...
_bulletFactory = BulletFactory(limit: 0.3);
gameRef.add(_bulletFactory);
...
}
}

现在我们的player可以发射子弹了:

优化:

​ 可以看到我们的bullet是从plaer的上面发射出去的,我们可以重写int priority()方法,指定 Player的渲染顺序,返回的数值越大,越靠近上层。这里,我们的Player返回了100.

Bullet中添加:

1
2
3
4
5
6
class Bullet extends SpriteComponent {
...
@override
int priority() => 10;
...
}

Player中添加:

1
2
3
4
5
6
class Player extends PositionComponent with HasGameRef<BiuBiuGame> {
...
@override
int priority() => 100;
...
}

7.创建敌人

创建./lib/component/enemy/enemy.dart文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import 'dart:ui';

import 'package:flame/animation.dart';
import 'package:flame/components/component.dart';
import 'package:flame/components/mixins/has_game_ref.dart';

import '../../biu_biu_game.dart';

enum EnemyState { LIVING, HIT, DESTROY }

class Enemy extends PositionComponent with HasGameRef<BiuBiuGame> {
Animation livingAnimation;
Animation hitAnimation;
Animation destroyAnimation;

EnemyState state = EnemyState.LIVING;

int life;
int power;
double speed;
int score;
bool isDestroy = false;

Enemy({this.life = 1, this.power = 1, this.speed = 150, this.score = 1});

@override
void render(Canvas c) {
prepareCanvas(c);
switch (state) {
case EnemyState.LIVING:
livingAnimation?.getSprite()?.render(c, width: width, height: height);
break;
case EnemyState.HIT:
hitAnimation?.getSprite()?.render(c, width: width, height: height);
break;
case EnemyState.DESTROY:
destroyAnimation?.getSprite()?.render(c, width: width, height: height);
break;
}
}

@override
void update(double dt) {
super.update(dt);
switch (state) {
case EnemyState.LIVING:
livingAnimation?.update(dt);
break;
case EnemyState.HIT:
hitAnimation?.update(dt);
if (hitAnimation!=null&&hitAnimation.done()) {
state = EnemyState.LIVING;
}
break;
case EnemyState.DESTROY:
destroyAnimation?.update(dt);
if (destroyAnimation.done()) {
isDestroy = true;
}
break;
}
//战机生成后向下移动
y += speed * dt;
//超出屏幕销毁
if (y > gameRef.size.height + height) {
isDestroy = true;
}
}

void hurt(int power) {
life -= power;
if (life > 0) {
state = EnemyState.HIT;
} else {
state = EnemyState.DESTROY;
}
}
@override
bool destroy() => isDestroy;
@override
int priority() => 2;
}

敌机设计:

小飞机 战斗机 飞船
生命值 1 3 5
移动速度 5 3 2
分数 10 50 100

MiniPlane

./lib/component/enemy/mini_plane.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import 'package:flame/animation.dart';
import 'package:flame/sprite.dart';

import 'enemy.dart';

class MiniPlane extends Enemy {
MiniPlane({int score = 10}) : super(life: 1, score: score);

@override
void onMount() {
//57*43
width = gameRef.tileSize;
height = 43 / 57 * width;
//速度
speed = 5 * gameRef.tileSize;
livingAnimation = Animation.spriteList(
[
Sprite("enemy1.png"),
],
stepTime: 0.2,
loop: true,
);
destroyAnimation = Animation.spriteList(
[
Sprite("enemy1_down1.png"),
Sprite("enemy1_down2.png"),
Sprite("enemy1_down3.png"),
Sprite("enemy1_down4.png"),
],
stepTime: 0.2,
loop: false,
);
}
}

Warplane

./lib/component/enemy/warplane.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import 'package:flame/animation.dart';
import 'package:flame/sprite.dart';

import 'enemy.dart';

class Warplane extends Enemy {
Warplane({int score = 50}) : super(life: 3, score: score);

@override
void onMount() {
//69*95
width = 1.5 * gameRef.tileSize;
height = 95 / 69 * width;
speed = 3 * gameRef.tileSize;
livingAnimation = Animation.spriteList(
[
Sprite("enemy2.png"),
],
stepTime: 0.2,
loop: true,
);
hitAnimation = Animation.spriteList(
[
Sprite("enemy2_hit.png"),
],
stepTime: 0.2,
loop: false,
);
destroyAnimation = Animation.spriteList(
[
Sprite("enemy2_down1.png"),
Sprite("enemy2_down2.png"),
Sprite("enemy2_down3.png"),
Sprite("enemy2_down4.png"),
],
stepTime: 0.2,
loop: false,
);
}
}

ShipEnemy

./lib/component/enemy/ship_enemy.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import 'package:flame/animation.dart';
import 'package:flame/sprite.dart';

import 'enemy.dart';

class ShipEnemy extends Enemy {
ShipEnemy({int score = 100}) : super(life: 5, score: score);

@override
void onMount() {
//165*260
width = 3 * gameRef.tileSize;
height = 260 / 165 * width;
speed = 2 * gameRef.tileSize;
livingAnimation = Animation.spriteList(
[
Sprite("enemy3_n1.png"),
Sprite("enemy3_n2.png"),
],
stepTime: 0.2,
loop: true,
);
hitAnimation = Animation.spriteList(
[
Sprite("enemy3_hit.png"),
],
stepTime: 0.2,
loop: false,
);
destroyAnimation = Animation.spriteList(
[
Sprite("enemy3_down1.png"),
Sprite("enemy3_down2.png"),
Sprite("enemy3_down3.png"),
Sprite("enemy3_down4.png"),
Sprite("enemy3_down5.png"),
Sprite("enemy3_down6.png"),
],
stepTime: 0.2,
loop: false,
);
}
}

EnemyFactory

./lib/component/enemy/enemy_factory.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import 'dart:math';

import 'package:flame/components/mixins/has_game_ref.dart';
import 'package:flame/position.dart';
import 'package:flame/time.dart';
import 'package:flutter/cupertino.dart';

import '../../biu_biu_game.dart';
import 'enemy.dart';
import 'mini_plane.dart';
import 'ship_enemy.dart';
import 'warplane.dart';

class EnemyFactory with HasGameRef<BiuBiuGame> {
Timer _timer;
Random _random = Random();

EnemyFactory({@required BiuBiuGame game, double limit = 1}) {
gameRef = game;
_timer = Timer(limit, repeat: true, callback: () {
gameRef.addLater(generate());
});
_timer.start();
}

void update(double dt) {
_timer.update(dt);
}

Enemy generate() {
switch (_random.nextInt(3)) {
case 1:
return MiniPlane()..setByPosition(randomPosition(gameRef.tileSize, 43 / 57 * gameRef.tileSize));
break;
case 2:
return Warplane()..setByPosition(randomPosition(1.5 * gameRef.tileSize, (95 / 69) * 1.5 * gameRef.tileSize));
break;
default:
return ShipEnemy()..setByPosition(randomPosition(3 * gameRef.tileSize, (260 / 165) * 3 * gameRef.tileSize));
break;
}
}
//随机生成位置
Position randomPosition(double width, height) {
return Position(_random.nextDouble() * (gameRef.size.width - width), -height);
}
}

./lib/biu_biu_game.dart中生成敌人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BiuBiuGame extends BaseGame with PanDetector {
...
EnemyFactory _enemyFactory;
...
BiuBiuGame(Size size) {
...
//添加敌人生产工厂组件
_enemyFactory = EnemyFactory(game: this);
...
}
@override
void update(double t) {
...
_enemyFactory?.update(t);
...
}
}

运行程序可以看到如下界面:

8.添加碰撞

./lib/biu_biu_game.dart中添加检测碰撞方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class BiuBiuGame extends BaseGame with PanDetector {

@override
void update(double t) {
...
//碰撞检测
collide();
...
}

...
void collide() {
var bullets = components.whereType<Bullet>().toList();
components.whereType<Enemy>().forEach((enemy) {
// player 和 enemy 之间的碰撞检测
if (enemy.state != EnemyState.DESTROY && player.life > 0 && player.toRect().overlaps(enemy.toRect())) {
//碰撞全部销毁
enemy.hurt(enemy.life);
player.hurt(player.life);
return;
}
// enemy 和 bullet 之间的碰撞检测
bullets.forEach((bullet) {
//当player生命值大于0,enemy状态不为DESTROY时,才进行bullet和enemy的碰撞检测
if (enemy.state != EnemyState.DESTROY && player.life > 0 && bullet.toRect().overlaps(enemy.toRect())) {
enemy.hurt(bullet.power);
bullet.isDestroy = true;
}
});
});
}
...
}

运行程序可以看到如下界面:

在游戏中我们会发现,有时我们的player并没有和enemy发生接触却被判定游戏失败.这是因为在碰撞检测中,我们使用的是PositionComponentRect toRect()方法返回的矩形进行判断的,这导致我们的碰撞判定范围大于我们的图片显示范围.

开启 debugMode 我们可以清楚的看到原因:

./lib/biu_biu_game.dart添加:

1
2
3
4
5
6
class BiuBiuGame extends BaseGame with PanDetector, HasWidgetsOverlay {
...
@override
bool debugMode() => true;
...
}

重启游戏我们看到:

碰撞优化:

 这里我们进行一个简单的优化,使用`Rect`中的`deflate()`方法来缩小用于碰撞检测的矩形 .

​ 修改./lib/biu_biu_game.dart中的collide()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void collide() {
var bullets = components.whereType<Bullet>().toList();
components.whereType<Enemy>().forEach((enemy) {
// player 和 enemy 之间的碰撞检测
if (enemy.state != EnemyState.DESTROY &&
player.life > 0 &&
player.toRect().deflate(0.1 * player.width).overlaps(enemy.toRect().deflate(0.1 * enemy.width))) {
//碰撞全部销毁
enemy.hurt(enemy.life);
player.hurt(player.life);
gameOver();
return;
}
// enemy 和 bullet 之间的碰撞检测
bullets.forEach((bullet) {
//当player生命值大于0,enemy状态不为DESTROY时,才进行bullet和enemy的碰撞检测
if (enemy.state != EnemyState.DESTROY && player.life > 0 && bullet.toRect().overlaps(enemy.toRect())) {
enemy.hurt(bullet.power);
bullet.isDestroy = true;
//当enemy被击毁时,计算得分
if (enemy.state == EnemyState.DESTROY) {
score += enemy.score;
}
}
});
});
}

9.记录分数

./lib/biu_biu_game.dart中添加 score 用于记录分数,添加TextComponent用于显示结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class BiuBiuGame extends BaseGame with PanDetector {
...
int score = 0;
TextComponent scoreComponent;
...

...
BiuBiuGame(Size size) {
...
scoreComponent = TextComponent("SCORE $score", config: TextConfig(color: Color(0xffffffff)))
..x = 10
..y = 10;
...
}
...

@override
void render(Canvas canvas) {
super.render(canvas);
//最后渲染scoreComponent,使其在最上层
scoreComponent.render(canvas);
}

@override
void update(double t) {
super.update(t);
//生成敌人
_enemyFactory?.update(t);
//检测碰撞
collide();
//计算分数
scoreComponent.text = "SCORE $score";

}

void collide() {
var bullets = components.whereType<Bullet>().toList();
components.whereType<Enemy>().forEach((enemy) {
// player 和 enemy 之间的碰撞检测
if (enemy.state != EnemyState.DESTROY && player.life > 0 && player.toRect().overlaps(enemy.toRect())) {
//碰撞全部销毁
enemy.hurt(enemy.life);
player.hurt(player.life);
return;
}
// enemy 和 bullet 之间的碰撞检测
bullets.forEach((bullet) {
if (enemy.state != EnemyState.DESTROY && bullet.toRect().overlaps(enemy.toRect())) {
enemy.hurt(bullet.power);
bullet.isDestroy = true;
//当enemy被击毁时,计算得分
if (enemy.state == EnemyState.DESTROY) {
score += enemy.score;
}
}
});
});
}

10.游戏结果展示

player销毁后展示玩家最终得分并启动一个计时器3秒后重新开始游戏.

./lib/biu_biu_game.dart构造方法中的内容提取到init()方法中,添加一个Timer restarTimer.

完整的./lib/biu_biu_game.dart文件中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import 'dart:ui';

import 'package:flame/anchor.dart';
import 'package:flame/components/text_component.dart';
import 'package:flame/game.dart';
import 'package:flame/gestures.dart';
import 'package:flame/position.dart';
import 'package:flame/text_config.dart';
import 'package:flame/time.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';

import 'component/background.dart';
import 'component/bullet.dart';
import 'component/enemy/enemy.dart';
import 'component/enemy/enemy_factory.dart';
import 'component/player.dart';
// mixin HasWidgetsOverlay 可以添加flutter的widget.
class BiuBiuGame extends BaseGame with PanDetector, HasWidgetsOverlay {
double tileSize;
Player player;
EnemyFactory _enemyFactory;

int score;
TextComponent scoreComponent;
Timer restartTimer;

BiuBiuGame(Size size) {
resize(size);
init();
}

void init() {
components.clear();
score=0;
//添加背景组件
add(Background(size));
add(player = Player()
..anchor = Anchor.center
..setByPosition(Position(size.width / 2, size.height * 0.75)));
//添加敌人生产工厂组件
_enemyFactory = EnemyFactory(game: this);
scoreComponent = TextComponent("SCORE $score", config: TextConfig(color: Color(0xffffffff)))
..x = 10
..y = 10;
restartTimer = Timer(3.0, callback: () {
removeWidgetOverlay("gameOver");
init();
});
}

void gameOver() {
restartTimer.start();
addWidgetOverlay(
"gameOver",
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"您的最终分数:$score",
style: TextStyle(
color: Color(0xffffffff),
fontSize: 24,
fontWeight: FontWeight.w700,
),
),
],
),
));
}

@override
void resize(Size size) {
tileSize = size.width / 9;
super.resize(size);
}

@override
void render(Canvas canvas) {
super.render(canvas);
scoreComponent.render(canvas);
}

@override
void update(double t) {
super.update(t);
//生成敌人
_enemyFactory?.update(t);
restartTimer.update(t);
//检测碰撞
collide();
//计算分数
scoreComponent.text = "SCORE $score";
}

@override
void onPanUpdate(DragUpdateDetails details) {
if (player.toRect().contains(details.globalPosition)) {
player.move(details.delta);
}
}

void collide() {
var bullets = components.whereType<Bullet>().toList();
components.whereType<Enemy>().forEach((enemy) {
// player 和 enemy 之间的碰撞检测
if (enemy.state != EnemyState.DESTROY && player.life > 0 && player.toRect().overlaps(enemy.toRect())) {
//碰撞全部销毁
enemy.hurt(enemy.life);
player.hurt(player.life);
gameOver();
return;
}
// enemy 和 bullet 之间的碰撞检测
bullets.forEach((bullet) {
//当player生命值大于0,enemy状态不为DESTROY时,才进行bullet和enemy的碰撞检测
if (enemy.state != EnemyState.DESTROY && player.life > 0 && bullet.toRect().overlaps(enemy.toRect())) {
enemy.hurt(bullet.power);
bullet.isDestroy = true;
//当enemy被击毁时,计算得分
if (enemy.state == EnemyState.DESTROY) {
score += enemy.score;
}
}
});
});
}

@override
Color backgroundColor() => Color(0xffc3c8c9);

@override
bool debugMode() => false;
}

BiuBiuGame类mixin了 HasWidgetsOverlay.这个mixin类让我们可以方便的添加Flutter的widget到Game类中. Flame底层使用了Stack来展示 addWidgetOverlay添加的widget.

1
Stack(children: [widget.gameChild, ..._overlays.values.toList()]));

运行游戏可以看到:

显示游戏FPS值:

BaseGame中提供了double fps()这个方法获取fps值的方法,但是需要bool recordFps()方法返回true才进行记录 :

./lib/biu_biu_game.dart添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BiuBiuGame extends BaseGame with PanDetector, HasWidgetsOverlay {
...
@override
void update(double t) {
...
if(recordFps()){
scoreComponent.text+="\nFPS ${fps().toStringAsFixed(2)}";
}
...
}
@override
bool recordFps() =>true;
...
}

11.适配web

因为web端还未在Flutter正式版支持,需要切换到Flutter beta 版本,参考文章使用 Flutter 构建 Web 应用将Flutter切换到 beta版然后改造我们的项目:

修改./lib/main.dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//设置全屏 屏幕方向这些操作web端不支持需要进行判断
if (!kIsWeb) {
//设置全屏
await Flame.util.fullScreen();
//设置屏幕方向只能竖屏显示
await Flame.util.setPortraitDownOnly();
}

//获取屏幕size
final Size size = await Flame.util.initialDimensions();
//稍后创建BiuBiuGame类
runApp(BiuBiuGame(size).widget);
}

修改./lib/biu_biu_game.dart:

1
2
3
4
5
6
@override
void resize(Size size) {
//tileSize 设定一个最大值50.0
tileSize = min(size.width / 9, 50.0);
super.resize(size);
}