Flutter Flame游戏开发上手(1)

4,690 阅读4分钟

背景

收到一个游戏开发的需求,因为双端上线所以需要支持iOS Android两端.

3个Android RD,没有接触过游戏开发,快速学习上手Flutter支持双端.

计划游戏使用Flame框架,先大题熟悉下环境然后在开发的过程中学习和解决问题.

知识点

GameLoop是什么

游戏循环就是搭建一个游戏的脚手架.大部分游戏只需要2个方法即:

  1. render方法 用来绘制
  2. update方法 用来更新绘制参数. update当前帧和上一帧的差异.

比如表现一个正在下坠的圆 圆的坐标用y表示. render方法负责拿着y值去绘制圆, update方法负责更新y的变化. 互相配合完成了一个圆的下坠动画

如何把一个Flutter组件展示在Frame引擎上

利用 HasWidgetsOverlay 接口.

  1. removeWidgetOverlay方法移出Flutter组件
  2. addWidgetOverlay方法添加Flutter组件
class MyGame extends BaseGame with HasWidgetsOverlay, TapDetector {
  Size size;
  SpriteC spriteC;
  bool isPaused = false;

  MyGame(this.size) {
    spriteC = SpriteC.create();
  }

  @override
  void onTap() {
    super.onTap();
    if (isPaused) {
      removeWidgetOverlay('pausemenu');
      isPaused = false;
    } else {
      addWidgetOverlay(
          'pausemenu',
          Center(
            child: Container(
              width: 100,
              height: 100,
              color: Colors.blue[300],
              child: const Center(child: const Text('Pause')),
            ),
          ));
      isPaused = true;
    }
  }
}

如何把游戏展示在Flutter的一个组件上

Game提供一个Widget对象,可以直接获取最为Flutter的一个Widget使用.

  final game = MyGame(size);
  runApp(game.widget);

Debug功能是什么

  @override
  bool debugMode() => true;

  @override
  bool recordFps() => true;

开启Debug.

Debug就是允许调试,会自动打印组件的位置.

recordFps就是输出当前运行游戏的FPS.

组件(Components)

必要性: 就跟在Android/iOS上糊页面一样. 所有的页面,动画,特效都可以用Canvas来画, 实际上开发一般情况下用的却是ImageView,TextView,ListView这样的组件来完成. 没有人喜欢刀耕火种

组件重要方法:

除了render 和 update这种通用方法.

  // 初始化
  @override
  void onMount() {
    super.onMount();
  }

  // 回收前
  @override
  void onDestroy() {
    super.onDestroy();
  }
  
  // 还没搞清楚...?  好像在说绘制的位置跟设备之间的关系
  @override
  bool isHud() {
    return true;
  }

  // 标记可回收
  @override
  bool destroy() {
    return false;
  }

怎么画帧动画

即把

变成

游戏里面的人都是会动的,所以这是一个重要知识点.

在游戏里面展示 AnimationComponent

    const textureWidth = 96.0;
    const textureHeight = 96.0;
    add(AnimationComponent.sequenced(
      textureWidth * 2,
      textureHeight * 2,
      'minotaur.png',
      19,
      textureWidth: textureWidth,
      textureHeight: textureHeight,
      loop: true,
      stepTime: 0.15,
    ));

其中函数 AnimationComponent.sequenced 的参数说明如下:

    AnimationComponent.sequenced(
        double width, // 展示的宽度
        double height, // 展示的高度
        String imagePath,
        int amount, { // Sprit Sheet的帧数
      int amountPerRow, // 多行切图 两行之间的间隔
      double textureX = 0.0, // 展示的偏移值
      double textureY = 0.0,
      double textureWidth, // 切图一帧的宽度
      double textureHeight, // 切图一帧的高度
      double stepTime, // 播放间隔时间
      bool loop = true, // 是否循环播放
      this.destroyOnFinish = false, // 播放完了是否销毁
    })

