使用代码制作弹幕动画 - SJTU-Art-Center/ACLiveConsole GitHub Wiki
行动代号:DBS
弹幕系统由三个队列(Queue
)组成:接收队列 _danmakuQueue
,弹幕池队列 DanmakuPool
和显示文字队列 danmakuLabels
(只针对 气泡右下
样式)。每一次进队与出队由计时器精确控制。图中显示了一个弹幕的全程处理时间默认为 13s。
其中接收队列使用了 copyliu 的 弹幕姬
源码,并进行了适应性修改。保留弹幕姬源码模型是为了日后维护的方便。
果然,在 4 月 10 日,B 站更新了 弹幕协议v2
,AC Live Console 3.4
以前的版本不能正常地接收弹幕。在 3.6 版本中采用了 弹幕姬
1.1.1.28 版本的 DanmakuLoader.cs 和 DanmakuModel.cs 。
由于弹幕是随即产生,随即消失的,所以我们不能使用 XAML 前端代码设计界面(静态的),必须使用后台代码产生弹幕动画。
使用 WPF 的好处是其可以使用 GPU 渲染窗体,产生更为流畅的动画。相较于传统的 Winform 更加高级。
FocalDepthHover.xmal.cs
public partial class FocalDepthHover : Window {
// Line 33
public enum DanmuStyle { Plain, Bubble, BubbleFloat, BubbleCorner };
}
DanmuStyle
枚举类型定义了弹幕的几种样式,默认样式有:
代码 | 样式 |
---|---|
Plain |
普通样式 |
Bubble |
气泡样式 |
BubbleFloat |
随机位置产生悬浮的气泡 |
BubbleCorner |
在角落处产生堆叠的气泡 |
参数定义见 0764
弹幕样式。
层数n对应于f的n-1次方(n<=#L)。
根据近大远小的规律,前面的物体移动得快,后面的物体移动得慢,这里设置的情况是最前面的弹幕为 T 的一半,移动时间极限为 T (字体大小为 0 时,这是不可能达到的)。
由于字号与高斯模糊半径是等量纲的量,两者比例是相对应的,而第一层是不模糊的,同样的高斯模糊半径的极限为 B 。
这样就通过伪 3D 方式造出了 3D 效果。
在屏幕中央一定比例的区域内随机生成气泡起点,使用二次缓动函数缓出,移动窗口高度的三分之一。
屏幕占比应当尽量小,因为计算的是右下边距。
气泡弹出时会有一个字号从 5 至 S 的动画,所有的气泡都在第 1 层。淡入+字号+悬浮+淡出 动画组合。
弹幕池选中的弹幕弹出由两个故事板组成,第一部分是从无到有,第二部分是从有至无,该部分动画保持速度。底端滚动条的颜色由气泡颜色决定,高度由屏幕占比决定。
底部滚动条(含用户名)样式将增加一个用户名弹幕条目进入滚动队列,该弹幕为细体。
动画基本元素是 Animation
类的派生类,处于下面的事件中:
// Line 211
private void ReceiveDanmu(object sender, ReceivedDanmuArgs e)
{
//...
switch (DM_Style)
{
case DanmuStyle.Bubble:
case DanmuStyle.Plain:
//...
//故事板 Line 248
Storyboard storyboard = new Storyboard();
//添加X轴方向的动画 Line 251
DoubleAnimation doubleAnimation = new DoubleAnimation(this.Width, -label.Content.ToString().Length * hoverLayers.ElementAt(temp_ord).TextSize / 0.75,new Duration(TimeSpan.FromSeconds(hoverLayers.ElementAt(temp_ord).Hover_time)));
Storyboard.SetTarget(doubleAnimation, label);
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("(Canvas.Left)"));
storyboard.Children.Add(doubleAnimation);
//故事板即将开始 Line 259
storyboard.Completed += new EventHandler(Storyboard_over);
storyboard.Begin();
//...
break;
}
//...
}
Line 249
=> 创建故事板(Storyboard
)。
Line 252
=> 创建一个双精度型动画(DoubleAnimation
)。不同的数值类型 WPF 提供了不同类的动画。参数:(From, To, Duration)
。
在新建动画时可以设置两个参数:开始时间
BeginTime
和缓动函数EasingFunction
。关于标准缓动函数的定义详见 Easing Functions 。值得一提的是,在弹幕设置选单里,数值变化采用了不同的缓动函数。
Line 253
=> 设置动画目标。绑定到标签 label
上。
Line 254
=> 设置动画目标属性路径。将属性路径(PropertyPath
)设定为标签在 Canvas
中的左边距值(Left
)。
Line 255
=> 向故事板添加动画对象。
Line 260
=> 添加故事板结束动作,Storyboard_over
被建立为 EventHandler
,结束后将会销毁该弹幕,以防止内存泄漏。
Line 261
=> 故事板开始,其涵盖的动画随即开始。
故事板可以含有多个动画,并设置不同的开始时间和结束时间。
当你写入一个新的弹幕样式,需要在DanmuStyle
枚举类型里添加你的定义 MyStyle
,并且在主窗口 MainWindow.xaml.cs
的复选框 ComboBoxStyle
控件中加入复选框项 ComboBoxItem
,其 Selected
的行为如下:
FocalDepthHover.DM_Style = FocalDepthHover.DanmuStyle.MyStyle;
气泡样式(tipLabel
)是在 XAML 文件内定义的。由一个圆角矩形和一个带填充的三角形组成。由于没有使用合并路径,导致该气泡在调整其不透明度时,三角形会盖不住圆角矩形的描边出现 BUG。如果要解决这个问题,需要纯后台的代码实现,比较复杂,所以这里偷懒了,欢迎修 BUG 。
堆叠式弹幕的特点是,一次只发送一个弹幕,所以需要通过一个队列来存储弹幕,并设置计时器,每 0.5s 出队一个弹幕输出。
气泡右下样式与底部滚动条样式属于这一类型的弹幕。
主窗口的设置更改后,会将 SettingChanged
改为 true
,这样在接收到下一个弹幕后,会调用 Regen()
函数,重新生成 List<HoverLayer>
的实例化对象。
窗体间的弹幕通道是通过 ReceicedDanmu
实现的。MainWindow
发出信号,由 FocalHoverLayer
的实例化对象处理弹幕事件。
这一部分采用了 WordCloud
nuget 包,没有时间去阅读相关文献,根据预想,应该是一个动态的词云,前后可以显示每一个词的地位变化。但是由于时间有限,只能做到静态的切换,真正要做到这一点需要从头写代码,并且使用一篇论文里相关度较高词云的生成算法(该算法无法计算遮罩词云)。
弹幕出队后,如果正在收集弹幕词云(wcCollecting
),将会启用 JIEBA.NET
分词器将弹幕分成一些词语收入 danmudic
词云词典中(里面包含每个词语的文本与出现数量,使用的 Dictionary<string,int>
是基于红黑树的一个词典)。开始生成后,先执行剪枝操作,对禁用词进行删除。之后再复制为 DecDic
降序序列词典,存储统计数据,判定完是否是彩色词云后就会调用 WordCloud
开始生成词云,进度条开始显示。
生成完成后的词云将会被存入内存流中,存储到本地的同时(可以在 Resources/WordCloud.png 看到),被放在公屏上。
如果启用了自动生成,这个时候就会开始下一轮的生成,淡出原来的词云,淡入新的词云。否则等待计时器触发下一轮生成。
这一部分阐述弹幕抽奖的原理。
B站弹幕姬获取弹幕后,出队后立刻检测是否符合抽奖弹幕,如果符合,则同时:构造 NameLabel
存储用户名和弹幕发出时间并加入展示队列 queueNameLabel
,并且加入存储层 GiftingNameList
。当抽奖完毕后将使用 RNGCryptoServiceProvider
真随机数生成器循环性地生成几个随机数选出天选之人,并输出到中奖者显示。
动画部分,抽奖中的用户名是滚动展示的,滚动时间由相邻弹幕发出时间差计算,刷的弹幕越快,动画滚动越快,这一部分保持时间。但是这部分有一个BUG,当0.5s内同时有两个弹幕出现时,抽奖中的显示很有可能卡住,暂时不做解决。中奖者,如果用户名的长度超过了屏幕长度,则会滚动显示,分为两个故事板:中段到左端,左端到结束,先慢后快。
结束后,抽奖会有记录,记入 Gift.txt
中。