Extension API 🌐 在线版

扩展开发 API 文档 为 Objector 积木编程平台开发自定义扩展

快速开始

扩展是 Objector 的核心扩展机制,允许你添加自定义积木和执行逻辑。只需 3 步:

  1. 创建 .js.json 文件
  2. 定义积木(外观 + 参数)和执行器(逻辑)
  3. 放入项目的 extensions/ 目录
// extensions/my-ext.js — 最简扩展示例
ExtensionManager.registerExtension({
  id: 'my_ext',
  name: '我的扩展',
  color: '#FF6B6B',
  blocks: [
    {
      type: 'ext_hello',
      label: '打招呼 {name}',
      shape: 'stack',
      params: [{ name: 'name', type: 'string', default: '世界' }],
    },
  ],
  executors: {
    ext_hello: function(params) {
      alert('你好, ' + params.name + '!');
    },
  },
});

文件结构

你的项目/
├── project.json          // 项目文件
├── extensions/           // 扩展目录(自动加载)
│   ├── my-ext.js         // JS 格式扩展
│   ├── math-utils.json   // JSON 格式扩展
│   └── ...
└── ...
扩展在打开项目时自动从 extensions/ 目录加载,支持 .js.json 两种格式。

JSON / JS 格式对比

JSON 格式 (.json)JS 格式 (.js)
执行器写法函数字符串直接写函数
可读性一般
灵活性受限(纯字符串)完全灵活
推荐场景简单积木、数据驱动复杂逻辑、异步操作

扩展对象结构

字段类型必填说明
idstring扩展唯一标识(同时作为分类 ID)
namestring显示名称(积木面板中的分类名)
colorstring积木颜色(十六进制),默认 #888888
blocksArray积木定义数组
executorsObject执行器映射:{ 积木type: 函数 }

积木定义

每个积木对象包含以下字段:

