core库demo更新

This commit is contained in:
YHH
2025-10-08 20:52:31 +08:00
parent 69c46f32eb
commit 610232e6b0
21 changed files with 2020 additions and 1536 deletions

View File

@@ -0,0 +1,99 @@
import { Scene, Core } from '@esengine/ecs-framework';
export interface DemoInfo {
id: string;
name: string;
description: string;
category: string;
icon: string;
}
export abstract class DemoBase {
protected scene: Scene;
protected canvas: HTMLCanvasElement;
protected ctx: CanvasRenderingContext2D;
protected controlPanel: HTMLElement;
protected isRunning: boolean = false;
protected animationFrameId: number | null = null;
protected lastTime: number = 0;
constructor(canvas: HTMLCanvasElement, controlPanel: HTMLElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
this.controlPanel = controlPanel;
this.scene = new Scene({ name: this.getInfo().name });
// 设置canvas大小
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
}
abstract getInfo(): DemoInfo;
abstract setup(): void;
abstract createControls(): void;
protected resizeCanvas() {
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
}
public start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = performance.now();
// 设置当前场景到Core
Core.setScene(this.scene);
this.scene.begin();
this.loop();
}
public stop() {
this.isRunning = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
public destroy() {
this.stop();
this.scene.end();
}
protected loop = () => {
if (!this.isRunning) return;
// 计算deltaTime
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
this.lastTime = currentTime;
// 更新ECS框架
Core.update(deltaTime);
// 渲染
this.render();
// 继续循环
this.animationFrameId = requestAnimationFrame(this.loop);
}
protected abstract render(): void;
protected showToast(message: string, icon: string = '✅') {
const toast = document.getElementById('toast')!;
const toastMessage = document.getElementById('toastMessage')!;
const toastIcon = toast.querySelector('.toast-icon')!;
toastIcon.textContent = icon;
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
}

View File

@@ -0,0 +1,387 @@
import { DemoBase, DemoInfo } from './DemoBase';
import {
Component,
ECSComponent,
Entity,
EntitySystem,
Serializable,
Serialize,
SerializeAsMap
} from '@esengine/ecs-framework';
// ===== 组件定义 =====
@ECSComponent('SerDemo_Position')
@Serializable({ version: 1, typeId: 'SerDemo_Position' })
class PositionComponent extends Component {
@Serialize() x: number = 0;
@Serialize() y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('SerDemo_Velocity')
@Serializable({ version: 1, typeId: 'SerDemo_Velocity' })
class VelocityComponent extends Component {
@Serialize() vx: number = 0;
@Serialize() vy: number = 0;
constructor(vx: number = 0, vy: number = 0) {
super();
this.vx = vx;
this.vy = vy;
}
}
@ECSComponent('SerDemo_Renderable')
@Serializable({ version: 1, typeId: 'SerDemo_Renderable' })
class RenderableComponent extends Component {
@Serialize() color: string = '#ffffff';
@Serialize() radius: number = 10;
constructor(color: string = '#ffffff', radius: number = 10) {
super();
this.color = color;
this.radius = radius;
}
}
@ECSComponent('SerDemo_Player')
@Serializable({ version: 1, typeId: 'SerDemo_Player' })
class PlayerComponent extends Component {
@Serialize() name: string = 'Player';
@Serialize() level: number = 1;
@Serialize() health: number = 100;
@SerializeAsMap() inventory: Map<string, number> = new Map();
constructor(name: string = 'Player') {
super();
this.name = name;
}
}
// ===== 系统定义 =====
class MovementSystem extends EntitySystem {
update() {
const entities = this.scene.entities.buffer;
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent);
const vel = entity.getComponent(VelocityComponent);
if (pos && vel) {
pos.x += vel.vx;
pos.y += vel.vy;
// 边界反弹
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
}
}
}
}
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super();
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
update() {
// 清空画布
this.ctx.fillStyle = '#0a0a15';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 渲染所有实体
const entities = this.scene.entities.buffer;
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent);
const render = entity.getComponent(RenderableComponent);
if (pos && render) {
this.ctx.fillStyle = render.color;
this.ctx.beginPath();
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
this.ctx.fill();
// 如果是玩家,显示名字
const player = entity.getComponent(PlayerComponent);
if (player) {
this.ctx.fillStyle = 'white';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
}
}
}
}
}
export class SerializationDemo extends DemoBase {
private renderSystem!: RenderSystem;
private jsonData: string = '';
private binaryData: Buffer | null = null;
getInfo(): DemoInfo {
return {
id: 'serialization',
name: '场景序列化',
description: '演示场景的序列化和反序列化功能支持JSON和二进制格式',
category: '核心功能',
icon: '💾'
};
}
setup() {
// @ECSComponent装饰器会自动注册组件到ComponentRegistry
// ComponentRegistry会被序列化系统自动使用无需手动注册
// 添加系统
this.renderSystem = new RenderSystem(this.canvas);
this.scene.addEntityProcessor(new MovementSystem());
this.scene.addEntityProcessor(this.renderSystem);
// 创建初始实体
this.createInitialEntities();
// 创建控制面板
this.createControls();
}
private createInitialEntities() {
// 创建玩家
const player = this.scene.createEntity('Player');
player.addComponent(new PositionComponent(600, 300));
player.addComponent(new VelocityComponent(2, 1.5));
player.addComponent(new RenderableComponent('#4a9eff', 15));
const playerComp = new PlayerComponent('Hero');
playerComp.level = 5;
playerComp.health = 100;
playerComp.inventory.set('sword', 1);
playerComp.inventory.set('potion', 5);
player.addComponent(playerComp);
// 创建一些随机实体
for (let i = 0; i < 5; i++) {
this.createRandomEntity();
}
// 设置场景数据
this.scene.sceneData.set('weather', 'sunny');
this.scene.sceneData.set('gameTime', 12.5);
this.scene.sceneData.set('difficulty', 'normal');
}
private createRandomEntity() {
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
entity.addComponent(new PositionComponent(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height
));
entity.addComponent(new VelocityComponent(
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 3
));
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
entity.addComponent(new RenderableComponent(
colors[Math.floor(Math.random() * colors.length)],
5 + Math.random() * 10
));
}
createControls() {
this.controlPanel.innerHTML = `
<div class="control-section">
<h4>实体控制</h4>
<div class="button-group">
<button id="addEntity" class="secondary">添加随机实体</button>
<button id="clearEntities" class="danger">清空所有实体</button>
</div>
</div>
<div class="control-section">
<h4>序列化操作</h4>
<div class="button-group">
<button id="serializeJSON">序列化为JSON</button>
<button id="serializeBinary" class="success">序列化为二进制</button>
<button id="deserialize" class="secondary">反序列化恢复</button>
</div>
</div>
<div class="control-section">
<h4>本地存储</h4>
<div class="button-group">
<button id="saveLocal" class="success">保存到LocalStorage</button>
<button id="loadLocal" class="secondary">从LocalStorage加载</button>
</div>
</div>
<div class="control-section">
<h4>场景数据</h4>
<div class="input-group">
<label>天气</label>
<input type="text" id="weather" value="sunny" placeholder="sunny/rainy/snowy">
</div>
<div class="input-group">
<label>游戏时间</label>
<input type="number" id="gameTime" value="12.5" step="0.1" min="0" max="24">
</div>
<button id="updateSceneData" class="secondary">更新场景数据</button>
</div>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">实体数量</div>
<div class="stat-value" id="entityCount">0</div>
</div>
<div class="stat-item">
<div class="stat-label">JSON大小</div>
<div class="stat-value" id="jsonSize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">二进制大小</div>
<div class="stat-value" id="binarySize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">压缩率</div>
<div class="stat-value" id="compressionRatio">0%</div>
</div>
</div>
<div class="control-section">
<h4>序列化数据预览</h4>
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0; word-break: break-all;" id="dataPreview">
点击序列化按钮查看数据...
</div>
</div>
`;
// 绑定事件
this.bindEvents();
}
private bindEvents() {
document.getElementById('addEntity')!.addEventListener('click', () => {
this.createRandomEntity();
this.updateStats();
this.showToast('添加了一个随机实体');
});
document.getElementById('clearEntities')!.addEventListener('click', () => {
this.scene.destroyAllEntities();
this.createInitialEntities();
this.updateStats();
this.showToast('场景已重置');
});
document.getElementById('serializeJSON')!.addEventListener('click', () => {
this.jsonData = this.scene.serialize({ format: 'json', pretty: true }) as string;
this.updateDataPreview(this.jsonData, 'json');
this.updateStats();
this.showToast('已序列化为JSON格式');
});
document.getElementById('serializeBinary')!.addEventListener('click', () => {
this.binaryData = this.scene.serialize({ format: 'binary' }) as Buffer;
const base64 = this.binaryData.toString('base64');
this.updateDataPreview(`Binary Data (Base64):\n${base64.substring(0, 500)}...`, 'binary');
this.updateStats();
this.showToast('已序列化为二进制格式', '🔐');
});
document.getElementById('deserialize')!.addEventListener('click', () => {
const data = this.binaryData || this.jsonData;
if (!data) {
this.showToast('请先执行序列化操作', '⚠️');
return;
}
this.scene.deserialize(data, {
strategy: 'replace'
// componentRegistry会自动从ComponentRegistry获取无需手动传入
});
this.updateStats();
this.showToast('场景已恢复');
});
document.getElementById('saveLocal')!.addEventListener('click', () => {
const jsonData = this.scene.serialize({ format: 'json' }) as string;
localStorage.setItem('ecs_demo_scene', jsonData);
this.showToast('已保存到LocalStorage', '💾');
});
document.getElementById('loadLocal')!.addEventListener('click', () => {
const data = localStorage.getItem('ecs_demo_scene');
if (!data) {
this.showToast('LocalStorage中没有保存的场景', '⚠️');
return;
}
this.scene.deserialize(data, {
strategy: 'replace'
// componentRegistry会自动从ComponentRegistry获取无需手动传入
});
this.updateStats();
this.showToast('已从LocalStorage加载', '📂');
});
document.getElementById('updateSceneData')!.addEventListener('click', () => {
const weather = (document.getElementById('weather') as HTMLInputElement).value;
const gameTime = parseFloat((document.getElementById('gameTime') as HTMLInputElement).value);
this.scene.sceneData.set('weather', weather);
this.scene.sceneData.set('gameTime', gameTime);
this.showToast('场景数据已更新');
});
// 初始更新统计
this.updateStats();
}
private updateDataPreview(data: string, format: string) {
const preview = document.getElementById('dataPreview')!;
if (format === 'json') {
const truncated = data.length > 1000 ? data.substring(0, 1000) + '\n...(truncated)' : data;
preview.textContent = truncated;
} else {
preview.textContent = data;
}
}
private updateStats() {
const entityCount = this.scene.entities.count;
document.getElementById('entityCount')!.textContent = entityCount.toString();
// 计算JSON大小
if (this.jsonData) {
const jsonSize = new Blob([this.jsonData]).size;
document.getElementById('jsonSize')!.textContent = this.formatBytes(jsonSize);
}
// 计算二进制大小
if (this.binaryData) {
const binarySize = this.binaryData.length;
document.getElementById('binarySize')!.textContent = this.formatBytes(binarySize);
// 计算压缩率
if (this.jsonData) {
const jsonSize = new Blob([this.jsonData]).size;
const ratio = ((1 - binarySize / jsonSize) * 100).toFixed(1);
document.getElementById('compressionRatio')!.textContent = `${ratio}%`;
}
}
}
private formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
protected render() {
// RenderSystem会处理渲染
}
}

