笔者对精细化实验的定义

当游戏要新增或改动一个功能时, 如果开发者不能确其会对用户体验造成什么影响, 就会用做实验的方式将功能发布到线上, 即将用户分为对照组和实验组, 看两组用户的数据表现, 来判断该功能的好坏. 如果实验组数据好于对照组, 则应用实验组, 反之应用对照组. 不论应用哪一组, 没有被应用的那一组的硬编码就可以删除掉, 因此实验组与对照组逻辑的代码只是会临时插入到项目中, 只要实验应用了之后把非应用实验分支的代码删干净, 长期下来是不会对整个项目代码结构有什么影响. 但是我们团队在按照上述模式推进了一段时间之后发现了这种模式存在问题, 同一个版本上线的多个实验之间存在交叉, 且会对单个实验的结果产生影响, 而且有的实验短期内是实验组的数据好, 而长期又变成了对照组的数据好. 因此, 我们认为之前对实验数据的结论不准确, 我们决定将一段周期内最早的那个版本的项目状态定义为基线组, 在这一周期内的每个版本会上线的实验都不会应用, 而是会一直在线上跑, 随着实验越开越多, 实验之间的交叉问题越来越严重, 又因为这些实验的分支代码不会删除, 代码也会变得越来越难以维护, 上述这种在一段周期上线大量不会应用实验, 且希望每个实验都能交叉, 在一段周期后会形成上百、上千条实验分支的实验, 就是精细化实验, 对应的, 我们团队就需要一套能够管理精细化实验的框架.

精细化框架的实现思路

每个实验能够交叉, 是十分美好的愿景. 但是, 在实际开发中我们发现, 经常会有互为互斥关系的实验存在, 比如两个实验同时对模块A做了改动, 或者实验一影响模块ABC, 实验二影响模块ABD, 这种情况是不可避免的, 因此, 精细化框架要做的事情, 不仅仅是驱动这些实验, 还要制定一套规范, 处理实验之间的冲突问题.

本篇博客笔者会以UI换皮实验和动效换皮实验为例子, 阐述UI框架、动效播放框架的实现思路, 以及这些框架和精细化框架的协同过程.

从ABTest框架说起

我们最初使用的AB测试系统十分简陋, AB测系统在启动时从磁盘上加载上来所有的AB实验信息, 根据设备的uid, 判断一个用户该表现为对照组还是实验组是根据用户使用设备的uid, 我们在后台配置一个实验的时候, 有多少个实验组就会生成几个“桶”, 这些桶里面装的是用户设备的uid, 用户设备启动游戏的时候, 程序能够知道当前设备在哪个桶里, 但是在某一个桶里并不意味着就一定表现为这个桶所对应的实验组, 还有其他的限制条件, 比如安装版本必须大于某一个版本或者必须是新用户, 或者必须是某一国家地区的等等. 总之, 业务层只需要把它们关系的实验字段传入, 就能够拿到当前设备对应实验所在的实验组是哪一个.

精细化实验模块登场

精细化实验要做的, 就是把ABTest框架和业务层解耦, 作为两者之间的桥梁. 当你想让两个模块解耦时, 最直接的方式就是在两个模块通讯的接口处增加桥接层, 有了这层桥接, ABTest中庞大的数据不至于一下子涌向业务层, 而是在精细化实验层整合、处理, 精细化实验层会把ABTest中影响同一个业务的实验们按照人为定义的规则进行整合、冲突处理, 然后将结果转换成对应业务能够识别的格式——精细化实验层需要和每一个具体的业务定义一套协议, 这套协议能够描述, 经过多个实验的多重影响, 该业务最终的表现效果是怎样的. 我们计划使用Json作为这些实验配置的载体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{
"1024": {
"name": "uiTest",
"experimentSceneType": "UIStyle",
"experimentParam": {
"group1": [
[
"homePage",
1
],
[
"playPage",
1
],
[
"resultPage",
1
],
[
"settingPage",
1
]
],
"group2": [
[
"homePage",
2
],
[
"playPage",
2
],
[
"resultPage",
2
],
[
"settingPage",
2
]
]
}
},
"1025": {
"name": "uiTest2",
"experimentSceneType": "UIStyle",
"experimentParam": {
"group1": [
[
"homePage",
1
],
[
"playPage",
2
],
[
"resultPage",
1
],
[
"settingPage",
4
]
],
"group2": [
[
"homePage",
2
],
[
"playPage",
3
],
[
"resultPage",
5
],
[
"settingPage",
2
]
]
}
}
}

对上述Json格式做一下说明, 最外层的数字代表实验ID, name是实验名, experimentSceneType是实验场景, experimentParam能够描述实验场景下的实验内容, group代表实验分组, 不同实验场景下group字段下的结构不一样, 在UIStyle实验场景下是一个列表, 每个元素代表一个UI, 和该UI使用的styleId.

程序启动时必定加载上面两个实验, 并命中其中一个实验组, 而这两个实验是互斥的, 因此会在精细化实验层进行处理. 具体的处理规则, 则需要和策划团队一起制定, 硬编码进精细化实验模块中, 等业务场景足够丰富的时候, 也许可以从中提取出一套规则来支持配置. 不过这就不在本篇博客要讨论的范畴内了. 总之, 经过精细化层的处理之后, UIStyle实验场景会得到一组类似group字段内的列表, 这里面存储了所有UI的styleId, UIManager侧维护着每个UI每个styleId的映射关系. 有精细化层传来的参数, UIManager就可以给每个UI设置运行时styleId了.

我们设计的实验表格如下:

UI框架的配表

音频框架配表

动效配表

实验配置