likes
comments
collection
share

Animated从Native Driver到Unity Driver

作者站长头像
站长
· 阅读数 29

背景

Unity Animated Driver是以前和小泉的一位学长一起基于RN的Animated组件抽象出来的了一种算法与数据驱动思想——染色遍历更新算法与节点数据流动思想并在Unity上进行的实现,这一算法与思想具有较高的通用性,正好分享一下。

1. 整体架构

在React Native中,Animated组件(补间动画)是一个高频率使用的动画组件,其分为JS Driver和Native Driver两种驱动方式,后者的性能会更好,不容易出现丢帧。我们在Unity组件中实现了一个Unity Driver动画。

JS Driver和Unity Driver的区别: 前者是每一帧在js侧改变动画值,再传递到Unity;后者是js一次性把所有帧的数据传递到Unity,然后由Unity进行动画值更新。所以使用Unity Diver的动画可以避免js <=> unity的频繁双向通信,从而提高动画的性能,避免丢帧。

Animated从Native Driver到Unity Driver

Unity Driver的整体架构参考了React Native的动画架构,方法进行了对齐,所以前端在使用时和RN依然保持完全一致。

如下图所示。从js侧执行动画start指令开始,进入到unity c#部分。主要会经过如下几个过程:

  1. 将js侧指定需要调用的unity方法,加入队列中,按序执行。
  2. 创建动画节点,动画节点对应js侧的props、style、value等,每个节点拥有自己的唯一标识。
  3. 将unity侧的ui组件,与props节点进行绑定,以进行ui属性更新操作
  4. 启动动画计算函数,js侧会传入从第一帧到最后一帧的所有数据,unity根据参数创建一个动画driver。
  5. 动画driver在每一帧进行数据更新,将当前帧的value,通过节点数据传递,最终更新到相应的ui组件的props上,ui获得相应的变化。

Animated从Native Driver到Unity Driver

2. JS侧

  • 前端动画组件
   const [op] = useState(new Animated.Value(0));
   Animated.timing(op, {
      toValue: 1,
      duration: 50000,
      useNativeDriver: true
    }).start();
    return <Animated.View style={[styles.animated, { opacity: op }]} />

以Animated.View组件举例,而在Animated.View的style中,指定了一个Animated.Value动画值。

所以一个简单的Animated.View被创建时,实际上被分为了三个部分。Animated.View的props对应一个props节点,props的父节点为style节点,style节点的父节点为value节点。当value的节点值进行变化时,会最终传递到props节点进行属性更新。每一个节点前端会有一个唯一的标识tag,从1开始自增。

Animated从Native Driver到Unity Driver

至于为什么要这么设计,将会在后面的章节中进行详细说明。

  • useNativeDriver

