写点什么

最右 JS2Flutter 框架——动画、小游戏的实现(四)

用户头像
刘剑
关注
发布于: 2020 年 08 月 11 日
最右JS2Flutter框架——动画、小游戏的实现(四)

1、概述

动画和小游戏看起来是两个不太相关的话题,但其实它们都依赖于 Vsync 机制的建立,对动画依赖于 Vsync 机制不太理解的同学,可以查看 Gityuan 的博客——深入理解Flutter动画原理[1],最右目前所采用的小游戏引擎是Flame[2],其 GameLoop 也是借助于 Ticker(依赖 Vsync)实现 Game 的不断刷新。可见要实现动画和小游戏,我们必须给 Client 侧提供 Vsync 机制。


2、Vsync 机制

我们先看看 Flutter 是如何建立 Vsync 机制的,在深入理解Flutter动画原理[1]文章中,虽然着重点是在动画流程上,但提到了注册 Vsync,比较细心的同学可能会发现文末那张图的 Choreographer,Choreographer 的作用就是接收底层的 Vsync 信号,为上层 App 的渲染提供稳定的时机,这点信息 Android 同学应该很快能捕捉到。我们再去求证一下,Android 端在 VsyncWaiter 里利用 Choreographer 给 Flutter 提供了 Vsync 时机。

    private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate = new FlutterJNI.AsyncWaitForVsyncDelegate() {        @Override        public void asyncWaitForVsync(long cookie) {            Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {                @Override                public void doFrame(long frameTimeNanos) {                    float fps = windowManager.getDefaultDisplay().getRefreshRate();                    long refreshPeriodNanos = (long) (1000000000.0 / fps);                    FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);                }            });        }    };
复制代码

而我们是在 iOS 端,要给 Client 侧提供 Vsync 时机,我们去看看 iOS 端 Flutter 是如何实现的,iOS 端的实现在 flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm 里面。

