Flutter&Flame仿微信飛機(jī)大戰(zhàn)

一. 游戲介紹

使用 Flutter & Flame 模仿微信飛機(jī)大戰(zhàn).

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

預(yù)覽: 飛機(jī)大戰(zhàn)體驗地址

image

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

image

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

image

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組件時膘流,會對mixinHasGameRef<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)行游戲可以看到下面畫面:

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移動狀態(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了:

image

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ā)射子彈了:

image

優(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)行程序可以看到如下界面:

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

        //當(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)行程序可以看到如下界面:

image

在游戲中我們會發(fā)現(xiàn),有時我們的player并沒有和enemy發(fā)生接觸卻被判定游戲失敗.這是因為在碰撞檢測中,我們使用的是PositionComponentRect toRect()方法返回的矩形進(jìn)行判斷的,這導(dǎo)致我們的碰撞判定范圍大于我們的圖片顯示范圍.

開啟 debugMode 我們可以清楚的看到原因:

./lib/biu_biu_game.dart添加:


class BiuBiuGame extends BaseGame with PanDetector, HasWidgetsOverlay {

    ...

    @override

bool debugMode() => true;

    ...

}

重啟游戲我們看到:

image

碰撞優(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)行游戲可以看到:

image

顯示游戲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);

  }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載戈咳,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市著蛙,隨后出現(xiàn)的幾起案子删铃,更是在濱河造成了極大的恐慌,老刑警劉巖册踩,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泳姐,死亡現(xiàn)場離奇詭異,居然都是意外死亡暂吉,警方通過查閱死者的電腦和手機(jī)胖秒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來慕的,“玉大人阎肝,你說我怎么就攤上這事“菇郑” “怎么了风题?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嫉父。 經(jīng)常有香客問我沛硅,道長,這世上最難降的妖魔是什么绕辖? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任摇肌,我火速辦了婚禮,結(jié)果婚禮上仪际,老公的妹妹穿的比我還像新娘围小。我一直安慰自己,他們只是感情好树碱,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布肯适。 她就那樣靜靜地躺著,像睡著了一般成榜。 火紅的嫁衣襯著肌膚如雪框舔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天赎婚,我揣著相機(jī)與錄音雨饺,去河邊找鬼。 笑死惑淳,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的饺窿。 我是一名探鬼主播歧焦,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绢馍?” 一聲冷哼從身側(cè)響起向瓷,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舰涌,沒想到半個月后猖任,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡瓷耙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年朱躺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搁痛。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡长搀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鸡典,到底是詐尸還是另有隱情源请,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布彻况,位于F島的核電站谁尸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏纽甘。R本人自食惡果不足惜良蛮,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贷腕。 院中可真熱鬧背镇,春花似錦、人聲如沸泽裳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涮总。三九已至胸囱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瀑梗,已是汗流浹背烹笔。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留抛丽,地道東北人谤职。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像亿鲜,于是被迫代替她去往敵國和親允蜈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353