从调用Animated.timing的start方法(useNativeDriver指定为true)开始,前端开始调用unity侧的动画模块。主要的步骤有

  1. createAnimatedNode(tag: number, config: AnimatedNodeConfig),创建动画节点方法,以上面的代码举例,这里会调用三次,分辨创建propsNode、styleNode、valueNode。传入node的唯一tag,和节点所需的初始参数。

  2. connectAnimatedNodeToView(nodeTag: number, viewTag: number),将props节点和其对应的ui组件绑定,让unity侧知道需要去更改哪一个组件的属性

  3. connectAnimatedNodes(parentTag: number, childTag: number),告诉unity侧,节点之间的父子关系

  4. startAnimatingNode( animationId: number,nodeTag: number, config: AnimatingNodeConfig, endCallback: EndCallback) 动画计算启动函数,

    1. animationId代表动画任务的唯一tag,每一个Animated.timing对应一个。

    2. config为前端计算好的每一帧的value值,例如:

      • {
            "type":"frames",
            "frames":[0,0.00004167780049047652,0.00016547821823185047,0.000369604584550567,0.0006523277110618067,0.0010119824303159884,0.0014469643621069755,0.001955726887729255,0.0025367783160924326,0.0031886792270536635,0.00391003997863186,0.004699518365938566,0.005555817420714579,0.006477683341311495,0.007463903543814926,0.00851330482578147,0.009624751634763128,0.010797144434428736,0.012029418161669185,0.013320540768597471,0.01466951184383165,0.01607536130788321,0.017537148177869555,0.019053959397130938,0.020624908725662642,0.022249135687575597,0.0239258045720757,0.02565410348470558,0.027433243445826185,0.02926245753352885,0.03114100006836614,0.03306814583746998,0.035043189355793605,0.037065444162366484,0.039134242149594704,0.041248932923769244,0.04340888319506699,0.04561347619544095,0.04786211112290018,0.050154202610776584,0.05248918022066495,0.054866487957805556,0.05728558380775621,0.05974593929327171,0.0622470390503763,0.06478838042267626,0.06736947307301808,0.06998983861165177,0.07264901024010965,0.0753465324100578,0.07808196049642172,0.08085486048412967,0.0836648086678552,0.08651139136417726,0.08939420463561068,0.09231285402599128,0.09526695430673081,0.09825612923348566,0.10128001131280881,0.10433824157838187,0.1074304693764467,0.11055635216007924,0.11371555529197039,0.11690775185539871,0.12013262247309947,0.12338985513375347,0.12667914502583713,0.13000019437859223,0.1333527123098899,0.13673641468078024,0.14015102395653434,0.1435962690739995,0.14707188531510362,0.15057761418636037,0.15411320330423758,0.15767840628626967,0.16127298264780415,0.16489669770428944,0.16854932247902343,0.1722306336162962,0.17594041329987548,0.1796784491767961,0.18344453428643145,0.18723846699483832,0.19106005093438447,0.1949090949486824,0.19878541304287242,0.20268882433931515,0.20661915303877412,0.21057622838718804,0.21455988464815853,0.2185699610812993,0.22260630192661982,0.22666875639514922,0.23075717866603054,0.23487142789035506,0.2390113682020438,0.2431768687361206,0.24736780365477481,0.25158405218165686,0.2558254986449122,0.2600920325295234,0.2643835485396004,0.2686999466713477,0.2730411322975259,0.27740701626433145,0.28179751500174777,0.2862125506485493,0.2906520511933087,0.2951159506329397,0.29960418915051723,0.30411671331437284,0.3086534763007436,0.31321443814259536,0.3177995660076388,0.32240883450901814,0.3270422260527175,0.3316997312263809,0.3363813492350347,0.3410870883901541,0.3458169666596566,0.3505710122878073,0.35534926449572535,0.3601517742752817,0.3649786052917889,0.36982983491413485,0.37470555539511197,0.37960587522989786,0.38453092072730544,0.38948083783704907,0.39445579428755173,0.3994559821037411,0.4044816205942806,0.40953295992484834,0.41461028543156236,0.41971392288123327,0.4248442449603241,0.4300016793842919,0.4351867191831664,0.4403999359714757,0.44564199741037264,0.45091369072659593,0.45621595527865255,0.4615499291904674,0.466917018965844,0.4723190090623469,0.4777582468629599,0.4832379868440287,0.48876313204089356,0.49434229653361633,0.5,0.5056577034663834,0.5112368679591065,0.5167620131559714,0.52224175313704,0.527680990937653,0.533082981034156,0.5384500708095326,0.5437840447213474,0.5490863092734041,0.5543580025896273,0.5596000640285242,0.5648132808168337,0.5699983206157081,0.5751557550396758,0.5802860771187668,0.5853897145684377,0.5904670400751517,0.5955183794057196,0.6005440178962589,0.6055442057124483,0.6105191621629509,0.6154690792726947,0.6203941247701021,0.625294444604888,0.6301701650858652,0.6350213947082111,0.6398482257247184,0.6446507355042748,0.6494289877121927,0.6541830333403433,0.658912911609846,0.6636186507649653,0.668300268773619,0.6729577739472824,0.677591165490982,0.6822004339923612,0.6867855618574046,0.6913465236992566,0.6958832866856272,0.7003958108494828,0.7048840493670603,0.7093479488066913,0.7137874493514507,0.7182024849982522,0.7225929837356686,0.7269588677024741,0.7313000533286521,0.7356164514603998,0.7399079674704766,0.7441745013550877,0.7484159478183432,0.7526321963452252,0.7568231312638793,0.7609886317979564,0.7651285721096449,0.7692428213339695,0.7733312436048507,0.7773936980733802,0.7814300389187007,0.7854401153518413,0.7894237716128121,0.7933808469612259,0.7973111756606848,0.8012145869571277,0.8050909050513176,0.8089399490656155,0.8127615330051617,0.8165554657135685,0.8203215508232039,0.8240595867001245,0.8277693663837038,0.8314506775209766,0.8351033022957105,0.8387270173521959,0.8423215937137303,0.8458867966957624,0.8494223858136396,0.8529281146848964,0.8564037309260004,0.8598489760434658,0.8632635853192199,0.8666472876901101,0.8699998056214077,0.8733208549741629,0.8766101448662466,0.8798673775269006,0.8830922481446013,0.8862844447080296,0.8894436478399207,0.8925695306235534,0.8956617584216181,0.8987199886871912,0.9017438707665144,0.9047330456932692,0.9076871459740087,0.9106057953643892,0.9134886086358227,0.9163351913321448,0.9191451395158703,0.9219180395035783,0.9246534675899422,0.9273509897598903,0.9300101613883482,0.9326305269269819,0.9352116195773237,0.9377529609496238,0.9402540607067283,0.9427144161922438,0.9451335120421944,0.947510819779335,0.9498457973892234,0.9521378888770998,0.954386523804559,0.956591116804933,0.9587510670762307,0.9608657578504053,0.9629345558376335,0.9649568106442064,0.96693185416253,0.9688589999316338,0.9707375424664711,0.9725667565541738,0.9743458965152945,0.9760741954279243,0.9777508643124244,0.9793750912743374,0.980946040602869,0.9824628518221304,0.9839246386921168,0.9853304881561683,0.9866794592314025,0.9879705818383308,0.9892028555655713,0.9903752483652368,0.9914866951742185,0.9925360964561851,0.9935223166586885,0.9944441825792855,0.9953004816340615,0.9960899600213682,0.9968113207729463,0.9974632216839076,0.9980442731122707,0.998553035637893,0.998988017569684,0.9993476722889382,0.9996303954154494,0.9998345217817681,0.9999583221995095,1],
            "toValue":1
        }
        

