云烟记事录

少主小册

好久好久没有写文章了,就像我之前说的,我实在是太懒了。

前不久写了个游戏,叫雪球快跑,andorid,ios都已经上架,搜就能搜到。
毕竟是第一次写真正上架了的游戏,还是有很多感触的,会一一写成文章记录下来。
这一篇,就从设计开始吧。

从大学开始,提到设计,想到的就是OO,那么究竟什么是OO呢?
面向对象这个词语实在是用得太多了,关联的东西也很多,无非第一下想到的就是继承,
多态什么的一堆词语。对于面向对象,我的理解其本质是数据和行为的聚合,
其它都是这个衍生出来的。因此,我去思考设计,其实无非就是如何去组织数据和行为,
数据和行为聚合在一起的,是面向对象,分离的则不是。当然在实际实现中,
这个的界限是很难区分清楚的,设计本来就是一个糊涂事…
很难去给出一个清晰的定义或者准则,所以,如果我继续这么玩概念虚下去,
那这篇文章也就没有存在的理由了,下面,
我会从《雪球快跑》这个游戏的具体设计开始说起,
这个设计未必好,但是是我自己的一种思路。

首先介绍一下雪球快跑这个游戏吧,(ps:有兴趣的可以去玩玩,能不能玩到100分)。

雪球快跑是一个冒险跑酷类的游戏,玩家通过点击屏幕操控重力,
雪球会根据重力来上下掉落,在地面上面的长时间滚动会导致雪球增大,
玩家需要控制重力来让雪球躲过黑洞,闪电,陨石等障碍物。
游戏本身很简单了,元素也不多,操作也简单,那么,我们如何去划分它会比较合理呢?

天下所有的设计者都其实是在寻求一个设计来说服自己,让自己安心。
大部分之前精心设计考虑了大量情况的扩展都没有用,最后被重构了,
可是在设计的时候还是会纠结来纠结去,期待给出一个完美的答案,
不幸的是我就是这样的性格,于是在设计雪球快跑的时候,考虑了各种扩展的问题,
当然到最后也没有精力去具体实现,设计过程的本身,也是一种乐趣。

这个游戏的设计和实现都是基于 cocos2dx 这个游戏框架的,对于 cocos2dx 这个框架,
我就不在这篇文章里面详细介绍了,简单地说一些概念,
Sprite 可以认为是绘制在屏幕上的实体,Layer 来承载 Sprite,Scene 来组织 Layer。
后面的设计会经常提及这几个玩意,因此,如果完全不知道我在说什么的,
建议先去了解一下 cocos2dx 这个框架。事实上这几个抽象是游戏设计的一种通用抽象,
聚集了无数前人的智慧。

正式切入主题吧。
这个游戏看上去简单,可是第一次设计的时候,会发现其实挺复杂的。
比如说,当雪球碰到各种障碍物之后的表现,就有好多种,
又比如雪球虽然看上去在动,但是当雪球到了某个位置之后实际上就不动了,
也就是说事实上这时候是背景在动,又比如障碍物是随机生成的,
那么障碍物出现的策略是什么,如何保证雪球一定能过?
重力反转之后,雪球需要有坠落效果,遇到上下的地面都要停止坠落。
雪球方向反转之后滚动的方向也需要变化,等等。
这么多复杂的特性,会让人有种无从下手的感觉。

游戏和我工作从事的后端服务器类的程序有一点很大的区别,在于游戏是有屏幕显示的,
复杂的显示效果和后端逻辑结合起来,会扰乱人的思维。
因此,我们不妨将整个游戏分为两个部分,一部分为展示,一部分为逻辑处理。
定义一个中间的结构,展示的时候仅仅是读取这个中间结构的值去绘图展示,
逻辑处理仅仅是改变这个中间结构的值,这样就将展示和逻辑处理分离开了。
可能这么说有点抽象,我举个例子吧,比如我们有一个逻辑是,球遇到黑洞会被吸进去。
那么按照不分开的写法,代码(伪代码)应该是:

if ball.contact_with(black_hole) then ball.die and screen.show_ball_sucked()

而按照分离的写法则应该是:

if ball.contact_with(black_hold) then ball.die and ball.status = sucked

然后再绘图的代码位置:

