第09章-脚本运行时开发
第九章:脚本运行时开发
9.1 脚本系统概述
Astral3D内置了脚本运行时环境,支持JavaScript和TypeScript脚本的编写和执行,允许用户在不修改源代码的情况下扩展编辑器功能、自定义场景逻辑和实现自动化操作。
9.1.1 脚本系统特点
| 特点 | 说明 |
|---|---|
| 热更新 | 脚本修改后立即生效,无需重启编辑器 |
| 沙箱隔离 | 脚本在安全的沙箱环境中执行,防止恶意代码 |
| TypeScript支持 | 支持TypeScript编写,提供类型检查和智能提示 |
| API完整 | 提供完整的场景、对象、渲染等API |
| 调试友好 | 支持断点调试和控制台日志 |
9.1.2 脚本架构
┌─────────────────────────────────────────────────────────────────────┐
│ 脚本运行时架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Script Editor (脚本编辑器) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │
│ │ │ 代码编辑器 │ │ 文件管理器 │ │ 输出控制台 │ │ │
│ │ │ Monaco │ │ Tree View │ │ Console │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Script Compiler (编译器) │ │
│ │ TypeScript → JavaScript → AST → Execution │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Sandbox (沙箱) │ │
│ │ ┌─────────────────────────────────────────────────────────┐│ │
│ │ │ Isolated Context ││ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││ │
│ │ │ │ Script │ │ Script │ │ Script │ │ Script │ ││ │
│ │ │ │ A │ │ B │ │ C │ │ D │ ││ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ││ │
│ │ └─────────────────────────────────────────────────────────┘│ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Script API (脚本API) │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │Scene│ │Object│ │Math │ │Anim │ │Event│ │ UI │ │Time │ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
9.2 脚本编辑器
9.2.1 打开脚本编辑器
通过以下方式打开脚本编辑器:
- 菜单栏:
工具 > 脚本编辑器 - 快捷键:
F11 - 工具栏:点击脚本图标
9.2.2 编辑器界面
┌─────────────────────────────────────────────────────────────────┐
│ 脚本编辑器 [_][□][×] │
├──────────────────┬──────────────────────────────────────────────┤
│ 文件 │ main.ts │
│ ├─ main.ts │ ─────────────────────────────────────────────│
│ ├─ helper.ts │ 1│ import { scene, object } from 'astral'; │
│ ├─ config.json │ 2│ │
│ └─ types.d.ts │ 3│ // 场景初始化 │
│ │ 4│ export function onInit() { │
│ [+ 新建] [× 删除]│ 5│ console.log('脚本初始化'); │
│ │ 6│ } │
│ │ 7│ │
│ │ 8│ // 每帧更新 │
│ │ 9│ export function onUpdate(delta: number) { │
│ │10│ // 更新逻辑 │
│ │11│ } │
│ │ │
├──────────────────┴──────────────────────────────────────────────┤
│ 控制台 │
│ > 脚本初始化 │
│ > FPS: 60 │
│ [运行] [停止] [重载] [清空] │
└─────────────────────────────────────────────────────────────────┘
9.2.3 快捷键
| 快捷键 | 功能 |
|---|---|
| Ctrl+S | 保存脚本 |
| Ctrl+Enter | 运行脚本 |
| Ctrl+. | 停止脚本 |
| Ctrl+R | 重新加载脚本 |
| Ctrl+Space | 代码补全 |
| F5 | 开始调试 |
| F9 | 切换断点 |
| F10 | 单步跳过 |
| F11 | 单步进入 |
9.3 脚本API
9.3.1 全局API
// 控制台输出
console.log('普通日志');
console.warn('警告日志');
console.error('错误日志');
console.info('信息日志');
console.table({ a: 1, b: 2 });
// 时间函数
setTimeout(callback, delay);
setInterval(callback, interval);
clearTimeout(id);
clearInterval(id);
// 帧相关
requestAnimationFrame(callback);
cancelAnimationFrame(id);
// 数学函数
Math.sin, Math.cos, Math.tan
Math.PI, Math.E
Math.random()
Math.floor, Math.ceil, Math.round
Math.min, Math.max
Math.pow, Math.sqrt
9.3.2 场景API
import { scene } from 'astral';
// 获取场景对象
const myObject = scene.getObjectByName('建筑');
const allMeshes = scene.getObjectsByType('Mesh');
// 添加对象
scene.add(newObject);
// 移除对象
scene.remove(object);
// 遍历场景
scene.traverse((object) => {
console.log(object.name);
});
// 场景属性
scene.background = new Color(0x000000);
scene.fog = new Fog(0xcccccc, 10, 100);
9.3.3 对象API
import { object } from 'astral';
// 创建对象
const box = object.createBox({ width: 1, height: 1, depth: 1 });
const sphere = object.createSphere({ radius: 1 });
const cylinder = object.createCylinder({ radius: 0.5, height: 2 });
const plane = object.createPlane({ width: 10, height: 10 });
// 变换操作
object.setPosition(obj, { x: 0, y: 1, z: 0 });
object.setRotation(obj, { x: 0, y: Math.PI / 4, z: 0 });
object.setScale(obj, { x: 2, y: 2, z: 2 });
object.move(obj, { x: 1, y: 0, z: 0 });
object.rotate(obj, { x: 0, y: 0.1, z: 0 });
// 属性设置
object.setVisible(obj, true);
object.setCastShadow(obj, true);
object.setReceiveShadow(obj, true);
// 材质设置
object.setColor(obj, 0xff0000);
object.setOpacity(obj, 0.5);
object.setMaterial(obj, material);
// 获取信息
const bounds = object.getBounds(obj);
const center = object.getCenter(obj);
const worldPos = object.getWorldPosition(obj);
9.3.4 相机API
import { camera } from 'astral';
// 获取/设置相机位置
camera.setPosition({ x: 0, y: 10, z: 20 });
const pos = camera.getPosition();
// 设置观察目标
camera.lookAt({ x: 0, y: 0, z: 0 });
// 相机动画
await camera.flyTo(
{ x: 10, y: 5, z: 10 }, // 位置
{ x: 0, y: 0, z: 0 }, // 目标
2000 // 时长(ms)
);
// 聚焦到对象
camera.focusOn(object, 1.5);
// 相机属性
camera.setFOV(60);
camera.setNear(0.1);
camera.setFar(10000);
// 视图切换
camera.setView('top');
camera.setView('front');
camera.setView('perspective');
9.3.5 事件API
import { events } from 'astral';
// 监听事件
events.on('objectSelected', (object) => {
console.log('选中:', object.name);
});
events.on('objectAdded', (object) => {
console.log('添加:', object.name);
});
events.on('sceneChanged', () => {
console.log('场景变化');
});
// 一次性监听
events.once('loadComplete', () => {
console.log('加载完成');
});
// 移除监听
const handler = (obj) => console.log(obj);
events.on('objectSelected', handler);
events.off('objectSelected', handler);
// 触发事件
events.emit('customEvent', { data: 'hello' });
9.3.6 动画API
import { anim } from 'astral';
// 创建补间动画
anim.tween(object, {
position: { x: 10, y: 0, z: 0 },
rotation: { y: Math.PI },
duration: 2000,
easing: 'easeOutQuad',
onUpdate: (progress) => {
console.log('进度:', progress);
},
onComplete: () => {
console.log('完成');
}
});
// 序列动画
anim.sequence([
{ target: obj1, props: { position: { x: 5 } }, duration: 1000 },
{ target: obj2, props: { position: { x: 10 } }, duration: 1000 },
{ target: obj3, props: { position: { x: 15 } }, duration: 1000 }
]);
// 并行动画
anim.parallel([
{ target: obj1, props: { position: { y: 5 } }, duration: 1000 },
{ target: obj2, props: { scale: { x: 2, y: 2, z: 2 } }, duration: 1000 }
]);
// 停止动画
const tweenId = anim.tween(object, { ... });
anim.stop(tweenId);
anim.stopAll();
9.3.7 工具API
import { tools } from 'astral';
// 测量
const distance = tools.measureDistance(point1, point2);
const area = tools.measureArea(points);
const volume = tools.measureVolume(mesh);
// 射线检测
const hit = tools.raycast(screenX, screenY);
const hits = tools.raycastAll(screenX, screenY);
// 碰撞检测
const colliding = tools.checkCollision(obj1, obj2);
// 包围盒
const bbox = tools.getBoundingBox(object);
const worldBBox = tools.getWorldBoundingBox(object);
// 坐标转换
const screenPos = tools.worldToScreen(worldPos);
const worldPos = tools.screenToWorld(screenPos, depth);
9.4 脚本生命周期
9.4.1 生命周期函数
// 脚本初始化(只调用一次)
export function onInit() {
console.log('脚本初始化');
// 初始化变量、加载资源等
}
// 每帧更新
export function onUpdate(deltaTime: number) {
// deltaTime: 距上一帧的时间(秒)
// 更新逻辑、动画等
}
// 脚本销毁
export function onDestroy() {
console.log('脚本销毁');
// 清理资源、移除事件监听等
}
// 场景加载完成
export function onSceneLoad() {
console.log('场景加载完成');
}
// 对象选择变化
export function onSelectionChange(objects: Object3D[]) {
console.log('选中对象:', objects.length);
}
// 窗口大小变化
export function onResize(width: number, height: number) {
console.log('窗口大小:', width, height);
}
// 键盘事件
export function onKeyDown(event: KeyboardEvent) {
console.log('按键:', event.key);
}
export function onKeyUp(event: KeyboardEvent) {
console.log('释放:', event.key);
}
// 鼠标事件
export function onMouseDown(event: MouseEvent) {
console.log('鼠标按下:', event.button);
}
export function onMouseUp(event: MouseEvent) {
console.log('鼠标释放:', event.button);
}
export function onMouseMove(event: MouseEvent) {
// 鼠标移动
}
export function onClick(event: MouseEvent) {
console.log('点击');
}
export function onDoubleClick(event: MouseEvent) {
console.log('双击');
}
9.4.2 完整脚本示例
// 旋转动画脚本
import { scene, object, events } from 'astral';
// 脚本状态
let targetObject: Object3D | null = null;
let rotationSpeed = 0.5;
let isRotating = true;
// 初始化
export function onInit() {
console.log('旋转脚本初始化');
// 获取目标对象
targetObject = scene.getObjectByName('旋转物体');
if (!targetObject) {
console.warn('未找到目标对象');
}
}
// 每帧更新
export function onUpdate(delta: number) {
if (targetObject && isRotating) {
// 每帧旋转
object.rotate(targetObject, {
x: 0,
y: rotationSpeed * delta,
z: 0
});
}
}
// 键盘控制
export function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case ' ': // 空格键切换旋转
isRotating = !isRotating;
console.log(isRotating ? '开始旋转' : '停止旋转');
break;
case 'ArrowUp': // 加速
rotationSpeed += 0.1;
console.log('速度:', rotationSpeed.toFixed(2));
break;
case 'ArrowDown': // 减速
rotationSpeed = Math.max(0.1, rotationSpeed - 0.1);
console.log('速度:', rotationSpeed.toFixed(2));
break;
}
}
// 选择变化时更新目标
export function onSelectionChange(objects: Object3D[]) {
if (objects.length > 0) {
targetObject = objects[0];
console.log('切换目标:', targetObject.name);
}
}
// 清理
export function onDestroy() {
targetObject = null;
console.log('旋转脚本销毁');
}
9.5 高级脚本技巧
9.5.1 异步操作
import { scene, camera, anim } from 'astral';
// 异步加载
export async function onInit() {
// 显示加载进度
console.log('开始加载资源...');
// 加载模型
const model = await scene.loadModel('models/building.glb');
scene.add(model);
// 等待动画完成
await anim.tween(model, {
position: { y: 0 },
duration: 1000
});
// 相机飞行
await camera.flyTo(
{ x: 10, y: 5, z: 10 },
{ x: 0, y: 0, z: 0 },
2000
);
console.log('初始化完成');
}
// 使用Promise.all并行加载
export async function loadMultiple() {
const [model1, model2, model3] = await Promise.all([
scene.loadModel('models/a.glb'),
scene.loadModel('models/b.glb'),
scene.loadModel('models/c.glb')
]);
scene.add(model1, model2, model3);
}
9.5.2 状态机
// 简单状态机
type State = 'idle' | 'moving' | 'rotating' | 'scaling';
let currentState: State = 'idle';
let targetObject: Object3D | null = null;
const stateHandlers = {
idle: (delta: number) => {
// 空闲状态
},
moving: (delta: number) => {
if (targetObject) {
object.move(targetObject, { x: delta * 2, y: 0, z: 0 });
}
},
rotating: (delta: number) => {
if (targetObject) {
object.rotate(targetObject, { x: 0, y: delta, z: 0 });
}
},
scaling: (delta: number) => {
if (targetObject) {
const scale = 1 + Math.sin(Date.now() * 0.001) * 0.1;
object.setScale(targetObject, { x: scale, y: scale, z: scale });
}
}
};
export function onUpdate(delta: number) {
stateHandlers[currentState](delta);
}
export function setState(newState: State) {
console.log(`状态: ${currentState} -> ${newState}`);
currentState = newState;
}
export function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case '1': setState('idle'); break;
case '2': setState('moving'); break;
case '3': setState('rotating'); break;
case '4': setState('scaling'); break;
}
}
9.5.3 自定义组件
// 组件基类
abstract class Component {
object: Object3D;
enabled: boolean = true;
constructor(object: Object3D) {
this.object = object;
}
abstract update(delta: number): void;
dispose(): void {}
}
// 自动旋转组件
class AutoRotate extends Component {
speed: { x: number, y: number, z: number };
constructor(object: Object3D, speed = { x: 0, y: 1, z: 0 }) {
super(object);
this.speed = speed;
}
update(delta: number) {
if (!this.enabled) return;
object.rotate(this.object, {
x: this.speed.x * delta,
y: this.speed.y * delta,
z: this.speed.z * delta
});
}
}
// 跟随组件
class Follow extends Component {
target: Object3D;
offset: Vector3;
smoothness: number;
constructor(object: Object3D, target: Object3D, offset = { x: 0, y: 5, z: 10 }) {
super(object);
this.target = target;
this.offset = offset;
this.smoothness = 0.1;
}
update(delta: number) {
if (!this.enabled || !this.target) return;
const targetPos = object.getPosition(this.target);
const desiredPos = {
x: targetPos.x + this.offset.x,
y: targetPos.y + this.offset.y,
z: targetPos.z + this.offset.z
};
const currentPos = object.getPosition(this.object);
object.setPosition(this.object, {
x: currentPos.x + (desiredPos.x - currentPos.x) * this.smoothness,
y: currentPos.y + (desiredPos.y - currentPos.y) * this.smoothness,
z: currentPos.z + (desiredPos.z - currentPos.z) * this.smoothness
});
}
}
// 组件管理
const components: Component[] = [];
export function onInit() {
const cube = scene.getObjectByName('Cube');
const sphere = scene.getObjectByName('Sphere');
if (cube) {
components.push(new AutoRotate(cube, { x: 0, y: 0.5, z: 0 }));
}
if (sphere && cube) {
components.push(new Follow(sphere, cube));
}
}
export function onUpdate(delta: number) {
components.forEach(comp => comp.update(delta));
}
export function onDestroy() {
components.forEach(comp => comp.dispose());
components.length = 0;
}
9.6 脚本调试
9.6.1 控制台调试
// 日志级别
console.log('普通信息');
console.info('提示信息');
console.warn('警告信息');
console.error('错误信息');
// 格式化输出
console.log('对象位置: x=%d, y=%d, z=%d', pos.x, pos.y, pos.z);
// 表格输出
console.table([
{ name: 'Cube', x: 0, y: 0, z: 0 },
{ name: 'Sphere', x: 5, y: 0, z: 0 }
]);
// 分组
console.group('对象信息');
console.log('名称:', obj.name);
console.log('位置:', obj.position);
console.groupEnd();
// 计时
console.time('加载耗时');
await loadSomething();
console.timeEnd('加载耗时');
// 断言
console.assert(value > 0, '值必须大于0');
// 堆栈跟踪
console.trace('调用堆栈');
9.6.2 断点调试
在脚本编辑器中:
- 点击行号左侧设置断点
- 按F5开始调试
- 代码执行到断点时暂停
- 使用调试面板查看变量
- F10单步跳过,F11单步进入
export function onUpdate(delta: number) {
// 设置断点在这里
const objects = scene.getAllObjects();
objects.forEach(obj => {
// 或者在这里
if (obj.name === 'Target') {
processTarget(obj); // F11进入此函数
}
});
}
9.6.3 性能分析
// 性能监控
import { debug } from 'astral';
export function onInit() {
// 启用性能统计
debug.enableStats();
}
export function onUpdate(delta: number) {
// 开始计时
debug.begin('UpdateLogic');
// ... 更新逻辑 ...
// 结束计时
debug.end('UpdateLogic');
}
// 内存监控
console.log('内存使用:', debug.getMemoryUsage());
// 渲染信息
const info = debug.getRenderInfo();
console.log('Draw Calls:', info.drawCalls);
console.log('Triangles:', info.triangles);
console.log('Points:', info.points);
9.7 脚本热更新
9.7.1 热更新机制
// 脚本会自动检测文件变化并重新加载
// 状态会被保留,只有代码被更新
// 定义可序列化的状态
let state = {
count: 0,
targetName: '',
config: {}
};
// 导出状态保存函数
export function onSaveState() {
return state;
}
// 导出状态恢复函数
export function onRestoreState(savedState: typeof state) {
state = savedState;
console.log('状态已恢复');
}
// 正常使用状态
export function onUpdate(delta: number) {
state.count++;
}
9.7.2 手动重载
import { script } from 'astral';
// 重载当前脚本
script.reload();
// 重载指定脚本
script.reloadScript('helper.ts');
// 重载所有脚本
script.reloadAll();
9.8 脚本安全
9.8.1 沙箱限制
脚本在沙箱中运行,以下操作被禁止:
- 访问
window、document等浏览器全局对象 - 访问文件系统
- 发起网络请求(除非通过授权的API)
- 修改原型链
- 使用
eval和Function构造函数
9.8.2 API权限
// 部分API需要权限
import { network, file } from 'astral';
// 网络请求(需要network权限)
const data = await network.fetch('https://api.example.com/data');
// 文件操作(需要file权限)
await file.write('config.json', JSON.stringify(config));
const content = await file.read('config.json');
9.9 本章小结
本章详细介绍了Astral3D的脚本运行时开发,主要内容包括:
- 脚本系统概述:特点和架构设计
- 脚本编辑器:界面和快捷键
- 脚本API:场景、对象、相机、事件、动画、工具等API
- 生命周期函数:初始化、更新、销毁等
- 高级技巧:异步操作、状态机、自定义组件
- 调试方法:控制台、断点、性能分析
- 热更新:状态保存与恢复
- 安全机制:沙箱限制和API权限
通过本章的学习,读者应该能够使用脚本系统扩展Astral3D的功能,实现自定义的场景逻辑和交互效果。
下一章预告:第十章将介绍Astral3D的二次开发入门,包括SDK集成、项目配置、基础开发流程等内容。

浙公网安备 33010602011771号