View File

@@ -0,0 +1,835 @@
import { DemoBase, DemoInfo } from './DemoBase';
import { Component, ECSComponent, WorkerEntitySystem, EntitySystem, Matcher, Entity, ECSSystem, PlatformManager, Time } from '@esengine/ecs-framework';
import { BrowserAdapter } from '../platform/BrowserAdapter';
// ============ 组件定义 ============
@ECSComponent('WorkerDemo_Position')
class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
set(x: number, y: number): void {
this.x = x;
this.y = y;
}
}
@ECSComponent('WorkerDemo_Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
constructor(dx: number = 0, dy: number = 0) {
super();
this.dx = dx;
this.dy = dy;
}
set(dx: number, dy: number): void {
this.dx = dx;
this.dy = dy;
}
}
@ECSComponent('WorkerDemo_Physics')
class Physics extends Component {
mass: number = 1;
bounce: number = 0.8;
friction: number = 0.95;
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
super();
this.mass = mass;
this.bounce = bounce;
this.friction = friction;
}
}
@ECSComponent('WorkerDemo_Renderable')
class Renderable extends Component {
color: string = '#ffffff';
size: number = 5;
shape: 'circle' | 'square' = 'circle';
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
super();
this.color = color;
this.size = size;
this.shape = shape;
}
}
@ECSComponent('WorkerDemo_Lifetime')
class Lifetime extends Component {
maxAge: number = 5;
currentAge: number = 0;
constructor(maxAge: number = 5) {
super();
this.maxAge = maxAge;
this.currentAge = 0;
}
isDead(): boolean {
return this.currentAge >= this.maxAge;
}
}
// ============ 系统定义 ============
interface PhysicsEntityData {
id: number;
x: number;
y: number;
dx: number;
dy: number;
mass: number;
bounce: number;
friction: number;
radius: number;
}
interface PhysicsConfig {
gravity: number;
canvasWidth: number;
canvasHeight: number;
groundFriction: number;
}
@ECSSystem('PhysicsWorkerSystem')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
private physicsConfig: PhysicsConfig;
constructor(enableWorker: boolean, canvasWidth: number, canvasHeight: number) {
const defaultConfig = {
gravity: 100,
canvasWidth,
canvasHeight,
groundFriction: 0.98
};
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
super(
Matcher.empty().all(Position, Velocity, Physics),
{
enableWorker,
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
systemConfig: defaultConfig,
useSharedArrayBuffer: true
}
);
this.physicsConfig = defaultConfig;
}
protected extractEntityData(entity: Entity): PhysicsEntityData {
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
const physics = entity.getComponent(Physics)!;
const renderable = entity.getComponent(Renderable)!;
return {
id: entity.id,
x: position.x,
y: position.y,
dx: velocity.dx,
dy: velocity.dy,
mass: physics.mass,
bounce: physics.bounce,
friction: physics.friction,
radius: renderable.size
};
}
protected workerProcess(
entities: PhysicsEntityData[],
deltaTime: number,
systemConfig?: PhysicsConfig
): PhysicsEntityData[] {
const config = systemConfig || this.physicsConfig;
const result = entities.map(e => ({ ...e }));
for (let i = 0; i < result.length; i++) {
const entity = result[i];
entity.dy += config.gravity * deltaTime;
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
} else if (entity.x >= config.canvasWidth - entity.radius) {
entity.x = config.canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
} else if (entity.y >= config.canvasHeight - entity.radius) {
entity.y = config.canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
entity.dx *= config.groundFriction;
}
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const ball1 = result[i];
const ball2 = result[j];
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = ball1.radius + ball2.radius;
if (distance < minDistance && distance > 0) {
const nx = dx / distance;
const ny = dy / distance;
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
ball1.x -= separationX;
ball1.y -= separationY;
ball2.x += separationX;
ball2.y += separationY;
const relativeVelocityX = ball2.dx - ball1.dx;
const relativeVelocityY = ball2.dy - ball1.dy;
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0) continue;
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
ball1.dx -= impulseX / ball1.mass;
ball1.dy -= impulseY / ball1.mass;
ball2.dx += impulseX / ball2.mass;
ball2.dy += impulseY / ball2.mass;
const energyLoss = 0.98;
ball1.dx *= energyLoss;
ball1.dy *= energyLoss;
ball2.dx *= energyLoss;
ball2.dy *= energyLoss;
}
}
}
return result;
}
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
if (!entity || !entity.enabled) return;
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
if (!position || !velocity) return;
position.set(result.x, result.y);
velocity.set(result.dx, result.dy);
}
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
Object.assign(this.physicsConfig, newConfig);
this.updateConfig({ systemConfig: this.physicsConfig });
}
public getPhysicsConfig(): PhysicsConfig {
return { ...this.physicsConfig };
}
protected getDefaultEntityDataSize(): number {
return 9;
}
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return;
// 在第一个位置存储当前实体数量
const currentEntityCount = Math.floor(offset / 9) + 1;
sharedArray[0] = currentEntityCount;
// 数据从索引9开始存储第一个9个位置用作元数据区域
const dataOffset = offset + 9;
sharedArray[dataOffset + 0] = entityData.id;
sharedArray[dataOffset + 1] = entityData.x;
sharedArray[dataOffset + 2] = entityData.y;
sharedArray[dataOffset + 3] = entityData.dx;
sharedArray[dataOffset + 4] = entityData.dy;
sharedArray[dataOffset + 5] = entityData.mass;
sharedArray[dataOffset + 6] = entityData.bounce;
sharedArray[dataOffset + 7] = entityData.friction;
sharedArray[dataOffset + 8] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return null;
// 数据从索引9开始存储
const dataOffset = offset + 9;
return {
id: sharedArray[dataOffset + 0],
x: sharedArray[dataOffset + 1],
y: sharedArray[dataOffset + 2],
dx: sharedArray[dataOffset + 3],
dy: sharedArray[dataOffset + 4],
mass: sharedArray[dataOffset + 5],
bounce: sharedArray[dataOffset + 6],
friction: sharedArray[dataOffset + 7],
radius: sharedArray[dataOffset + 8]
};
}
protected getSharedArrayBufferProcessFunction(): any {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
const config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
const actualEntityCount = sharedFloatArray[0];
// 基础物理更新
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset = i * 9 + 9;
const id = sharedFloatArray[offset + 0];
if (id === 0) continue;
let x = sharedFloatArray[offset + 1];
let y = sharedFloatArray[offset + 2];
let dx = sharedFloatArray[offset + 3];
let dy = sharedFloatArray[offset + 4];
const bounce = sharedFloatArray[offset + 6];
const friction = sharedFloatArray[offset + 7];
const radius = sharedFloatArray[offset + 8];
// 应用重力
dy += config.gravity * deltaTime;
// 更新位置
x += dx * deltaTime;
y += dy * deltaTime;
// 边界碰撞
if (x <= radius) {
x = radius;
dx = -dx * bounce;
} else if (x >= config.canvasWidth - radius) {
x = config.canvasWidth - radius;
dx = -dx * bounce;
}
if (y <= radius) {
y = radius;
dy = -dy * bounce;
} else if (y >= config.canvasHeight - radius) {
y = config.canvasHeight - radius;
dy = -dy * bounce;
dx *= config.groundFriction;
}
// 空气阻力
dx *= friction;
dy *= friction;
// 写回数据
sharedFloatArray[offset + 1] = x;
sharedFloatArray[offset + 2] = y;
sharedFloatArray[offset + 3] = dx;
sharedFloatArray[offset + 4] = dy;
}
// 碰撞检测
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset1 = i * 9 + 9;
const id1 = sharedFloatArray[offset1 + 0];
if (id1 === 0) continue;
let x1 = sharedFloatArray[offset1 + 1];
let y1 = sharedFloatArray[offset1 + 2];
let dx1 = sharedFloatArray[offset1 + 3];
let dy1 = sharedFloatArray[offset1 + 4];
const mass1 = sharedFloatArray[offset1 + 5];
const bounce1 = sharedFloatArray[offset1 + 6];
const radius1 = sharedFloatArray[offset1 + 8];
for (let j = 0; j < actualEntityCount; j++) {
if (i === j) continue;
const offset2 = j * 9 + 9;
const id2 = sharedFloatArray[offset2 + 0];
if (id2 === 0) continue;
const x2 = sharedFloatArray[offset2 + 1];
const y2 = sharedFloatArray[offset2 + 2];
const dx2 = sharedFloatArray[offset2 + 3];
const dy2 = sharedFloatArray[offset2 + 4];
const mass2 = sharedFloatArray[offset2 + 5];
const bounce2 = sharedFloatArray[offset2 + 6];
const radius2 = sharedFloatArray[offset2 + 8];
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
const deltaX = x2 - x1;
const deltaY = y2 - y1;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const minDistance = radius1 + radius2;
if (distance < minDistance && distance > 0) {
const nx = deltaX / distance;
const ny = deltaY / distance;
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
x1 -= separationX;
y1 -= separationY;
const relativeVelocityX = dx2 - dx1;
const relativeVelocityY = dy2 - dy1;
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0) continue;
const restitution = (bounce1 + bounce2) * 0.5;
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1;
const energyLoss = 0.98;
dx1 *= energyLoss;
dy1 *= energyLoss;
}
}
sharedFloatArray[offset1 + 1] = x1;
sharedFloatArray[offset1 + 2] = y1;
sharedFloatArray[offset1 + 3] = dx1;
sharedFloatArray[offset1 + 4] = dy1;
}
};
}
}
@ECSSystem('RenderSystem')
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.empty().all(Position, Renderable));
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
protected override process(entities: Entity[]): void {
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (const entity of entities) {
const position = entity.getComponent(Position);
const renderable = entity.getComponent(Renderable);
if (!position || !renderable) continue;
this.ctx.fillStyle = renderable.color;
this.ctx.beginPath();
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
this.ctx.fill();
}
}
}
@ECSSystem('LifetimeSystem')
class LifetimeSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(Lifetime));
}
protected override process(entities: Entity[]): void {
const deltaTime = Time.deltaTime;
for (const entity of entities) {
const lifetime = entity.getComponent(Lifetime);
if (!lifetime) continue;
lifetime.currentAge += deltaTime;
if (lifetime.isDead()) {
entity.destroy();
}
}
}
}
// ============ Demo类 ============
export class WorkerSystemDemo extends DemoBase {
private physicsSystem!: PhysicsWorkerSystem;
private renderSystem!: RenderSystem;
private lifetimeSystem!: LifetimeSystem;
private currentFPS = 0;
private frameCount = 0;
private fpsUpdateTime = 0;
private elements: { [key: string]: HTMLElement } = {};
getInfo(): DemoInfo {
return {
id: 'worker-system',
name: 'Worker System',
description: '演示 ECS 框架中的多线程物理计算能力',
category: '核心功能',
icon: '⚙️'
};
}
setup(): void {
// 注册浏览器平台适配器
const browserAdapter = new BrowserAdapter();
PlatformManager.getInstance().registerAdapter(browserAdapter);
// 初始化系统
this.physicsSystem = new PhysicsWorkerSystem(true, this.canvas.width, this.canvas.height);
this.renderSystem = new RenderSystem(this.canvas);
this.lifetimeSystem = new LifetimeSystem();
this.physicsSystem.updateOrder = 1;
this.lifetimeSystem.updateOrder = 2;
this.renderSystem.updateOrder = 3;
this.scene.addSystem(this.physicsSystem);
this.scene.addSystem(this.lifetimeSystem);
this.scene.addSystem(this.renderSystem);
// 创建控制面板
this.createControls();
// 初始化UI元素引用
this.initializeUIElements();
this.bindEvents();
// 生成初始实体
this.spawnInitialEntities(1000);
}
createControls(): void {
this.controlPanel.innerHTML = `
<div style="background: #2a2a2a; padding: 20px; border-radius: 8px; height: 100%; overflow-y: auto;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">实体数量:</label>
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100"
style="width: 100%; margin-bottom: 5px;">
<span id="entityCountValue" style="color: #fff;">1000</span>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">Worker 设置:</label>
<button id="toggleWorker" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
禁用 Worker
</button>
</div>
<div style="margin-bottom: 15px;">
<button id="spawnParticles" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
生成粒子爆炸
</button>
<button id="clearEntities" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
清空所有实体
</button>
<button id="resetDemo" style="width: 100%; padding: 8px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
重置演示
</button>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">物理参数:</label>
<input type="range" id="gravity" min="0" max="500" value="100" step="10"
style="width: 100%; margin-bottom: 5px;">
<label style="color: #ccc;">重力: <span id="gravityValue">100</span></label>
<input type="range" id="friction" min="0" max="100" value="95" step="5"
style="width: 100%; margin-top: 10px; margin-bottom: 5px;">
<label style="color: #ccc;">摩擦力: <span id="frictionValue">95%</span></label>
</div>
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px;">
<h3 style="margin-top: 0; color: #4a9eff;">性能统计</h3>
<div style="margin: 5px 0; color: #ccc;">FPS: <span id="fps" style="color: #4eff4a;">0</span></div>
<div style="margin: 5px 0; color: #ccc;">实体数量: <span id="entityCountStat" style="color: #fff;">0</span></div>
<div style="margin: 5px 0; color: #ccc;">Worker状态: <span id="workerStatus" style="color: #ff4a4a;">未启用</span></div>
<div style="margin: 5px 0; color: #ccc;">Worker负载: <span id="workerLoad" style="color: #fff;">N/A</span></div>
</div>
</div>
`;
}
protected render(): void {
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.fpsUpdateTime >= 1000) {
this.currentFPS = this.frameCount;
this.frameCount = 0;
this.fpsUpdateTime = currentTime;
}
this.updateUI();
}
private initializeUIElements(): void {
const elementIds = [
'entityCount', 'entityCountValue', 'toggleWorker',
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad'
];
for (const id of elementIds) {
const element = document.getElementById(id);
if (element) {
this.elements[id] = element;
}
}
}
private bindEvents(): void {
if (this.elements.entityCount && this.elements.entityCountValue) {
const slider = this.elements.entityCount as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.entityCountValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const count = parseInt(slider.value);
this.spawnInitialEntities(count);
});
}
if (this.elements.toggleWorker) {
this.elements.toggleWorker.addEventListener('click', () => {
const workerEnabled = this.toggleWorker();
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
});
}
if (this.elements.gravity && this.elements.gravityValue) {
const slider = this.elements.gravity as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.gravityValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const gravity = parseInt(slider.value);
this.updateWorkerConfig({ gravity });
});
}
if (this.elements.friction && this.elements.frictionValue) {
const slider = this.elements.friction as HTMLInputElement;
slider.addEventListener('input', () => {
const value = parseInt(slider.value);
this.elements.frictionValue.textContent = `${value}%`;
});
slider.addEventListener('change', () => {
const friction = parseInt(slider.value) / 100;
this.updateWorkerConfig({ friction });
});
}
if (this.elements.spawnParticles) {
this.elements.spawnParticles.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.spawnParticleExplosion(centerX, centerY, 100);
});
}
if (this.elements.clearEntities) {
this.elements.clearEntities.addEventListener('click', () => {
this.clearAllEntities();
});
}
if (this.elements.resetDemo) {
this.elements.resetDemo.addEventListener('click', () => {
(this.elements.entityCount as HTMLInputElement).value = '1000';
this.elements.entityCountValue.textContent = '1000';
(this.elements.gravity as HTMLInputElement).value = '100';
this.elements.gravityValue.textContent = '100';
(this.elements.friction as HTMLInputElement).value = '95';
this.elements.frictionValue.textContent = '95%';
this.spawnInitialEntities(1000);
this.updateWorkerConfig({ gravity: 100, friction: 0.95 });
});
}
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.spawnParticleExplosion(x, y, 30);
});
}
private updateUI(): void {
const workerInfo = this.physicsSystem.getWorkerInfo();
if (this.elements.fps) {
this.elements.fps.textContent = this.currentFPS.toString();
}
if (this.elements.entityCountStat) {
this.elements.entityCountStat.textContent = this.scene.entities.count.toString();
}
if (this.elements.workerStatus) {
if (workerInfo.enabled) {
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
this.elements.workerStatus.style.color = '#4eff4a';
} else {
this.elements.workerStatus.textContent = '禁用';
this.elements.workerStatus.style.color = '#ff4a4a';
}
}
if (this.elements.workerLoad) {
const entityCount = this.scene.entities.count;
if (workerInfo.enabled && entityCount > 0) {
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
} else {
this.elements.workerLoad.textContent = 'N/A';
}
}
}
private spawnInitialEntities(count: number = 1000): void {
this.clearAllEntities();
for (let i = 0; i < count; i++) {
this.createParticle();
}
}
private createParticle(): void {
const entity = this.scene.createEntity(`Particle_${Date.now()}_${Math.random()}`);
const x = Math.random() * (this.canvas.width - 20) + 10;
const y = Math.random() * (this.canvas.height - 20) + 10;
const dx = (Math.random() - 0.5) * 200;
const dy = (Math.random() - 0.5) * 200;
const mass = Math.random() * 3 + 2;
const bounce = 0.85 + Math.random() * 0.15;
const friction = 0.998 + Math.random() * 0.002;
const colors = [
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44'
];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 6 + 3;
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, friction));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(5 + Math.random() * 10));
}
private spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
for (let i = 0; i < count; i++) {
const entity = this.scene.createEntity(`Explosion_${Date.now()}_${i}`);
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
const distance = Math.random() * 30;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
const speed = 100 + Math.random() * 150;
const dx = Math.cos(angle) * speed;
const dy = Math.sin(angle) * speed;
const mass = 0.5 + Math.random() * 1;
const bounce = 0.8 + Math.random() * 0.2;
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 4 + 2;
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, 0.999));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(2 + Math.random() * 3));
}
}
private clearAllEntities(): void {
const entities = [...this.scene.entities.buffer];
for (const entity of entities) {
entity.destroy();
}
}
private toggleWorker(): boolean {
const workerInfo = this.physicsSystem.getWorkerInfo();
const newWorkerEnabled = !workerInfo.enabled;
// 保存当前物理配置
const currentConfig = this.physicsSystem.getPhysicsConfig();
this.scene.removeSystem(this.physicsSystem);
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled, this.canvas.width, this.canvas.height);
this.physicsSystem.updateOrder = 1;
// 恢复物理配置
this.physicsSystem.updatePhysicsConfig(currentConfig);
this.scene.addSystem(this.physicsSystem);
return newWorkerEnabled;
}
private updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
if (config.gravity !== undefined || config.friction !== undefined) {
const physicsConfig = this.physicsSystem.getPhysicsConfig();
this.physicsSystem.updatePhysicsConfig({
gravity: config.gravity ?? physicsConfig.gravity,
groundFriction: config.friction ?? physicsConfig.groundFriction
});
}
}
}