if ball.status == sucked then screen.show_ball_sucked()

似乎看上去代码变多了, 但是第二种写法的好处在于,我们把逻辑分离了,
当思考球撞到黑洞的时候,我们想的是求被黑洞吸进去了,
那至于吸进去的时候有没有旋转,有没有变小,我不管。
然后在写绘图的时候,则是,如果球现在的状态是被吸进去,那么我们就吸进去吧。
我才不管他为啥被吸进去。

有了上面的这个认知,接下来的设计会轻松很多。
这是一个跑酷类的游戏,这类游戏有一个特点,除非游戏结束,
否则游戏是一直自己往前推进的,而不是玩家来控制进度。
类比一下,其实这类游戏和电影没啥两样,剧情在不停的往前发展,
玩家最多就是一个厉害一点的观众,时不时能稍微影响下剧情。

于是,我们就可以建立这样一个模型,首先有个游戏的主体,自己在不停的演进,
比如球在自己滚啊,受重力的影响啊,镜头在不停的跟踪球啊,
球在看是不是和障碍物撞了啊,等等。接着是交互系统,
也就是说玩家可以通过点击屏幕去改变球的参数,但是这个改变实际上是延迟的,
也就是说,玩家的点击仅仅就是改变了一个值而已,真正起作用,
还是游戏自我演化的时候起作用。
然后就是我们上面提到过的那个显示模块,会不停的读取主体的数据去刷新屏幕。

这样我们游戏的大体模型就出来了,我们来具体看看主体部分应该怎么设计。
首先,会有一个 World 这样一个类,它扮演着总体容器的这样一个角色。
所有的游戏中的实体,可见或者不可见的,我们都可以抽象出一个父类叫 Entity。
比如 Ball,BlackHole,Meteor,LightingBall ,Ground,
Background 都是 Entity 的特化,这样所有的 Entity 的生命周期都由 World 去掌控了。
这样我们只要去分别实现各个类,发现这个游戏就完了。。。真的是这样么?

。。。当然不是了。。。不然我还写个啥?这时候球还只能够一个劲地往前滚,
一会儿就滚出屏幕了,而且那些障碍物的分布是怎样呢?换句话说,地图是怎么出来的呢?
于是,我们势必要抽象一个 camera 来跟踪球的位置,让球始终在左边 1/3 的地方。
记得之前我反复强调的显示和逻辑分离么?这里就是最好的应用场景了,
想象一下你的逻辑里面不是球在往前滚而是背景在回退吧,
各种变速碰撞上下,是不是整个人都不好了?还好我们将这两部分逻辑抽开了,
球还是往前滚吧,camera 跟着滚就是了。

好吧,还剩一个麻烦事,地图怎么玩呢?抽象一个 Loader 对象来做这件事吧,
现在是随机地图,于是用一个随机序列就可以了,若是之后要使用手动写地图,
那换个 loader 就好了,当然,
这里面用到了一些算法来保证障碍物出现的合理性,这篇文章谈设计,不说那些。

好,到这里为止,雏形出来了,可是马上就能发现,World 这个类,做的事情太多了吧:
又要去管理 Entity 的生命周期,又要去驱动 camera 跟踪球,
又要去看球和障碍物们是不是碰撞了,还要调用 Loader 去拿接下来的地图。
这哪吃得消啊,抽出一个 logic 的类来做点事情吧,
World 老老实实管理生命周期就行了,甚至都不需要去理解 Entity 是什么,
至于后面的玩法改变,比如加了个球什么的,替换 logic 吧。

到此为止,雪球快跑的初步架构是已经出来了,扩展性还是不错的,
很容易添加和删除东西。但是,如果就这么写着,各个模块并行,
你依然会发现逻辑不好描述,比如说重力改变了,而你的球正往前滚得欢呢,
你怎么去影响他呢?

这里又说到游戏里面另外一个通用的设计了,所有的游戏都是一个巨大无比的循环,
根据屏幕刷新的帧数或者是其他参数来决定游戏循环计算的速度,也就是说,
你看上去一直在进行的游戏,其实从本质上只是一个一直在进行的迭代计算而已,
而你考虑的逻辑,只是每一次迭代中的算法,这么看,是不是很多复杂的情况都简单了?
比如你的重力改变了,没问题啊根本,应为我每次迭代重力都是一个参数,
你要么影响这次,要么影响下次,反正我也只是计算而已。

