简单地编写一个Scratch扩展

随着图形化编程的兴起,比如 Scratch编程猫和 Mind+ 等等变得越来越受欢迎。同时,像 Scratch 原有的内容已经不足以给各位提供足够的函数了。像 TurboWarpGandi IDE、Scratch Lab、Scratch X 等就提供了丰富的非原版函数库(即扩展,mod)。比如,你可以访问 TurboWarp扩展库 来查看 TurboWarp 为 Scratchers 提供的扩展。扩展和其他编程语言的“库函数”相似,都是添加原来没有的函数内容。如果你认为这些内容还不够你用,那你就得自己写了。怎么写呢?接下来就听我娓娓道来!

准备工作

很显然,你需要一个好用的文本编辑器(比如 VSCode、Notepad++等,当然,你直接用记事本也可以)、一个可以加载自定义扩展的 Scratch Mod (比如 TurboWarpGandi IDE 扩展模式等)、以及一些 JavaScript 基础。

如果你自恃拥有强大的编程能力和足够的英语基础,你可以前往 TurboWarp 扩展文档 查看更官方更权威的扩展教程。

基本格式

首先,我将为你展示 Scratch 扩展文件的基本结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function (Scratch) {
"use strict";

Scratch.translate.setup({...});

class ExtName {
getInfo() {
return {
id: ...
name: ...
...
};
}

Function(arg) {...}
}

Scratch.extensions.register(new ExtName());
})(Scratch);

如上,这就是最基本的 Scratch 扩展格式。其中,Scratch.translate.setup()为你的扩展提供了 l10n (本地化)支持,可以提供这个函数来设置多语言。之后就是定义一个类了,这个类当中包含了你的扩展的信息(getInfo)和函数定义。最后,通过Scratch.extensions.register()函数来注册这个扩展。

接下来,让我们慢慢讲具体的内容~

Scratch.translate.setup() l10n 支持

在 Scratch 扩展中,Scratch.translate.setup()的语法如下:

1
2
3
4
5
6
7
8
9
10
Scratch.translate.setup({
"en": {
"_key": "translation",
...
},
"zh-cn": {
"_key": "翻译",
...
}
})

给这个函数提供一个 JSON 参数,在之后重新调用Scratch.translate()函数就可以将key字符串自动转换为对应的值(也就是翻译内容)。比如,我在上面设置了"_key"对应的值是"翻译",那么,当我的编辑器语言为简体中文(zh-cn)的时候,Scratch.translate("key")就会返回"翻译"

这是由 Scratch 本身提供的翻译功能,善用它会取得不错的成效。

getInfo() 给你的扩展编写信息

getInfo()函数中,你应该为其定义编写一堆内容,包括了扩展ID扩展名扩展积木颜色扩展积木等等。接下来我会给出一个简单的 getInfo() 示例,它可以创建一个 COMMAND 块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getInfo() {
return {
id: "SampleExt",
name: "示例扩展",
color1: "#66ccff",
blocks: [
{
opcode: "SampleBlock",
blockType: Scratch.BlockType.COMMAND,
text: "示例积木"
}
]
}
}

如上,在getInfo()的定义中,你会看到一些键值对,比如idnamecolor1等等。其中,id是扩展的唯一标识符,name是扩展在工具箱(也就是左侧那个版块)显示的名称,color1则是扩展的颜色,color1\2\3分别对应了块的背景色、边框色和激活或选中时的颜色。

在上面,我省略了一些键值对,我把它们放到下面了:

类型 功能
menuIconURI string(dataURL) 设置显示在侧栏左侧按钮上的图标
blockIconURI string(dataURL) 设置显示在块左侧的图标
docsURI string(URL) 设置扩展文档的链接,当有这个内容时,会自动创建一个文档按钮
disableMonitor boolen 是否禁用监控器,如果为true,将允许这个可以返回值的块在舞台上显示一个监视器,就像变量一样
hideFromPalette boolen 是否在工具箱中隐藏,这是很有用的,有利于向后兼容
filter Scratch.TargetType.SPRITE或Scratch.TargetType.STAGE 一个过滤器,选择在哪些内容中显示,比如填写[Scratch.TargetType.SPRITE]将只显示在角色的工具箱中。当然,如果填入[]的效果和上一条的效果一样,但是我们应该尽量使用上一个来隐藏某个块。

block后面对应的 Array 中,就是对扩展积木的定义。在这里,我定义了一个 COMMAND 块,上面显示的内容是“示例积木”,它的操作码(opcode)是”SampleBlock”。