startAnimatingNode方法调用后,指定的Animated.Value值就会在unity侧进行计算,用到这个值的相应组件边会开始进行动画播放。

3 节点操作指令队列

Animated从Native Driver到Unity Driver

对于JS侧发送的相关动画的方法调用,在Unity侧UnityAnimatedModule会先将这些调用命令存入到一个指令队列(OperationQueue)。

// UnityAnimateModule
[ReactMethod(false)]
public void createAnimatedNode(int tag, Dictionary<string, object> config)
{
    var operateExecute = new Action<UnityAnimatedNodesManager>(nodesManager =>
    {
        nodesManager.CreateAnimatedNode(tag, config);
    });
    AddOperation(new UIThreadOperation(operateExecute));
}

在前端调用connectAnimatedNodeToView时,Unity侧会注册队列执行的监听到管理UI渲染的UiManagerModule中。

// UnityAnimateModule
public void connectAnimatedNodeToView(int animatedNodeTag, int viewTag)
{
    InitializeLifecycleEventListenersForViewTag();
    
    var operateExecute = new Action<UnityAnimatedNodesManager>(nodesManager =>
    {
        nodesManager.ConnectAnimatedNodeToView(animatedNodeTag, viewTag);
    });
    AddOperation(new UIThreadOperation(operateExecute));
}