好,到这里,游戏本身的设计就完了,当然有很多细节会需要关注,
但是这些不是我这篇文章要关注的重点了。

这边文章的重点,真正开始了:

我们来回顾一下先前的游戏的设计,看看和我们已知的一些其他知识,
有什么相同的地方呢?

大循环,这个抽象,有没有让你想起什么来呢? 。。。。。。
是的,这可不就是一个服务器么。。。只不过服务器是一个请求过来,
对整个请求的处理作为一个循环,而游戏是你自定的参数或者是屏幕的帧数;
既然如此,那服务器设计用到的东西,完全可以照搬到游戏中嘛,
内存池的管理什么的都能用上了。那么我们更进一步,在服务器中,
拿 nginx 来说,一条 request 一般是一条数据,一堆的模块挂载上去之后各种处理。
那我们进化一下如何?在函数式编程中,有一种东西叫做 closure,
这玩意各种定义一大堆,简单的说,就是一段包含了运行环境的可执行代码。
想象一下,你的 nginx 接收的每个 request 是自执行的,
nginx 做的只是给它分配一个线程,是不是发现逻辑清晰好多?
这么做在服务器上未必合适,因为服务器要求的是一个稳定的持续可维护可测试的逻辑。
但是在游戏里面这么做,逻辑不要太简单了。
这就是我之前一直没有提的 event 机制了。
循环里面事实上触发了一堆的 event 去自己执行。
event 机制在 cocos2dx 中已经实现了,用得非常轻松。

好,和服务器的对比完了,我们继续来看这个大循环的抽象,怎么感觉有点眼熟呢?
。。。。。。想想当年你在大学或者是找工作的时候,绞尽脑汁的去想算法。
思路是什么,是不是演化一次迭代?是不是找数列的通项公式?。。。
好吧,万物是相同的。

好了,说够了大循环了,我们来看看别的。我上面举出那个例子,
雪球被黑洞吸收的时候的那个例子里面,显示和逻辑是怎么分开的?
逻辑改变了雪球的状态,显示根据状态去显示了。我偷偷藏了一点没说:
后来 logic 发现雪球挂了。。。于是判定游戏结束。
这里面有一个关键点是啥?
没错,就是状态机。状态机,只要 998,程序代码,你值得拥有。
状态机这个玩意的应用太多了,字符串匹配算法,
编译机制的词法推导语法推导,这不都是状态机么?
是的,游戏就是一个特殊的编译器。。。
我这么说是不是太不像话了?可是你仔细想想,他们其实没有本质区别的,
人如果能从处理字符串中得到乐趣,那编译工作又有啥不是一个游戏的呢?

啰啰嗦嗦说了一堆,并不是试图去教什么东西,我只是一个小小求学者,
一直在试图用很多理由来说服自己这个设计不是那么差,于是就有了上面的文字。

最后打个广告:

雪球快跑, 孤独的小雪球跑啊跑,完全停不下来啊, 你和朋友谁跑得最远呢? 谁能跑到100分?

附一张300分的截图。。。 不要看我,肯定不是我玩的。。。

ios传送门:

android传送门:

blade blade的背景我也就不详细介绍了,总体来说,
它是腾讯开源的一个构建工具,基于scons而非make,采用python编写,
目前的版本定位于linux下的C++程序。
它的设计思路来源于google的官方博客上的一篇文章,
[Build in the Cloud: How the Build System works] Build in the Cloud
好吧,刚刚发现上面的网址挂了~~,大家将就一下,看[这里] ch bic吧,
有兴趣的同学可以去看一下。事实上这是一个系列,
里面讲述了google对大规模协作开发的理解,推荐大家看看。

blade是一个基于scons的构建系统,它做的主要工作是分析代码之间的依赖,
然后生成相应的scons的配置文件SConstruct,由于这一系列生成的中间文件的路径,
都是有blade根据规则自动创建,可以保证每个文件只会被编译一次,
而且中间结果可以被复用,这样就大大提高了大规模构建时的效率。
这篇文章仅仅对blade中的build过程进行分析,不会对run以及test进行描述,
事实上blade对test的支持相当的好,也许有时间研究了我会再写一篇文章来描述,
这是后话,暂且不提。