View File

@@ -0,0 +1,12 @@
import { DemoBase } from './DemoBase';
import { SerializationDemo } from './SerializationDemo';
import { WorkerSystemDemo } from './WorkerSystemDemo';
export { DemoBase, SerializationDemo, WorkerSystemDemo };
// Demo注册表
export const DEMO_REGISTRY: typeof DemoBase[] = [
SerializationDemo,
WorkerSystemDemo,
// 更多demos可以在这里添加
];

View File

@@ -0,0 +1,171 @@
import { DEMO_REGISTRY, DemoBase } from './demos';
import { Core } from '@esengine/ecs-framework';
class DemoManager {
private demos: Map<string, typeof DemoBase> = new Map();
private currentDemo: DemoBase | null = null;
private canvas: HTMLCanvasElement;
private controlPanel: HTMLElement;
constructor() {
// 初始化ECS Core
Core.create({
debug: true,
enableEntitySystems: true
});
this.canvas = document.getElementById('demoCanvas') as HTMLCanvasElement;
this.controlPanel = document.getElementById('controlPanel') as HTMLElement;
// 注册所有demos
for (const DemoClass of DEMO_REGISTRY) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
this.demos.set(info.id, DemoClass);
tempInstance.destroy();
}
// 渲染demo列表
this.renderDemoList();
// 自动加载第一个demo
const firstDemo = DEMO_REGISTRY[0];
if (firstDemo) {
const tempInstance = new firstDemo(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
this.loadDemo(info.id);
}
}
private renderDemoList() {
const demoList = document.getElementById('demoList')!;
// 按分类组织demos
const categories = new Map<string, typeof DemoBase[]>();
for (const DemoClass of DEMO_REGISTRY) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
if (!categories.has(info.category)) {
categories.set(info.category, []);
}
categories.get(info.category)!.push(DemoClass);
}
// 渲染分类和demos
let html = '';
for (const [category, demoClasses] of categories) {
html += `<div class="demo-category">`;
html += `<div class="category-title">${category}</div>`;
for (const DemoClass of demoClasses) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
html += `
<div class="demo-item" data-demo-id="${info.id}">
<div class="demo-icon">${info.icon}</div>
<div class="demo-info">
<div class="demo-name">${info.name}</div>
<div class="demo-desc">${info.description}</div>
</div>
</div>
`;
}
html += `</div>`;
}
demoList.innerHTML = html;
// 绑定点击事件
demoList.querySelectorAll('.demo-item').forEach(item => {
item.addEventListener('click', () => {
const demoId = item.getAttribute('data-demo-id')!;
this.loadDemo(demoId);
});
});
}
private loadDemo(demoId: string) {
// 停止并销毁当前demo
if (this.currentDemo) {
this.currentDemo.destroy();
this.currentDemo = null;
}
// 显示加载动画
const loading = document.getElementById('loading')!;
loading.style.display = 'block';
// 延迟加载,给用户反馈
setTimeout(() => {
const DemoClass = this.demos.get(demoId);
if (!DemoClass) {
console.error(`Demo ${demoId} not found`);
loading.style.display = 'none';
return;
}
try {
// 创建新demo
this.currentDemo = new DemoClass(this.canvas, this.controlPanel);
const info = this.currentDemo.getInfo();
// 更新页面标题和描述
document.getElementById('demoTitle')!.textContent = info.name;
document.getElementById('demoDescription')!.textContent = info.description;
// 设置demo
this.currentDemo.setup();
// 显示控制面板
this.controlPanel.style.display = 'block';
// 启动demo
this.currentDemo.start();
// 更新菜单选中状态
document.querySelectorAll('.demo-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-demo-id') === demoId) {
item.classList.add('active');
}
});
loading.style.display = 'none';
console.log(`✅ Demo "${info.name}" loaded successfully`);
} catch (error) {
console.error(`Failed to load demo ${demoId}:`, error);
loading.style.display = 'none';
this.showError('加载演示失败:' + (error as Error).message);
}
}, 300);
}
private showError(message: string) {
const toast = document.getElementById('toast')!;
const toastMessage = document.getElementById('toastMessage')!;
const toastIcon = toast.querySelector('.toast-icon')!;
toastIcon.textContent = '❌';
toastMessage.textContent = message;
toast.style.borderColor = '#f5576c';
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
toast.style.borderColor = '#667eea';
}, 3000);
}
}
// 初始化
window.addEventListener('DOMContentLoaded', () => {
new DemoManager();
});