private void InitializeLifecycleEventListenersForViewTag()
{
    ...
    if (context != null)
    {
        UiManagerModule uiManager = context.moduleManager.GetUiManagerModule();
        uiManager.AddUIManagerEventListener(this);
        mInitialized = true;
    }
    ...
}

UiManagerModule会在进行UI渲染时触发监听从而将动画的OperationQueue加入到全局UI渲染队列UiViewOperationQueue里,当View渲染完成之后,去执行动画相关的指令。

//UiManager
public void OnBatchComplete()
{
    ...
    foreach (UiManagerListener listener in mUiManagerListeners)
    {//将Animate动画执行指令加入到UiViewOperationQueue中
        listener.WillDispatchViewUpdates(this);   
    }
    ...
    // 应用所有更新
    //执行View渲染,并在最后执行动画相关指令
    operationQueue.DispatchViewUpdates();
    ...
}

为什么按照批维度去处理动画相关指令

C#传入一个指令给到JS侧(比如初始化指令),JS侧会处理这一指令,并会以批维度返回一系列操作给到C#侧进行方法调用和UI操作。当涉及Ui操作时,onBatchComplete便是去执行JS侧返回的一系列需要C#执行的操作。

Animated从Native Driver到Unity Driver

动画本身依托于已经创建的组件(View、Image等等),因此将动画的一系列操作放置在全局UI渲染的指令队列UiViewOperationQueue的最后去执行是最为保险且合理的,为的是防止组件创建或者布局未结束就开始执行了相关动画而带来的问题,这也是动画为何要在全局UI渲染队列存在的基础上创建自身队列的重要原因之一。

4 节点数据流动

在第二节中提到过,js侧在创建一个Animated.View时,会创建多个节点。

这里先写一个稍微复杂一些的动画代码.

动画值部分包含了 两个 Animated.Value, 一个Animated.add。

然后有两个Animated.timing

三个Animated.View

第一个View指定了两个动画属性,borderRadius和opacity。

第二个View指定了一个动画属性, opacity。

第三个View指定了transform属性,其中包含了三个旋转、位移、缩放三个属性。

const [op1] = useState(new Animated.Value(0));
const [op2] = useState(new Animated.Value(0));
const [addOps] = useState(new Animated.add(op1, op2));


useEffect(() => {
        Animated.timing(op1, {
            toValue: 1,
            duration: 5000,
            useNativeDriver: true,
        }).start();
        Animated.timing(op2, {
            toValue: 100,
            duration: 5000,
            useNativeDriver: true,
        }).start();
 }, []);


return (
        <View style={{flex: 1, backgroundColor: 'white'}}>
            <Animated.View
                style={[
                    styles.animated,
                    {
                        borderRadius: addOps,
                        opacity: op1
                    },
                ]}
            />
            <Animated.View
                style={[
                    styles.animated,
                    {
                        opacity: op1
                    },

                ]}
            />
            <Animated.View
                style={[
                    styles.animated,
                    {
                        opacity: op1,
                        transform: [
                            {
                                rotate: op2.interpolate({
                                    inputRange: [0, 100],
                                    outputRange: ["0deg", "360deg"]
                                })
                            },
                            {
                                translateX: op2.interpolate({
                                    inputRange: [0, 50, 100],
                                    outputRange: [0, 300, 0]
                                })
                            },
                            {
                                scale: op2.interpolate({
                                    inputRange: [0, 50, 100],
                                    outputRange: [1, 2, 1]
                                })
                            }
                        ]
                    },

                ]}

            />
        </View>
 );

代码中的动画值、属性、组件,都会对应创建节点。

Animated.Value => Value节点

Animated.View.Props => Props节点

Animated.View.Props.Style => style节点

Animated.View.Props.Style.transform => transform节点

Animated.Value.interpolate => interpolate节点