为了读懂blade,势必要了解什么是scons,scons是一个用python写的构建工具,
这里是它的[官网] scons,这篇文章的重点不在scons上面,
所以不会对它的好处和特点进行详细的说明,大家可以参考[这里] scons good
scons是通过读取项目根目录下的SConstruct文件来开始执行构建的,事实上,
SConstruct就是一个python脚本,scons为构建提供了一系列函数,
开发者在写SConstruct时,其实就是在写一个python的脚本。
这里介绍一下scons的简单用法:

  • scons : 执行SConstruct中的脚本
  • scons -c : 相当于make clean,会根据默认规则和SConstruct定义的规则做Clean
  • scons -Q : 只显示编译信息,去除多余的打印信息

scons为它的配置脚本提供了一系列的函数用来构建,这里列举一些常用的:

  • Program : 生成可执行文件,如Program('foo','foo.cc')
    将会生成一个名为foo的可执行文件
  • Object : 故名思议,就是用来生成目标文件的函数,如Object('foo.cc')
    在linux下将会生成foo.o
  • Library : 生成静态库和动态库文件,
    其中SharedLibrary将会仅仅生生成.so,而StaticLibrary将会生成.a
  • SourceSignatures : 判断源文件是否更改,可以通过时间戳或者MD5或者二者来判断
  • TargetSignatures : 判断目标文件是否更改,可以根据编译结果或者是内容来判断
  • Ignore : 用于忽略某个依赖关系,如Ignore(foo, 'foo.h')
    则当foo.h改变时,不会重新编译foo
  • Depends : 用于显示的指定某个依赖关系,如Depends(foo, joke)
    会将joke作为foo的依赖,joke的变更将会导致foo的重新编译
  • Command : 用于执行命令行命令,这个命令比较复杂,这里不详细展开,
    给出官方的[UserGuide] scons UserGuide-Command
  • Enviroment : 用于设定环境变量
  • Builder : 用于构建自己的Builder,如
    foobld = Builder(action = 'foobuild <$SOURCE> <$TARGET>')
    这个用法也相对复杂,这里不详细展开,
    可以参考官方的[UserGuide] scons UserGuide-Builder

以上用法的详细信息都可以在scons的[官方文档] scons UserGuide上找到。

介绍完了scons,我们可以正式开始研究blade了。

首先我们来看一下blade的BUILD文件,在BUILD文件中,常用的函数有cc_library
cc_testcc_binaryproto_library等等,这些函数,做了两件事情,
首先创建一个target,然后将这个target注册给blade。

这里就不得不解释一下什么是target,在blade中,定义了一个抽象的父类叫Target,
它用于描述一个抽象的scons的target,子类如CcLibraryCcBinary都会继承于它,
它定义了get_rulesscons_rules两个公共的抽象方法,
用于获取自身相关的scons rule。target中定义了一系列属性,用于描述该target,
我们主要需要关心的有下面这些:

  • name:target的name,就是在BUILD文件中定义的name属性,用来唯一标识一个target
  • path:这个属性通过blade来产生,用来标志target的路径
  • srcs:这个属性通过BUILD文件中的srcs来定义,存储了target相应的源码文件,
    也有可能是一个目录
  • deps:这个属性通过BUILD文件中得deps来定义,存储了target的依赖
  • expand_deps:这个属性通过blade生成,用来表示target中deps的deps
  • data:这个属性用来存储各种额外的信息,比如warning,incs等等

上面曾经说到,BUILD文件中你调用的函数,将会把target注册给blade,
于是blade就可以调用该target的scons_rules来生成SConstruct中的相应部分。

下面我以一个来举一个例子,来描述blade的build的流程。
首先我们创建一个创建了Fool和Me的代码如下:

接着我们建立BUILD文件如下:

通过blade build命令,
blade将会在blade-bin/test目录下build出一个libfool.a和libme.a。