- (instancetype)initWithTaskRunner:(fml::RefPtr<fml::TaskRunner>)task_runner                          callback:(flutter::VsyncWaiter::Callback)callback {  self = [super init];
if (self) { callback_ = std::move(callback); display_link_ = fml::scoped_nsobject<CADisplayLink> { [[CADisplayLink displayLinkWithTarget:self selector:@selector(onDisplayLink:)] retain] }; display_link_.get().paused = YES;
task_runner->PostTask([client = [self retain]]() { [client->display_link_.get() addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; [client release]; }); }
return self;}
...
- (void)onDisplayLink:(CADisplayLink*)link { fml::TimePoint frame_start_time = fml::TimePoint::Now(); fml::TimePoint frame_target_time = frame_start_time + fml::TimeDelta::FromSecondsF(link.duration);
display_link_.get().paused = YES;
callback_(frame_start_time, frame_target_time);}
复制代码

到这里大家应该都明白了,我们也可以用跟系统一样的方式,利用 CADisplayLink 在 Native 给 Client 侧建立起 Vsync 机制。


3、Animation

JS2Flutter 框架是由 Client 侧去驱动 Host 侧的渲染的,想要实现 UI 上的变化基本上都是 Client 侧的虚拟树发生变化,从而驱动 Host 侧真实 Widget 树的变化。很多同学可能会想到,可以在动画插值过程中,通过不断的重建虚拟树去实现动画,但其实这种做法是效率很低的,也没必要,动画只是影响 Widget 树边界的形变(矩阵变换),并不会引起 Widget 树结构的变化,所以我们可以只让 Host 侧真实的 Widget 做这个动画,Client 侧保证动画的值和状态实时更新,保证逻辑上的正确性就可以了。


要让真实的 Widget 树执行动画,就意味着必须在 Host 侧构建真实的 Animation、AnimationController,在 Client 侧只是纯粹的 Api 代理,我们只需要把 Client 侧创建 Animation、AnimationController 和 Host 侧的真身对应起来即可。


AnimationController 的构造还依赖于 TickerProvider,当 Client 侧的 AnimationController 创建时,我们也需要在 Host 侧创建真身,那真身依赖的 TickerProvider 该从何而来呢?还记得我们在最右JS2Flutter框架——渲染机制[3]中 AppLifecycleState 的实现吗?借助 AppContainer,由于它的生命周期等于整个 Flutter App 的生命周期,可以用它来提供可靠的 TickerProvider。另一个问题就是保证 Client 侧 Animation 的值和状态的准确性,借助我们在上一篇文章最右JS2Flutter框架通信机制[4]中讲述的双向同步通信机制,可以通过监听真实 Animation 的变化,从而同步修改 Client 侧 Animation。


很多业务场景需要监听 Animation 的更新去做 UI 上的变化,在这种使用场景下,难免会带来虚拟树的重建,我们尽可能做更小粒度的 Widget 树更新。举个例子,我们要实现一个翻卡动画,当动画执行到一半的时候,我们需要将背面显示出来,这种情况我们只做卡片内容的更新。

  Widget build(BuildContext context) {    final front = widget.childFront;    final back = widget.childBack;    Matrix4 transform = Matrix4.identity()..rotateY(_animation.value);    return AnimatedBuilder(      animation: _animation,      builder: (BuildContext context, Widget child) {        return Transform(          transform: transform,          alignment: Alignment.center,          child: IndexedStack(            alignment: Alignment.center,            children: <Widget>[              front,              back,            ],            index: _animationCtr.value < 0.5 ? 0 : 1,          ),        );      },    );  }
复制代码


4、小游戏

最右目前所采用的小游戏引擎是Flame[2],要实现小游戏的能力,我们必须先对 Flame 的实现有一定了解,尤其是 Flame 是如何去绘制的,这里直接抛出结论,有兴趣的同学可以去查看源码,其实核心就在这里:

class GameRenderBox extends RenderBox with WidgetsBindingObserver {  BuildContext context;  Game game;  GameLoop gameLoop;
GameRenderBox(this.context, this.game) { gameLoop = GameLoop(gameLoopCallback); }
...
void gameLoopCallback(double dt) { if (!attached) { return; } game.recordDt(dt); game.update(dt); markNeedsPaint(); }
@override void paint(PaintingContext context, Offset offset) { context.canvas.save(); context.canvas.translate( game.builder.offset.dx + offset.dx, game.builder.offset.dy + offset.dy); game.render(context.canvas); context.canvas.restore(); }
...}
复制代码

每次 Vsync 的时候,会回调 gameLoopCallback,每次都会标记刷新,把 Game 的 Component 画到 Canvas 上,Component 会确定自己的位置以及所绘制的内容,小游戏的渲染都是通过 Canvas 去绘制,所以我们先要支持 Canvas 能力。


我们先看看 Flutter 是如何实现 Canvas 的,我们以 rotate 为例:

void rotate(double radians) native 'Canvas_rotate';
复制代码

Framework 层提供的 Canvas,最终实际上调到了 Engine 层 flutter/lib/ui/painting/canvas.cc 的同名函数,进而调用 SkCanvas 的同名函数。我们也采用相同的策略,Client 侧声明镜像的 Canvas,提供与 Flutter Canvas 对等的能力,Client 侧镜像 Canvas 函数的调用,直接通过通信渠道转化为 Flutter Canvas 函数的调用。最右为了实现 Canvas 的高效绘制,对于 Canvas 指令的数据化采用 StandardMessageCodec 去实现。


所以我们只需要按照 Flame 的思路去实现就好了,当 Native 通知 Client 侧 Vsync 的时候,收集画在 Canvas 上的指令,然后把这些指令通过 StandardMessageCodec 数据化,传递到 Host 侧,再把指令解析出来,还原这些指令操作,让 Host 侧预占坑的 Game 绘制到 Canvas 上即可。


5、结束语

本文主要阐述了 JS2Flutter 框架 Vsync 机制的建立,以及 Animation 和小游戏的实现。综合前面的几篇文章,相信大家对 JS2Flutter 框架有了更多的了解,希望能对大家有所启发和帮助,最右将在 Flutter 动态化道路上持续探索,欢迎关注。


6、参考文献

[1]:深入理解 Flutter 动画原理 http://gityuan.com/2019/07/13/flutter_animator/

[2]:Flame https://github.com/flame-engine/flame

[3]:最右 JS2Flutter 框架——渲染机制 https://xie.infoq.cn/article/5c2dbdac0a27bb55863d0be25

[4]:最右 JS2Flutter 框架——通信机制 https://xie.infoq.cn/article/f23e562e3aa7f3c198eb40a83


发布于: 2020 年 08 月 11 日阅读数: 984
用户头像

刘剑

关注

还未添加个人签名 2020.06.28 加入

最右 App Android 工程师,自2019年初加入最右,主要从事 Flutter 相关领域的技术探索,负责 Flutter 的动态化在最右 App 落地及成功实践。

评论

发布
暂无评论
最右JS2Flutter框架——动画、小游戏的实现(四)