Bulletproof 的新弹幕对象架构设计

Bulletproof 重构的工作进展不错,主要是花了时间基于 WebGL 写了 GLantern,将基础的绘制剥离了,以此为基础写弹幕相关的就轻松了。

九月份的测试是概念的验证,架构极其混乱。当时 TypeScript 版本还在 1.4,import 语法限制很多(只有 import x = require("file");),而且 TS 和 C# 那一点点微妙的差距我还没很好地体会出来,最后就变大文件乱引用加一堆hack的混合物了。当时也没什么大的目标,能做出一个能执行代码的高级弹幕的原型就可以了,因为自己都不知道能不能做成。——是,有先例,但是看着这么多已有的代码就开始仰望巨大的工程了。

既然验证通过了,就要开始做架构了。(地基打好了,就开始规划柱梁,再装修。)考虑到应用场合,姑且做了一个。

新的源码在通过一些测试用例后会推上来。莽然融合 WebGL 的测试的各种东西放在了 native-webgl-obsolete 分支(看名字就知道是废弃了)。

详情请展开。

先扯一点远的。我觉得 Win32 啊 COM 啊 COFF 啊这些架构/格式都是神级的设计,经历了多少的时间和实践检验哪……完毕。

第一次运行测试的测试,直接读入之前的 kanpai-madoka.js。MotionGroup 之类的就省了,按照所有值默认显示出来,第一个静帧。

第一次运行测试

设计还算合理。怎么说呢?在规划之后(停留在想法上,没写下来——这是个缺陷,因为我经常忘东西 = =),动手实施,很快就写出来了。运行测试,改了几个 NotImplementedError(我会在所有未实现的函数里引发 NotImplementedError 而不是指定默认值,这是学习 C# 的抽象函数实现模板中引发 System.NotImplementedException 的方式)之后就能正常工作了。(这种方式只能证明,当前的设计是一个有效解,而不能证明是最优解……)

然后我开始讲目前用到的 Bulletproof 新架构。(以下是 SVG 图形,如果你看得别扭的话……其实 Chrome 一旦对于没指定字体的文字应用系统默认字体,也就是宋体,就开始乱套了。)

首先是整体(包视角)的设计。

对应的文件结构是这样的:

D:\SOURCE\JAVASCRIPT\BULLETPROOF\SRC
│ Bulletproof.ts

├─bilibili
│ │ BiliBiliDanmakuApiContainer.ts
│ │ IBiliBiliDanmakuApiContract.ts
│ │
│ └─danmaku_api
│ │ BiliBiliDamakuApiObject.ts
│ │ Display.ts
│ │ Global.ts
│ │
│ └─data_types
│ ICommentBitmapCreateParams.ts
│ ICommentButtonCreateParams.ts
│ IGeneralCreateParams.ts
│ IMotion.ts
│ IMotionPropertyAnimation.ts

└─danmaku
│ DanmakuBase.ts
│ DanmakuCoordinator.ts
│ DanmakuKind.ts
│ DanmakuLayoutManagerBase.ts
│ DanmakuProviderBase.ts

├─code
│ │ CodeDanmaku.ts
│ │ CodeDanmakuLayoutManager.ts
│ │ CodeDanmakuProvider.ts
│ │
│ └─dco
│ DCOHelper.ts
│ DCShape.ts
│ IDanmakuCreatedObject.ts
│ IDCExtraCreateParams.ts

├─mode7
└─simple

首先,在一个全局包 Bulletproof 中有一个全局(对于每个实例而言)类 Bulletproof。每个 Bulletproof 对象会创建一个弹幕的综合管理类 DanmakuCoordinator,用于控制弹幕的产生(兼顾视觉效果与渲染效率)。每个 DanmakuCoordinator 会创建 *DanmakuProvider 对象,提供不同类型的弹幕控制(管理)服务。理论上,可以通过反向注入的方式添加遵循新逻辑的弹幕类型,不过目前还是使用着固定索引的方式,和 BiliBili 的当前值对应(准备先实现普通弹幕、定位弹幕、代码弹幕)。一个 *DanmakuProvider 拥有一个 *DanmakuLayoutManager 实例,该实例负责根据情况计算所属弹幕种类中的某个弹幕的应在位置。每个弹幕种类有自己的实现类 *Danmaku,负责更新与绘制所属的弹幕内容。

接下来是显示核心 *Danmaku 的设计。我目前实现了大部分的 CodeDanmaku,就以此为例子。

CodeDanmaku 继承自 DisplayObjectContainer,为其创建的代码弹幕提供一个统一的视觉元素根节点。同时,每个 CodeDanmaku 有独立的 BiliBiliDanmakuApiContainer 实例,作为高级弹幕代码与 Bulletproof(以及底层的 GLantern)的通信接口。例如,以下的代码会被分配到一个具体的 CodeDanmaku(假设名字为 cd1)中执行:

var s = $.createShape();

执行后,s 的逻辑和视觉父亲自动是 cd1。这段代码是代入了 cd1.apiContainer 的各个对象(对 BiliBili 高级弹幕 API 的模拟),因此保证不同的代码弹幕的创建能自动与对应的 CodeDanmaku 挂钩,执行是分离的。但是目前的实现问题(用 Function),如果突破了简易沙箱,还是会相互影响的。例如,在另一个高级弹幕中输入内容:

s.alpha = 0.4;

就会发现,前面创建的图形透明度发生了变化——此时 s 是一个全局变量。这一点以后考虑采用jabbany的 WebWorker 方案,更安全而且不会造成污染——毕竟浏览器的沙箱帮挡着,除了伪造通讯之外是没有能破坏主线程的方法的。

这些弹幕都被作为 DisplayObject(之后的普通弹幕和定位弹幕也会采用此策略)加入到了根 Stage 的孩子集合中,因而更新和绘制是随着元素树进行的。

最后再回顾一下 DanmakuCoordinator-*CodeDanmakuProvider 的设计。这种预先创建并索引的关系和 ShaderManager-ShaderBase 是一样的。这一点模仿了 Pixi。当初 GLantern 起稿的时候,shader管理是按照最后一个 WebGL 测试的形式写的,每个 WebGLRenderTarget 带着专属的 ShaderBase,如果要实现不同功能,还得实时销毁并创建绑定新的 ShaderBase。后来咬着牙动了大手术,将整个蹩脚的设计改成了现在的 ShaderManager。感谢这个 ShaderManager,后来就一直没有担心过shader资源分配的问题,而且添加新shader非常方便。

分享到 评论