节点之间的层级关系如下图所示,value是最高节点,value中存放了当前帧的实际值,它的值在图结构中一步步传递给各个子节点,最终到View的props上进行属性更新。

Animated从Native Driver到Unity Driver

而value的值又从何而来呢?在第二节中提到过,在前端执行Animated.timing.start()方法后,最终会走到unity侧的startAnimatingNode方法,这个方法中传递了动画任务的标签,和从初始值到最终值的每一帧数据,以及触发这个任务的相关ValueNode的唯一标签。unity执行这个方法后,会创建一个AnimatedDriver,关联上对应的ValueNode,在之后每一帧的函数调用中,AnimatedDriver会去更新ValueNode的值。

js为什么要这么设计出这种节点架构?

依托于js的自由度,前端的组件以及组件style属性会有非常多样性的组合。这种节点架构主要拥有如下优点:

  • 解耦性:组件和style之间可以1对n、1对1、n对n,互相解耦,可以灵活实现各种动画需求。 可扩展性:这种节点架构,可以不仅限于支持style属性,只要有渐变的需求,组件上的任意属性都是可以方便的被unity driver支持的。例如去动态修改untiy 材质中的属性。
  • 可扩展性:这种节点架构,可以不仅限于支持style属性,只要有渐变的需求,组件上的任意属性都是可以方便的被unity driver支持的。例如去动态修改untiy 材质中的属性。

5 节点遍历更新

每一帧的函数调用,会去调用UnityAnimatedNodesManagerRunUpdates(),去执行真正的动画节点遍历更新。

UnityAnimatedNodesManager nodesManager = GetNodesManager();
if (nodesManager != null && nodesManager.HasActiveAnimations())
{
    nodesManager.RunUpdates(frameTimeNanos);
}
...

public void RunUpdates(long frameTimeNanos)
{
    bool hasFinishedAnimations = false;
    foreach (var node in mUpdatedNodes)
    {
        mRunUpdateNodeList.AddLast(node.Value);
    }
    
    foreach (var activeAnimation in mActiveAnimations) {
        AnimationDriver animation = activeAnimation.Value;
        animation.RunAnimationStep(frameTimeNanos);
        AnimatedNode valueNode = animation.mAnimatedValue;
        mRunUpdateNodeList.AddLast(valueNode);
        if (animation.mHasFinished) {
            hasFinishedAnimations = true;
        }
    }
    
    UpdateNodes(mRunUpdateNodeList);
}

在执行RunUpdates时,会先将需要更新的动画节点放到链表中,同时会将UnityDriver中计算修改的ValueAnimatedNode同样放到链表中。接下来就开始执行节点的遍历更新。

节点遍历主要采用的是“基于广度优先的染色更新算法”。

算法的核心是根据动画节点的两个属性mBFSColormActiveIncomingNodes来决定是否执行节点的更新操作。

mBFSColor标记当前节点颜色用于决定哪些节点需要更新。

mActiveIncomingNodes标记该节点更新前的直接关联节点个数用于决定节点更新的时机。

public abstract class AnimatedNode
{
    public static int INITIAL_BFS_COLOR = 0;
    ...
    public int mActiveIncomingNodes = 0;
    public int mBFSColor = INITIAL_BFS_COLOR;
    ...    
}

节点遍历更新过程通过队列形式来实现,主要可分两个阶段。

第一个阶段是遍历最初的节点链表,存放到节点队列,然后广度遍历染色后的节点队列中各个动画节点及子节点,修改各节点的mActiveIncomingNodes属性。

在第一阶段主要分为三个步骤:

  1. 每次遍历节点前,对全局颜色mAnimatedGraphBFSColor进行修改,用以标记当前处于活跃状态的节点。
mAnimatedGraphBFSColor++; /* use new color */
if (mAnimatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
    //0用于新生成的节点
mAnimatedGraphBFSColor++;
}
  1. 对链表中节点染色,然后存放到队列中

Animated从Native Driver到Unity Driver

