Flutter&Flame仿微信飞机大战

一. 游戏介绍

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

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

预览: 飞机大战体验地址

image

二.创建项目

创建项目


flutter create biubiubiu

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


  flame: ^0.24.0

  flame_scrolling_sprite: ^0.0.2

flame 游戏引擎

flame_scrolling_sprite 图片滚屏组件

2.添加资源

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


├─assets

│  ├─audio

│  ├─font

│  └─images

pubspec.yaml中配置资源文件:


#~~~

  assets:

    - assets/audio/   

    - assets/images/   

    - assets/images/ui/

#~~~

3.初始化项目

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


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


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 可看到如下效果:

image

4.创建背景

在./lib创建文件夹component;然后在component文件夹下创建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: 滚动区域宽度
  • height: 滚动区域高度
  • verticalSpeed: 垂直滚动速度

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


///...

BiuBiuGame(Size size) {

    this.size = size;

    //添加背景组件

    add(Background(size));

  }

///...

运行app可看到如下效果:

image

5.创建Player

创建./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 设置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:


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

    ....

  }

}

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

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

image

控制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移动状态

    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了:

image

6. 发射子弹

创建./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 用来产生子弹:


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:


class Player extends PositionComponent with HasGameRef<BiuBiuGame> {

  ...

  BulletFactory _bulletFactory;

  ...



  @override

  void onMount() {

  ...

    _bulletFactory = BulletFactory(limit: 0.3);

    gameRef.add(_bulletFactory);

  ...

  }

}

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

image

优化:

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

Bullet中添加:


class Bullet extends SpriteComponent {

    ...

    @override

  int priority() => 10;

    ...

}

Player中添加:


class Player extends PositionComponent with HasGameRef<BiuBiuGame> {

  ...

  @override

  int priority() => 100;

  ...

}

7.创建敌人

创建./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;

    }

    //战机生成后向下移动

    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


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;

    }

  }

//随机生成位置

  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) {

        ...

        //添加敌人生产工厂组件

  _enemyFactory = EnemyFactory(game: this);

        ...

    }

      @override

  void update(double t) {

        ...

    _enemyFactory?.update(t);

    ...

  }

}

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

image

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) {

        //当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;

        }

      });

    });

  }

    ...

}

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

image

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

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

./lib/biu_biu_game.dart添加:


class BiuBiuGame extends BaseGame with PanDetector, HasWidgetsOverlay {

    ...

    @override

bool debugMode() => true;

    ...

}

重启游戏我们看到:

image

碰撞优化:

这里我们进行一个简单的优化,使用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) {

        //当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用于显示结果:


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文件中的内容:


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.

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

运行游戏可以看到:

image

显示游戏FPS值:

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

./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 构建 Web 应用将Flutter切换到 beta版然后改造我们的项目:

修改./lib/main.dart:


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:


  @override

  void resize(Size size) {

    //tileSize 设定一个最大值50.0

    tileSize = min(size.width / 9, 50.0);

    super.resize(size);

  }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342