弹幕动画的核心:LNDanmakuTrackController

640 阅读6分钟

这篇文章介绍LNDanmakuMaster中控制弹幕核心动画的轨道模块,这篇文章的前置文章是:LNDanmakuMaster

轨道的职责划分

弹幕框架本质上来说就是一种辅助使用者做动画的工具:使用者给出自己需要放到屏幕上的视图,弹幕框架为目标视图运行动画,让其可以在屏幕中动态地展示出来;动画的核心就是弹幕轨道。

轨道组件在这里被划分为了Track/TrackController两部分,Track更像是一种动画规则,TrackController才是动画真正的执行人,一个方便理解这两者关系的方法是:TrackController是干活的人,Track就是工具;如果Track是剪刀,TrackController就是用剪刀的人,剪什么,怎么剪都是TrackController做主,这样做的意义是同一个剪刀可以被不同的人用,同一个人也可以用其他工具。所以我们通常讲的轨道在这里都是指一个TrackController+Track的组合。

我们已经介绍过Clock封装了CADisplayLink,并不断给出那些消逝了的小段时间片回调,通过这些回调,我们会不断从每个弹幕个体(Attributes)中扣除他们的剩余时间,就像人的生命一样,当他们的时间被扣成0时,就意味着这个弹幕的播放周期结束了。而在弹幕生命的进度不断前进的过程中,TrackController总能根据Track提供的动画状态与时间进度的映射,并更新自己包含的所有弹幕动画状态。当一个弹幕播放器中所有轨道都被同一个Clock驱动时,所有轨道以相同时间速率运行。

Track

以一个最为普遍的横向轨道为例,LNDanmakuHorizontalMoveTrack属性如下:

@interface LNDanmakuHorizontalMoveTrack : LNDanmakuAbstractTrack
@property (nonatomic, assign) CGPoint startPosition;
@property (nonatomic, assign) CGFloat width;
@end

一个横向轨道包含两个属性:起始点和持续的长度,我们可以根据这两个属性在任意一个视图上划出一条线段,当一个弹幕被放置到这个轨道上后,会从这个线段的右侧移动到左侧,这个过程持续的时间就是这条弹幕Attributes内规定的totalAliveTime。 在这个过程中Track提供了弹幕更新函数:

@implementation LNDanmakuHorizontalMoveTrack
- (void)updateAttributes:(LNDanmakuAbstractAttributes *)attributes
{
    [super updateAttributes:attributes];
    float percent = attributes.currentPercent;
    CGFloat totalDistance = self.width + attributes.size.width;
    CGFloat currentDistance = totalDistance * percent;
    CGFloat currentX = self.startPosition.x + self.width - currentDistance;
    CGFloat currentY = self.startPosition.y - attributes.size.height/2.f;
    attributes.position = CGPointMake(currentX, currentY);
}
@end

这个函数的计算过程:

  1. 对于任意一个传进来的弹幕attributes,我们首先计算出了这条弹幕需要移动的总距离,这个距离是 (屏幕的宽度+弹幕的宽度),因为弹幕出现和消失条件分别是 左侧首次出现 和 右侧首次消失。
  2. 根据总距离和弹幕已经更新好的percent(Attributes文中提到过这个percent,意思是已存活时间和总存活时间之比),计算出了这条弹幕当前应当移动的距离。
  3. 从弹幕的右侧起点累加这当前应当移动的距离就是弹幕当前应在的位置。
  4. 更新Attributes中的空间信息。 以上就是条形轨道的Track对一条弹幕的更新过程。

TrackController

同样以条形轨道为例,条形轨道继承自一种Base移动轨道,因为所有的移动类型轨道都需要进行追击问题处理来避免弹幕重叠,因此我抽象出了Base移动轨道来让那些类似的移动轨道继承,之后的sin形状轨道或者圆形轨道都是继承自这个基类;此处我们暂先忽略这个基类额外的特性,只考虑它的刷新过程是如何作用在弹幕上的:

@interface LNDanmakuHorizontalMoveTrackController : LNDanmakuBaseMoveTrackController
@property (nonatomic, strong, readonly) LNDanmakuHorizontalMoveTrack *horizontalTrack;
@end
@interface LNDanmakuBaseMoveTrackController : LNDanmakuAbstractTrackController
@property (nonatomic, assign) NSTimeInterval spaceTimeInterval;
@end

轨道属性 spaceTimeInterval是两个弹幕之间的最小时间间隔,前文提到过了,LNDanmakuMaster的所有控制都以时间为唯一准则进行处理,所以这个间隔也是以时间为单位计算的,不同速弹幕之间的时间间隔一致。