View File

@@ -0,0 +1,204 @@
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig
} from '@esengine/ecs-framework';
/**
* 浏览器平台适配器
*/
export class BrowserAdapter implements IPlatformAdapter {
public readonly name = 'browser';
public readonly version: string;
constructor() {
this.version = this.getBrowserInfo();
}
/**
* 检查是否支持Worker
*/
public isWorkerSupported(): boolean {
return typeof Worker !== 'undefined';
}
/**
* 检查是否支持SharedArrayBuffer
*/
public isSharedArrayBufferSupported(): boolean {
return typeof SharedArrayBuffer !== 'undefined' && this.checkSharedArrayBufferEnabled();
}
/**
* 获取硬件并发数CPU核心数
*/
public getHardwareConcurrency(): number {
return navigator.hardwareConcurrency || 4;
}
/**
* 创建Worker
*/
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
if (!this.isWorkerSupported()) {
throw new Error('浏览器不支持Worker');
}
try {
return new BrowserWorker(script, options);
} catch (error) {
throw new Error(`创建浏览器Worker失败: ${(error as Error).message}`);
}
}
/**
* 创建SharedArrayBuffer
*/
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
if (!this.isSharedArrayBufferSupported()) {
return null;
}
try {
return new SharedArrayBuffer(length);
} catch (error) {
console.warn('SharedArrayBuffer创建失败:', error);
return null;
}
}
/**
* 获取高精度时间戳
*/
public getHighResTimestamp(): number {
return performance.now();
}
/**
* 获取平台配置
*/
public getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: this.getHardwareConcurrency(),
supportsModuleWorker: false,
supportsTransferableObjects: true,
maxSharedArrayBufferSize: 1024 * 1024 * 1024, // 1GB
workerScriptPrefix: '',
limitations: {
noEval: false,
requiresWorkerInit: false
}
};
}
/**
* 获取浏览器信息
*/
private getBrowserInfo(): string {
const userAgent = navigator.userAgent;
if (userAgent.includes('Chrome')) {
const match = userAgent.match(/Chrome\/([0-9.]+)/);
return match ? `Chrome ${match[1]}` : 'Chrome';
} else if (userAgent.includes('Firefox')) {
const match = userAgent.match(/Firefox\/([0-9.]+)/);
return match ? `Firefox ${match[1]}` : 'Firefox';
} else if (userAgent.includes('Safari')) {
const match = userAgent.match(/Version\/([0-9.]+)/);
return match ? `Safari ${match[1]}` : 'Safari';
}
return 'Unknown Browser';
}
/**
* 检查SharedArrayBuffer是否真正可用
*/
private checkSharedArrayBufferEnabled(): boolean {
try {
new SharedArrayBuffer(8);
return true;
} catch {
return false;
}
}
}
/**
* 浏览器Worker封装
*/
class BrowserWorker implements PlatformWorker {
private _state: 'running' | 'terminated' = 'running';
private worker: Worker;
constructor(script: string, options: WorkerCreationOptions = {}) {
this.worker = this.createBrowserWorker(script, options);
}
public get state(): 'running' | 'terminated' {
return this._state;
}
public postMessage(message: any, transfer?: Transferable[]): void {
if (this._state === 'terminated') {
throw new Error('Worker已被终止');
}
try {
if (transfer && transfer.length > 0) {
this.worker.postMessage(message, transfer);
} else {
this.worker.postMessage(message);
}
} catch (error) {
throw new Error(`发送消息到Worker失败: ${(error as Error).message}`);
}
}
public onMessage(handler: (event: { data: any }) => void): void {
this.worker.onmessage = (event: MessageEvent) => {
handler({ data: event.data });
};
}
public onError(handler: (error: ErrorEvent) => void): void {
this.worker.onerror = handler;
}
public terminate(): void {
if (this._state === 'running') {
try {
this.worker.terminate();
this._state = 'terminated';
} catch (error) {
console.error('终止Worker失败:', error);
}
}
}
/**
* 创建浏览器Worker
*/
private createBrowserWorker(script: string, options: WorkerCreationOptions): Worker {
try {
// 创建Blob URL
const blob = new Blob([script], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
// 创建Worker
const worker = new Worker(url, {
type: options.type || 'classic',
credentials: options.credentials,
name: options.name
});
// 清理Blob URL延迟清理确保Worker已加载
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
return worker;
} catch (error) {
throw new Error(`无法创建浏览器Worker: ${(error as Error).message}`);
}
}
}