操作码是最重要的一个键,因为它对应了后面对这个块相应的函数的定义。假如我要为这个块编写代码,我就应该在后面定义一个SampleBlock()函数,Scratch 在尝试将这些代码块释放到 Scratch 虚拟机 中的时候,会找到对应操作码的函数,绑定在块上,以便在编辑器中执行。

在定义一个块的时候,你可以使用一下内容:

类型 介绍
opcode string 该块运行时应该运行的函数的名称。例如,如果这是“Sample”,那么将运行类的“Sample”方法。每个扩展中的每个操作码必须是唯一的,因此多个扩展可以每个都有一个操作码为“Sample”的块
blockType Scratch.BlockType.* 用来决定块的形状,具体见下面一节
text string 将出现在块编辑器中的文本。当然,这里可能会出现一些特殊的语法,这将在后面讨论
arguments object 块中可输入的内容。是可选内容,同样在后面讨论

当然,还有些高级的内容我们可以在后面介绍。

BlockType 块的类型

玩多了 Scratch ,你应该知道,在 Scratch 中,有很多种不同的块,有像拼图一样的,有圆圆的,有六边形的,也有帽子形状的,还有像个嘴巴一样的 C 的形状的。这就是不同的积木类型,在定义它们的时候,需要根据需求设置它们的类型,接下来我将介绍三个最常见的块类型:

COMMAND 块

你应该注意到了,在上一个小节中,我说的是创建一个 COMMAND 块,那什么是 COMMAND 块呢?COMMAND 的意思是指令,命令,也就是说,这个块将执行一个函数,但是不返回内容。所以它在 Scratch 中,就是那种拼图形状的块了。

REPORTER 块

我们在使用一些获取值的块的时候,它们通常是圆圆的,比如变量块。这就是 REPORTER 块,它们可以返回函数在运行之后返回的值给 Scratch 编辑器。

BOOLEN 块

当然,我们也会遇到一些判断相关的块,它们返回一个布尔值,要么真要么假。这就是 BOOLEN 块,它和 REPORTER 块很像,都可以返回函数的返回值。虽然口头上说的是返回布尔值,但是它们实际上是可以返回其他类型的值的。

帽子块和C型块将在后面介绍。

编写函数

现在,我为你提供一个最简单的扩展示例,这是在 TurboWarp 扩展文档上抄来的,它会返回一个随机数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class random {
getInfo() {
return {
id: 'random',
name: '我去 随机数!',
blocks: [
{
opcode: 'random',
blockType: Scratch.BlockType.REPORTER,
text: '随机数'
}
]
};
}

random() {
return Math.random();
}
}

Scratch.extensions.register(new random());

在这里,我定义了一个操作码为 randomREPORTER 块,上面显示的文本为 随机数 ,之后,根据操作码 random 我在后面定义了一个 random() 函数,并返回一个随机数。这个随机数将会返回到你的 Scratch 编辑器中,你可以在 TurboWarp 加载这个扩展试试。

处理参数

恭喜你,已经学会了简单的扩展编写了,但是这完全不可能完成你的需求。你不可能只写一些只返回内容不处理信息的块(诶你可能真要写一个 Navigator 只返回信息呢)。所以传入参数和处理参数是一个很重要的内容。

首先,我们要学会如何编写一个可以传入参数的块。这其实十分简单,在块定义的text中,使用一个方括号可以创建一个参数框,之后可以在下面定义参数框传入的参数是什么类型,比如String,Boolen等。格式如下:

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
(function (Scratch) {
"use strict";

class ExtName {
getInfo() {
return {
id: "extname",
name: "测试扩展",
color1: "#66ccff",
blocks: [
{
opcode: "Function",
blockType: Scratch.BlockType.COMMAND,
text: "测试函数 [ARG]",
arguments: {
ARG: {
type: Scratch.ArgumentType.STRING,
defaultValue: "默认值"
}
}
}
]
};
}

......

}

Scratch.extensions.register(new ExtName());
})(Scratch);

其中,arguments之后的键值对可以定义参数的类型和默认值。比如我在上述示例中,就在块中插入了一个ARG参数,然后在arguments中定义了其为STRING类型的参数(即字符串参数)。当然,传入的参数不止有字符串,在下面列出所有可以传入的内容:

类型 介绍
Scratch.ArgumentType.STRING 任意文本 apple, 123, true
Scratch.ArgumentType.NUMBER 任意数字 123
Scratch.ArgumentType.BOOLEAN 一个布尔值,只能塞入布尔块 true
Scratch.ArgumentType.COLOR 一个十六进制的输入框,可以调用Scratch自带的颜色选择器 #ff4c4c
Scratch.ArgumentType.ANGLE 一个角度输入框,90表示向右,逆时针增加 90, 180
Scratch.ArgumentType.MATRIX 一个5x5的矩阵输入框,每个位置只能输入0或1 11101010101…
Scratch.ArgumentType.NOTE 一个音符输入框,可以调用Scratch自带的音符键盘 ?
Scratch.ArgumentType.IMAGE 显示的是一个内联图像,并非实际的输入项。相关内容稍后会介绍。 N/A
Scratch.ArgumentType.COSTUME 该精灵内的服装名称 costume1
Scratch.ArgumentType.SOUND 该精灵内的声音名称。 recording1

Scratch虚拟机需要知道传入的参数的名字是什么,所以在定义参数的时候,需要使用一个方括号来定义参数的名字,比如[ARG],这样我们就可以获得一个名字叫ARG的参数了,然后就可以在arguments中定义参数的类型和默认值了。

同时,我们也可以通过这个名字来在函数里获取传入的参数了,就像下面那样:

1
2
3
4
5
Function(args){
let x = args.ARG;
console.log(x);
}

静态的列表

如果你仔细观察,你会发现有些块会有个下拉框,从中可以选择一个项来获取参数。比如在运动模块中,你可以看到面向 [DIR],这个块中的DIR可以提供下拉框选择面向哪个角色或者鼠标指针。在生活中我们不乏遇到选择,所以处理一个列表也是重要的一环。

列表项目的定义和普通参数一样,不过我们需要再arguments中将defaultValue改为menu,这样Scratch就会在定义中读取一个数组作为静态列表。格式如下:

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

class Strings1 {
getInfo() {
return {
id: 'strings1example',
name: 'Encoding',
blocks: [
{
opcode: 'convert',
blockType: Scratch.BlockType.REPORTER,
text: 'convert [TEXT] to [FORMAT]',
arguments: {
TEXT: {
type: Scratch.ArgumentType.STRING,
defaultValue: 'Apple'
},
FORMAT: {
type: Scratch.ArgumentType.STRING,
menu: 'FORMAT_MENU'
}
}
}
],
menus: {
FORMAT_MENU: {
acceptReporters: true,
items: ['uppercase', 'lowercase']
}
}
};
}

convert (args) {
if (args.FORMAT === 'uppercase') {
// 注意到这段代码中的 toString() 调用:TEXT 可能是数字或布尔值,
// 所以我们必须先将其转换为字符串,以便它具有 toUpperCase() 函数,
// 否则就会出现错误!
// 要记住:参数的“类型”只是编辑器给出的建议;它从未被强制执行。
return args.TEXT.toString().toUpperCase();
} else {
return args.TEXT.toString().toLowerCase();
}
}
}
Scratch.extensions.register(new Strings1());

这是我在TurboWarp上抄来的实例,其中,处理了用户输入强制转换为字符串。这是一个很明智的选择,也是个很重要的选择,可以避免很多因为错误输入带来的问题。上述代码中,FORMAT_MENU就是一个列表。在getInfo中添加了一个menus属性,之后定义了FORMAT_MENU列表,其中items是一个数组,也就是列表中显示的项目。

如果你希望文本和输入的值不同,你可以像这样编写items:

1
2
3
4
5
6
7
FORMAT_MENU: {
acceptReporter: true,
items: [
{text:"大写" ,value:"uppercase"},
{text:"小写" ,value:"lowercase"}
]
}

这样,下拉框中将不显示uppercaselowercase,而是显示大写小写

我们可以看到,有个acceptReporter键值对,这是用来确认这个列表的位置是否可以塞入一个高级的东西,比如REPORTER块和BOOLEN块。如果填写true,那么就可以塞入一个变量之类的东西:这种情况,字符串处理就十分必要了。

在前面,我提到了使用Scratch.translate()进行多语言支持,在text后面你依然可以使用这个函数来确定显示的文本是什么。


简单地编写一个Scratch扩展
http://cyberneko.cn/2025/03/09/How-to-write-a-Scratch-extension/
作者
Cyberexplorer
发布于
2025年3月9日
许可协议