@implementation LNDanmakuBaseMoveTrackController
- (void)update:(NSTimeInterval)elapsingTime
{
    if (elapsingTime < 0.f) {
        return;
    }
    NSMutableSet<LNDanmakuAbstractAttributes *> *shouldUnloadAttributes = [[NSMutableSet alloc] init];
    for (LNDanmakuAbstractAttributes *attributes in self.currentAttributesMSet) {
        attributes.currentAliveTime += elapsingTime;
        NSTimeInterval restAliveTime = attributes.totalAliveTime - attributes.currentAliveTime;
        if (restAliveTime <= 0) {
            [shouldUnloadAttributes addObject:attributes];
        } else {
            [self.track updateAttributes:attributes];
        }
    }
    
    for (LNDanmakuAbstractAttributes *attributes in shouldUnloadAttributes) {
        [self unloadAttributes:attributes];
    }
    [self checkCanLoadCandidate];
}
@end

这个函数是TrackController精简后的更新方法,其大体流程如下:

  • 轨道控制器接收到了一小段流逝的时间(通常是0.0167)。
  • 遍历当前的轨道上的所有弹幕,为每条弹幕的存活时间加上这段已经流逝的时间。
  • 如果在增加这段时间后,这条弹幕的存活时间已经超过了他的总存活时间,这条弹幕会被放到一个卸载集合中,并在函数的尾部被一起卸载掉。
  • 如果在增加这段时间后,这条弹幕的存活时间没有达到总存活时间,TrackController调用自己的Track更新这条弹幕的位置。

以上就是一个TrackController配合一个Track更新弹幕的过程,因为抽象出了Track,所以这样一个TrackController可以搭配多种轨道来实现不同的运动效果,例如条形轨道和圆形轨道内部分别是这样实现的:

@interface LNDanmakuHorizontalMoveTrackController : LNDanmakuBaseMoveTrackController
@property (nonatomic, strong) LNDanmakuHorizontalMoveTrack *horizontalTrack;
@end

@implementation LNDanmakuHorizontalMoveTrackController
- (LNDanmakuAbstractTrack *)track
{
    return self.horizontalTrack;
}
- (LNDanmakuHorizontalMoveTrack *)horizontalTrack
{
    if (!_horizontalTrack) {
        _horizontalTrack = [[LNDanmakuHorizontalMoveTrack alloc] init];
    }
    return _horizontalTrack;
}
@end
@interface LNDanmakuCircleTrackController : LNDanmakuBaseMoveTrackController
@property (nonatomic, strong, readonly) LNDanmakuCircleTrack *circleTrack;
@end

@implementation LNDanmakuCircleTrackController
- (LNDanmakuAbstractTrack *)track
{
    return self.circleTrack;
}
- (LNDanmakuCircleTrack *)circleTrack
{
    if (!_circleTrack) {
        _circleTrack = [[LNDanmakuCircleTrack alloc] init];
    }
    return _circleTrack;
}
@end

其实他们都继承自移动轨道基类,只是在track方法中返回了不同类型的Track而已,表现出来的动画却相差甚远,这种组合方式让TrackController和Track都得到了复用: yVlmjO.gif yuzLeP.gif

其他的轨道

LNDanmakuMaster提供了3种BaseTrackController:

  • 移动类型:LNDanmakuBaseMoveTrackController
  • 定点类型:LNDanmakuBaseStableTrackController
  • 自由类型:LNDanmakuBaseFreeTrackController 其中前两种一般可以满足大部分弹幕需求。

LNDanmakuMaster也原生实现了6种TrackController+Track组合:

  • 水平移动:LNDanmakuHorizontalMoveTrackController(最常见的那种)
  • 定点轨道:LNDanmakuStableTrackController(类似B站那种中间一列,这个可以设置在任意点)
  • 心形轨道:LNDanmakuChristinaTrackController(一个心形的图案)
  • 圆形轨道:LNDanmakuCircleTrackController(一个圆形的图案)
  • pop动画轨道:LNDanmakuPopTrackController(用衰减正弦曲线模拟的spring动画,通常使用一组数值已经是计算好的静态数值,所以不需要担心计算带来的性能问题)
  • 波浪轨道:LNDanmakuSinTrackController(sin函数的形状,与条形十分类似,加上了上下波动和弹幕的旋转)

因此,通过更改Attributes提供的size、position、opacity和transform属性作出的效果已经能够涵盖绝大部分使用场景了;即便使用者有非常特殊的需求,也可以通过继承Attributes、自定义TrackController实现,只要是继承自Abstract类的组件都可以接入到整个框架中来。

最后,Christina轨道的由来:我在编写代码的时候突然想起了2016年张宇老师的一堂课里,普及了一个关于笛卡尔的知识点:“你们知道百岁山广告里面那个乞丐是谁吗?是的,那个就是笛卡尔”,之后声情并茂地给我们讲完了整个故事,虽然后来我百度了一下,那个故事可能并不是真实的,但我仍然觉得很浪漫,或许我也可以用代码实现一些浪漫的东西,我这样想着,Christina就是那个公主的名字。