当在命令行输入了blade build命令,blade做了什么呢?
我们可以从blade_main.py开始看起:

  • 首先自然是要需要解析出build这个动作,将它和相应的函数映射起来,
    可以看到blade_main中调用了CmdArguments()这个方法,
    这个方法由command_args模块提供,这个方法将会从命令行中parse出command和target、
    options,返回的是blade中真正的处理函数名。
  • 接着,blade将找到BLADE_ROOT所在的目录,将当前目录切换到BLADE_ROOT所在的目录,
    然后通过configparse这个模块,会解析出blade的config。
    blade不支持多进程编译,也就是说,
    一台机器上只允许有一个blade在同一个BLADE_ROOT下工作,
    blade采用了文件锁确保了这个机制。
  • 之后,blade_main初始化了一个blade的实例,调用它的blade.generate()的方法,
    在这一步骤中,blade的源代码中给这一句的注释是# Build the targets
    我个人觉得这个注释不太对,事实上blade本身不会真的去build一些target,
    它只是去生成一个scons的SConstruct,而真正的build是在_build方法中,
    在这个方法中,blade会开启一个新的进程,在这个进程中打开scons,
    这时候,scons才会真正的去build源代码。
  • 最后,blade_main会删除生成的SConstruct。

那么接下来,我们就进入blade的generate方法,来看一看它干了什么?
blade的generate方法如下:

blade generate
1
2
3
4
5
def generate(self):
"""Generate the build script. """
self.load_targets()
self.analyze_targets()
self.generate_build_rules()

可以看到,blade首先会加载所有的目标,这里就包括了去重,依赖展开等等,
然后,blade会去分析目标,得到一个层层依赖的顺序,
最后生成相应的build rule,写入SConstruct中。

到现在为止,关键路径已经出来了,我们来逐层深入,
在blade的load_target()函数中做了一大堆的判断,我们暂时不需要关心,
最后它调用了load_build_files模块的load_targets()函数,我们来看看它的实现:

load_build_files load_targets
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
87
88
89
90
91
92
93
94
def load_targets(target_ids, working_dir, blade_root_dir, blade):
"""load_targets.

Parse and load targets, including those specified in command line
and their direct and indirect dependencies, by loading related BUILD
files. Returns a map which contains all these targets.

"""
target_database = blade.get_target_database()

# targets specified in command line
cited_targets = set()
# cited_targets and all its dependencies
related_targets = {}
# source dirs mentioned in command line
source_dirs = []
# to prevent duplicated loading of BUILD files
processed_source_dirs = set()

direct_targets = []
all_command_targets = []
# Parse command line target_ids. For those in the form of <path>:<target>,
# record (<path>,<target>) in cited_targets; for the rest (with <path>
# but without <target>), record <path> into paths.
for target_id in target_ids:
if target_id.find(':') == -1:
source_dir, target_name = target_id, '*'
else:
source_dir, target_name = target_id.rsplit(':', 1)

source_dir = relative_path(os.path.join(working_dir, source_dir),
blade_root_dir)

if target_name != '*' and target_name != '':
cited_targets.add((source_dir, target_name))
elif source_dir.endswith('...'):
source_dir = source_dir[:-3]
if not source_dir:
source_dir = './'
source_dirs.append((source_dir, WARN_IF_FAIL))
for root, dirs, files in os.walk(source_dir):
# Skip over subdirs starting with '.', e.g., .svn.
dirs[:] = [d for d in dirs if not d.startswith('.')]
for d in dirs:
source_dirs.append((os.path.join(root, d), IGNORE_IF_FAIL))
else:
source_dirs.append((source_dir, ABORT_IF_FAIL))

direct_targets = list(cited_targets)

# Load BUILD files in paths, and add all loaded targets into
# cited_targets. Together with above step, we can ensure that all
# targets mentioned in the command line are now in cited_targets.
for source_dir, action_if_fail in source_dirs:
_load_build_file(source_dir,
action_if_fail,
processed_source_dirs,
blade)

for key in target_database:
cited_targets.add(key)
all_command_targets = list(cited_targets)

# Starting from targets specified in command line, breath-first
# propagate to load BUILD files containing directly and indirectly
# dependent targets. All these targets form related_targets,
# which is a subset of target_databased created by loading BUILD files.
while cited_targets:
source_dir, target_name = cited_targets.pop()
target_id = (source_dir, target_name)
if target_id in related_targets:
continue

_load_build_file(source_dir,
ABORT_IF_FAIL,
processed_source_dirs,
blade)

