Scratch 2.0 源代码分析
当你看到这篇文章的时候,相信你对 scratch2.0 的源代码已经很感兴趣了。
或许你是想对其改进,也有可能你想把 scratch 用在像 Arduino 这样的硬件上,
不管你有何目的,了解其源代码是你实现目的的基础,本人自己在读 scratch2.0
源代码时作的一些笔记方便理解,同时,也愿意与大家共同分享自己的笔记,但
是不保证自己的理解完全正确,对于有误的地方希望各位多多谅解,但非常欢迎
与各位同仁分享交流。在分享本人的理解时,已经假设您了解 ActionScript3.0 的
相关语法知识,熟悉其编程机制,对于语言之类的基础知识,本人不再祥细解释。
一.整体分析
在 Src 文件夹中,除去 assets 文件夹中的图片文件,我样可以看到有 258 个
源(.as)文件,这么多的源文件,数十万行代码,如何下手?
任何程序都有入口,而我们的入口则是 build.xml,所以我们从 build.xml 入
手分析(这里用 Ant 编译时入口处,但用 Flash Builder 就不同了)。
1. build.xml
此文件可以说是源代码的入口文件,为什么这么说呢?因为它负责整个源文
件的编译规则,就像 linux 中的 Makefile,从 build.xml 文件内容中可以看到,它
编译的时候,首先定位到了 Scratch.as 文件,这是整个工程的主程序。所以下面
我们开始分析 Scratch.as。
2. Scratch.as
ActionScript 中,程序的入口函数是是 Scratch 类的构造函数,Scratch 的构
造函数很短,我们看一下:
public function Scratch() {
loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.U
NCAUGHT_ERROR, uncaughtErrorHandler);
app = this
determineJSAccess();
}
从构造函数中可以看到,程序首先添加一个异常错误的监听事件,回调函数
为 uncaugh- ErrorHandler,之后再运行 determineJSAccess()函数,在这个函数中调
用了初始化函数 initialize,在该函数中,完成了事 UI 的加载以及对事件的监听。
stage.addEventListener(MouseEvent.MOUSE_DOWN, gh.mouseDown);
stage.addEventListener(MouseEvent.MOUSE_MOVE, gh.mouseMove);
stage.addEventListener(MouseEvent.MOUSE_UP, gh.mouseUp);
stage.addEventListener(MouseEvent.MOUSE_WHEEL, gh.mouseWheel);
stage.addEventListener('rightClick', gh.rightMouseClick);
stage.addEventListener(KeyboardEvent.KEY_DOWN, runtime.keyDown);
stage.addEventListener(KeyboardEvent.KEY_UP, runtime.keyUp);
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown);
stage.addEventListener(Event.ENTER_FRAME, step);
stage.addEventListener(Event.RESIZE, onResize);
在对不同事件进行监听之后,整个框架就基本上建立了,比如当鼠标按下时,
就会响应 gh.mouseDown 函数,整个程序就在等待事件的触发阶段。
二.Script 分析
此块内容非常重要,如果您想改进 Scratch 成为你自己想要的软件,那么你
必然需要添加自己的 paletterSelectorItem 和相应的 Block,所以了解其添加的来
龙去脉是非常必要的。
为了分析 Script 模块,我们首先看看 Script 模块的构成,如下图所示 :
其主要由 ScriptsTab,paletterSelector,paletterSelectorItem,block 组成,了
解其构成,对在程序中完成其实现有一定作用,在下面的分析中会提到这个模块。
1.添加 scriptsTab
addParts()[scratch]->addChild(tabsPart)[scratch]-> addChild(scriptsTab)[tabspart]
2. 在 scriptsTab 中添加选项
addChild(selector = new PaletteSelector(app))[scriptsParts]-> initCategories()[Palette-
Selector] -> PaletteSelectorItem[PaletteSelector]来实现添加 scriptTab 中的选项
在 PaletteSelector 定义了 categories:Array = ['Motion', 'Looks', 'Sound', 'Pen',
'Data', 'Events', 'Control', 'Sensing', 'Operators', 'More Blocks'], 在 PaletteSelector 的构
造函数中调用了 initCategories 函数,在 initCategories 通过调用 for (i = 0; i <
categories.le- ngth; i++)来给每个选项 new PaletteSelectorItem 产生一个 scriptsTab 中
添加一个选项,所以,如果我们希望添加自己的选项,只需要在 categories 数组中
3. 在不选项上添加不同的 block
程序初始化时在 scratch.as 中的 installStage()函数通过调用 ScriptsPart.as 中的
resetCateory 函数,将 BlockPalette 框里选择为 Motion 的 Block,在 PaletteSelectorItem
类中注册了 addEvent- Listener(MouseEvent.CLICK, mouseUp),当点击 Script 的某个选
项时,从 PaletteSelectorItem 中的 mouseUp 函数中:
PaletteSelector(parent).select(categoryID, event.shiftKey);
可以看到,调用 PaletteSelector 的 select 函数来选择不同选项下的 Block。在 select
中,是通过调用:app.getPaletteBuilder().showBlocksForCategory(selectedCategory,
(id != oldID), shiftKey)来加载 Block。在 Spec.as 的 commands 数组定义不同选项中 Block,
所以在 showBlocksForCategory 函数中是通过根据不同 SelectorItem 的 ID 来去匹配
三.Block 类分析
commands 数组中的 Block 命令,最后以 new Block 来产生一 Block,所以,添加自
己选项的 Block 就需要在 commands 数组中添加自己的 block 命令。
是如何画出不同形状的 Block?在这一节中,将会讨论这个问题。
上一节分析到通过 new Block 来产生“积木”,那么 Block 是怎么产生的呢?
4. Block 的形状是如何画出的
这个问题首先得看一下 Block 的构造函数定义:
Block(spec:String, type:String = " ", color:int = 0xD00000, op:* = 0, default-
Args:Array = null)
我们举个例子来说明,Specs.as 中 Commands 的第一条数据:
["move %n steps", " " ,
1 , "forward:",10],
spec:Block 的说明,也就是一个 Block 上的文字及输入框的内容。"move
%n steps"
type:Block 的开关标识,通过 type 不同,画不同形状的 Block。" "
color:标识 Block 的颜色。
op:Block 的操作码,在识别不同 Block 的功能时通过操作码来实现。“forward:”
defaultArgs:Block 的参数。10
数据中的“1”标识了该 Block 属于哪一个选项,在这里“1”代表该 Block
属于 Motion 选项。
所以在 Specs.as 中已经定义了全部的 Block 内容,new Block 时候,已经通
过 Block 的构造函数将 Block 的属性告诉了 Block 类,知道了 Block 的属性就可
以构建不同的 Block。而最终画图形的是通过 new BlockShape 来实现,下面我
们看一下 BlockShape 类,其构造函数为:
BlockShape(shape:int = 1, color:int = 0xFFFFFF)
Shape:标识了 Block 形状。
Color:标识了 Block 颜色。
其中,Shape 的标识通过 BlockShape 的定义的常数来识别:
// Shapes
public static const RectShape:int = 1;
public static const BooleanShape:int = 2;
public static const NumberShape:int = 3;
public static const CmdShape:int = 4;
public static const FinalCmdShape:int = 5;
public static const CmdOutlineShape:int = 6;
public static const HatShape:int = 7;
public static const ProcHatShape:int = 8;
// C-shaped blocks
public static const LoopShape:int = 9;
public static const FinalLoopShape:int = 10;
// E-shaped blocks
public static const IfElseShape:int = 11;
从它的定义看出,Scratch 中所有的 Block 形状应该由这 11 种不同的形状
模块构成。在 BlockShape 构造函数中调用了 setShape(shape:int),这个函数中
通过一个 switch(shape)语句来选择 drawFunction,我们看一下第一个 case。
case RectShape:drawFunction = drawRectShape; break
如果 shape 为 RectShape,那么 drawFunction 被赋值 drawRectShape 函数,
稍微看一下在 drawRectShape 的实现。
drawRectShape(g:Graphics):void { g.drawRect(0, 0, w, topH) }
drawRectShape 的实现很简单,用 Graphics 类的函数 drawRect 来画出一
个矩形,其它的画图函数也类型,都是通过 Graphics 中的函数来实现图形的表
示。
但是我们发现,在 setShape 中只是对 drawFunction 赋值,但没有执行
drawFunction,所以在 setShape 并没有开始画图形,通过追踪 drawFunction,
我们在 BlockShape 中找到了 reDraw 函数,该函数调用了 drawFunction,但
又是在哪调用了 reDraw 函数呢?在整个 BlockShape 中我们都没找到我们想要
的 reDraw 调用。这时又要回到 Block.as 中了,在其构造函数中,base=new
BlockShape(…)后,调用了 setSpec 函数,setSpec 函数中调用了 fixArgLayout
函数,在 fixArgLayout 函数倒数第三行,我们看到了 base.redraw(),这时,
我们终于松一口气了。我们把这个过程有图例表示一下:
Block()->new BlockShape()->setSpec()->fixArgLayout()->base.redraw()
5. Block 如何响应事件
四.点击运行如何解析程序
当点击绿色小旗帜为什么程序就开始运行?小精灵(Sprite)就动了起来?我
们先看一下这些精灵活动的地方。
在舞台部分(StagePart.as)上主要有三个按扭,为何按下绿色的旗帜就可以
StagePart 图
运行程序?我们看一下 StagePart 的构造函数:
public function StagePart(app:Scratch) {
this.app = app;
outline = new Shape();
addChild(outline);
addTitleAndInfo();
addRunStopButtons();
addTurboIndicator();
addFullScreenButton();
addXYReadouts();
addStageSizeButton();
fixLayout();
addEventListener(MouseEvent.MOUSE_WHEEL, mouseWheel);
}
在构造函数中,我们看到有 addRunStopButtons()函数,所以我们可以猜到那
两个关键的 Button 就应该是在这个函数中出生的,于是到 addRunStopButtons()
函数中走一趟势在必行了:
private function addRunStopButtons():void {
function startAll(b:IconButton):void { playButtonPressed(b.lastEvent) }
function stopAll(b:IconButton):void { app.runtime.stopAll() }
runButton = new IconButton(startAll, 'greenflag');
runButton.actOnMouseUp();
addChild(runButton);
stopButton = new IconButton(stopAll, 'stop');
addChild(stopButton);
}
分析这个函数我们可以看到,它添加两个 IconButton,同时,对“greenflag”
按钮绑定了 playButtonPressed 函数,我们又得转到这个函数中看一下究竟。
public function playButtonPressed(evt:MouseEvent):void {
„„
}
app.runtime.startGreenFlags(firstTime);
函数中其它部分我们就不看了,但是我们找到关键的语句:
app.runtime.startGreenFlags(firstTime)
为了探明 startGreenFlags()函数的本质,我们进入 ScratchRuntime.as 查看这
个函数。
public function startGreenFlags(firstTime:Boolean = false):void {
function startIfGreenFlag(stack:Block, target:ScratchObj):void {
if (stack.op == 'whenGreenFlag') interp.toggleThread(stack, target);
}
„„
setTimeout(function():void {
allStacksAndOwnersDo(startIfGreenFlag);
}, 0);
它是通过 setTimeout 函数来添加一个事件,事件是立即执行,因为第二个
参数是 0,执行的函数是 allStacksAndOwenersDo(),其内容如下:
public function allStacksAndOwnersDo(f:Function):void {
var stage:ScratchStage = app.stagePane;
var stack:Block;
for (var i:int = stage.numChildren - 1; i >= 0; i--) {
var o:* = stage.getChildAt(i);
if (o is ScratchObj) {
for each (stack in ScratchObj(o).scripts) f(stack, o);
}
}
for each (stack in stage.scripts) f(stack, stage);
}
在这个函数中,通过 for 函数遍历舞台上所有的精灵,一个舞台上可以有多
个精灵,而且每一个精灵有自己的程序来控制其行为。
对于每个精灵的程序其实对应的就 Scripts,我们知道,程序就是通过
ScriptsTab 中的 Block 搭建的,在搭建这个程序的时候,
舞台上多个精灵图
五. 进程运行
整个 scratch 的运行是通过 ScratchRuntime.as 来维护的,流程如下:
initialize()[scratch.as]-> stage.addEventListener(Event.ENTER_FRAME,
step) [scrat- ch.as] -> runtime.stepRuntime()[scratch.as] -> interp.stepThreads()
[scratchRun- time.as];
添加过程示例:
添加完成后,效果图如下:
一. 添加一个 PaletteSelectorItem:MK Robots 第一步:
在 PaletteSelector.as 中,将 categories 修改为:
private static const categories:Array = [
'Motion', 'Looks', 'Sound', 'Pen', 'Data',// column 1
'Events', 'Control', 'Sensing', 'Operators','MK Robots']; // column 2
第二步:在 Specs.as 中添加相应的常量
public static const motionCategory:int = 1;
public static const looksCategory:int = 2;
public static const eventsCategory:int = 5;
public static const controlCategory:int = 6;
public static const operatorsCategory:int = 8;
public static const dataCategory:int = 9;
public static const myBlocksCategory:int = 10;
public static const listCategory:int = 12;
public static const MkRobotsCategory:int = 13;
public static const extensionsCategory:int = 20;
第三步:在 Specs.as 修改 categories 为:
public static const categories:Array = [
// id
color
0xbb42c3],
0x0e9a6c], // Scratch 1.4: 0x009870
0xc88330],
0xe1a91a],
0x2ca5e2],
0x4a6cd4],
0x8a55d7],
category name
"undefined", 0xD42828],
"Motion",
"Looks",
"Sound",
"Pen",
"Events",
"Control",
"Sensing",
"Operators",0x5cb712],
"Data",
[0,
[1,
[2,
[3,
[4,
[5,
[6,
[7,
[8,
[9,
[10, "More Blocks",
[11, "Parameter", parameterColor],
[12, "List",
[13, 'MK Robots',
[20, "Extension", extensionsColor],
variableColor],
listColor],
procedureColor],
procedureColor],
];
二. 在 MK Robots 下添加一 Block
第一步:在 Specs.as 中在 commadns 数组添加一元素
//MK Robots
["设置数字口 %n 为 %d.output",
" ",13,"digitalIo:",
3,1],
第二步:在 primitives 包下添加一个类 ArduinoPrims.as,整个类如下:
/*
*西南交通大学创客空间&魅客科技
*/
// ArduinoPrims.as
// sf.Deng , April 11th,2015