第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 打开脚本编辑器

通过以下方式打开脚本编辑器:

  1. 菜单栏:工具 > 脚本编辑器
  2. 快捷键:F11
  3. 工具栏:点击脚本图标

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 断点调试

在脚本编辑器中:

  1. 点击行号左侧设置断点
  2. 按F5开始调试
  3. 代码执行到断点时暂停
  4. 使用调试面板查看变量
  5. 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 沙箱限制

脚本在沙箱中运行,以下操作被禁止:

  • 访问 windowdocument 等浏览器全局对象
  • 访问文件系统
  • 发起网络请求(除非通过授权的API)
  • 修改原型链
  • 使用 evalFunction 构造函数

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的脚本运行时开发,主要内容包括:

  1. 脚本系统概述:特点和架构设计
  2. 脚本编辑器:界面和快捷键
  3. 脚本API:场景、对象、相机、事件、动画、工具等API
  4. 生命周期函数:初始化、更新、销毁等
  5. 高级技巧:异步操作、状态机、自定义组件
  6. 调试方法:控制台、断点、性能分析
  7. 热更新:状态保存与恢复
  8. 安全机制:沙箱限制和API权限

通过本章的学习,读者应该能够使用脚本系统扩展Astral3D的功能,实现自定义的场景逻辑和交互效果。


下一章预告:第十章将介绍Astral3D的二次开发入门,包括SDK集成、项目配置、基础开发流程等内容。


posted @ 2026-01-10 13:17  我才是银古  阅读(18)  评论(0)    收藏  举报