字段类型必填说明
typestring积木类型标识(全局唯一)
labelstring显示文本,{xxx} 为参数占位符
shapestring积木形状(见下方)
paramsArray参数定义数组
portsObject连接端口(默认自动推断)
subBlocksArrayC 型积木的子代码槽名
getLabelfunction动态标签函数
dynamicParamsboolean启用可扩展输入(右键菜单添加参数)
dynamicParamsPrefixstring动态参数名前缀(默认 item
dynamicParamsDefaultany动态参数默认值(默认 ''

积木形状 shape

shape 决定积木的外观和连接方式:

移动 10 步 stack — 普通堆叠积木,可上下连接

x 坐标 reporter — 圆形返回值积木,可嵌入参数槽

碰到边缘? boolean — 菱形条件积木,用于 if/while

重复 10 次 c-block — C 型积木,包含子代码

当绿旗被点击 hat — 帽子积木,事件入口
shapeflowInflowOut用途
stack普通命令
reporter返回值,嵌入参数槽
boolean条件判断
c-block循环/条件容器
hat事件触发入口

连接端口 ports

默认根据 shape 自动推断,也可手动指定:

{
  ports: {
    flowIn: true,   // 是否有上方流入端口
    flowOut: true,  // 是否有下方流出端口
  }
}

参数定义 params

每个参数对象:

字段类型说明
namestring参数标识(对应 label 中的 {name}
typestring参数类型(见下表)
defaultany默认值
optionsArraydropdown 类型的选项列表
getOptionsfunction动态获取下拉选项的函数

参数类型

type说明示例
string文本输入{ default: 'hello' }
number数字输入{ default: 10 }
dropdown下拉选择{ default: 'A', options: ['A','B','C'] }
block代码块引用(可用 BlockRunner 运行){ default: null }
// 参数定义示例
params: [
  { name: 'text', type: 'string', default: '你好' },
  { name: 'count', type: 'number', default: 5 },
  { name: 'mode', type: 'dropdown', default: '升序',
    options: ['升序', '降序'] },
]

子代码槽 subBlocks

用于 C 型积木(c-block),定义内部可嵌套的代码区域:

{
  type: 'ext_repeat',
  label: '重复 {n} 次',
  shape: 'c-block',
  subBlocks: ['body'],   // 定义名为 'body' 的子代码槽
  params: [{ name: 'n', type: 'number', default: 10 }],
}
子代码槽名通常为 'body',一个 C 型积木可以有多个槽(如 if-else 的 ifelse)。

动态标签 getLabel

当静态 label 不够用时(如显示动态参数),可用函数生成标签:

{
  label: '调用 {name}',  // 基础标签(无动态参数时使用)
  getLabel: function(block) {
    let label = '调用 ' + (block.params.name || '函数');
    const extras = block._extraParams || [];
    if (extras.length > 0) {
      label += '(';
      extras.forEach((ep, i) => {
        label += '{' + ep.name + '}';
        if (i < extras.length - 1) label += ', ';
      });
      label += ')';
    }
    return label;
  },
}

可扩展输入 dynamicParams

在积木定义中设置 dynamicParams: true,用户即可通过右键菜单动态添加参数:

{
  type: 'ext_call',
  label: '调用函数',
  shape: 'stack',
  dynamicParams: true,            // 启用可扩展输入
  dynamicParamsPrefix: 'arg',     // 参数名前缀(默认 'item')
  dynamicParamsDefault: '0',     // 参数默认值(默认 '')
  getLabel: function(block) { /* 见上方 */ },
}
添加的参数名自动为 arg1arg2...(取决于 prefix),在执行器中通过 params.arg1 访问。

执行器函数

执行器定义积木被运行时的逻辑:

executors: {
  ext_my_block: async function(params, scope) {
    // params: 解析后的参数对象
    // scope:  当前执行作用域
    // 函数可以是同步或异步(async)
  },
}

params 对象

包含积木定义的所有参数值(已自动解析 reporter 连接):

// 假设积木定义了 params: [{name:'count', type:'number', default:10}]
params.count  // → 10 或用户输入的值,或连接的 reporter 返回值

// 动态参数也包含在内
params.arg1   // → 第一个动态参数
params.arg2   // → 第二个动态参数

// 特殊属性:原始积木对象(用于 BlockRunner)
params._block // → 当前积木的原始对象(含 subBlocks、paramConnections 等)

scope 对象

当前执行上下文:

属性说明
scope.self当前对象实例(在类方法/初始化中)
scope[变量名]局部变量
scope.__className类名(当 scope 是对象实例时)

返回值

返回值说明
任意值reporter 积木的返回值
undefined / 不返回stack 积木正常结束
'__RETURN__'中断当前函数/方法执行
'__STOP__'停止整个程序运行

执行器中可用的全局 API

API说明
console.log(msg)调试输出到控制台
EditorState.blocks当前精灵的积木数据对象
StageManager舞台管理(精灵控制、运动、克隆等)→ 详情
SoundManager声音管理(播放/停止/音量)→ 详情
SensingInput输入检测(鼠标位置、键盘状态)→ 详情
ExtensionManager扩展管理器(注册/查询扩展)
BlockRegistry积木注册表(查询积木定义)
BlockRunner积木运行器(运行 C 型子代码 / block 参数)→ 详情
Math / Date标准 JavaScript 全局对象
运行程序时,执行引擎自动收集所有精灵的积木,所有精灵的事件和脚本会同时生效。

registerExtension(extDef)

注册一个扩展。如果同一 ID 已存在则先卸载旧版。

ExtensionManager.registerExtension({
  id: 'my_ext',
  name: '我的扩展',
  color: '#FF6B6B',
  blocks: [...],
  executors: {...},
});
// 返回: true(成功)/ false(失败)

加载方法

方法参数说明
loadFromJSON(jsonStr)JSON 字符串从 JSON 解析并注册
loadFromJS(jsContent)JS 代码字符串执行 JS 脚本(内部调用 registerExtension)
loadFromFile(filePath)文件路径自动检测 .js / .json 并加载
loadFromProject(projectPath)项目目录路径加载 extensions/ 下所有扩展文件

查询与管理

方法返回值说明
getExtensions()Array获取所有已加载的扩展定义
getExecutor(blockType)function / null获取指定积木类型的执行器
unregisterExtension(extId)boolean卸载指定扩展

StageManager API

控制舞台上的精灵。所有方法通过 StageManager.方法名() 调用。

查询精灵

方法返回值说明
getSprites()Array<Sprite>获取所有精灵对象
getSprite(idx)Sprite获取指定索引的精灵
getActiveSprite()Sprite获取当前激活的精灵
getActiveSpriteIdx()number获取当前激活精灵的索引
getSpriteByName(name)Sprite / null通过名称查找精灵
getSpriteIndexByName(name)number通过名称获取索引(-1=未找到)
getSpriteCount()number获取精灵总数
STAGE_W / STAGE_Hnumber舞台宽度/高度(480/360)

控制精灵

方法说明
setActiveSprite(idx)设置激活精灵(同步切换代码区)
addSprite(name)添加新精灵,返回 Sprite 对象
removeSprite(idx)删除精灵(至少保留一个)
cloneSprite(idx)克隆精灵(复制所有属性),返回新索引

运动控制

方法说明
moveSprite(idx, steps)沿方向前进指定步数
rotateSprite(idx, deg)旋转指定度数
setSpritePos(idx, x, y)设置坐标
setSpriteDir(idx, deg)设置朝向
setSpriteVisible(idx, v)设置可见性
setSpriteSize(idx, size)设置大小(百分比)
setSpriteSay(idx, text)显示说话气泡
bounceSprite(idx)碰到边缘反弹

速度系统

方法说明
setVelocity(idx, vx, vy)设置速度向量
updateVelocity(idx)根据速度更新位置
applyFriction(idx, factor)应用摩擦力(0~1)
applyGravity(idx, g)应用重力
bounceEdgeVelocity(idx, e)弹性碰撞边缘

追踪与旋转

方法说明
moveTowards(idx, tx, ty, steps)向目标移动
moveAwayFrom(idx, tx, ty, steps)远离目标移动
pointTowards(idx, tx, ty)朝向目标
orbitAround(idx, tx, ty, deg, r)绕目标旋转

碰撞检测

方法返回值说明
isTouchingEdge(idx)boolean是否碰到边缘
isTouchingSprite(idx1, idx2)boolean两个精灵是否碰撞
getDistanceToPoint(idx, x, y)number到目标点的距离
getDirectionToPoint(idx, x, y)number到目标点的方向

造型控制

方法说明
setSpriteCostume(idx, path)设置精灵造型
setSpriteCostumeByName(idx, name)通过造型库名称设置
clearSpriteCostume(idx)清除造型
精灵对象属性可以直接读取:sprite.xsprite.ysprite.directionsprite.sizesprite.visiblesprite.namesprite.color

SoundManager API

控制声音播放。所有方法通过 SoundManager.方法名() 调用。

方法返回值说明
play(name)boolean播放声音(不等待)
playAndWait(name)Promise<boolean>播放声音并等待完成
stop(name)boolean停止指定声音
stopAll()-停止所有声音
setVolume(vol)-设置音量(0~100)
changeVolume(delta)-改变音量(+/-)
getVolume()number获取当前音量
getSoundNames()Array获取已加载的声音列表
isPlaying(name)boolean检查声音是否正在播放
声音文件放在项目的 sounds/ 目录下(支持 mp3、wav、ogg),加载后通过文件名(无扩展名)引用。
// 在扩展执行器中使用声音
ext_play_bgm: async function(params) {
  const name = params.soundName;
  if (SoundManager.isPlaying(name)) {
    SoundManager.stop(name);
  } else {
    await SoundManager.playAndWait(name);
  }
},

SensingInput API

检测键盘和鼠标输入状态。所有方法通过 SensingInput.方法名() 调用。

方法/属性返回值说明
mouseXnumber鼠标在舞台上的 X 坐标
mouseYnumber鼠标在舞台上的 Y 坐标
mouseDownboolean鼠标是否按下
keyPressed(key)boolean检测按键是否按下
// 跟随鼠标的精灵
ext_follow_mouse: function(params) {
  const idx = StageManager.getActiveSpriteIdx();
  StageManager.setSpritePos(idx, SensingInput.mouseX, SensingInput.mouseY);
},

JSON 格式示例:数学工具

{
  "id": "ext_math",
  "name": "数学工具",
  "color": "#4ECDC4",
  "blocks": [
    {
      "type": "ext_sqrt",
      "label": "√ {num}",
      "shape": "reporter",
      "ports": { "flowIn": false, "flowOut": false },
      "params": [{ "name": "num", "type": "number", "default": 0 }]
    },
    {
      "type": "ext_random",
      "label": "随机数 {min} 到 {max}",
      "shape": "reporter",
      "ports": { "flowIn": false, "flowOut": false },
      "params": [
        { "name": "min", "type": "number", "default": 1 },
        { "name": "max", "type": "number", "default": 100 }
      ]
    },
    {
      "type": "ext_clamp",
      "label": "限制 {val} 在 {min} ~ {max} 之间",
      "shape": "reporter",
      "ports": { "flowIn": false, "flowOut": false },
      "params": [
        { "name": "val", "type": "number", "default": 0 },
        { "name": "min", "type": "number", "default": 0 },
        { "name": "max", "type": "number", "default": 100 }
      ]
    }
  ],
  "executors": {
    "ext_sqrt": "function(params) { return Math.sqrt(Number(params.num)); }",
    "ext_random": "function(params) { var min=Number(params.min),max=Number(params.max); return Math.floor(Math.random()*(max-min+1))+min; }",
    "ext_clamp": "function(params) { var v=Number(params.val),lo=Number(params.min),hi=Number(params.max); return Math.max(lo,Math.min(hi,v)); }"
  }
}

JS 格式示例:网络工具

/**
 * extensions/network-tools.js
 * 网络工具扩展 - 异步操作示例
 */

ExtensionManager.registerExtension({
  id: 'ext_network',
  name: '网络工具',
  color: '#45B7D1',

  blocks: [
    {
      type: 'ext_fetch',
      label: '获取 {url} 的内容',
      shape: 'reporter',
      ports: { flowIn: false, flowOut: false },
      params: [{ name: 'url', type: 'string', default: 'https://api.example.com' }],
    },
    {
      type: 'ext_log',
      label: '输出日志 {msg}',
      shape: 'stack',
      ports: { flowIn: true, flowOut: true },
      params: [{ name: 'msg', type: 'string', default: '' }],
    },
    {
      type: 'ext_delay',
      label: '等待 {ms} 毫秒',
      shape: 'stack',
      ports: { flowIn: true, flowOut: true },
      params: [{ name: 'ms', type: 'number', default: 1000 }],
    },
  ],

  executors: {
    // 异步:网络请求
    ext_fetch: async function(params) {
      try {
        const resp = await fetch(params.url);
        return await resp.text();
      } catch (e) {
        return '错误: ' + e.message;
      }
    },
    // 同步:输出日志
    ext_log: function(params) {
      console.log('[日志]', params.msg);
    },
    // 异步:延时
    ext_delay: async function(params) {
      await new Promise(r => setTimeout(r, Number(params.ms)));
    },
  },
});

可扩展参数示例:自定义函数调用

ExtensionManager.registerExtension({
  id: 'ext_func',
  name: '自定义函数',
  color: '#E056A0',

  blocks: [
    {
      type: 'ext_func_call',
      label: '调用 {name}',
      shape: 'stack',
      ports: { flowIn: true, flowOut: true },
      params: [{ name: 'name', type: 'string', default: '函数' }],
      dynamicParams: true,             // ✅ 启用可扩展输入
      dynamicParamsPrefix: 'arg',       // 参数名: arg1, arg2, ...
      dynamicParamsDefault: '',          // 默认空字符串
      getLabel: function(block) {
        let label = '调用 ' + (block.params.name || '函数');
        const extras = block._extraParams || [];
        if (extras.length > 0) {
          label += '(' + extras.map(ep => '{' + ep.name + '}').join(', ') + ')';
        }
        return label;
      },
    },
  ],

  executors: {
    ext_func_call: function(params) {
      const name = params.name;
      // 收集所有动态参数
      const args = [];
      let i = 1;
      while (params['arg' + i] !== undefined) {
        args.push(params['arg' + i]);
        i++;
      }
      console.log('调用', name, '参数:', args);
    },
  },
});

BlockRunner API

让扩展中的 C 型积木和含 block 类型参数的积木能够运行内部子代码。两者用法一致,通过 BlockRunner 全局对象调用。

为什么需要 BlockRunner?

普通 stack / reporter 积木只需处理参数值即可。但 C 型积木(循环、条件等)和 block 参数(传入的代码块引用)需要执行子积木链,这要求访问执行引擎内部函数。BlockRunner 将这些能力暴露给扩展。

方法列表

方法参数说明
runSubBlock(block, subName, scope)block: 原始积木对象
subName: 子槽名(通常 'body'
scope: 作用域
运行 C 型积木内部的子代码链
runBlockParam(blockRef, scope)blockRef: block 参数值
scope: 作用域
运行 block 类型参数引用的积木链
run(blockOrId, scope)blockOrId: 积木对象或 ID
scope: 作用域
运行任意积木链
扩展执行器的 params._block 包含当前积木的原始对象,用于传给 runSubBlock

示例:自定义循环 C 型积木

ExtensionManager.registerExtension({
  id: 'ext_loop',
  name: '自定义循环',
  color: '#FFBF00',
  blocks: [
    {
      type: 'ext_repeat_n',
      label: '重复 {n} 次',
      shape: 'c-block',
      ports: { flowIn: true, flowOut: true },
      subBlocks: ['body'],
      params: [{ name: 'n', type: 'number', default: 10 }],
    },
  ],
  executors: {
    ext_repeat_n: async function(params, scope) {
      const n = Number(params.n);
      const block = params._block;  // ← 获取原始积木对象
      for (let i = 0; i < n; i++) {
        const r = await BlockRunner.runSubBlock(block, 'body', { ...scope, __i: i });
        if (r === '__RETURN__') return r;
      }
    },
  },
});

示例:block 参数运行传入的代码块

{
  type: 'ext_run_twice',
  label: '运行两次 {code}',
  shape: 'stack',
  params: [{ name: 'code', type: 'block', default: null }],
}

// 执行器
ext_run_twice: async function(params, scope) {
  const blockRef = params.code;  // {__blockRef: 'xxx'}
  for (let i = 0; i < 2; i++) {
    await BlockRunner.runBlockParam(blockRef, scope);
  }
},
block 类型参数的值(params.code)与 C 型槽内的子积木在运行时无本质区别,均可通过 BlockRunner 执行。

C 型积木示例:条件循环

// C 型积木通过 BlockRunner.runSubBlock 执行子代码

ExtensionManager.registerExtension({
  id: 'ext_control',
  name: '高级控制',
  color: '#FFBF00',
  blocks: [
    {
      type: 'ext_repeat',
      label: '重复 {n} 次',
      shape: 'c-block',
      ports: { flowIn: true, flowOut: true },
      subBlocks: ['body'],
      params: [{ name: 'n', type: 'number', default: 10 }],
    },
    {
      type: 'ext_if',
      label: '如果 {cond}',
      shape: 'c-block',
      ports: { flowIn: true, flowOut: true },
      subBlocks: ['body'],
      params: [{ name: 'cond', type: 'boolean', default: false }],
    },
  ],
  executors: {
    ext_repeat: async function(params, scope) {
      const n = Number(params.n);
      for (let i = 0; i < n; i++) {
        const r = await BlockRunner.runSubBlock(params._block, 'body', scope);
        if (r === '__RETURN__') return r;
      }
    },
    ext_if: async function(params, scope) {
      if (params.cond === true || params.cond === 'true') {
        return await BlockRunner.runSubBlock(params._block, 'body', scope);
      }
    },
  },
});
更多 C 型积木和 block 参数用法,参见 BlockRunner API

精灵控制示例

在扩展中控制精灵运动、碰撞检测和克隆:

ExtensionManager.registerExtension({
  id: 'ext_sprite_ai',
  name: '精灵 AI',
  color: '#FF9F43',
  blocks: [
    {
      type: 'ext_chase_target',
      label: '精灵 {name} 追踪 {target}',
      shape: 'stack',
      params: [
        { name: 'name', type: 'string', default: '精灵1' },
        { name: 'target', type: 'string', default: '精灵2' },
      ],
    },
    {
      type: 'ext_spawn_clone',
      label: '克隆精灵 {name} 并偏移 {dx} {dy}',
      shape: 'stack',
      params: [
        { name: 'name', type: 'string', default: '精灵1' },
        { name: 'dx', type: 'number', default: 50 },
        { name: 'dy', type: 'number', default: 0 },
      ],
    },
    {
      type: 'ext_is_near',
      label: '{a} 接近 {b} 距离 {dist}',
      shape: 'boolean',
      ports: { flowIn: false, flowOut: false },
      params: [
        { name: 'a', type: 'string', default: '精灵1' },
        { name: 'b', type: 'string', default: '精灵2' },
        { name: 'dist', type: 'number', default: 50 },
      ],
    },
  ],
  executors: {
    ext_chase_target: function(params) {
      const idx = StageManager.getSpriteIndexByName(params.name);
      const tidx = StageManager.getSpriteIndexByName(params.target);
      if (idx < 0 || tidx < 0) return;
      const t = StageManager.getSprite(tidx);
      StageManager.moveTowards(idx, t.x, t.y, 3);
      StageManager.pointTowards(idx, t.x, t.y);
    },
    ext_spawn_clone: function(params) {
      const idx = StageManager.getSpriteIndexByName(params.name);
      if (idx < 0) return;
      const newIdx = StageManager.cloneSprite(idx);
      if (newIdx !== null) {
        const s = StageManager.getSprite(newIdx);
        s.x += Number(params.dx);
        s.y += Number(params.dy);
      }
    },
    ext_is_near: function(params) {
      const a = StageManager.getSpriteByName(params.a);
      const b = StageManager.getSpriteByName(params.b);
      if (!a || !b) return false;
      const dx = a.x - b.x, dy = a.y - b.y;
      return Math.sqrt(dx*dx + dy*dy) <= Number(params.dist);
    },
  },
});

多精灵脚本系统

Objector 支持为每个精灵编写独立的脚本。切换精灵时,代码区自动显示该精灵的积木:

工作原理

  • 每个精灵拥有独立的积木脚本存储
  • 点击精灵切换时,代码区自动更新为该精灵的代码
  • 运行程序时,所有精灵的脚本同时生效(事件触发、定时器等)
  • 保存项目时,每个精灵的脚本随精灵数据一起存储

扩展中使用

// 获取特定精灵的积木数据
const sprite = StageManager.getSpriteByName('敌人');
if (sprite) {
  const blockCount = Object.keys(sprite.blocks || {}).length;
  console.log('敌人有', blockCount, '个积木');
}

// 获取所有精灵的合并积木
const allBlocks = StageManager.getAllBlocks();
const startBlocks = Object.values(allBlocks).filter(
  b => b.type === 'event_start'
);
旧项目(单个脚本文件)会自动将所有积木分配给第一个精灵,保持完全兼容。

内置扩展清单

以下扩展已预装在 Objector 中:

扩展ID颜色积木数说明
时间ext_time8时间戳、日期格式化、计时器
绘图ext_drawing9画线/圆/矩形、写文字、画笔控制
字符串ext_string11长度/截取/替换/大小写/分割/包含
颜色ext_color3RGB/十六进制/随机颜色

最佳实践

1. type 命名规范

建议使用 ext_扩展名_功能名 格式,避免与其他扩展冲突:

// ✅ 好
type: 'ext_math_sqrt'
type: 'ext_game_spawn_enemy'

// ❌ 避免
type: 'sqrt'          // 太通用,容易冲突
type: 'my_block_1'   // 无意义

2. 参数类型转换

参数值始终是字符串,使用时需要手动转换:

const num = Number(params.count);
const str = String(params.text);
const bool = params.flag === 'true';

3. 错误处理

ext_divide: function(params) {
  const b = Number(params.b);
  if (b === 0) return '错误: 除数不能为0';
  return Number(params.a) / b;
},

4. 异步操作

// 使用 async 函数处理异步操作
ext_wait: async function(params) {
  await new Promise(r => setTimeout(r, Number(params.ms)));
},

5. 热更新

修改扩展文件后,重新打开项目即可自动加载最新版本。同一 ID 的扩展会先卸载再重新注册。