Categories
程式開發

最右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)task_runner
callback:(flutter::VsyncWaiter::Callback)callback {
self = [super init];

if (self) {
callback_ = std::move(callback);
display_link_ = fml::scoped_nsobject {
[[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: [
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"