if target_id not in target_database:
console.error_exit('%s: target //%s:%s does not exists' % (
_find_depender(target_id, blade), source_dir, target_name))

related_targets[target_id] = target_database[target_id]
for key in related_targets[target_id].expanded_deps:
if key not in related_targets:
cited_targets.add(key)

# Iterating to get svn root dirs
for path, name in related_targets:
root_dir = path.split('/')[0].strip()
if root_dir not in blade.svn_root_dirs and '#' not in root_dir:
blade.svn_root_dirs.append(root_dir)

return direct_targets, all_command_targets, related_targets

这个函数很长,里面的变量名也晦涩难懂,我贴出来也没指望有人有人会把它真正的读完,
但是,不需要完全的去分析每个细节,我们基本上可以看出这个函数做了两件事情:

  1. 找到所有的target
  2. load相应的BUILD file

事实上仔细看这个函数的实现,
它无非是将target和它deps中所有target所在的路径解析出来,
然后调用_load_build_file这个函数,去加载相应的配置文件,
这个递归过程在这里采用了循环的方式,使得代码读起来有点不容易,
不过循环的方式可以减少栈的深度,有助于效率的提高。
现在一切矛头都指向了_load_build_file这个函数,我们来看看它的实现:

load_build_files _load_build_file
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
def _load_build_file(source_dir, action_if_fail, processed_source_dirs, blade):
"""_load_build_file to load the BUILD and place the targets into database.

Invoked by _load_targets. Load and execute the BUILD
file, which is a Python script, in source_dir. Statements in BUILD
depends on global variable current_source_dir, and will register build
target/rules into global variables target_database. If path/BUILD
does NOT exsit, take action corresponding to action_if_fail. The
parameters processed_source_dirs refers to a set defined in the
caller and used to avoid duplicated execution of BUILD files.

"""

# Initialize the build_target at first time, to be used for BUILD file
# loaded by execfile
global build_target
if build_target is None:
build_target = TargetAttributes(blade.get_options())
build_rules.register_variable('build_target', build_target)

source_dir = os.path.normpath(source_dir)
# TODO(yiwang): the character '#' is a magic value.
if source_dir in processed_source_dirs or source_dir == '#':
return
processed_source_dirs.add(source_dir)

if not os.path.exists(source_dir):
_report_not_exist(source_dir, source_dir, blade)

old_current_source_path = blade.get_current_source_path()
blade.set_current_source_path(source_dir)
build_file = os.path.join(source_dir, 'BUILD')
if os.path.exists(build_file):
try:
# The magic here is that a BUILD file is a Python script,
# which can be loaded and executed by execfile().
execfile(build_file, build_rules.get_all(), None)
except SystemExit:
console.error_exit('%s: fatal error, exit...' % build_file)
except:
console.error_exit('Parse error in %s, exit...\n%s' % (
build_file, traceback.format_exc()))
else:
if action_if_fail == ABORT_IF_FAIL:
_report_not_exist(source_dir, build_file, blade)

blade.set_current_source_path(old_current_source_path)

又是一大段的代码,对路径的各种操作,但是还好这些都是细节,如果不去实现,
不需要太过关心,事实上这一段内容的核心是一句代码两行注释:

1
2
3
# The magic here is that a BUILD file is a Python script,
# which can be loaded and executed by execfile().
execfile(build_file, build_rules.get_all(), None)

到这里,是不是有种茅塞顿开的感觉?
在这篇文章的早些时候我曾经花过一下篇幅介绍BUILD,现在我们来回头看一下,
以上面我举的那个BUILD文件为例,execfile函数将会执行BUILD文件中的cc_library函数。
我们来看一下cc_library函数的实现,这个函数在cc_targets模块中,我们可以看一下:

cc_targets cc_library
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
def cc_library(name,
srcs=[],
deps=[],
warning='yes',
defs=[],
incs=[],
export_incs=[],
optimize=[],
always_optimize=False,
pre_build=False,
prebuilt=False,
link_all_symbols=False,
deprecated=False,
extra_cppflags=[],
extra_linkflags=[],
**kwargs):
"""cc_library target. """
target = CcLibrary(name,
srcs,
deps,
warning,
defs,
incs,
export_incs,
optimize,
always_optimize,
prebuilt or pre_build,
link_all_symbols,
deprecated,
extra_cppflags,
extra_linkflags,
blade.blade,
kwargs)
if pre_build:
console.warning("//%s:%s: 'pre_build' has been deprecated, "
"please use 'prebuilt'" % (target.path,
target.name))
blade.blade.register_target(target)

