【Flutter&Flame游戏 - 拾】探索构件 | Component 生命周期回调

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 11 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列文章一览:


1. Component 生命周期回调一览

所谓生命周期,就是一个对象从生到死的过程。在上一篇中介绍过 Component 的生命周期状态 (LifecycleState) 有如下六种。可能很多人分不清什么是生命周期,什么是生命周期回调。

生命周期,本质上是一种 状态 ,也就是说它是一种数据;而生命周期回调是一个函数,或说方法,一般来说该函数会在状态切换时触发,从而让外界可以感知到对象的状态变化,以此实现某些特定的逻辑。 Component 中的生命周期回调方法如下:

一般来说,常用的是如下六个回调,先简单认识一下:

  • onGameResize : 顶层画布尺寸变化时
  • onLoad:资源加载时
  • onMount:添加到父节点时
  • onRemove:从父节点移除时
  • update:跟随 Ticker 不断触发
  • render:新帧渲染时触发

2. onGameResize 和 onLoad

如下可以看出,在生命周期状态从 uninitialized 切换到 loading 时,会触发一次 onGameResize;紧接着触发 onLoad 异步方法。在 483 行所示,异步任完成后,生命周期状态将置为 loaded


如下通过断点查看一下自定义的 Ball 组件 onLoad 方法触发时,方法栈的情况。可见在 Flutter 程序的开始,BuildOwner#buildScope 构建组件时, _GameWidgetState 会触发 loaderFuture 的方法。在父构件执行 add 方法,会先触发该子构建的 onLoad 方法来加载资源。可就是说,通过这个回调,可以给构件准备资源的机会。


3. onMount 和 onRemove

这两个是一对反义词,onMount 方法在生命周期状态变为 mounted 之前触发。让使用者知道该构件节点添加到构件树的确切时机。


当某个组件被父节点踢出群聊时,会触发onRemove 方法,之后紧接着将生命周期状态置为 removed 。让使用者知道该构件节点添加到构件树的确切时机。


4. update 和 render

前面我们对这两个方法已经有所了解,这两者都是一个持续不断的回调,一般每隔 16.66ms 触发一次,也就是一秒钟触发 60 次 。update 方法本质上由 Ticker 触发,这点可以通过断点调试进行应证,如下所示:


render 方法本质上是在帧绘制期间被触发的,也就是 RendererBinding.drawFrame 方法。这个看过 《Flutter 渲染机制 - 聚沙成塔》 的朋友对这些应该比较熟悉,没看过也没有关系。 Ticker 触发新帧的申请,回调 update 方法,在新帧来临是触发 drawFrame 方法,回调 render 方法,所以这两者的先后关系是很明确的。


如下是着六个回调方法顺序的简单示意,其中 updaterender 方法是在 Ticker 循环中不断触发的,当 Ticker 停止时,这两个方法也会停止回调。另外当该组件被移除之后,也不会继续回调updaterender


5. 运动圆

下面通过一个小案例来梳理一下 Component 的生命周期回调。如下,小圆不停运动,在碰到桌面后反弹,代码详见 【10/01】


onLoad 方法中,可以对画笔、位置、速度、加速度等属性进行初始化:

final Paint _paint = Paint()
  ..style = PaintingStyle.stroke
  ..strokeWidth = 1;

Vector2 v = Vector2.zero(); // 速度 px/s
Vector2 a = Vector2.zero(); // 加速度 px/s^2

@override
Future<void> onLoad() async {
  _paint.color = color;
  position = gameRef.size / 2;
  v = Vector2(80, 50);
}
复制代码

render 方法中进行绘制圆:

@override
void render(Canvas canvas) {
  super.render(canvas);
  canvas.translate(size.x / 2, size.y / 2);
  canvas.drawCircle(Offset.zero, size.x / 2, _paint);
}
复制代码


update 中,根据运动学格式,在 dt 的时间内,更新速度和位移的值,小球即可运动。另外小球的碰壁反弹可以通过位置校验来处理 ,Flame 中有对于碰撞的简单封装,但这里还是自己手动校验,体会一下简单的配置检测。

速度的合成.png

碰撞分析png

@override
void update(double dt) {
  super.update(dt);
  v += a * dt;
  position += v * dt;
  Vector2 winSize = gameRef.size;
  //限定下边界
  if (position.y > winSize.y - size.y/2) {
    position.y = winSize.y - size.y/2;
    v.y = -v.y;
  }
  //限定上边界
  if (position.y < size.y/2) {
    position.y = size.y/2;
    v.y = -v.y;
  }
  //限定左边界
  if (position.x < size.x/2) {
    position.x = size.x/2;
    v.x = -v.x;
  }
  //限定右边界
  if (position.x > winSize.x - size.x/2) {
    position.x = winSize.x - size.x/2;
    v.x = -v.x;
  }
}
复制代码

下面可以继续拓展,比如在点击屏幕时添加通过 Ball ,双击屏幕时移除 Ball 列表的第一个。效果如下,代码详见 【10/02】

class TolyGame extends FlameGame with TapDetector,DoubleTapDetector{
  
  int _counter = 0;
  
  @override
  Future<void> onLoad() async {
    addABall();
  }

  void addABall(){
    Ball ball = Ball(tag: 'tag$_counter');
    add(ball);
    _counter++;
  }

  @override
  void onTap() {
    addABall();
  }

  @override
  void onDoubleTap() {
   List<Ball> balls = children.whereType<Ball>().toList();
   if(balls.isNotEmpty){
     balls.first.removeFromParent();
   }
  }
}
复制代码

这样在移除时 Ball 自身可以通过 onRemove 监听到事件:


到这里,我们就对 FlameComponent 的生命周期回调有了较深的理解。这个知识点是非常重要的,希望大家可以好好消化吸收。那本文就到这里,明天见 ~

\

猜你喜欢

转载自juejin.im/post/7105187089448501285