一. 游戲介紹
使用 Flutter & Flame 模仿微信飛機(jī)大戰(zhàn).
源碼: https://github.com/Flame-CN/biubiubiu.git
預(yù)覽: 飛機(jī)大戰(zhàn)體驗地址
二.創(chuàng)建項目
創(chuàng)建項目
flutter create biubiubiu
1. 在pubspec.yaml
文件中添加依賴
flame: ^0.24.0
flame_scrolling_sprite: ^0.0.2
flame 游戲引擎
flame_scrolling_sprite 圖片滾屏組件
2.添加資源
根目錄創(chuàng)建assets
文件夾,將資源文件放入其中:
├─assets
│ ├─audio
│ ├─font
│ └─images
在 pubspec.yaml
中配置資源文件:
#~~~
assets:
- assets/audio/
- assets/images/
- assets/images/ui/
#~~~
3.初始化項目
刪除test
文件夾下內(nèi)容, 清空 ./lib/main.dart
內(nèi)容并添加以下內(nèi)容:
import 'package:flame/flame.dart';
import 'package:flutter/material.dart';
import 'biu_biu_game.dart';
void main() async{
WidgetsFlutterBinding.ensureInitialized();
//設(shè)置全屏
await Flame.util.fullScreen();
//設(shè)置屏幕方向只能豎屏顯示
await Flame.util.setPortraitDownOnly();
//獲取屏幕size
Size size = await Flame.util.initialDimensions();
//稍后創(chuàng)建BiuBiuGame類
runApp(BiuBiuGame(size).widget);
}
BiuBiuGame().widget
就是一個Flutter的widget,因此你可以把它放在Fluuter中的任何地方.
3.創(chuàng)建Game類
創(chuàng)建文件./lib/biu_biu_game.dart
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()方法 設(shè)置背景色為
0xffc3c8c9
,與隨后要使用的背景圖片基色一致. 如果不這樣設(shè)置,圖片滾動時圖片連接處會有一條縫隙.
運(yùn)行 app 可看到如下效果:
4.創(chuàng)建背景
在./lib創(chuàng)建文件夾component
;然后在component文件夾下創(chuàng)建background.dart
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: 滾動區(qū)域?qū)挾?/li>
- height: 滾動區(qū)域高度
- verticalSpeed: 垂直滾動速度
在 ./lib/biu_biu_game.dart
中將 背景組件加入gmae中:
///...
BiuBiuGame(Size size) {
this.size = size;
//添加背景組件
add(Background(size));
}
///...
運(yùn)行app可看到如下效果:
5.創(chuàng)建Player
創(chuàng)建./lib/component/player.dart
文件
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 設(shè)置player 寬度為 1 tileSize
width = gameRef.tileSize;
height = 126 / 102 * gameRef.tileSize;
//設(shè)置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);
}
}
}
? 創(chuàng)建Player
類 荤牍, Player
繼承PositionComponent
類独旷,用來記錄Player的位置,Plaer mixin
HasGameRef<BiuBiuGame>
,這樣當(dāng)我們在 BiuBiuGame
中調(diào)用add
方法添加我們的Player時章贞,BiuBiuGame
會將BiuBiuGame
的引用賦值給 HasGameRef
中的gameRef
.這樣我們可以很方便的在 Player
類中使用BiuBiuGame
.
? 因為我們需要根據(jù)BiuBiuGame
中的size
來計算Player的大小,所以需要在在onMount()
方法中初始化Player
.
我們的
BiubiuGame
繼承了BaseGame
,當(dāng)使用add
方法添加component
組件時膘流,會對mixin
了HasGameRef<T>
和Resizable
等組件進(jìn)行處理,然后調(diào)用component
組件的onMount()
方法.
在BiuBiuGame
中添加一個Player
:
class BiuBiuGame extends BaseGame {
...
Player player;
...
BiuBiuGame(Size size) {
....
add(player = Player()
..anchor = Anchor.center//設(shè)置player的中心點
//設(shè)置player的位置在寬度的中心,高度的1/4處
..setByPosition(Position(size.width / 2, size.height * 0.75)));
....
}
}
篇幅原因會在已有的類中新添加內(nèi)容時使用
...
包裹新添加的內(nèi)容店溢,詳細(xì)代碼參照源碼.
運(yùn)行游戲可以看到下面畫面:
控制player
的移動:
在Player
中添加void move(Offset offset)
方法:
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
中添加拖動的手勢控制:
class BiuBiuGame extends BaseGame with PanDetector {
...
@override
void onPanStart(DragStartDetails details) {
//拖動的起始點如果在player上,改變player移動狀態(tài)
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) {
//拖動結(jié)束時
if (player.onMove) {
onPanCancel();
}
}
@override
void onPanCancel() {
if (player.onMove) {
player.onMove = false;
}
}
...
}
運(yùn)行游戲我們可以控制我們的player
了:
6. 發(fā)射子彈
創(chuàng)建./lib/component/bullet.dart
文件
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 用來產(chǎn)生子彈:
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
類來執(zhí)行定時任務(wù),Timer
類接收三個參數(shù):
limit
:必填參數(shù) 任務(wù)間隔時間 單位 秒repeat
: 可選默認(rèn)false
callback
: 可選 需要執(zhí)行的任務(wù)(回調(diào)函數(shù))
給plaer裝備武器系統(tǒng)--在./lib/component/player.dart
中添加 BulletFactory
:
class Player extends PositionComponent with HasGameRef<BiuBiuGame> {
...
BulletFactory _bulletFactory;
...
@override
void onMount() {
...
_bulletFactory = BulletFactory(limit: 0.3);
gameRef.add(_bulletFactory);
...
}
}
現(xiàn)在我們的player可以發(fā)射子彈了:
優(yōu)化:
? 可以看到我們的bullet
是從plaer的上面發(fā)射出去的,我們可以重寫int priority()
方法委乌,指定 Player
的渲染順序,返回的數(shù)值越大床牧,越靠近上層。這里遭贸,我們的Player
返回了100.
在Bullet
中添加:
class Bullet extends SpriteComponent {
...
@override
int priority() => 10;
...
}
在Player
中添加:
class Player extends PositionComponent with HasGameRef<BiuBiuGame> {
...
@override
int priority() => 100;
...
}
7.創(chuàng)建敵人
創(chuàng)建./lib/component/enemy/enemy.dart
文件:
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;
}
//戰(zhàn)機(jī)生成后向下移動
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;
}
敵機(jī)設(shè)計:
小飛機(jī) | 戰(zhàn)斗機(jī) | 飛船 | |
---|---|---|---|
生命值 | 1 | 3 | 5 |
移動速度 | 5 | 3 | 2 |
分?jǐn)?shù) | 10 | 50 | 100 |
MiniPlane
./lib/component/enemy/mini_plane.dart
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
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
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
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;
}
}
//隨機(jī)生成位置
Position randomPosition(double width, height) {
return Position(_random.nextDouble() * (gameRef.size.width - width), -height);
}
}
在./lib/biu_biu_game.dart
中生成敵人
class BiuBiuGame extends BaseGame with PanDetector {
...
EnemyFactory _enemyFactory;
...
BiuBiuGame(Size size) {
...
//添加敵人生產(chǎn)工廠組件
_enemyFactory = EnemyFactory(game: this);
...
}
@override
void update(double t) {
...
_enemyFactory?.update(t);
...
}
}
運(yùn)行程序可以看到如下界面:
8.添加碰撞
在./lib/biu_biu_game.dart
中添加檢測碰撞方法:
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) {
//當(dāng)player生命值大于0,enemy狀態(tài)不為DESTROY時,才進(jìn)行bullet和enemy的碰撞檢測
if (enemy.state != EnemyState.DESTROY && player.life > 0 && bullet.toRect().overlaps(enemy.toRect())) {
enemy.hurt(bullet.power);
bullet.isDestroy = true;
}
});
});
}
...
}
運(yùn)行程序可以看到如下界面:
在游戲中我們會發(fā)現(xiàn),有時我們的player并沒有和enemy發(fā)生接觸卻被判定游戲失敗.這是因為在碰撞檢測中,我們使用的是PositionComponent
的 Rect toRect()
方法返回的矩形進(jìn)行判斷的,這導(dǎo)致我們的碰撞判定范圍大于我們的圖片顯示范圍.
開啟 debugMode
我們可以清楚的看到原因:
在./lib/biu_biu_game.dart
添加:
class BiuBiuGame extends BaseGame with PanDetector, HasWidgetsOverlay {
...
@override
bool debugMode() => true;
...
}
重啟游戲我們看到:
碰撞優(yōu)化:
這里我們進(jìn)行一個簡單的優(yōu)化,使用Rect
中的deflate()
方法來縮小用于碰撞檢測的矩形 .
? 修改./lib/biu_biu_game.dart
中的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().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) {
//當(dāng)player生命值大于0,enemy狀態(tài)不為DESTROY時,才進(jìn)行bullet和enemy的碰撞檢測
if (enemy.state != EnemyState.DESTROY && player.life > 0 && bullet.toRect().overlaps(enemy.toRect())) {
enemy.hurt(bullet.power);
bullet.isDestroy = true;
//當(dāng)enemy被擊毀時,計算得分
if (enemy.state == EnemyState.DESTROY) {
score += enemy.score;
}
}
});
});
}
9.記錄分?jǐn)?shù)
在./lib/biu_biu_game.dart
中添加 score
用于記錄分?jǐn)?shù),添加TextComponent
用于顯示結(jié)果:
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();
//計算分?jǐn)?shù)
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;
//當(dāng)enemy被擊毀時,計算得分
if (enemy.state == EnemyState.DESTROY) {
score += enemy.score;
}
}
});
});
}
10.游戲結(jié)果展示
player
銷毀后展示玩家最終得分并啟動一個計時器3秒后重新開始游戲.
把./lib/biu_biu_game.dart
構(gòu)造方法中的內(nèi)容提取到init()
方法中,添加一個Timer restarTimer
.
完整的./lib/biu_biu_game.dart
文件中的內(nèi)容:
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)));
//添加敵人生產(chǎn)工廠組件
_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(
"您的最終分?jǐn)?shù):$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();
//計算分?jǐn)?shù)
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) {
//當(dāng)player生命值大于0,enemy狀態(tài)不為DESTROY時,才進(jìn)行bullet和enemy的碰撞檢測
if (enemy.state != EnemyState.DESTROY && player.life > 0 && bullet.toRect().overlaps(enemy.toRect())) {
enemy.hurt(bullet.power);
bullet.isDestroy = true;
//當(dāng)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.Stack(children: [widget.gameChild, ..._overlays.values.toList()]));
運(yùn)行游戲可以看到:
顯示游戲FPS值:
BaseGame
中提供了double fps()
這個方法獲取fps值的方法,但是需要bool recordFps()
方法返回true
才進(jìn)行記錄 :
在./lib/biu_biu_game.dart
添加:
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 構(gòu)建 Web 應(yīng)用將Flutter切換到 beta版然后改造我們的項目:
修改./lib/main.dart
:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//設(shè)置全屏 屏幕方向這些操作web端不支持需要進(jìn)行判斷
if (!kIsWeb) {
//設(shè)置全屏
await Flame.util.fullScreen();
//設(shè)置屏幕方向只能豎屏顯示
await Flame.util.setPortraitDownOnly();
}
//獲取屏幕size
final Size size = await Flame.util.initialDimensions();
//稍后創(chuàng)建BiuBiuGame類
runApp(BiuBiuGame(size).widget);
}
修改./lib/biu_biu_game.dart
:
@override
void resize(Size size) {
//tileSize 設(shè)定一個最大值50.0
tileSize = min(size.width / 9, 50.0);
super.resize(size);
}