现在巧妙之处就真正出来了,这个函数其实就是创建一个target对象,
并且将它注册进blade,供后面使用,所以其实遍历一次BUILD文件,
所有的target都自动加载了,在读这段代码的时候,有一种感觉,
就是本来加载就如同收麦子一样,非常辛苦,需要blade一个个去收集,
而现在是走过每片田地,麦子自己就跳进来了。当然这个比喻并不是很准确,
但这个设计的确是相当的巧妙,让人觉得享受。

事实上到这里,后面的代码阅读起来已经没有难度了,现在所有的target已经加载,
接下来就是target的分析了,我们回到blade的主干,analyze_targets的实现比较简单,
我这里就不贴上来逐步分析了,
它最后追溯到dependency_analyzer模块的analyze_deps函数中,
我们来看一下这个函数的实现:

dependency_analyzer analyze_deps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def analyze_deps(related_targets):
"""analyze the dependency relationship between targets.

Input: related targets after loading targets from BUILD files.
{(target_path, target_name) : (target_data), ...}

Output:the targets that are expanded and the keys sorted
[all the targets keys] - sorted
{(target_path, target_name) : (target_data with deps expanded), ...}

"""
_expand_deps(related_targets)
keys_list_sorted = _topological_sort(related_targets)

return keys_list_sorted

可以看到,其实这个地方就是根据依赖关系,将所有的target做了一个拓扑排序,
这个拓扑排序的算法其实很有意思,当然不是我这篇文章的重点,
不过有兴趣的同学可以去看一下。

现在内存中的target都是完整而且有序的了,我们可以安心地生成SConstruct了,
在这一步中,之前target的设计优势就体现了,
每个target根据自己的属性生成自己的scons rule,最后汇总起来,加上文件头,
就是一个完整的SConstruct文件了。我们可以看一下我例子中用blade生成的SConstruct:

可以看到,上面有很多复杂的东西,这篇文章都没有提到,
事实上为了保证编译环境的干净,以及目录结构的清晰,blade还做了环境变量的Clone、
路径的变换等许多工作,希望了解细节的同学可以自己去读一下源代码。

最后,来提一下blade设计上的现有不足:

  • 首先,大家可以发现blade这个模块全局贯穿各大模块,
    而事实上很多模块是作为工具模块存在的,不需要了解blade的所有信息,
    直接将blade当做参数传递没有做到信息的隐蔽性。
  • 其次,现在的rule_generator在生成SConstruct文件头的时候,是固定生成一些Builder,
    这样就导致如果新增添一种target的类型,会需要修改现有代码,
    可以考虑将Builder的生成分散在各个target中,每个新的target只需要去注册就ok了。
  • blade中对Clean的考虑比较少,会有很多中间结果删不掉,
    可以通过在target中定义Clean操作来实现。

总体来说,blade是一款非常优秀的开源软件,它的理念很多都是超前的,
这种源码分发,统一环境的理念,事实上对一个公司是一个颠覆式的东西,
“万丈高楼起于基石”,一切创新都将变得事半功倍。

已经很晚了,之前一直没有下定决心来写一个blog,一来我有点洁癖,
所有的东西都要自己完全搞懂了才愿意动手,
所以曾经想尝试自己去写一个静态代码的生成器,当然最后不了了之了,
二来写一个自己的blog,就意味着额外的工作量,而我是一个很懒的人,
所以每每思及写作之苦,便又退缩了几分。

下周公司小组里面要做一个blade的分享,之前的分享大家都写了很漂亮的文档,
让我压力有点大,于是琢磨着是不是借此机会,干脆把这个blog搭建出来,
于是就有了这个《云烟记事录》。

这个小站将以技术分享学习为主,当然也同样会记录我生活的喜怒哀乐,
作为我小小人生的一个旁观者,也许若干年后回头看,会有别样的心情。

0%