提供给Flutter组件展示 AnimationWidget

  await Flame.images.load('minotaur.png');
  final _animationSpriteSheet = SpriteSheet(
    imageName: 'minotaur.png',
    columns: 19,
    rows: 1,
    textureWidth: 96,
    textureHeight: 96,
  );
  _animation = _animationSpriteSheet.createAnimation(
    0,
    stepTime: 0.2,
    to: 5,
  );
  
  // 使用
       Container(
              width: 200,
              height: 200,
              child: AnimationWidget(animation: _animation),
            ),

怎么画SVG动画 SvgComponent

    Svg svg = Svg('android.svg');
    SvgComponent android = SvgComponent.fromSvg(100, 100, svg);
    android.x = 100;
    android.y = 100;

怎么画组合控件 ComposedComponent

和原生组合控件相似的做法.

class GameOverPanel extends PositionComponent
    with Resizable, HasGameRef, Tapable, ComposedComponent {

  GameOverPanel(Image spriteImage) : super() {
    gameOverText = GameOverText(spriteImage);
    gameOverRestart = GameOverRestart(spriteImage);

    components..add(gameOverText)..add(gameOverRestart);
  }

  bool visible = false;

  GameOverText gameOverText;
  GameOverRestart gameOverRestart;

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

class GameOverText extends SpriteComponent with Resizable {
  GameOverText(Image spriteImage)
      : super.fromSprite(
    GameOverConfig.textWidth,
    GameOverConfig.textHeight,
    Sprite.fromImage(
      spriteImage,
      x: 955.0,
      y: 26.0,
      width: GameOverConfig.textWidth,
      height: GameOverConfig.textHeight,
    ),
  );

  @override
  void resize(Size size) {
    if (width > size.width * 0.8) {
      width = size.width * 0.8;
    }
    y = size.height * .25;
    x = (size.width / 2) - width / 2;
  }
}

class GameOverRestart extends SpriteComponent with Resizable {
  GameOverRestart(Image spriteImage)
      : super.fromSprite(
    GameOverConfig.restartWidth,
    GameOverConfig.restartHeight,
    Sprite.fromImage(
      spriteImage,
      x: 2.0,
      y: 2.0,
      width: GameOverConfig.restartWidth,
      height: GameOverConfig.restartHeight,
    ),
  );

  @override
  void resize(Size size) {
    y = size.height * .75;
    x = (size.width / 2) - GameOverConfig.restartWidth / 2;
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/19/172c89c8d5d4aff0~tplv-t2oaga2asx-image.image)
  }
}

怎么绘制视差动画 ParallaxComponent

简单来说就是怎么把

组合起来生成

  final images = [
      ParallaxImage("bg.png"),
      ParallaxImage("mountain-far.png"),
      ParallaxImage("mountains.png"),
      ParallaxImage("trees.png"),
      ParallaxImage("foreground-trees.png"),
    ];

    final parallaxComponent = ParallaxComponent(images,
        baseSpeed: const Offset(20, 0), layerDelta: const Offset(100, 0));

月亮&天空离我们远所以移动得慢一些

树木离我们近所以移动得快一些

组合起来很神奇,这个效果我很喜欢.TOT

怎么画可拉伸图片(Android的.9.png图) NineTileBox

如下例:

// 定义
  final sprite = Sprite('nine-box.png');
    nineTileBox = NineTileBox(sprite, tileSize: 8, destTileSize: 24);
    
// 使用
 nineTileBox.draw(canvas, x, y, 200, 500);

两个角各占4个像素,合起来就是8个像素, 图片总共24个像素. 这就是上面得参数得含义.

UE切图的话需要保持图片尺寸是3的倍数,另外这里比起.9.png有个缺点是他必须保证4个角是对称的. 对于非对称,比如上面8个像素 下面只需要4个像素的图片不支持.

物理引擎 Box2DComponent

Flame支持Box2D.即组件 Box2DComponent.我理解就是可以实现物理体积 和 碰撞的能力.

这是一个很Powerful得组件,应该会有些深度,需要后面花时间仔细研究下.这里先熟悉他的作用范围就行.

网站:box2d.org/

Box2D是一款免费的开源二维物理引擎,由Erin Catto使用C++编写,在zlib授权下发布。它已被用于蜡笔物理学、愤怒的小鸟、地狱边境、Rolando、Fantastic ...