Queue<AnimatedNode> nodesQueue = new Queue<AnimatedNode>();
//染色,放到节点队列中
foreach (var node in nodes)
{
    if (node.mBFSColor != mAnimatedGraphBFSColor)
    {
        node.mBFSColor = mAnimatedGraphBFSColor;
        activeNodesCount++;
        nodesQueue.Enqueue(node);
    }
}
  1. 广度优先遍历队列节点及子节点,并进行染色和修改mActiveIncomingNodes

Animated从Native Driver到Unity Driver

while (nodesQueue.Count != 0) {
    AnimatedNode nextNode = nodesQueue.Dequeue();
    if (nextNode.mChildren != null) {
        for (int i = 0; i < nextNode.mChildren.Count; i++) {
            AnimatedNode child = nextNode.mChildren[i];
            child.mActiveIncomingNodes++;
            if (child.mBFSColor != mAnimatedGraphBFSColor) {
                //子节点有可能不在初始需要更新的节点中,但子节点也需要随着父节点而改变,因此染色
child.mBFSColor = mAnimatedGraphBFSColor;
                activeNodesCount++;
                nodesQueue.Enqueue(child);
            }
        }
    }
}

第二阶段是再次遍历最初的节点链表,存放到节点队列,然后广度遍历新染色后的节点队列中的节点及子节点并执行动画节点的更新。

  1. 对全局颜色mAnimatedGraphBFSColor进行修改,用以标记执行更新操作的节点。
mAnimatedGraphBFSColor++;
if (mAnimatedGraphBFSColor == AnimatedNode.INITIAL_BFS_COLOR) {
mAnimatedGraphBFSColor++;
}
  1. 对链表中节点染色,然后将没有前置关联的节点存放到队列中
foreach (var node in nodes) {
    //链表中本轮中没有 处于或将要处于"播放"的父节点的 动画节点颜色,存入到队列
    //后面颜色的判断是防止执行过更新的节点再次执行。
if (node.mActiveIncomingNodes == 0 && node.mBFSColor != mAnimatedGraphBFSColor) {
        node.mBFSColor = mAnimatedGraphBFSColor;
        updatedNodesCount++;
        nodesQueue.Enqueue(node);
    }
}

Animated从Native Driver到Unity Driver

  1. 广度优先遍历队列节点以及子节点入队列

先遍历队列中现存节点,执行更新操作,然后广度优先遍历节点子节点,当子节点满足和步骤2同样条件时子节点入列,继续进行节点的更新操作。

Animated从Native Driver到Unity Driver

while (nodesQueue.Count != 0) {
    AnimatedNode nextNode = nodesQueue.Dequeue();
    try {
        nextNode.Update();
        if (nextNode is PropsAnimatedNode) {
            // Send property updates to native view manager
((PropsAnimatedNode) nextNode).UpdateView();
        }
    } catch (Exception e) {
...
    }
    if (nextNode is ValueAnimatedNode) {
((ValueAnimatedNode) nextNode).OnValueUpdate();
    }
    //广度遍历子节点并更新mActiveIncomingNodes并进行染色
if (nextNode.mChildren != null) {
        for (int i = 0; i < nextNode.mChildren.Count; i++) {
            AnimatedNode child = nextNode.mChildren[i];
            child.mActiveIncomingNodes--;
            //颜色防止被播放过的再播放了
if (child.mBFSColor != mAnimatedGraphBFSColor && child.mActiveIncomingNodes == 0) {
                child.mBFSColor = mAnimatedGraphBFSColor;
                updatedNodesCount++;
                nodesQueue.Enqueue(child);
            }
            ...
        }
    }
}

两阶段广度遍历意义

第一阶段广度遍历是为了确定本轮动画需要更新的节点以及每个节点的父节点个数即确定更新时机。

第二阶段广度遍历则是根据第一阶段遍历结果去按照每个节点更新时机进行实际节点的更新。

转载自:https://juejin.cn/post/7399496845275955210
评论
请登录