虚拟机 API
VM(虚拟机)是运行 Scratch 项目的核心引擎。对于扩展开发者,VM 提供对项目执行、角色管理、积木解释和运行时事件的访问。此 API 主要通过 Scratch.vm 供非沙盒化扩展使用。
概述
VM 管理:
- 项目执行和控制
- 角色和目标管理
- 积木执行和编译
- 运行时事件和监控
- 扩展集成
- 资源和存储管理
访问 VM
// 仅非沙盒化扩展
if (!Scratch.extensions.unsandboxed) {
throw new Error('VM API 需要非沙盒化扩展');
}
const vm = Scratch.vm;
const runtime = vm.runtime;
项目控制
基本执行
// 启动项目(绿旗)
vm.greenFlag();
// 停止所有脚本
vm.stopAll();
// 手动执行单步(用于调试)
vm.runtime._step();
// 检查项目是否正在运行
const isRunning = vm.runtime.threads.length > 0;
加速模式
// 启用/禁用加速模式
vm.setTurboMode(true); // 启用加速模式
vm.setTurboMode(false); // 禁用加速模式
// 监听加速模式变化
vm.on('TURBO_MODE_ON', () => {
console.log('加速模式已启用');
});
vm.on('TURBO_MODE_OFF', () => {
console.log('加速模式已禁用');
});
性能选项
// 设置帧率(30 或 60 FPS)
vm.setFramerate(60);
// 启用/禁用插值
vm.setInterpolation(true);
// 配置编译器选项
vm.setCompilerOptions({
enabled: true,
warpTimer: false
});
// 设置运行时选项
vm.setRuntimeOptions({
maxClones: 300,
miscLimits: true,
fencing: true
});
项目管理
加载项目
// 从 JSON 字符串加载项目
const projectData = '{"targets": [...], "meta": {...}}';
await vm.loadProject(projectData);
// 从文件缓冲区加载项目
const projectBuffer = new ArrayBuffer(/* project data */);
await vm.loadProject(projectBuffer);
// 清除当前项目
vm.clear();
项目信息
// 获取项目为 JSON
const projectJson = vm.toJSON();
// 获取项目元数据
const runtime = vm.runtime;
const projectName = runtime.getTargetForStage().sprite.name;
// 检查项目是否有更改
vm.on('PROJECT_CHANGED', () => {
console.log('项目有未保存的更改');
});
资源管理
// 获取所有项目资源
const assets = vm.assets;
// 向角色添加造型
const targetId = 'sprite1';
const costumeData = {
name: 'costume1',
dataFormat: 'png',
asset: assetBuffer,
md5ext: 'hash.png'
};
await vm.addCostume(costumeData, targetId);
// 向角色添加声音
const soundData = {
name: 'sound1',
dataFormat: 'wav',
asset: soundBuffer,
md5ext: 'hash.wav'
};
await vm.addSound(soundData, targetId);
// 向舞台添加背景
await vm.addBackdrop('backdrop.png', backdropObject);
角色管理
访问角色
const runtime = vm.runtime;
// 获取所有角色(角色 + 舞台)
const allTargets = runtime.targets;
// 获取舞台
const stage = runtime.getTargetForStage();
// 获取所有角色(不包括舞台)
const sprites = runtime.getSpriteTargets();
// 通过 ID 获取角色
const target = runtime.getTargetById('targetId');
// 通过名称获取角色
const namedTarget = runtime.getSpriteTargetByName('Sprite1');
// 获取当前正在编辑的角色
const editingTarget = vm.editingTarget;
角色属性
// 角色属性
const target = runtime.getSpriteTargetByName('Sprite1');
if (target) {
// 位置和外观
console.log('位置:', target.x, target.y);
console.log('方向:', target.direction);
console.log('大小:', target.size);
console.log('可见性:', target.visible);
// 标识
console.log('名称:', target.getName());
console.log('是原始角色:', target.isOriginal);
console.log('是舞台:', target.isStage);
// 当前造型/背景
console.log('当前造型:', target.currentCostume);
console.log('造型名称:', target.sprite.costumes[target.currentCostume].name);
}
创建和管理克隆
// 创建克隆
const originalSprite = runtime.getSpriteTargetByName('Sprite1');
if (originalSprite) {
const clone = originalSprite.makeClone();
if (clone) {
runtime.addTarget(clone);
// 设置克隆的位置
clone.setXY(100, 50);
clone.setDirection(90);
}
}
// 查找角色的所有克隆
const clones = runtime.targets.filter(target =>
!target.isStage &&
!target.isOriginal &&
target.sprite === originalSprite.sprite
);
// 删除克隆
const cloneToDelete = clones[0];
if (cloneToDelete && !cloneToDelete.isOriginal) {
runtime.disposeTarget(cloneToDelete);
}
变量和数据
全局变量
const stage = runtime.getTargetForStage();
// 访问变量
const variables = stage.variables;
for (const [id, variable] of Object.entries(variables)) {
console.log(`${variable.name}: ${variable.value} (${variable.type})`);
}
// 获取特定变量
const scoreVar = stage.lookupVariableByNameAndType('score', '');
if (scoreVar) {
console.log('分数:', scoreVar.value);
scoreVar.value = 100;
}
// 创建新变量
stage.createVariable('newVar', 'myVariable', '');
角色变量
const sprite = runtime.getSpriteTargetByName('Sprite1');
if (sprite) {
// 局部变量
const localVars = sprite.variables;
// 获取/设置局部变量
const localVar = sprite.lookupVariableByNameAndType('localScore', '');
if (localVar) {
localVar.value = 50;
}
}
列表
const stage = runtime.getTargetForStage();
// 访问列表
const lists = Object.values(stage.variables).filter(v => v.type === 'list');
// 获取特定列表
const itemsList = stage.lookupVariableByNameAndType('items', 'list');
if (itemsList) {
console.log('列表内容:', itemsList.value);
// 修改列表
itemsList.value.push('新项目');
itemsList.value[0] = '第一项';
}
积木执行
启动脚本
// 启动帽子积木
const startedThreads = runtime.startHats('event_whenflagclicked');
console.log(`启动了 ${startedThreads.length} 个线程`);
// 带条件启动帽子积木
const broadcastThreads = runtime.startHats('event_whenbroadcastreceived', {
BROADCAST_OPTION: 'message1'
});
// 启动特定的积木栈
const blockId = 'some-block-id';
const target = runtime.getSpriteTargetByName('Sprite1');
const thread = runtime._pushThread(blockId, target);
手动线程管理
高级 / 内部
为了更精细的控制,你可以手动推送线程并检查它们的状态。
// 手动启动线程
// _pushThread(blockId, target, opts)
const thread = runtime._pushThread(startBlockId, target, {
stackClick: true // 作为栈点击处理(如果正在运行则重新启动)
});
// 线程状态常量
// 0: RUNNING
// 1: PROMISE_WAIT
// 2: YIELD
// 3: YIELD_TICK
// 4: DONE
if (thread.status === 4) {
console.log('线程已完成');
}
线程管理
// 获取所有运行中的线程
const threads = runtime.threads;
console.log(`${threads.length} 个线程正在运行`);
// 查找特定角色的线程
const spriteThreads = threads.filter(thread =>
thread.target.getName() === 'Sprite1'
);
// 停止特定角色的线程
const target = runtime.getSpriteTargetByName('Sprite1');
runtime.stopForTarget(target);
// 线程属性
threads.forEach(thread => {
console.log('线程角色:', thread.target.getName());
console.log('顶部积木:', thread.topBlock);
console.log('状态:', thread.status);
console.log('是否已编译:', thread.isCompiled);
});
运行时事件
项目生命周期
// 项目执行
vm.on('PROJECT_RUN_START', () => {
console.log('项目开始运行');
});
vm.on('PROJECT_RUN_STOP', () => {
console.log('项目已停止');
});
vm.on('PROJECT_CHANGED', () => {
console.log('项目已被修改');
});
// 项目加载
vm.on('PROJECT_LOADED', () => {
console.log('项目加载完成');
});
角色事件
runtime.on('targetWasCreated', (target, originalTarget) => {
console.log('新角色已创建:', target.getName());
if (originalTarget) {
console.log('克隆自:', originalTarget.getName());
}
});
runtime.on('TARGETS_UPDATE', (emitProjectChanged) => {
console.log('角色列表已更新');
if (emitProjectChanged) {
console.log('项目应标记为已更改');
}
});
积木事件
// 积木发光(视觉反馈)
runtime.on('SCRIPT_GLOW_ON', (glowData) => {
console.log('脚本发光中:', glowData.id);
});
runtime.on('SCRIPT_GLOW_OFF', (glowData) => {
console.log('脚本停止发光:', glowData.id);
});
runtime.on('BLOCK_GLOW_ON', (glowData) => {
console.log('积木发光中:', glowData.id);
});
runtime.on('BLOCK_GLOW_OFF', (glowData) => {
console.log('积木停止发光:', glowData.id);
});
监视器事件
runtime.on('MONITORS_UPDATE', (monitors) => {
console.log('变量监视器已更新:', monitors.length);
monitors.forEach(monitor => {
console.log(`${monitor.opcode}: ${monitor.value}`);
});
});
扩展集成
扩展管理
const extensionManager = vm.extensionManager;
// 检查扩展是否已加载
const isLoaded = extensionManager.isExtensionLoaded('pen');
// 获取已加载的扩展
const loadedExtensions = extensionManager.getLoadedExtensions();
// 加载扩展(非沙盒化)
if (extensionManager.loadExtensionURL) {
await extensionManager.loadExtensionURL('https://example.com/extension.js');
}
扩展存储
// 访问扩展特定存储
const extensionId = 'myextension';
const storage = runtime.extensionStorage[extensionId] || {};
// 存储数据
storage.settings = { volume: 0.5, enabled: true };
runtime.extensionStorage[extensionId] = storage;
// 扩展存储会随项目持久化
I/O 和设备
输入设备
// 鼠标
const mouse = runtime.ioDevices.mouse;
console.log('鼠标位置:', mouse.getClientX(), mouse.getClientY());
console.log('鼠标按下:', mouse.getIsDown());
// 键盘
const keyboard = runtime.ioDevices.keyboard;
console.log('空格键按下:', keyboard.getKeyIsDown(32));
// 通过名称检查按键
const spacePressed = keyboard.getKeyIsDown('space');
时钟和计时
const clock = runtime.ioDevices.clock;
// 获取当前时间
const currentTime = clock.projectTimer();
// 重置计时器
clock.resetProjectTimer();
// 获取系统时间
const systemTime = clock.systemTime();
编译和性能
编译器状态
// 检查编译器是否已启用
const compilerEnabled = runtime.compilerOptions.enabled;
// 获取编译错误
vm.on('COMPILE_ERROR', (target, error) => {
console.log('编译错误于', target.getName(), ':', error);
});
// 预编 译项目
if (runtime.precompile) {
runtime.precompile();
}
### 编译器访问
**高级 / 内部**
非沙盒化扩展可以通过 `vm.exports` 访问编译器基础设施。这主要用于[编译器补丁](../advanced-techniques/compiler-patching.md)。
```javascript
// 检查编译器导出
if (vm.exports) {
// 访问生成器
const JSGenerator = vm.exports.JSGenerator;
const IRGenerator = vm.exports.IRGenerator;
// 访问辅助类
const { TypedInput, TYPE_UNKNOWN } = JSGenerator.exports;
}
[!WARNING] 编译器 API 是内部的,可能会发生变化。使用前请务必检查是否存在。
运行时钩子
高级
为了深度集成,你可以钩入运行时进程。有关详细信息,请参阅 GUI API:
runtime._convertBlockForScratchBlocks(自定义积木序列化)ScratchBlocks集成
### 性 能监控
```javascript
// 启用性能分析
if (runtime.enableProfiling) {
runtime.enableProfiling();
}
// 监控帧率
vm.on('FRAMERATE_CHANGED', (framerate) => {
console.log('帧率已更改:', framerate);
});
// 监控运行时间
console.log('当前运行时间:', runtime.currentStepTime);
高级功能
舞台尺寸
// 设置自定义舞台尺寸
vm.setStageSize(640, 360);
// 监听舞台尺寸变化
vm.on('STAGE_SIZE_CHANGED', (width, height) => {
console.log('舞台尺寸已更改:', width, 'x', height);
});
// 获取当前舞台尺寸
console.log('舞台尺寸:', runtime.stageWidth, 'x', runtime.stageHeight);
云变量
// 检查云变量
vm.on('HAS_CLOUD_DATA_UPDATE', (hasCloudData) => {
if (hasCloudData) {
console.log('项目有云变量');
}
});
// 访问云变量限制
console.log('云变量限制:', runtime.cloudOptions.limit);
调试模式
// 启用调试模式
vm.enableDebug();
// 禁用调试模式
vm.disableDebug();
// 检查调试状态
const debugEnabled = runtime.debug;
实用示例
自定义报告器扩展
class SystemInfoExtension {
getInfo() {
return {
id: 'systeminfo',
name: 'System Info',
blocks: [
{
opcode: 'getProjectInfo',
blockType: Scratch.BlockType.REPORTER,
text: 'project [INFO]',
arguments: {
INFO: {
type: Scratch.ArgumentType.STRING,
menu: 'INFO_MENU'
}
}
}
],
menus: {
INFO_MENU: {
items: ['name', 'sprite count', 'thread count', 'framerate']
}
}
};
}
getProjectInfo(args) {
const vm = Scratch.vm;
const runtime = vm.runtime;
switch (args.INFO) {
case 'name':
return runtime.getTargetForStage().sprite.name;
case 'sprite count':
return runtime.targets.length - 1; // 排除舞台
case 'thread count':
return runtime.threads.length;
case 'framerate':
return runtime.frameRate || 30;
default:
return '';
}
}
}
事件监听器扩展
class EventMonitorExtension {
constructor() {
this.eventLog = [];
this.setupListeners();
}
setupListeners() {
const vm = Scratch.vm;
const runtime = vm.runtime;
runtime.on('targetWasCreated', (target) => {
this.eventLog.push(`克隆已创建: ${target.getName()}`);
});
vm.on('PROJECT_RUN_START', () => {
this.eventLog.push('项目已启动');
});
vm.on('PROJECT_RUN_STOP', () => {
this.eventLog.push('项目已停止');
});
}
getInfo() {
return {
id: 'eventmonitor',
name: 'Event Monitor',
blocks: [
{
opcode: 'getLastEvent',
blockType: Scratch.BlockType.REPORTER,
text: 'last event'
},
{
opcode: 'clearEvents',
blockType: Scratch.BlockType.COMMAND,
text: 'clear event log'
}
]
};
}
getLastEvent() {
return this.eventLog[this.eventLog.length - 1] || '';
}
clearEvents() {
this.eventLog = [];
}
}
错误处理
// 安全的 VM 访问
function safeVMOperation(operation) {
try {
if (!Scratch.extensions.unsandboxed) {
console.warn('VM API 需要非沙盒化扩展');
return null;
}
const vm = Scratch.vm;
if (!vm) {
console.warn('VM 不可用');
return null;
}
return operation(vm);
} catch (error) {
console.error('VM 操作失败:', error);
return null;
}
}
// 用法
const spriteCount = safeVMOperation(vm => vm.runtime.targets.length - 1);
最佳实践
- 访问 VM API 前检查非沙盒化状态
- 完美处理缺失的角色 - 角色可能会被删除
- 尽可能监听事件而不是轮询
- 为异步操作安全存储引用
- 尊重项目状态 - 执行期间不要修改
- 在线程安全的操作中操作正在运行的项目
- 插件禁用时清理事件监听器
VM API 提供了对 Scratch 执行引擎的深度访问,使扩展能够与正在运行的项目 创建复杂的交互。在使用这些 API 时要负责任,以在保持稳定性和性能的同时增强 Scratch 体验。