From dfd0dfc7f91572ad9511e53fe24264fac119a9a5 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Mon, 8 Dec 2025 17:02:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(worker):=20=E6=B7=BB=E5=8A=A0=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=B0=8F=E6=B8=B8=E6=88=8F=20Worker=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=92=8C=20Worker=20Generator=20CLI=20(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(worker): 添加微信小游戏 Worker 支持和 Worker Generator CLI - 新增 @esengine/worker-generator 包,用于从 WorkerEntitySystem 生成 Worker 文件 - WorkerEntitySystem 添加 workerScriptPath 配置项,支持预编译 Worker 脚本 - CLI 工具支持 --wechat 模式,自动转换 ES6+ 为 ES5 语法 - 修复微信小游戏 Worker 消息格式差异(res 直接是数据,无需 .data) - 更新中英文文档,添加微信小游戏支持章节 * docs: 更新 changelog,添加 v2.3.1 说明并标注 v2.3.0 为废弃 * fix: 修复 CI 检查问题 - 移除 cli.ts 中未使用的 toKebabCase 函数 - 修复 generator.ts 中正则表达式的 ReDoS 风险(使用 [ \t] 替代 \s*) - 更新 changelog 版本号(2.3.1 -> 2.3.2) * docs: 移除未发布版本的 changelog 条目 * fix(worker-generator): 使用 TypeScript 编译器替代手写正则进行 ES5 转换 - 修复 CodeQL 检测的 ReDoS 安全问题 - 使用 ts.transpileModule 进行安全可靠的代码转换 - 移除所有可能导致回溯的正则表达式 --- docs/changelog.md | 16 +- docs/en/changelog.md | 14 +- docs/en/guide/worker-system.md | 570 ++++++++++++ .../guide/platform-adapter/wechat-minigame.md | 870 +++++++----------- docs/guide/worker-system.md | 164 ++++ .../core-demos/src/demos/WorkerSystemDemo.ts | 4 +- examples/core-demos/workers/physics-worker.js | 277 ++++++ .../core-demos/workers/worker-mapping.json | 6 + .../src/ECS/Systems/WorkerEntitySystem.ts | 50 +- packages/worker-generator/README.md | 137 +++ packages/worker-generator/package.json | 56 ++ packages/worker-generator/src/cli.ts | 172 ++++ packages/worker-generator/src/generator.ts | 325 +++++++ packages/worker-generator/src/index.ts | 41 + packages/worker-generator/src/parser.ts | 273 ++++++ packages/worker-generator/src/types.ts | 85 ++ packages/worker-generator/tsconfig.json | 20 + pnpm-lock.yaml | 61 +- 18 files changed, 2595 insertions(+), 546 deletions(-) create mode 100644 docs/en/guide/worker-system.md create mode 100644 examples/core-demos/workers/physics-worker.js create mode 100644 examples/core-demos/workers/worker-mapping.json create mode 100644 packages/worker-generator/README.md create mode 100644 packages/worker-generator/package.json create mode 100644 packages/worker-generator/src/cli.ts create mode 100644 packages/worker-generator/src/generator.ts create mode 100644 packages/worker-generator/src/index.ts create mode 100644 packages/worker-generator/src/parser.ts create mode 100644 packages/worker-generator/src/types.ts create mode 100644 packages/worker-generator/tsconfig.json diff --git a/docs/changelog.md b/docs/changelog.md index 8810595f..709a19b9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,7 +4,21 @@ --- -## v2.3.0 (2025-12-06) +## v2.3.1 (2025-12-07) + +### Bug Fixes + +- **类型导出修复**: 修复 v2.3.0 中的类型导出问题 + - 解决 `ServiceToken` 跨包类型兼容性问题 + - 修复 `editor-app` 和 `behavior-tree-editor` 中的类型引用 + +--- + +## v2.3.0 (2025-12-06) ⚠️ DEPRECATED + +> **警告**: 此版本存在类型导出问题,请升级到 v2.3.1 或更高版本。 +> +> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later. ### Features diff --git a/docs/en/changelog.md b/docs/en/changelog.md index bf201974..11746197 100644 --- a/docs/en/changelog.md +++ b/docs/en/changelog.md @@ -4,7 +4,19 @@ This document records the version update history of the `@esengine/ecs-framework --- -## v2.3.0 (2025-12-06) +## v2.3.1 (2025-12-07) + +### Bug Fixes + +- **Type export fix**: Fix type export issues in v2.3.0 + - Resolve `ServiceToken` cross-package type compatibility issues + - Fix type references in `editor-app` and `behavior-tree-editor` + +--- + +## v2.3.0 (2025-12-06) ⚠️ DEPRECATED + +> **Warning**: This version has type export issues. Please upgrade to v2.3.1 or later. ### Features diff --git a/docs/en/guide/worker-system.md b/docs/en/guide/worker-system.md new file mode 100644 index 00000000..8305dfa8 --- /dev/null +++ b/docs/en/guide/worker-system.md @@ -0,0 +1,570 @@ +# Worker System + +The Worker System (WorkerEntitySystem) is a multi-threaded processing system based on Web Workers in the ECS framework. It's designed for compute-intensive tasks, fully utilizing multi-core CPU performance for true parallel computing. + +## Core Features + +- **True Parallel Computing**: Execute compute-intensive tasks in background threads using Web Workers +- **Automatic Load Balancing**: Automatically distribute workload based on CPU core count +- **SharedArrayBuffer Optimization**: Zero-copy data sharing for improved large-scale computation performance +- **Graceful Degradation**: Automatic fallback to main thread processing when Workers are not supported +- **Type Safety**: Full TypeScript support and type checking + +## Basic Usage + +### Simple Physics System Example + +```typescript +interface PhysicsData { + id: number; + x: number; + y: number; + vx: number; + vy: number; + mass: number; + radius: number; +} + +@ECSSystem('Physics') +class PhysicsWorkerSystem extends WorkerEntitySystem { + constructor() { + super(Matcher.all(Position, Velocity, Physics), { + enableWorker: true, // Enable Worker parallel processing + workerCount: 8, // Worker count, auto-limited to hardware capacity + entitiesPerWorker: 100, // Entities per Worker + useSharedArrayBuffer: true, // Enable SharedArrayBuffer optimization + entityDataSize: 7, // Data size per entity + maxEntities: 10000, // Maximum entity count + systemConfig: { // Configuration passed to Worker + gravity: 100, + friction: 0.95 + } + }); + } + + // Data extraction: Convert Entity to serializable data + protected extractEntityData(entity: Entity): PhysicsData { + const position = entity.getComponent(Position); + const velocity = entity.getComponent(Velocity); + const physics = entity.getComponent(Physics); + + return { + id: entity.id, + x: position.x, + y: position.y, + vx: velocity.x, + vy: velocity.y, + mass: physics.mass, + radius: physics.radius + }; + } + + // Worker processing function: Pure function executed in Worker + protected workerProcess( + entities: PhysicsData[], + deltaTime: number, + config: any + ): PhysicsData[] { + return entities.map(entity => { + // Apply gravity + entity.vy += config.gravity * deltaTime; + + // Update position + entity.x += entity.vx * deltaTime; + entity.y += entity.vy * deltaTime; + + // Apply friction + entity.vx *= config.friction; + entity.vy *= config.friction; + + return entity; + }); + } + + // Apply results: Apply Worker processing results back to Entity + protected applyResult(entity: Entity, result: PhysicsData): void { + const position = entity.getComponent(Position); + const velocity = entity.getComponent(Velocity); + + position.x = result.x; + position.y = result.y; + velocity.x = result.vx; + velocity.y = result.vy; + } + + // SharedArrayBuffer optimization support + protected getDefaultEntityDataSize(): number { + return 7; // id, x, y, vx, vy, mass, radius + } + + protected writeEntityToBuffer(entityData: PhysicsData, offset: number): void { + if (!this.sharedFloatArray) return; + + this.sharedFloatArray[offset + 0] = entityData.id; + this.sharedFloatArray[offset + 1] = entityData.x; + this.sharedFloatArray[offset + 2] = entityData.y; + this.sharedFloatArray[offset + 3] = entityData.vx; + this.sharedFloatArray[offset + 4] = entityData.vy; + this.sharedFloatArray[offset + 5] = entityData.mass; + this.sharedFloatArray[offset + 6] = entityData.radius; + } + + protected readEntityFromBuffer(offset: number): PhysicsData | null { + if (!this.sharedFloatArray) return null; + + return { + id: this.sharedFloatArray[offset + 0], + x: this.sharedFloatArray[offset + 1], + y: this.sharedFloatArray[offset + 2], + vx: this.sharedFloatArray[offset + 3], + vy: this.sharedFloatArray[offset + 4], + mass: this.sharedFloatArray[offset + 5], + radius: this.sharedFloatArray[offset + 6] + }; + } +} +``` + +## Configuration Options + +The Worker system supports rich configuration options: + +```typescript +interface WorkerSystemConfig { + /** Enable Worker parallel processing */ + enableWorker?: boolean; + /** Worker count, defaults to CPU core count, auto-limited to system maximum */ + workerCount?: number; + /** Entities per Worker for load distribution control */ + entitiesPerWorker?: number; + /** System configuration data passed to Worker */ + systemConfig?: any; + /** Enable SharedArrayBuffer optimization */ + useSharedArrayBuffer?: boolean; + /** Float32 count per entity in SharedArrayBuffer */ + entityDataSize?: number; + /** Maximum entity count (for SharedArrayBuffer pre-allocation) */ + maxEntities?: number; + /** Pre-compiled Worker script path (for platforms like WeChat Mini Game that don't support dynamic scripts) */ + workerScriptPath?: string; +} +``` + +### Configuration Recommendations + +```typescript +constructor() { + super(matcher, { + // Decide based on task complexity + enableWorker: this.shouldUseWorker(), + + // Worker count: System auto-limits to hardware capacity + workerCount: 8, // Request 8 Workers, actual count limited by CPU cores + + // Entities per Worker (optional) + entitiesPerWorker: 200, // Precise load distribution control + + // Enable SharedArrayBuffer for many simple calculations + useSharedArrayBuffer: this.entityCount > 1000, + + // Set according to actual data structure + entityDataSize: 8, // Ensure it matches data structure + + // Estimated maximum entity count + maxEntities: 10000, + + // Global configuration passed to Worker + systemConfig: { + gravity: 9.8, + friction: 0.95, + worldBounds: { width: 1920, height: 1080 } + } + }); +} + +private shouldUseWorker(): boolean { + // Decide based on entity count and complexity + return this.expectedEntityCount > 100; +} + +// Get system info +getSystemInfo() { + const info = this.getWorkerInfo(); + console.log(`Worker count: ${info.workerCount}/${info.maxSystemWorkerCount}`); + console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`); + console.log(`Current mode: ${info.currentMode}`); +} +``` + +## Processing Modes + +The Worker system supports two processing modes: + +### 1. Traditional Worker Mode + +Data is serialized and passed between main thread and Workers: + +```typescript +// Suitable for: Complex computation logic, moderate entity count +constructor() { + super(matcher, { + enableWorker: true, + useSharedArrayBuffer: false, // Use traditional mode + workerCount: 2 + }); +} + +protected workerProcess(entities: EntityData[], deltaTime: number): EntityData[] { + // Complex algorithm logic + return entities.map(entity => { + // AI decisions, pathfinding, etc. + return this.complexAILogic(entity, deltaTime); + }); +} +``` + +### 2. SharedArrayBuffer Mode + +Zero-copy data sharing, suitable for many simple calculations: + +```typescript +// Suitable for: Many entities with simple calculations +constructor() { + super(matcher, { + enableWorker: true, + useSharedArrayBuffer: true, // Enable shared memory + entityDataSize: 6, + maxEntities: 10000 + }); +} + +protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction { + return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, config: any) { + const entitySize = 6; + for (let i = startIndex; i < endIndex; i++) { + const offset = i * entitySize; + + // Read data + let x = sharedFloatArray[offset]; + let y = sharedFloatArray[offset + 1]; + let vx = sharedFloatArray[offset + 2]; + let vy = sharedFloatArray[offset + 3]; + + // Physics calculations + vy += config.gravity * deltaTime; + x += vx * deltaTime; + y += vy * deltaTime; + + // Write back data + sharedFloatArray[offset] = x; + sharedFloatArray[offset + 1] = y; + sharedFloatArray[offset + 2] = vx; + sharedFloatArray[offset + 3] = vy; + } + }; +} +``` + +## Use Cases + +The Worker system is particularly suitable for: + +### 1. Physics Simulation +- **Gravity systems**: Gravity calculations for many entities +- **Collision detection**: Complex collision algorithms +- **Fluid simulation**: Particle fluid systems +- **Cloth simulation**: Vertex physics calculations + +### 2. AI Computation +- **Pathfinding**: A*, Dijkstra algorithms +- **Behavior trees**: Complex AI decision logic +- **Swarm intelligence**: Boid, fish school algorithms +- **Neural networks**: Simple AI inference + +### 3. Data Processing +- **Bulk entity updates**: State machines, lifecycle management +- **Statistical calculations**: Game data analysis +- **Image processing**: Texture generation, effect calculations +- **Audio processing**: Sound synthesis, spectrum analysis + +## Best Practices + +### 1. Worker Function Requirements + +```typescript +// Recommended: Worker processing function is a pure function +protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] { + // Only use parameters and standard JavaScript APIs + return entities.map(entity => { + // Pure computation logic, no external state dependencies + entity.y += entity.velocity * deltaTime; + return entity; + }); +} + +// Avoid: Using external references in Worker function +protected workerProcess(entities: PhysicsData[], deltaTime: number): PhysicsData[] { + // this and external variables are not available in Worker + return entities.map(entity => { + entity.y += this.someProperty; // Error + return entity; + }); +} +``` + +### 2. Data Design + +```typescript +// Recommended: Reasonable data design +interface SimplePhysicsData { + x: number; + y: number; + vx: number; + vy: number; + // Keep data structure simple for easy serialization +} + +// Avoid: Complex nested objects +interface ComplexData { + transform: { + position: { x: number; y: number }; + rotation: { angle: number }; + }; + // Complex nested structures increase serialization overhead +} +``` + +### 3. Worker Count Control + +```typescript +// Recommended: Flexible Worker configuration +constructor() { + super(matcher, { + // Specify needed Worker count, system auto-limits to hardware capacity + workerCount: 8, // Request 8 Workers + entitiesPerWorker: 100, // 100 entities per Worker + enableWorker: this.shouldUseWorker(), // Conditional enable + }); +} + +private shouldUseWorker(): boolean { + // Decide based on entity count and complexity + return this.expectedEntityCount > 100; +} + +// Get actual Worker info +checkWorkerConfiguration() { + const info = this.getWorkerInfo(); + console.log(`Requested Workers: 8`); + console.log(`Actual Workers: ${info.workerCount}`); + console.log(`System maximum: ${info.maxSystemWorkerCount}`); + console.log(`Entities per Worker: ${info.entitiesPerWorker || 'auto'}`); +} +``` + +### 4. Performance Monitoring + +```typescript +// Recommended: Performance monitoring +public getPerformanceMetrics(): WorkerPerformanceMetrics { + return { + ...this.getWorkerInfo(), + entityCount: this.entities.length, + averageProcessTime: this.getAverageProcessTime(), + workerUtilization: this.getWorkerUtilization() + }; +} +``` + +## Performance Optimization Tips + +### 1. Compute Intensity Assessment +Only use Workers for compute-intensive tasks to avoid thread overhead for simple calculations. + +### 2. Data Transfer Optimization +- Use SharedArrayBuffer to reduce serialization overhead +- Keep data structures simple and flat +- Avoid frequent large data transfers + +### 3. Degradation Strategy +Always provide main thread fallback to ensure normal operation in environments without Worker support. + +### 4. Memory Management +Clean up Worker pools and shared buffers promptly to avoid memory leaks. + +### 5. Load Balancing +Use `entitiesPerWorker` parameter to precisely control load distribution, avoiding idle Workers while others are overloaded. + +## WeChat Mini Game Support + +WeChat Mini Game has special Worker limitations and doesn't support dynamic Worker script creation. ESEngine provides the `@esengine/worker-generator` CLI tool to solve this problem. + +### WeChat Mini Game Worker Limitations + +| Feature | Browser | WeChat Mini Game | +|---------|---------|------------------| +| Dynamic scripts (Blob URL) | Supported | Not supported | +| Worker count | Multiple | Maximum 1 | +| Script source | Any | Must be in code package | +| SharedArrayBuffer | Requires COOP/COEP | Limited support | + +### Using Worker Generator CLI + +#### 1. Install the Tool + +```bash +pnpm add -D @esengine/worker-generator +``` + +#### 2. Configure workerScriptPath + +Configure `workerScriptPath` in your WorkerEntitySystem subclass: + +```typescript +@ECSSystem('Physics') +class PhysicsWorkerSystem extends WorkerEntitySystem { + constructor() { + super(Matcher.all(Position, Velocity, Physics), { + enableWorker: true, + workerScriptPath: 'workers/physics-worker.js', // Specify Worker file path + systemConfig: { + gravity: 100, + friction: 0.95 + } + }); + } + + protected workerProcess( + entities: PhysicsData[], + deltaTime: number, + config: any + ): PhysicsData[] { + // Physics calculation logic + return entities.map(entity => { + entity.vy += config.gravity * deltaTime; + entity.x += entity.vx * deltaTime; + entity.y += entity.vy * deltaTime; + return entity; + }); + } + + // ... other methods +} +``` + +#### 3. Generate Worker Files + +Run the CLI tool to automatically extract `workerProcess` functions and generate WeChat Mini Game compatible Worker files: + +```bash +# Basic usage +npx esengine-worker-gen --src ./src --wechat + +# Full options +npx esengine-worker-gen \ + --src ./src \ # Source directory + --wechat \ # Generate WeChat Mini Game compatible code + --mapping \ # Generate worker-mapping.json + --verbose # Verbose output +``` + +The CLI tool will: +1. Scan source directory for all `WorkerEntitySystem` subclasses +2. Read each class's `workerScriptPath` configuration +3. Extract `workerProcess` method body +4. Convert to ES5 syntax (WeChat Mini Game compatible) +5. Generate to configured path + +#### 4. Configure game.json + +Configure workers directory in WeChat Mini Game's `game.json`: + +```json +{ + "deviceOrientation": "portrait", + "workers": "workers" +} +``` + +#### 5. Project Structure + +``` +your-game/ +├── game.js +├── game.json # Configure "workers": "workers" +├── src/ +│ └── systems/ +│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js' +└── workers/ + ├── physics-worker.js # Auto-generated + └── worker-mapping.json # Auto-generated +``` + +### Temporarily Disabling Workers + +If you need to temporarily disable Workers (e.g., for debugging), there are two ways: + +#### Method 1: Configuration Disable + +```typescript +constructor() { + super(matcher, { + enableWorker: false, // Disable Worker, use main thread processing + // ... + }); +} +``` + +#### Method 2: Platform Adapter Disable + +Return Worker not supported in custom platform adapter: + +```typescript +class MyPlatformAdapter implements IPlatformAdapter { + isWorkerSupported(): boolean { + return false; // Return false to disable Worker + } + // ... +} +``` + +### Important Notes + +1. **Re-run CLI tool after each `workerProcess` modification** to generate new Worker files + +2. **Worker functions must be pure functions**, cannot depend on `this` or external variables: + ```typescript + // Correct: Only use parameters + protected workerProcess(entities, deltaTime, config) { + return entities.map(e => { + e.y += config.gravity * deltaTime; + return e; + }); + } + + // Wrong: Using this + protected workerProcess(entities, deltaTime, config) { + return entities.map(e => { + e.y += this.gravity * deltaTime; // Cannot access this in Worker + return e; + }); + } + ``` + +3. **Pass configuration data via `systemConfig`**, not class properties + +4. **Developer tool warnings can be ignored**: + - `getNetworkType:fail not support` - WeChat DevTools internal behavior + - `SharedArrayBuffer will require cross-origin isolation` - Development environment warning, won't appear on real devices + +## Online Demo + +See the complete Worker system demo: [Worker System Demo](https://esengine.github.io/ecs-framework/demos/worker-system/) + +The demo showcases: +- Multi-threaded physics computation +- Real-time performance comparison +- SharedArrayBuffer optimization +- Parallel processing of many entities + +The Worker system provides powerful parallel computing capabilities for the ECS framework, allowing you to fully utilize modern multi-core processor performance, offering efficient solutions for complex game logic and compute-intensive tasks. diff --git a/docs/guide/platform-adapter/wechat-minigame.md b/docs/guide/platform-adapter/wechat-minigame.md index d4d5f83c..e3605ae8 100644 --- a/docs/guide/platform-adapter/wechat-minigame.md +++ b/docs/guide/platform-adapter/wechat-minigame.md @@ -6,409 +6,198 @@ ## 特性支持 -- ✅ **Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json) -- ❌ **SharedArrayBuffer**: 不支持 -- ❌ **Transferable Objects**: 不支持(只支持可序列化对象) -- ✅ **高精度时间**: 使用 `Date.now()` 或 `wx.getPerformance()` -- ✅ **设备信息**: 完整的微信小游戏设备信息 +| 特性 | 支持情况 | 说明 | +|------|----------|------| +| **Worker** | ✅ 支持 | 需要使用预编译文件,配置 `workerScriptPath` | +| **SharedArrayBuffer** | ❌ 不支持 | 微信小游戏环境不支持 | +| **Transferable Objects** | ❌ 不支持 | 只支持可序列化对象 | +| **高精度时间** | ✅ 支持 | 使用 `wx.getPerformance()` | +| **设备信息** | ✅ 支持 | 完整的微信小游戏设备信息 | -## 完整实现 +## WorkerEntitySystem 使用方式 -```typescript -import type { - IPlatformAdapter, - PlatformWorker, - WorkerCreationOptions, - PlatformConfig, - WeChatDeviceInfo -} from '@esengine/ecs-framework'; +### 重要:微信小游戏 Worker 限制 -/** - * 微信小游戏平台适配器 - * 支持微信小游戏环境 - */ -export class WeChatMiniGameAdapter implements IPlatformAdapter { - public readonly name = 'wechat-minigame'; - public readonly version: string; - private systemInfo: any; +微信小游戏的 Worker 有以下限制: +- **Worker 脚本必须在代码包内**,不能动态生成 +- **必须在 `game.json` 中配置** `workers` 目录 +- **最多只能创建 1 个 Worker** - constructor() { - // 获取微信小游戏版本信息 - this.systemInfo = this.getSystemInfo(); - this.version = this.systemInfo.version || 'unknown'; - } +因此,使用 `WorkerEntitySystem` 时有两种方式: +1. **推荐:使用 CLI 工具自动生成** Worker 文件 +2. 手动创建 Worker 文件 - /** - * 检查是否支持Worker - */ - public isWorkerSupported(): boolean { - // 微信小游戏支持Worker,通过wx.createWorker创建 - return typeof wx !== 'undefined' && typeof wx.createWorker === 'function'; - } +### 方式一:使用 CLI 工具自动生成(推荐) - /** - * 检查是否支持SharedArrayBuffer(不支持) - */ - public isSharedArrayBufferSupported(): boolean { - return false; // 微信小游戏不支持SharedArrayBuffer - } +我们提供了 `@esengine/worker-generator` 工具,可以自动从你的 TypeScript 代码中提取 `workerProcess` 函数并生成微信小游戏兼容的 Worker 文件。 - /** - * 获取硬件并发数 - */ - public getHardwareConcurrency(): number { - // 微信小游戏官方限制:最多只能创建 1 个 Worker - return 1; - } +#### 安装 - /** - * 创建Worker - * @param script 脚本内容或文件路径 - * @param options Worker创建选项 - */ - public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker { - if (!this.isWorkerSupported()) { - throw new Error('微信小游戏不支持Worker'); - } +```bash +pnpm add -D @esengine/worker-generator +# 或 +npm install --save-dev @esengine/worker-generator +``` - try { - return new WeChatWorker(script, options); - } catch (error) { - throw new Error(`创建微信Worker失败: ${(error as Error).message}`); - } - } +#### 使用 - /** - * 创建SharedArrayBuffer(不支持) - */ - public createSharedArrayBuffer(length: number): SharedArrayBuffer | null { - return null; // 微信小游戏不支持SharedArrayBuffer - } +```bash +# 扫描 src 目录,生成 Worker 文件到 workers 目录 +npx esengine-worker-gen --src ./src --out ./workers --wechat - /** - * 获取高精度时间戳 - */ - public getHighResTimestamp(): number { - // 尝试使用微信的性能API,否则使用Date.now() - if (typeof wx !== 'undefined' && wx.getPerformance) { - const performance = wx.getPerformance(); - return performance.now(); - } - return Date.now(); - } +# 查看帮助 +npx esengine-worker-gen --help +``` - /** - * 获取平台配置 - */ - public getPlatformConfig(): PlatformConfig { - return { - maxWorkerCount: 1, // 微信小游戏最多支持 1 个 Worker - supportsModuleWorker: false, // 不支持模块Worker - supportsTransferableObjects: this.checkTransferableObjectsSupport(), - maxSharedArrayBufferSize: 0, - workerScriptPrefix: '', - limitations: { - noEval: true, // 微信小游戏限制eval使用 - requiresWorkerInit: false, - memoryLimit: this.getMemoryLimit(), - workerNotSupported: false, - workerLimitations: [ - '最多只能创建 1 个 Worker', - '创建新Worker前必须先调用 Worker.terminate()', - 'Worker脚本必须为项目内相对路径', - '需要在 game.json 中配置 workers 路径', - '使用 worker.onMessage() 而不是 self.onmessage', - '需要基础库 1.9.90 及以上版本' - ] - }, - extensions: { - platform: 'wechat-minigame', - systemInfo: this.systemInfo, - appId: this.systemInfo.host?.appId || 'unknown' - } - }; - } +#### 参数说明 - /** - * 获取微信小游戏设备信息 - */ - public getDeviceInfo(): WeChatDeviceInfo { - return { - // 设备基础信息 - brand: this.systemInfo.brand, - model: this.systemInfo.model, - platform: this.systemInfo.platform, - system: this.systemInfo.system, - benchmarkLevel: this.systemInfo.benchmarkLevel, - cpuType: this.systemInfo.cpuType, - memorySize: this.systemInfo.memorySize, - deviceAbi: this.systemInfo.deviceAbi, - abi: this.systemInfo.abi, +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `-s, --src ` | 源代码目录 | `./src` | +| `-o, --out ` | 输出目录 | `./workers` | +| `-w, --wechat` | 生成微信小游戏兼容代码 | `false` | +| `-m, --mapping` | 生成 worker-mapping.json | `true` | +| `-t, --tsconfig ` | TypeScript 配置文件路径 | 自动查找 | +| `-v, --verbose` | 详细输出 | `false` | - // 窗口信息 - screenWidth: this.systemInfo.screenWidth, - screenHeight: this.systemInfo.screenHeight, - screenTop: this.systemInfo.screenTop, - windowWidth: this.systemInfo.windowWidth, - windowHeight: this.systemInfo.windowHeight, - pixelRatio: this.systemInfo.pixelRatio, - statusBarHeight: this.systemInfo.statusBarHeight, - safeArea: this.systemInfo.safeArea, +#### 示例输出 - // 应用信息 - version: this.systemInfo.version, - language: this.systemInfo.language, - theme: this.systemInfo.theme, - SDKVersion: this.systemInfo.SDKVersion, - enableDebug: this.systemInfo.enableDebug, - fontSizeSetting: this.systemInfo.fontSizeSetting, - host: this.systemInfo.host - }; - } +``` +🔧 ESEngine Worker Generator - /** - * 异步获取完整的平台配置 - */ - public async getPlatformConfigAsync(): Promise { - // 可以在这里添加异步获取设备性能信息的逻辑 - const baseConfig = this.getPlatformConfig(); +Source directory: /project/src +Output directory: /project/workers +WeChat mode: Yes - // 尝试获取设备性能信息 - try { - const benchmarkLevel = await this.getBenchmarkLevel(); - baseConfig.extensions = { - ...baseConfig.extensions, - benchmarkLevel - }; - } catch (error) { - console.warn('获取性能基准失败:', error); - } +Scanning for WorkerEntitySystem classes... - return baseConfig; - } +✓ Found 1 WorkerEntitySystem class(es): + - PhysicsSystem (src/systems/PhysicsSystem.ts) - /** - * 检查是否支持Transferable Objects - */ - private checkTransferableObjectsSupport(): boolean { - // 微信小游戏不支持 Transferable Objects - // 基础库 2.20.2 之前只支持可序列化的 key-value 对象 - // 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects - return false; - } +Generating Worker files... - /** - * 获取系统信息 - */ - private getSystemInfo(): any { - if (typeof wx !== 'undefined' && wx.getSystemInfoSync) { - try { - return wx.getSystemInfoSync(); - } catch (error) { - console.warn('获取微信系统信息失败:', error); - return {}; - } - } - return {}; - } +✓ Successfully generated 1 Worker file(s): + - PhysicsSystem -> workers/physics-system-worker.js - /** - * 获取内存限制 - */ - private getMemoryLimit(): number { - // 微信小游戏通常有内存限制 - const memorySize = this.systemInfo.memorySize; - if (memorySize) { - // 解析内存大小字符串(如 "4GB") - const match = memorySize.match(/(\d+)([GM]B)?/i); - if (match) { - const value = parseInt(match[1], 10); - const unit = match[2]?.toUpperCase(); +📝 Usage: +1. Copy the generated files to your project's workers/ directory +2. Configure game.json (WeChat): { "workers": "workers" } +3. In your System constructor, add: + workerScriptPath: 'workers/physics-system-worker.js' +``` - if (unit === 'GB') { - return value * 1024 * 1024 * 1024; - } else if (unit === 'MB') { - return value * 1024 * 1024; - } - } - } +#### 在构建流程中集成 - // 默认限制为512MB - return 512 * 1024 * 1024; - } - - /** - * 异步获取设备性能基准 - */ - private async getBenchmarkLevel(): Promise { - return new Promise((resolve) => { - if (typeof wx !== 'undefined' && wx.getDeviceInfo) { - wx.getDeviceInfo({ - success: (res: any) => { - resolve(res.benchmarkLevel || 0); - }, - fail: () => { - resolve(0); - } - }); - } else { - resolve(this.systemInfo.benchmarkLevel || 0); - } - }); - } -} - -/** - * 微信Worker封装 - */ -class WeChatWorker implements PlatformWorker { - private _state: 'running' | 'terminated' = 'running'; - private worker: any; - private scriptPath: string; - private isTemporaryFile: boolean = false; - - constructor(script: string, options: WorkerCreationOptions = {}) { - if (typeof wx === 'undefined' || typeof wx.createWorker !== 'function') { - throw new Error('微信小游戏不支持Worker'); - } - - try { - // 判断 script 是文件路径还是脚本内容 - if (this.isFilePath(script)) { - // 直接使用文件路径 - this.scriptPath = script; - this.isTemporaryFile = false; - this.worker = wx.createWorker(this.scriptPath, { - useExperimentalWorker: true // 启用实验性Worker获得更好性能 - }); - } else { - // 微信小游戏不支持动态脚本内容,只能使用文件路径 - // 将脚本内容写入文件系统 - this.scriptPath = this.writeScriptToFile(script, options.name); - this.isTemporaryFile = true; - this.worker = wx.createWorker(this.scriptPath, { - useExperimentalWorker: true - }); - } - } catch (error) { - throw new Error(`创建微信Worker失败: ${(error as Error).message}`); - } - } - - /** - * 判断是否为文件路径 - */ - private isFilePath(script: string): boolean { - // 简单判断:如果包含 .js 后缀且不包含换行符或分号,认为是文件路径 - return script.endsWith('.js') && - !script.includes('\n') && - !script.includes(';') && - script.length < 200; // 文件路径通常不会太长 - } - - /** - * 将脚本内容写入文件系统 - * 注意:微信小游戏不支持blob URL,只能使用文件系统 - */ - private writeScriptToFile(script: string, name?: string): string { - const fs = wx.getFileSystemManager(); - const fileName = name ? `worker-${name}.js` : `worker-${Date.now()}.js`; - const filePath = `${wx.env.USER_DATA_PATH}/${fileName}`; - - try { - fs.writeFileSync(filePath, script, 'utf8'); - return filePath; - } catch (error) { - throw new Error(`写入Worker脚本文件失败: ${(error as Error).message}`); - } - } - - public get state(): 'running' | 'terminated' { - return this._state; - } - - public postMessage(message: any, transfer?: Transferable[]): void { - if (this._state === 'terminated') { - throw new Error('Worker已被终止'); - } - - try { - // 微信小游戏 Worker 只支持可序列化对象,忽略 transfer 参数 - this.worker.postMessage(message); - } catch (error) { - throw new Error(`发送消息到微信Worker失败: ${(error as Error).message}`); - } - } - - public onMessage(handler: (event: { data: any }) => void): void { - // 微信小游戏使用 onMessage 方法,不是 onmessage 属性 - this.worker.onMessage((res: any) => { - handler({ data: res }); - }); - } - - public onError(handler: (error: ErrorEvent) => void): void { - // 注意:微信小游戏 Worker 的错误处理可能与标准不同 - if (this.worker.onError) { - this.worker.onError(handler); - } - } - - public terminate(): void { - if (this._state === 'running') { - try { - this.worker.terminate(); - this._state = 'terminated'; - - // 清理临时脚本文件 - this.cleanupScriptFile(); - } catch (error) { - console.error('终止微信Worker失败:', error); - } - } - } - - /** - * 清理临时脚本文件 - */ - private cleanupScriptFile(): void { - // 只清理临时创建的文件,不清理用户提供的文件路径 - if (this.scriptPath && this.isTemporaryFile) { - try { - const fs = wx.getFileSystemManager(); - fs.unlinkSync(this.scriptPath); - } catch (error) { - console.warn('清理Worker脚本文件失败:', error); - } - } - } +```json +// package.json +{ + "scripts": { + "build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat", + "build": "pnpm build:workers && your-build-command" + } } ``` -## 使用方法 +### 方式二:手动创建 Worker 文件 -### 1. 复制代码 +如果你不想使用 CLI 工具,也可以手动创建 Worker 文件。 -将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`。 +在项目中创建 `workers/entity-worker.js`: -### 2. 注册适配器 +```javascript +// workers/entity-worker.js +// 微信小游戏 WorkerEntitySystem 通用 Worker 模板 -```typescript -import { PlatformManager } from '@esengine/ecs-framework'; -import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter'; +let sharedFloatArray = null; -// 检查是否在微信小游戏环境 -if (typeof wx !== 'undefined') { - const wechatAdapter = new WeChatMiniGameAdapter(); - PlatformManager.getInstance().registerAdapter(wechatAdapter); +worker.onMessage(function(e) { + const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data; + + try { + // 处理 SharedArrayBuffer 初始化 + if (type === 'init' && sharedBuffer) { + sharedFloatArray = new Float32Array(sharedBuffer); + worker.postMessage({ type: 'init', success: true }); + return; + } + + // 处理 SharedArrayBuffer 数据 + if (type === 'shared' && sharedFloatArray) { + processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig); + worker.postMessage({ id, result: null }); + return; + } + + // 传统处理方式 + if (entities) { + const result = workerProcess(entities, deltaTime, systemConfig); + + // 处理 Promise 返回值 + if (result && typeof result.then === 'function') { + result.then(function(finalResult) { + worker.postMessage({ id, result: finalResult }); + }).catch(function(error) { + worker.postMessage({ id, error: error.message }); + }); + } else { + worker.postMessage({ id, result: result }); + } + } + } catch (error) { + worker.postMessage({ id, error: error.message }); + } +}); + +/** + * 实体处理函数 - 根据你的业务逻辑修改此函数 + * @param {Array} entities - 实体数据数组 + * @param {number} deltaTime - 帧间隔时间 + * @param {Object} systemConfig - 系统配置 + * @returns {Array} 处理后的实体数据 + */ +function workerProcess(entities, deltaTime, systemConfig) { + // ====== 在这里编写你的处理逻辑 ====== + // 示例:物理计算 + return entities.map(function(entity) { + // 应用重力 + entity.vy += (systemConfig.gravity || 100) * deltaTime; + + // 更新位置 + entity.x += entity.vx * deltaTime; + entity.y += entity.vy * deltaTime; + + // 应用摩擦力 + entity.vx *= (systemConfig.friction || 0.95); + entity.vy *= (systemConfig.friction || 0.95); + + return entity; + }); +} + +/** + * SharedArrayBuffer 处理函数(可选) + */ +function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) { + if (!sharedFloatArray) return; + + // ====== 根据需要实现 SharedArrayBuffer 处理逻辑 ====== + // 注意:微信小游戏不支持 SharedArrayBuffer,此函数通常不会被调用 } ``` -### 3. WorkerEntitySystem 使用方式 +### 步骤 2:配置 game.json -微信小游戏适配器与 WorkerEntitySystem 配合使用,自动处理 Worker 脚本创建: +在 `game.json` 中添加 workers 配置: -#### 基本使用方式(推荐) +```json +{ + "deviceOrientation": "portrait", + "showStatusBar": false, + "workers": "workers" +} +``` + +### 步骤 3:使用 WorkerEntitySystem ```typescript import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework'; @@ -426,13 +215,17 @@ class PhysicsSystem extends WorkerEntitySystem { constructor() { super(Matcher.all(Transform, Velocity), { enableWorker: true, - workerCount: 1, // 微信小游戏限制只能创建1个Worker - systemConfig: { gravity: 100, friction: 0.95 } + workerCount: 1, // 微信小游戏限制只能创建 1 个 Worker + workerScriptPath: 'workers/entity-worker.js', // 指定预编译的 Worker 文件 + systemConfig: { + gravity: 100, + friction: 0.95 + } }); } protected getDefaultEntityDataSize(): number { - return 6; // id, x, y, vx, vy, mass + return 6; } protected extractEntityData(entity: Entity): PhysicsData { @@ -450,20 +243,15 @@ class PhysicsSystem extends WorkerEntitySystem { }; } - // WorkerEntitySystem 会自动将此函数序列化并写入临时文件 + // 注意:在微信小游戏中,此方法不会被使用 + // Worker 的处理逻辑在 workers/entity-worker.js 中的 workerProcess 函数里 protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] { return entities.map(entity => { - // 应用重力 entity.vy += config.gravity * deltaTime; - - // 更新位置 entity.x += entity.vx * deltaTime; entity.y += entity.vy * deltaTime; - - // 应用摩擦力 entity.vx *= config.friction; entity.vy *= config.friction; - return entity; }); } @@ -477,201 +265,219 @@ class PhysicsSystem extends WorkerEntitySystem { velocity.x = result.vx; velocity.y = result.vy; } + + // SharedArrayBuffer 相关方法(微信小游戏不支持,可省略) + protected writeEntityToBuffer(data: PhysicsData, offset: number): void {} + protected readEntityFromBuffer(offset: number): PhysicsData | null { return null; } } ``` -#### 使用预先创建的 Worker 文件(可选) +### 临时禁用 Worker(降级到同步模式) -如果你希望使用预先创建的 Worker 文件: +如果遇到问题,可以临时禁用 Worker: ```typescript -// 1. 在 game.json 中配置 Worker 路径 -/* -{ - "workers": "workers" +class PhysicsSystem extends WorkerEntitySystem { + constructor() { + super(Matcher.all(Transform, Velocity), { + enableWorker: false, // 禁用 Worker,使用主线程同步处理 + // ... 其他配置 + }); + } } -*/ +``` -// 2. 创建 workers/physics.js 文件 -// workers/physics.js 内容: -/* -// 微信小游戏 Worker 使用标准的 self.onmessage -self.onmessage = function(e) { - const { type, id, entities, deltaTime, systemConfig } = e.data; +## 完整适配器实现 - if (entities) { - // 处理物理计算 - const results = entities.map(entity => { - entity.vy += systemConfig.gravity * deltaTime; - entity.x += entity.vx * deltaTime; - entity.y += entity.vy * deltaTime; - return entity; +```typescript +import type { + IPlatformAdapter, + PlatformWorker, + WorkerCreationOptions, + PlatformConfig +} from '@esengine/ecs-framework'; + +/** + * 微信小游戏平台适配器 + */ +export class WeChatMiniGameAdapter implements IPlatformAdapter { + public readonly name = 'wechat-minigame'; + public readonly version: string; + private systemInfo: any; + + constructor() { + this.systemInfo = this.getSystemInfo(); + this.version = this.systemInfo.SDKVersion || 'unknown'; + } + + public isWorkerSupported(): boolean { + return typeof wx !== 'undefined' && typeof wx.createWorker === 'function'; + } + + public isSharedArrayBufferSupported(): boolean { + return false; + } + + public getHardwareConcurrency(): number { + return 1; // 微信小游戏最多 1 个 Worker + } + + public createWorker(scriptPath: string, options: WorkerCreationOptions = {}): PlatformWorker { + if (!this.isWorkerSupported()) { + throw new Error('微信小游戏环境不支持 Worker'); + } + + // scriptPath 必须是代码包内的文件路径 + const worker = wx.createWorker(scriptPath, { + useExperimentalWorker: true }); - self.postMessage({ id, result: results }); + return new WeChatWorker(worker); } -}; -*/ -// 3. 通过平台适配器直接创建(不推荐,WorkerEntitySystem会自动处理) -const adapter = PlatformManager.getInstance().getAdapter(); -const worker = adapter.createWorker('workers/physics.js'); + public createSharedArrayBuffer(length: number): SharedArrayBuffer | null { + return null; + } + + public getHighResTimestamp(): number { + if (typeof wx !== 'undefined' && wx.getPerformance) { + return wx.getPerformance().now(); + } + return Date.now(); + } + + public getPlatformConfig(): PlatformConfig { + return { + maxWorkerCount: 1, + supportsModuleWorker: false, + supportsTransferableObjects: false, + maxSharedArrayBufferSize: 0, + workerScriptPrefix: '', + limitations: { + noEval: true, // 重要:标记不支持动态脚本 + requiresWorkerInit: false, + memoryLimit: 512 * 1024 * 1024, + workerNotSupported: false, + workerLimitations: [ + '最多只能创建 1 个 Worker', + 'Worker 脚本必须在代码包内', + '需要在 game.json 中配置 workers 路径', + '需要使用 workerScriptPath 配置' + ] + }, + extensions: { + platform: 'wechat-minigame', + sdkVersion: this.systemInfo.SDKVersion + } + }; + } + + private getSystemInfo(): any { + if (typeof wx !== 'undefined' && wx.getSystemInfoSync) { + try { + return wx.getSystemInfoSync(); + } catch (error) { + console.warn('获取微信系统信息失败:', error); + } + } + return {}; + } +} + +/** + * 微信 Worker 封装 + */ +class WeChatWorker implements PlatformWorker { + private _state: 'running' | 'terminated' = 'running'; + private worker: any; + + constructor(worker: any) { + this.worker = worker; + } + + public get state(): 'running' | 'terminated' { + return this._state; + } + + public postMessage(message: any, transfer?: Transferable[]): void { + if (this._state === 'terminated') { + throw new Error('Worker 已被终止'); + } + this.worker.postMessage(message); + } + + public onMessage(handler: (event: { data: any }) => void): void { + this.worker.onMessage((res: any) => { + handler({ data: res }); + }); + } + + public onError(handler: (error: ErrorEvent) => void): void { + if (this.worker.onError) { + this.worker.onError(handler); + } + } + + public terminate(): void { + if (this._state === 'running') { + this.worker.terminate(); + this._state = 'terminated'; + } + } +} ``` -### 4. 获取设备信息 +## 注册适配器 ```typescript -const manager = PlatformManager.getInstance(); -if (manager.hasAdapter()) { - const adapter = manager.getAdapter(); - console.log('微信设备信息:', adapter.getDeviceInfo()); +import { PlatformManager } from '@esengine/ecs-framework'; +import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter'; + +// 在游戏启动时注册适配器 +if (typeof wx !== 'undefined') { + const adapter = new WeChatMiniGameAdapter(); + PlatformManager.getInstance().registerAdapter(adapter); } ``` ## 官方文档参考 -在使用微信小游戏 Worker 之前,建议先阅读官方文档: - - [wx.createWorker API](https://developers.weixin.qq.com/minigame/dev/api/worker/wx.createWorker.html) - [Worker.postMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.postMessage.html) - [Worker.onMessage API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.onMessage.html) -- [Worker.terminate API](https://developers.weixin.qq.com/minigame/dev/api/worker/Worker.terminate.html) ## 重要注意事项 -### Worker 限制和配置 +### Worker 限制 -微信小游戏的 Worker 有以下限制: - -- **数量限制**: 最多只能创建 1 个 Worker -- **版本要求**: 需要基础库 1.9.90 及以上版本 -- **脚本支持**: 不支持 blob URL,只能使用文件路径或写入文件系统 -- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头 -- **生命周期**: 创建新 Worker 前必须先调用 `Worker.terminate()` 终止当前 Worker -- **消息处理**: Worker 内使用标准的 `self.onmessage`,主线程使用 `worker.onMessage()` -- **实验性功能**: 支持 `useExperimentalWorker` 选项获得更好的 iOS 性能 - -#### Worker 配置(可选) - -如果使用预先创建的 Worker 文件,需要在 `game.json` 中添加 workers 配置: - -```json -{ - "deviceOrientation": "portrait", - "showStatusBar": false, - "workers": "workers", - "subpackages": [] -} -``` - -**注意**: 使用 WorkerEntitySystem 时无需此配置,框架会自动将脚本写入临时文件。 +| 限制项 | 说明 | +|--------|------| +| 数量限制 | 最多只能创建 1 个 Worker | +| 版本要求 | 需要基础库 1.9.90 及以上 | +| 脚本位置 | 必须在代码包内,不支持动态生成 | +| 生命周期 | 创建新 Worker 前必须先 terminate() | ### 内存限制 -微信小游戏有严格的内存限制: - - 通常限制在 256MB - 512MB - 需要及时释放不用的资源 -- 避免内存泄漏 - -### API 限制 - -- 不支持 `eval()` 函数 -- 不支持 `Function` 构造器 -- DOM API 受限 -- 文件系统 API 受限 - -## 性能优化建议 - -### 1. 分帧处理 +- 建议监听内存警告: ```typescript -class FramedProcessor { - private tasks: (() => void)[] = []; - private isProcessing = false; - - public addTask(task: () => void): void { - this.tasks.push(task); - if (!this.isProcessing) { - this.processNextFrame(); - } - } - - private processNextFrame(): void { - this.isProcessing = true; - const startTime = Date.now(); - const frameTime = 16; // 16ms per frame - - while (this.tasks.length > 0 && Date.now() - startTime < frameTime) { - const task = this.tasks.shift(); - if (task) task(); - } - - if (this.tasks.length > 0) { - setTimeout(() => this.processNextFrame(), 0); - } else { - this.isProcessing = false; - } - } -} -``` - -### 2. 内存管理 - -```typescript -class MemoryManager { - private static readonly MAX_MEMORY = 256 * 1024 * 1024; // 256MB - - public static checkMemoryUsage(): void { - if (typeof wx !== 'undefined' && wx.getPerformance) { - const performance = wx.getPerformance(); - const memoryInfo = performance.getEntries().find( - (entry: any) => entry.entryType === 'memory' - ); - - if (memoryInfo && memoryInfo.usedJSHeapSize > this.MAX_MEMORY * 0.8) { - console.warn('内存使用率过高,建议清理资源'); - // 触发垃圾回收或资源清理 - } - } - } -} +wx.onMemoryWarning(() => { + console.warn('收到内存警告,开始清理资源'); + // 清理不必要的资源 +}); ``` ## 调试技巧 ```typescript -// 检查微信小游戏环境 -if (typeof wx !== 'undefined') { - const adapter = new WeChatMiniGameAdapter(); +// 检查 Worker 配置 +const adapter = PlatformManager.getInstance().getAdapter(); +const config = adapter.getPlatformConfig(); - console.log('微信版本:', adapter.version); - console.log('设备信息:', adapter.getDeviceInfo()); - console.log('平台配置:', adapter.getPlatformConfig()); - - // 检查功能支持 - console.log('Worker支持:', adapter.isWorkerSupported()); - console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported()); -} +console.log('Worker 支持:', adapter.isWorkerSupported()); +console.log('最大 Worker 数:', config.maxWorkerCount); +console.log('平台限制:', config.limitations); ``` - -## 微信小游戏特殊API - -```typescript -// 获取设备性能等级 -if (typeof wx !== 'undefined' && wx.getDeviceInfo) { - wx.getDeviceInfo({ - success: (res) => { - console.log('设备性能等级:', res.benchmarkLevel); - } - }); -} - -// 监听内存警告 -if (typeof wx !== 'undefined' && wx.onMemoryWarning) { - wx.onMemoryWarning(() => { - console.warn('收到内存警告,开始清理资源'); - // 清理不必要的资源 - }); -} -``` \ No newline at end of file diff --git a/docs/guide/worker-system.md b/docs/guide/worker-system.md index e1e63eff..ca790e0e 100644 --- a/docs/guide/worker-system.md +++ b/docs/guide/worker-system.md @@ -145,6 +145,8 @@ interface WorkerSystemConfig { entityDataSize?: number; /** 最大实体数量(用于预分配SharedArrayBuffer) */ maxEntities?: number; + /** 预编译的Worker脚本路径(用于微信小游戏等不支持动态脚本的平台) */ + workerScriptPath?: string; } ``` @@ -605,4 +607,166 @@ public getPerformanceMetrics(): WorkerPerformanceMetrics { - SharedArrayBuffer优化 - 大量实体的并行处理 +## 微信小游戏支持 + +微信小游戏对 Worker 有特殊限制,不支持动态创建 Worker 脚本。ESEngine 提供了 `@esengine/worker-generator` CLI 工具来解决这个问题。 + +### 微信小游戏 Worker 限制 + +| 特性 | 浏览器 | 微信小游戏 | +|------|--------|-----------| +| 动态脚本 (Blob URL) | ✅ 支持 | ❌ 不支持 | +| Worker 数量 | 多个 | 最多 1 个 | +| 脚本来源 | 任意 | 必须是代码包内文件 | +| SharedArrayBuffer | 需要 COOP/COEP | 有限支持 | + +### 使用 Worker Generator CLI + +#### 1. 安装工具 + +```bash +pnpm add -D @esengine/worker-generator +``` + +#### 2. 配置 workerScriptPath + +在你的 WorkerEntitySystem 子类中配置 `workerScriptPath`: + +```typescript +@ECSSystem('Physics') +class PhysicsWorkerSystem extends WorkerEntitySystem { + constructor() { + super(Matcher.all(Position, Velocity, Physics), { + enableWorker: true, + workerScriptPath: 'workers/physics-worker.js', // 指定 Worker 文件路径 + systemConfig: { + gravity: 100, + friction: 0.95 + } + }); + } + + protected workerProcess( + entities: PhysicsData[], + deltaTime: number, + config: any + ): PhysicsData[] { + // 物理计算逻辑 + return entities.map(entity => { + entity.vy += config.gravity * deltaTime; + entity.x += entity.vx * deltaTime; + entity.y += entity.vy * deltaTime; + return entity; + }); + } + + // ... 其他方法 +} +``` + +#### 3. 生成 Worker 文件 + +运行 CLI 工具自动提取 `workerProcess` 函数并生成兼容微信小游戏的 Worker 文件: + +```bash +# 基本用法 +npx esengine-worker-gen --src ./src --wechat + +# 完整选项 +npx esengine-worker-gen \ + --src ./src \ # 源码目录 + --wechat \ # 生成微信小游戏兼容代码 + --mapping \ # 生成 worker-mapping.json + --verbose # 详细输出 +``` + +CLI 工具会: +1. 扫描源码目录,找到所有 `WorkerEntitySystem` 子类 +2. 读取每个类的 `workerScriptPath` 配置 +3. 提取 `workerProcess` 方法体 +4. 转换为 ES5 语法(微信小游戏兼容) +5. 生成到配置的路径 + +#### 4. 配置 game.json + +在微信小游戏的 `game.json` 中配置 workers 目录: + +```json +{ + "deviceOrientation": "portrait", + "workers": "workers" +} +``` + +#### 5. 项目结构 + +``` +your-game/ +├── game.js +├── game.json # 配置 "workers": "workers" +├── src/ +│ └── systems/ +│ └── PhysicsSystem.ts # workerScriptPath: 'workers/physics-worker.js' +└── workers/ + ├── physics-worker.js # 自动生成 + └── worker-mapping.json # 自动生成 +``` + +### 临时禁用 Worker + +如果需要临时禁用 Worker(例如调试时),有两种方式: + +#### 方式 1:配置禁用 + +```typescript +constructor() { + super(matcher, { + enableWorker: false, // 禁用 Worker,使用主线程处理 + // ... + }); +} +``` + +#### 方式 2:平台适配器禁用 + +在自定义平台适配器中返回不支持 Worker: + +```typescript +class MyPlatformAdapter implements IPlatformAdapter { + isWorkerSupported(): boolean { + return false; // 返回 false 禁用 Worker + } + // ... +} +``` + +### 注意事项 + +1. **每次修改 `workerProcess` 后都需要重新运行 CLI 工具**生成新的 Worker 文件 + +2. **Worker 函数必须是纯函数**,不能依赖 `this` 或外部变量: + ```typescript + // ✅ 正确:只使用参数 + protected workerProcess(entities, deltaTime, config) { + return entities.map(e => { + e.y += config.gravity * deltaTime; + return e; + }); + } + + // ❌ 错误:使用 this + protected workerProcess(entities, deltaTime, config) { + return entities.map(e => { + e.y += this.gravity * deltaTime; // Worker 中无法访问 this + return e; + }); + } + ``` + +3. **配置数据通过 `systemConfig` 传递**,而不是类属性 + +4. **开发者工具中的警告可以忽略**: + - `getNetworkType:fail not support` - 微信开发者工具内部行为 + - `SharedArrayBuffer will require cross-origin isolation` - 开发环境警告,真机不会出现 + Worker系统为ECS框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。 \ No newline at end of file diff --git a/examples/core-demos/src/demos/WorkerSystemDemo.ts b/examples/core-demos/src/demos/WorkerSystemDemo.ts index 96cf832b..0f2d00bd 100644 --- a/examples/core-demos/src/demos/WorkerSystemDemo.ts +++ b/examples/core-demos/src/demos/WorkerSystemDemo.ts @@ -123,7 +123,9 @@ class PhysicsWorkerSystem extends WorkerEntitySystem { enableWorker, workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1, systemConfig: defaultConfig, - useSharedArrayBuffer: true + useSharedArrayBuffer: true, + // 微信小游戏等平台需要配置此路径,CLI 工具会根据此路径生成 Worker 文件 + workerScriptPath: 'workers/physics-worker.js' } ); diff --git a/examples/core-demos/workers/physics-worker.js b/examples/core-demos/workers/physics-worker.js new file mode 100644 index 00000000..9e86f61b --- /dev/null +++ b/examples/core-demos/workers/physics-worker.js @@ -0,0 +1,277 @@ +/** + * Auto-generated Worker file for PhysicsWorkerSystem + * 自动生成的 Worker 文件 + * + * Source: F:/ecs-framework/examples/core-demos/src/demos/WorkerSystemDemo.ts + * Generated by @esengine/worker-generator + * + * 使用方式 | Usage: + * 1. 将此文件放入 workers/ 目录 + * 2. 在 game.json 中配置 "workers": "workers" + * 3. 在 System 中配置 workerScriptPath: 'workers/physics-worker-system-worker.js' + */ + +// 微信小游戏 Worker 环境 +// WeChat Mini Game Worker environment +let sharedFloatArray = null; +const ENTITY_DATA_SIZE = 9; + +worker.onMessage(function(res) { + // 微信小游戏 Worker 消息直接传递数据,不需要 .data + // WeChat Mini Game Worker passes data directly, no .data wrapper + var type = res.type; + var id = res.id; + var entities = res.entities; + var deltaTime = res.deltaTime; + var systemConfig = res.systemConfig; + var startIndex = res.startIndex; + var endIndex = res.endIndex; + var sharedBuffer = res.sharedBuffer; + + try { + // 处理 SharedArrayBuffer 初始化 + // Handle SharedArrayBuffer initialization + if (type === 'init' && sharedBuffer) { + sharedFloatArray = new Float32Array(sharedBuffer); + worker.postMessage({ type: 'init', success: true }); + return; + } + + // 处理 SharedArrayBuffer 数据 + // Handle SharedArrayBuffer data + if (type === 'shared' && sharedFloatArray) { + processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig); + worker.postMessage({ id: id, result: null }); + return; + } + + // 传统处理方式 + // Traditional processing + if (entities) { + var result = workerProcess(entities, deltaTime, systemConfig); + + // 处理 Promise 返回值 + // Handle Promise return value + if (result && typeof result.then === 'function') { + result.then(function(finalResult) { + worker.postMessage({ id: id, result: finalResult }); + }).catch(function(error) { + worker.postMessage({ id: id, error: error.message }); + }); + } else { + worker.postMessage({ id: id, result: result }); + } + } + } catch (error) { + worker.postMessage({ id: id, error: error.message }); + } +}); + +/** + * 实体处理函数 - 从 PhysicsWorkerSystem.workerProcess 提取 + * Entity processing function - extracted from PhysicsWorkerSystem.workerProcess + */ +function workerProcess(entities, deltaTime, systemConfig) { + var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var config = systemConfig || this.physicsConfig; +var result = entities.map(function (e) { return (__assign({}, e)); }); +for (var i = 0; i < result.length; i++) { + var 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 (var i = 0; i < result.length; i++) { + for (var j = i + 1; j < result.length; j++) { + var ball1 = result[i]; + var ball2 = result[j]; + var dx = ball2.x - ball1.x; + var dy = ball2.y - ball1.y; + var distance = Math.sqrt(dx * dx + dy * dy); + var minDistance = ball1.radius + ball2.radius; + if (distance < minDistance && distance > 0) { + var nx = dx / distance; + var ny = dy / distance; + var overlap = minDistance - distance; + var separationX = nx * overlap * 0.5; + var separationY = ny * overlap * 0.5; + ball1.x -= separationX; + ball1.y -= separationY; + ball2.x += separationX; + ball2.y += separationY; + var relativeVelocityX = ball2.dx - ball1.dx; + var relativeVelocityY = ball2.dy - ball1.dy; + var velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny; + if (velocityAlongNormal > 0) + continue; + var restitution = (ball1.bounce + ball2.bounce) * 0.5; + var impulseScalar = -(1 + restitution) * velocityAlongNormal / (1 / ball1.mass + 1 / ball2.mass); + var impulseX = impulseScalar * nx; + var impulseY = impulseScalar * ny; + ball1.dx -= impulseX / ball1.mass; + ball1.dy -= impulseY / ball1.mass; + ball2.dx += impulseX / ball2.mass; + ball2.dy += impulseY / ball2.mass; + var energyLoss = 0.98; + ball1.dx *= energyLoss; + ball1.dy *= energyLoss; + ball2.dx *= energyLoss; + ball2.dy *= energyLoss; + } + } +} +return result; + +} + +/** + * SharedArrayBuffer 处理函数 + * SharedArrayBuffer processing function + */ +function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) { + if (!sharedFloatArray) return; + var config = systemConfig || { + gravity: 100, + canvasWidth: 800, + canvasHeight: 600, + groundFriction: 0.98 +}; +var actualEntityCount = sharedFloatArray[0]; +// 基础物理更新 +for (var i = startIndex; i < endIndex && i < actualEntityCount; i++) { + var offset = i * 9 + 9; + var id = sharedFloatArray[offset + 0]; + if (id === 0) + continue; + var x = sharedFloatArray[offset + 1]; + var y = sharedFloatArray[offset + 2]; + var dx = sharedFloatArray[offset + 3]; + var dy = sharedFloatArray[offset + 4]; + var bounce = sharedFloatArray[offset + 6]; + var friction = sharedFloatArray[offset + 7]; + var 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 (var i = startIndex; i < endIndex && i < actualEntityCount; i++) { + var offset1 = i * 9 + 9; + var id1 = sharedFloatArray[offset1 + 0]; + if (id1 === 0) + continue; + var x1 = sharedFloatArray[offset1 + 1]; + var y1 = sharedFloatArray[offset1 + 2]; + var dx1 = sharedFloatArray[offset1 + 3]; + var dy1 = sharedFloatArray[offset1 + 4]; + var mass1 = sharedFloatArray[offset1 + 5]; + var bounce1 = sharedFloatArray[offset1 + 6]; + var radius1 = sharedFloatArray[offset1 + 8]; + for (var j = 0; j < actualEntityCount; j++) { + if (i === j) + continue; + var offset2 = j * 9 + 9; + var id2 = sharedFloatArray[offset2 + 0]; + if (id2 === 0) + continue; + var x2 = sharedFloatArray[offset2 + 1]; + var y2 = sharedFloatArray[offset2 + 2]; + var dx2 = sharedFloatArray[offset2 + 3]; + var dy2 = sharedFloatArray[offset2 + 4]; + var mass2 = sharedFloatArray[offset2 + 5]; + var bounce2 = sharedFloatArray[offset2 + 6]; + var radius2 = sharedFloatArray[offset2 + 8]; + if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) + continue; + var deltaX = x2 - x1; + var deltaY = y2 - y1; + var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + var minDistance = radius1 + radius2; + if (distance < minDistance && distance > 0) { + var nx = deltaX / distance; + var ny = deltaY / distance; + var overlap = minDistance - distance; + var separationX = nx * overlap * 0.5; + var separationY = ny * overlap * 0.5; + x1 -= separationX; + y1 -= separationY; + var relativeVelocityX = dx2 - dx1; + var relativeVelocityY = dy2 - dy1; + var velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny; + if (velocityAlongNormal > 0) + continue; + var restitution = (bounce1 + bounce2) * 0.5; + var impulseScalar = -(1 + restitution) * velocityAlongNormal / (1 / mass1 + 1 / mass2); + var impulseX = impulseScalar * nx; + var impulseY = impulseScalar * ny; + dx1 -= impulseX / mass1; + dy1 -= impulseY / mass1; + var energyLoss = 0.98; + dx1 *= energyLoss; + dy1 *= energyLoss; + } + } + sharedFloatArray[offset1 + 1] = x1; + sharedFloatArray[offset1 + 2] = y1; + sharedFloatArray[offset1 + 3] = dx1; + sharedFloatArray[offset1 + 4] = dy1; +} + +} diff --git a/examples/core-demos/workers/worker-mapping.json b/examples/core-demos/workers/worker-mapping.json new file mode 100644 index 00000000..ccd39610 --- /dev/null +++ b/examples/core-demos/workers/worker-mapping.json @@ -0,0 +1,6 @@ +{ + "generatedAt": "2025-12-08T08:57:32.415Z", + "mappings": { + "PhysicsWorkerSystem": "physics-worker.js" + } +} \ No newline at end of file diff --git a/packages/core/src/ECS/Systems/WorkerEntitySystem.ts b/packages/core/src/ECS/Systems/WorkerEntitySystem.ts index 617ef435..dd0f6293 100644 --- a/packages/core/src/ECS/Systems/WorkerEntitySystem.ts +++ b/packages/core/src/ECS/Systems/WorkerEntitySystem.ts @@ -34,6 +34,20 @@ export interface WorkerSystemConfig { entityDataSize?: number; /** 最大实体数量(用于预分配SharedArrayBuffer) */ maxEntities?: number; + /** + * 预编译 Worker 脚本路径(微信小游戏等不支持动态脚本的平台必需) + * Pre-compiled Worker script path (required for platforms like WeChat Mini Game that don't support dynamic scripts) + * + * @example + * ```typescript + * // 微信小游戏使用方式: + * // 1. 创建 Worker 文件: workers/physics-worker.js + * // 2. 在 game.json 配置 "workers": "workers" + * // 3. 指定路径: + * workerScriptPath: 'workers/physics-worker.js' + * ``` + */ + workerScriptPath?: string; } @@ -188,9 +202,10 @@ export type SharedArrayBufferProcessFunction = ( * ``` */ export abstract class WorkerEntitySystem extends EntitySystem { - protected config: Required> & { + protected config: Required> & { systemConfig?: any; entitiesPerWorker?: number; + workerScriptPath?: string; }; private workerPool: PlatformWorkerPool | null = null; private isProcessing = false; @@ -222,7 +237,8 @@ export abstract class WorkerEntitySystem extends EntitySystem ...(config.entitiesPerWorker !== undefined && { entitiesPerWorker: config.entitiesPerWorker }), useSharedArrayBuffer: config.useSharedArrayBuffer ?? this.isSharedArrayBufferSupported(), entityDataSize: config.entityDataSize ?? this.getDefaultEntityDataSize(), - maxEntities: config.maxEntities ?? 10000 + maxEntities: config.maxEntities ?? 10000, + ...(config.workerScriptPath !== undefined && { workerScriptPath: config.workerScriptPath }) }; @@ -300,16 +316,34 @@ export abstract class WorkerEntitySystem extends EntitySystem */ private initializeWorkerPool(): void { try { - const script = this.createWorkerScript(); - - // 在WorkerEntitySystem中处理平台相关逻辑 - const workers: PlatformWorker[] = []; const platformConfig = this.platformAdapter.getPlatformConfig(); - const fullScript = (platformConfig.workerScriptPrefix || '') + script; + const workers: PlatformWorker[] = []; + + // 判断使用外部脚本路径还是动态生成脚本 + // Determine whether to use external script path or dynamically generated script + let scriptOrPath: string; + + if (this.config.workerScriptPath) { + // 使用预编译的外部 Worker 文件(微信小游戏等平台) + // Use pre-compiled external Worker file (for WeChat Mini Game, etc.) + scriptOrPath = this.config.workerScriptPath; + this.logger.info(`${this.systemName}: 使用外部Worker文件: ${scriptOrPath}`); + } else if (platformConfig.limitations?.noEval) { + // 平台不支持动态脚本,且未提供外部脚本路径 + // Platform doesn't support dynamic scripts and no external script path provided + this.logger.error(`${this.systemName}: 当前平台不支持动态Worker脚本,请配置 workerScriptPath 指定预编译的Worker文件`); + this.config.enableWorker = false; + return; + } else { + // 动态生成 Worker 脚本(浏览器等支持的平台) + // Dynamically generate Worker script (for browsers and other supported platforms) + const script = this.createWorkerScript(); + scriptOrPath = (platformConfig.workerScriptPrefix || '') + script; + } for (let i = 0; i < this.config.workerCount; i++) { try { - const worker = this.platformAdapter.createWorker(fullScript, { + const worker = this.platformAdapter.createWorker(scriptOrPath, { name: `WorkerEntitySystem-${i}` }); workers.push(worker); diff --git a/packages/worker-generator/README.md b/packages/worker-generator/README.md new file mode 100644 index 00000000..a88bc8ea --- /dev/null +++ b/packages/worker-generator/README.md @@ -0,0 +1,137 @@ +# @esengine/worker-generator + +CLI tool to generate Worker files from `WorkerEntitySystem` classes for WeChat Mini Game and other platforms that don't support dynamic Worker scripts. + +## Why This Tool? + +WeChat Mini Game has strict Worker limitations: +- Cannot create Workers from Blob URLs or dynamic scripts +- Worker scripts must be pre-compiled files in the code package +- Maximum 1 Worker allowed + +This tool extracts your `workerProcess` method and generates compatible Worker files automatically. + +## Installation + +```bash +npm install -D @esengine/worker-generator +# or +pnpm add -D @esengine/worker-generator +``` + +## Usage + +### 1. Configure your WorkerEntitySystem + +Add `workerScriptPath` to specify where the Worker file should be generated: + +```typescript +@ECSSystem('Physics') +class PhysicsWorkerSystem extends WorkerEntitySystem { + constructor() { + super(Matcher.all(Position, Velocity), { + enableWorker: true, + workerScriptPath: 'workers/physics-worker.js', // Output path + systemConfig: { + gravity: 100, + friction: 0.95 + } + }); + } + + protected workerProcess( + entities: PhysicsData[], + deltaTime: number, + config: any + ): PhysicsData[] { + return entities.map(entity => { + entity.vy += config.gravity * deltaTime; + entity.x += entity.vx * deltaTime; + entity.y += entity.vy * deltaTime; + return entity; + }); + } +} +``` + +### 2. Run the Generator + +```bash +# Basic usage +npx esengine-worker-gen --src ./src --wechat + +# Full options +npx esengine-worker-gen \ + --src ./src \ # Source directory to scan + --out ./workers \ # Default output directory (if no workerScriptPath) + --wechat \ # Generate WeChat Mini Game compatible code (ES5) + --mapping \ # Generate worker-mapping.json + --verbose # Verbose output +``` + +### 3. Configure game.json (WeChat Mini Game) + +```json +{ + "workers": "workers" +} +``` + +## CLI Options + +| Option | Description | Default | +|--------|-------------|---------| +| `-s, --src ` | Source directory to scan | `./src` | +| `-o, --out ` | Output directory for Worker files | `./workers` | +| `-w, --wechat` | Generate WeChat Mini Game compatible code | `false` | +| `-m, --mapping` | Generate worker-mapping.json file | `true` | +| `-t, --tsconfig ` | Path to tsconfig.json | Auto-detect | +| `-v, --verbose` | Verbose output | `false` | + +## Output + +The tool generates: + +1. **Worker files** - JavaScript files containing the extracted `workerProcess` logic +2. **worker-mapping.json** - Mapping of class names to Worker file paths + +Example output: +``` +workers/ +├── physics-worker.js +└── worker-mapping.json +``` + +## Important Notes + +1. **Pure Functions**: Your `workerProcess` must be a pure function - it cannot use `this` or external variables + + ```typescript + // Correct + protected workerProcess(entities, deltaTime, config) { + return entities.map(e => { + e.y += config.gravity * deltaTime; // Use config parameter + return e; + }); + } + + // Wrong + protected workerProcess(entities, deltaTime, config) { + return entities.map(e => { + e.y += this.gravity * deltaTime; // Cannot access this! + return e; + }); + } + ``` + +2. **Re-run after changes**: Run the generator again after modifying `workerProcess` + +3. **ES5 Conversion**: When using `--wechat`, the tool converts: + - Arrow functions → regular functions + - `const`/`let` → `var` + - Spread operator → `Object.assign` + - Template literals → string concatenation + +## License + +MIT diff --git a/packages/worker-generator/package.json b/packages/worker-generator/package.json new file mode 100644 index 00000000..36f6e32f --- /dev/null +++ b/packages/worker-generator/package.json @@ -0,0 +1,56 @@ +{ + "name": "@esengine/worker-generator", + "version": "1.0.0", + "description": "CLI tool to generate Worker files from WorkerEntitySystem classes for WeChat Mini Game and other platforms", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "esengine-worker-gen": "./dist/cli.js" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "type-check": "tsc --noEmit", + "prepublishOnly": "pnpm run build" + }, + "keywords": [ + "esengine", + "ecs", + "worker", + "web-worker", + "wechat", + "minigame", + "code-generator", + "cli" + ], + "author": "ESEngine Team", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/worker-generator" + }, + "homepage": "https://github.com/esengine/ecs-framework/tree/master/packages/worker-generator", + "bugs": { + "url": "https://github.com/esengine/ecs-framework/issues" + }, + "engines": { + "node": ">=16.0.0" + }, + "dependencies": { + "chalk": "^4.1.2", + "commander": "^11.1.0", + "ts-morph": "^21.0.1" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/worker-generator/src/cli.ts b/packages/worker-generator/src/cli.ts new file mode 100644 index 00000000..1897c98d --- /dev/null +++ b/packages/worker-generator/src/cli.ts @@ -0,0 +1,172 @@ +#!/usr/bin/env node + +/** + * Worker Generator CLI + * 从 WorkerEntitySystem 子类生成 Worker 文件 + * Generate Worker files from WorkerEntitySystem subclasses + */ + +import { Command } from 'commander'; +import chalk from 'chalk'; +import * as path from 'path'; +import * as fs from 'fs'; +import { parseWorkerSystems } from './parser'; +import { generateWorkerFiles } from './generator'; +import type { GeneratorConfig } from './types'; + +const packageJson = require('../package.json'); + +const program = new Command(); + +program + .name('esengine-worker-gen') + .description('Generate Worker files from WorkerEntitySystem classes for WeChat Mini Game and other platforms') + .version(packageJson.version); + +program + .option('-s, --src ', 'Source directory to scan', './src') + .option('-o, --out ', 'Output directory for Worker files', './workers') + .option('-w, --wechat', 'Generate WeChat Mini Game compatible code', false) + .option('-m, --mapping', 'Generate worker-mapping.json file', true) + .option('-t, --tsconfig ', 'Path to tsconfig.json') + .option('-v, --verbose', 'Verbose output', false) + .action((options) => { + run(options); + }); + +function run(options: { + src: string; + out: string; + wechat: boolean; + mapping: boolean; + tsconfig?: string; + verbose: boolean; +}) { + console.log(chalk.cyan('\n🔧 ESEngine Worker Generator\n')); + + // 解析路径 + // Resolve paths + const srcDir = path.resolve(process.cwd(), options.src); + const outDir = path.resolve(process.cwd(), options.out); + + // 检查源目录是否存在 + // Check if source directory exists + if (!fs.existsSync(srcDir)) { + console.error(chalk.red(`Error: Source directory not found: ${srcDir}`)); + process.exit(1); + } + + // 查找 tsconfig.json + // Find tsconfig.json + let tsConfigPath = options.tsconfig; + if (!tsConfigPath) { + const defaultTsConfig = path.join(process.cwd(), 'tsconfig.json'); + if (fs.existsSync(defaultTsConfig)) { + tsConfigPath = defaultTsConfig; + } + } + + const config: GeneratorConfig = { + srcDir, + outDir, + wechat: options.wechat, + generateMapping: options.mapping, + tsConfigPath, + verbose: options.verbose, + }; + + console.log(chalk.gray(`Source directory: ${srcDir}`)); + console.log(chalk.gray(`Output directory: ${outDir}`)); + console.log(chalk.gray(`WeChat mode: ${options.wechat ? 'Yes' : 'No'}`)); + if (tsConfigPath) { + console.log(chalk.gray(`TypeScript config: ${tsConfigPath}`)); + } + console.log(); + + // 解析源文件 + // Parse source files + console.log(chalk.yellow('Scanning for WorkerEntitySystem classes...')); + const systems = parseWorkerSystems(config); + + if (systems.length === 0) { + console.log(chalk.yellow('\n⚠️ No WorkerEntitySystem subclasses found.')); + console.log(chalk.gray('Make sure your classes:')); + console.log(chalk.gray(' - Extend WorkerEntitySystem')); + console.log(chalk.gray(' - Have a workerProcess method')); + console.log(chalk.gray(' - Are in .ts files under the source directory')); + return; + } + + console.log(chalk.green(`\n✓ Found ${systems.length} WorkerEntitySystem class(es):`)); + for (const system of systems) { + const configStatus = system.workerScriptPath + ? chalk.green(`✓ workerScriptPath: '${system.workerScriptPath}'`) + : chalk.yellow('⚠ No workerScriptPath configured'); + console.log(chalk.gray(` - ${system.className}`)); + console.log(chalk.gray(` ${path.relative(process.cwd(), system.filePath)}`)); + console.log(` ${configStatus}`); + } + console.log(); + + // 生成 Worker 文件 + // Generate Worker files + console.log(chalk.yellow('Generating Worker files...')); + const result = generateWorkerFiles(systems, config); + + // 输出结果 + // Output results + console.log(); + if (result.success.length > 0) { + console.log(chalk.green(`✓ Successfully generated ${result.success.length} Worker file(s):`)); + for (const item of result.success) { + const relativePath = path.relative(process.cwd(), item.outputPath).replace(/\\/g, '/'); + if (item.configuredPath) { + console.log(chalk.green(` ✓ ${item.className} -> ${relativePath}`)); + } else { + console.log(chalk.yellow(` ⚠ ${item.className} -> ${relativePath} (需要配置 workerScriptPath)`)); + } + } + } + + if (result.errors.length > 0) { + console.log(chalk.red(`\n✗ Failed to generate ${result.errors.length} Worker file(s):`)); + for (const item of result.errors) { + console.log(chalk.red(` - ${item.className}: ${item.error}`)); + } + } + + // 提示未配置 workerScriptPath 的类 + // Remind about classes without workerScriptPath + if (result.skipped.length > 0) { + console.log(chalk.yellow('\n⚠️ 以下类未配置 workerScriptPath,请在构造函数中添加配置:')); + console.log(chalk.yellow(' The following classes need workerScriptPath configuration:\n')); + for (const item of result.skipped) { + console.log(chalk.white(` // ${item.className}`)); + console.log(chalk.cyan(` super(matcher, {`)); + console.log(chalk.cyan(` workerScriptPath: '${item.suggestedPath}',`)); + console.log(chalk.cyan(` // ... 其他配置`)); + console.log(chalk.cyan(` });`)); + console.log(); + } + } + + // 使用提示(只有当有已配置路径的成功项时) + // Usage tips (only when there are success items with configured path) + const configuredSuccess = result.success.filter(item => item.configuredPath); + if (configuredSuccess.length > 0) { + console.log(chalk.green('\n✅ 已按照代码中的 workerScriptPath 配置生成 Worker 文件!')); + console.log(chalk.gray(' Worker files generated according to workerScriptPath in your code!')); + console.log(chalk.gray('\n 下一步 | Next steps:')); + console.log(chalk.gray(' 1. 确保 game.json 配置了 workers 目录')); + console.log(chalk.gray(' Ensure game.json has workers directory configured')); + + if (options.mapping) { + console.log(chalk.gray('\n 已生成映射文件 | Mapping file generated:')); + console.log(chalk.white(` import mapping from '${path.relative(process.cwd(), outDir)}/worker-mapping.json'`)); + } + } + + console.log(); +} + +program.parse(); diff --git a/packages/worker-generator/src/generator.ts b/packages/worker-generator/src/generator.ts new file mode 100644 index 00000000..206b8443 --- /dev/null +++ b/packages/worker-generator/src/generator.ts @@ -0,0 +1,325 @@ +/** + * Worker 文件生成器 + * Worker file generator + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import type { WorkerSystemInfo, GeneratorConfig, GenerationResult, WorkerScriptMapping } from './types'; + +/** + * 生成 Worker 文件 + * Generate Worker files + */ +export function generateWorkerFiles( + systems: WorkerSystemInfo[], + config: GeneratorConfig +): GenerationResult { + const result: GenerationResult = { + success: [], + errors: [], + skipped: [], + }; + + for (const system of systems) { + try { + // 优先使用用户配置的 workerScriptPath + // Prefer user-configured workerScriptPath + let outputPath: string; + + if (system.workerScriptPath) { + // 用户已配置路径,使用该路径(相对于项目根目录) + // User has configured path, use it (relative to project root) + outputPath = path.resolve(process.cwd(), system.workerScriptPath); + if (config.verbose) { + console.log(` Using configured workerScriptPath: ${system.workerScriptPath}`); + } + } else { + // 未配置,使用默认输出目录 + // Not configured, use default output directory + // 确保输出目录存在 + if (!fs.existsSync(config.outDir)) { + fs.mkdirSync(config.outDir, { recursive: true }); + } + const outputFileName = `${toKebabCase(system.className)}-worker.js`; + outputPath = path.join(config.outDir, outputFileName); + + // 提示用户需要配置 workerScriptPath + // Remind user to configure workerScriptPath + result.skipped.push({ + className: system.className, + suggestedPath: path.relative(process.cwd(), outputPath).replace(/\\/g, '/'), + reason: 'No workerScriptPath configured', + }); + } + + // 确保输出目录存在 + // Ensure output directory exists + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const workerCode = config.wechat + ? generateWeChatWorkerCode(system) + : generateStandardWorkerCode(system); + + fs.writeFileSync(outputPath, workerCode, 'utf8'); + + result.success.push({ + className: system.className, + outputPath: outputPath, + configuredPath: system.workerScriptPath, + }); + + if (config.verbose) { + console.log(` Generated: ${outputPath}`); + } + } catch (error) { + result.errors.push({ + className: system.className, + filePath: system.filePath, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + // 生成映射文件 + // Generate mapping file + if (config.generateMapping && result.success.length > 0) { + generateMappingFile(result.success, config); + } + + return result; +} + +/** + * 生成微信小游戏 Worker 代码 + * Generate WeChat Mini Game Worker code + */ +function generateWeChatWorkerCode(system: WorkerSystemInfo): string { + const { workerProcessBody, workerProcessParams, sharedBufferProcessBody, entityDataSize } = system; + + return `/** + * Auto-generated Worker file for ${system.className} + * 自动生成的 Worker 文件 + * + * Source: ${system.filePath} + * Generated by @esengine/worker-generator + * + * 使用方式 | Usage: + * 1. 将此文件放入 workers/ 目录 + * 2. 在 game.json 中配置 "workers": "workers" + * 3. 在 System 中配置 workerScriptPath: 'workers/${toKebabCase(system.className)}-worker.js' + */ + +// 微信小游戏 Worker 环境 +// WeChat Mini Game Worker environment +let sharedFloatArray = null; +const ENTITY_DATA_SIZE = ${entityDataSize || 8}; + +worker.onMessage(function(res) { + // 微信小游戏 Worker 消息直接传递数据,不需要 .data + // WeChat Mini Game Worker passes data directly, no .data wrapper + var type = res.type; + var id = res.id; + var entities = res.entities; + var deltaTime = res.deltaTime; + var systemConfig = res.systemConfig; + var startIndex = res.startIndex; + var endIndex = res.endIndex; + var sharedBuffer = res.sharedBuffer; + + try { + // 处理 SharedArrayBuffer 初始化 + // Handle SharedArrayBuffer initialization + if (type === 'init' && sharedBuffer) { + sharedFloatArray = new Float32Array(sharedBuffer); + worker.postMessage({ type: 'init', success: true }); + return; + } + + // 处理 SharedArrayBuffer 数据 + // Handle SharedArrayBuffer data + if (type === 'shared' && sharedFloatArray) { + processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig); + worker.postMessage({ id: id, result: null }); + return; + } + + // 传统处理方式 + // Traditional processing + if (entities) { + var result = workerProcess(entities, deltaTime, systemConfig); + + // 处理 Promise 返回值 + // Handle Promise return value + if (result && typeof result.then === 'function') { + result.then(function(finalResult) { + worker.postMessage({ id: id, result: finalResult }); + }).catch(function(error) { + worker.postMessage({ id: id, error: error.message }); + }); + } else { + worker.postMessage({ id: id, result: result }); + } + } + } catch (error) { + worker.postMessage({ id: id, error: error.message }); + } +}); + +/** + * 实体处理函数 - 从 ${system.className}.workerProcess 提取 + * Entity processing function - extracted from ${system.className}.workerProcess + */ +function workerProcess(${workerProcessParams.entities}, ${workerProcessParams.deltaTime}, ${workerProcessParams.config}) { + ${convertToES5(workerProcessBody)} +} + +/** + * SharedArrayBuffer 处理函数 + * SharedArrayBuffer processing function + */ +function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) { + if (!sharedFloatArray) return; + ${sharedBufferProcessBody ? convertToES5(sharedBufferProcessBody) : '// No SharedArrayBuffer processing defined'} +} +`; +} + +/** + * 生成标准 Worker 代码(用于浏览器等环境) + * Generate standard Worker code (for browsers, etc.) + */ +function generateStandardWorkerCode(system: WorkerSystemInfo): string { + const { workerProcessBody, workerProcessParams, sharedBufferProcessBody, entityDataSize } = system; + + return `/** + * Auto-generated Worker file for ${system.className} + * 自动生成的 Worker 文件 + * + * Source: ${system.filePath} + * Generated by @esengine/worker-generator + */ + +let sharedFloatArray = null; +const ENTITY_DATA_SIZE = ${entityDataSize || 8}; + +self.onmessage = function(e) { + const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data; + + try { + // 处理 SharedArrayBuffer 初始化 + if (type === 'init' && sharedBuffer) { + sharedFloatArray = new Float32Array(sharedBuffer); + self.postMessage({ type: 'init', success: true }); + return; + } + + // 处理 SharedArrayBuffer 数据 + if (type === 'shared' && sharedFloatArray) { + processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig); + self.postMessage({ id, result: null }); + return; + } + + // 传统处理方式 + if (entities) { + const result = workerProcess(entities, deltaTime, systemConfig); + + if (result && typeof result.then === 'function') { + result.then(finalResult => { + self.postMessage({ id, result: finalResult }); + }).catch(error => { + self.postMessage({ id, error: error.message }); + }); + } else { + self.postMessage({ id, result }); + } + } + } catch (error) { + self.postMessage({ id, error: error.message }); + } +}; + +/** + * Entity processing function - extracted from ${system.className}.workerProcess + */ +function workerProcess(${workerProcessParams.entities}, ${workerProcessParams.deltaTime}, ${workerProcessParams.config}) { + ${workerProcessBody} +} + +/** + * SharedArrayBuffer processing function + */ +function processSharedArrayBuffer(startIndex, endIndex, deltaTime, systemConfig) { + if (!sharedFloatArray) return; + ${sharedBufferProcessBody || '// No SharedArrayBuffer processing defined'} +} +`; +} + +/** + * 生成映射文件 + * Generate mapping file + */ +function generateMappingFile( + success: Array<{ className: string; outputPath: string }>, + config: GeneratorConfig +): void { + const mapping: WorkerScriptMapping = { + generatedAt: new Date().toISOString(), + mappings: {}, + }; + + for (const item of success) { + // 使用相对于输出目录的路径 + // Use path relative to output directory + const relativePath = path.relative(config.outDir, item.outputPath).replace(/\\/g, '/'); + mapping.mappings[item.className] = relativePath; + } + + const mappingPath = path.join(config.outDir, 'worker-mapping.json'); + fs.writeFileSync(mappingPath, JSON.stringify(mapping, null, 2), 'utf8'); + + if (config.verbose) { + console.log(` Generated mapping: ${mappingPath}`); + } +} + +/** + * 将 camelCase/PascalCase 转换为 kebab-case + * Convert camelCase/PascalCase to kebab-case + */ +function toKebabCase(str: string): string { + return str + .replace(/([a-z])([A-Z])/g, '$1-$2') + .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') + .toLowerCase(); +} + +/** + * ES6+ 到 ES5 转换(用于微信小游戏兼容性) + * ES6+ to ES5 conversion (for WeChat Mini Game compatibility) + * + * 使用 TypeScript 编译器进行安全的代码转换 + * Uses TypeScript compiler for safe code transformation + */ +function convertToES5(code: string): string { + // 使用 ts-morph 已有的 TypeScript 依赖进行转换 + // Use ts-morph's TypeScript dependency for transformation + const ts = require('typescript'); + + const result = ts.transpileModule(code, { + compilerOptions: { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.None, + removeComments: false, + // 不生成严格模式声明 + noImplicitUseStrict: true, + } + }); + + return result.outputText; +} diff --git a/packages/worker-generator/src/index.ts b/packages/worker-generator/src/index.ts new file mode 100644 index 00000000..251d9cee --- /dev/null +++ b/packages/worker-generator/src/index.ts @@ -0,0 +1,41 @@ +/** + * @esengine/worker-generator + * + * CLI tool to generate Worker files from WorkerEntitySystem classes + * for WeChat Mini Game and other platforms that require pre-compiled Worker scripts. + * + * 从 WorkerEntitySystem 子类生成 Worker 文件的 CLI 工具 + * 用于微信小游戏等需要预编译 Worker 脚本的平台 + * + * @example + * ```bash + * # CLI 使用 | CLI Usage + * npx esengine-worker-gen --src ./src --out ./workers --wechat + * + * # 或者 | Or + * pnpm esengine-worker-gen -s ./src -o ./workers -w + * ``` + * + * @example + * ```typescript + * // API 使用 | API Usage + * import { parseWorkerSystems, generateWorkerFiles } from '@esengine/worker-generator'; + * + * const systems = parseWorkerSystems({ + * srcDir: './src', + * outDir: './workers', + * wechat: true, + * }); + * + * const result = generateWorkerFiles(systems, config); + * ``` + */ + +export { parseWorkerSystems } from './parser'; +export { generateWorkerFiles } from './generator'; +export type { + WorkerSystemInfo, + GeneratorConfig, + GenerationResult, + WorkerScriptMapping, +} from './types'; diff --git a/packages/worker-generator/src/parser.ts b/packages/worker-generator/src/parser.ts new file mode 100644 index 00000000..aa8f9e30 --- /dev/null +++ b/packages/worker-generator/src/parser.ts @@ -0,0 +1,273 @@ +/** + * AST 解析器 - 提取 WorkerEntitySystem 子类信息 + * AST Parser - Extract WorkerEntitySystem subclass information + */ + +import { Project, SyntaxKind, ClassDeclaration, MethodDeclaration, Node } from 'ts-morph'; +import * as path from 'path'; +import type { WorkerSystemInfo, GeneratorConfig } from './types'; + +/** + * 解析项目中的 WorkerEntitySystem 子类 + * Parse WorkerEntitySystem subclasses in the project + */ +export function parseWorkerSystems(config: GeneratorConfig): WorkerSystemInfo[] { + const project = new Project({ + tsConfigFilePath: config.tsConfigPath, + skipAddingFilesFromTsConfig: true, + }); + + // 添加源文件 + // Add source files + const globPattern = path.join(config.srcDir, '**/*.ts').replace(/\\/g, '/'); + project.addSourceFilesAtPaths(globPattern); + + const results: WorkerSystemInfo[] = []; + + for (const sourceFile of project.getSourceFiles()) { + const filePath = sourceFile.getFilePath(); + + // 跳过 node_modules 和 .d.ts 文件 + // Skip node_modules and .d.ts files + if (filePath.includes('node_modules') || filePath.endsWith('.d.ts')) { + continue; + } + + for (const classDecl of sourceFile.getClasses()) { + const info = extractWorkerSystemInfo(classDecl, filePath, config.verbose); + if (info) { + results.push(info); + } + } + } + + return results; +} + +/** + * 检查类是否继承自 WorkerEntitySystem + * Check if class extends WorkerEntitySystem + */ +function isWorkerEntitySystemSubclass(classDecl: ClassDeclaration): boolean { + const extendsClause = classDecl.getExtends(); + if (!extendsClause) { + return false; + } + + const extendsText = extendsClause.getText(); + + // 直接检查是否继承 WorkerEntitySystem + // Directly check if extends WorkerEntitySystem + if (extendsText.startsWith('WorkerEntitySystem')) { + return true; + } + + // 递归检查基类(如果需要) + // Recursively check base class (if needed) + // 这里简化处理,只检查直接继承 + // Simplified: only check direct inheritance + + return false; +} + +/** + * 提取 WorkerEntitySystem 子类信息 + * Extract WorkerEntitySystem subclass information + */ +function extractWorkerSystemInfo( + classDecl: ClassDeclaration, + filePath: string, + verbose?: boolean +): WorkerSystemInfo | null { + if (!isWorkerEntitySystemSubclass(classDecl)) { + return null; + } + + const className = classDecl.getName(); + if (!className) { + return null; + } + + if (verbose) { + console.log(` Found WorkerEntitySystem: ${className} in ${filePath}`); + } + + // 查找 workerProcess 方法 + // Find workerProcess method + const workerProcessMethod = classDecl.getMethod('workerProcess'); + if (!workerProcessMethod) { + if (verbose) { + console.log(` Warning: No workerProcess method found in ${className}`); + } + return null; + } + + // 提取方法体 + // Extract method body + const workerProcessBody = extractMethodBody(workerProcessMethod); + if (!workerProcessBody) { + if (verbose) { + console.log(` Warning: Could not extract workerProcess body from ${className}`); + } + return null; + } + + // 提取参数名 + // Extract parameter names + const params = workerProcessMethod.getParameters(); + const workerProcessParams = { + entities: params[0]?.getName() || 'entities', + deltaTime: params[1]?.getName() || 'deltaTime', + config: params[2]?.getName() || 'config', + }; + + // 尝试提取 getSharedArrayBufferProcessFunction + // Try to extract getSharedArrayBufferProcessFunction + let sharedBufferProcessBody: string | undefined; + const sharedBufferMethod = classDecl.getMethod('getSharedArrayBufferProcessFunction'); + if (sharedBufferMethod) { + sharedBufferProcessBody = extractSharedBufferFunctionBody(sharedBufferMethod); + } + + // 尝试提取 entityDataSize + // Try to extract entityDataSize + let entityDataSize: number | undefined; + const getDefaultEntityDataSizeMethod = classDecl.getMethod('getDefaultEntityDataSize'); + if (getDefaultEntityDataSizeMethod) { + entityDataSize = extractEntityDataSize(getDefaultEntityDataSizeMethod); + } + + // 尝试从构造函数中提取 workerScriptPath 配置 + // Try to extract workerScriptPath from constructor + const workerScriptPath = extractWorkerScriptPath(classDecl, verbose); + + return { + className, + filePath, + workerProcessBody, + workerProcessParams, + sharedBufferProcessBody, + entityDataSize, + workerScriptPath, + }; +} + +/** + * 提取方法体(去掉方法签名,保留函数体内容) + * Extract method body (remove method signature, keep function body content) + */ +function extractMethodBody(method: MethodDeclaration): string | null { + const body = method.getBody(); + if (!body) { + return null; + } + + // 获取方法体的文本 + // Get method body text + let bodyText = body.getText(); + + // 去掉外层的花括号 + // Remove outer braces + if (bodyText.startsWith('{') && bodyText.endsWith('}')) { + bodyText = bodyText.slice(1, -1).trim(); + } + + return bodyText; +} + +/** + * 提取 getSharedArrayBufferProcessFunction 返回的函数体 + * Extract function body returned by getSharedArrayBufferProcessFunction + */ +function extractSharedBufferFunctionBody(method: MethodDeclaration): string | undefined { + const body = method.getBody(); + if (!body) { + return undefined; + } + + // 查找 return 语句中的函数表达式 + // Find function expression in return statement + const returnStatements = body.getDescendantsOfKind(SyntaxKind.ReturnStatement); + for (const returnStmt of returnStatements) { + const expression = returnStmt.getExpression(); + if (expression) { + // 检查是否是函数表达式或箭头函数 + // Check if it's a function expression or arrow function + if (Node.isFunctionExpression(expression) || Node.isArrowFunction(expression)) { + const funcBody = expression.getBody(); + if (funcBody) { + let bodyText = funcBody.getText(); + if (bodyText.startsWith('{') && bodyText.endsWith('}')) { + bodyText = bodyText.slice(1, -1).trim(); + } + return bodyText; + } + } + } + } + + return undefined; +} + +/** + * 提取 entityDataSize 值 + * Extract entityDataSize value + */ +function extractEntityDataSize(method: MethodDeclaration): number | undefined { + const body = method.getBody(); + if (!body) { + return undefined; + } + + // 查找 return 语句 + // Find return statement + const returnStatements = body.getDescendantsOfKind(SyntaxKind.ReturnStatement); + for (const returnStmt of returnStatements) { + const expression = returnStmt.getExpression(); + if (expression && Node.isNumericLiteral(expression)) { + return parseInt(expression.getText(), 10); + } + } + + return undefined; +} + +/** + * 从构造函数中提取 workerScriptPath 配置 + * Extract workerScriptPath from constructor + */ +function extractWorkerScriptPath(classDecl: ClassDeclaration, verbose?: boolean): string | undefined { + // 查找构造函数 + // Find constructor + const constructors = classDecl.getConstructors(); + if (constructors.length === 0) { + return undefined; + } + + const constructor = constructors[0]!; + const body = constructor.getBody(); + if (!body) { + return undefined; + } + + const bodyText = body.getText(); + + // 使用正则表达式查找 workerScriptPath: 'xxx' 或 workerScriptPath: "xxx" + // Use regex to find workerScriptPath: 'xxx' or workerScriptPath: "xxx" + const patterns = [ + /workerScriptPath\s*:\s*['"]([^'"]+)['"]/, + /workerScriptPath\s*:\s*`([^`]+)`/, + ]; + + for (const pattern of patterns) { + const match = bodyText.match(pattern); + if (match && match[1]) { + if (verbose) { + console.log(` Found workerScriptPath: ${match[1]}`); + } + return match[1]; + } + } + + return undefined; +} diff --git a/packages/worker-generator/src/types.ts b/packages/worker-generator/src/types.ts new file mode 100644 index 00000000..48331672 --- /dev/null +++ b/packages/worker-generator/src/types.ts @@ -0,0 +1,85 @@ +/** + * Worker 生成器类型定义 + * Type definitions for Worker generator + */ + +/** + * 提取的 WorkerEntitySystem 信息 + * Extracted WorkerEntitySystem information + */ +export interface WorkerSystemInfo { + /** 类名 | Class name */ + className: string; + /** 源文件路径 | Source file path */ + filePath: string; + /** workerProcess 方法体 | workerProcess method body */ + workerProcessBody: string; + /** workerProcess 参数名 | workerProcess parameter names */ + workerProcessParams: { + entities: string; + deltaTime: string; + config: string; + }; + /** getSharedArrayBufferProcessFunction 方法体(可选)| getSharedArrayBufferProcessFunction body (optional) */ + sharedBufferProcessBody?: string; + /** entityDataSize 值(如果是字面量)| entityDataSize value (if literal) */ + entityDataSize?: number; + /** 用户配置的 workerScriptPath(从构造函数中提取)| User configured workerScriptPath */ + workerScriptPath?: string; +} + +/** + * 生成器配置 + * Generator configuration + */ +export interface GeneratorConfig { + /** 源代码目录 | Source directory */ + srcDir: string; + /** 输出目录 | Output directory */ + outDir: string; + /** 是否使用微信小游戏格式 | Whether to use WeChat Mini Game format */ + wechat?: boolean; + /** 是否生成映射文件 | Whether to generate mapping file */ + generateMapping?: boolean; + /** TypeScript 配置文件路径 | TypeScript config file path */ + tsConfigPath?: string; + /** 是否详细输出 | Verbose output */ + verbose?: boolean; +} + +/** + * 生成结果 + * Generation result + */ +export interface GenerationResult { + /** 成功生成的文件 | Successfully generated files */ + success: Array<{ + className: string; + outputPath: string; + /** 用户配置的路径(如果有)| User configured path (if any) */ + configuredPath?: string; + }>; + /** 失败的类 | Failed classes */ + errors: Array<{ + className: string; + filePath: string; + error: string; + }>; + /** 需要用户配置 workerScriptPath 的类 | Classes that need workerScriptPath configuration */ + skipped: Array<{ + className: string; + suggestedPath: string; + reason: string; + }>; +} + +/** + * Worker 脚本映射 + * Worker script mapping + */ +export interface WorkerScriptMapping { + /** 生成时间 | Generation timestamp */ + generatedAt: string; + /** 映射表:类名 -> Worker 文件路径 | Mapping: class name -> Worker file path */ + mappings: Record; +} diff --git a/packages/worker-generator/tsconfig.json b/packages/worker-generator/tsconfig.json new file mode 100644 index 00000000..d605338e --- /dev/null +++ b/packages/worker-generator/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e98e938..c1f3d378 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1563,6 +1563,25 @@ importers: specifier: ^5.3.3 version: 5.9.3 + packages/worker-generator: + dependencies: + chalk: + specifier: ^4.1.2 + version: 4.1.2 + commander: + specifier: ^11.1.0 + version: 11.1.0 + ts-morph: + specifier: ^21.0.1 + version: 21.0.1 + devDependencies: + '@types/node': + specifier: ^20.10.0 + version: 20.19.25 + typescript: + specifier: ^5.3.0 + version: 5.9.3 + packages/world-streaming: dependencies: '@esengine/ecs-framework': @@ -4113,6 +4132,9 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + '@ts-morph/common@0.22.0': + resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==} + '@tufjs/canonical-json@2.0.0': resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} engines: {node: ^16.14.0 || >=18.0.0} @@ -4928,6 +4950,9 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + code-block-writer@12.0.0: + resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} + coi-serviceworker@0.1.7: resolution: {integrity: sha512-bjSUqEngCPOkErY2vbyWsaIGCNRODYzlNycaREVw5s12/C8SM+RnRUUeX6pZbTtov6C52ZLY/+tvHK+BDxuUuA==} @@ -4962,6 +4987,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -6989,6 +7018,11 @@ packages: engines: {node: '>=10'} hasBin: true + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -8411,6 +8445,9 @@ packages: jest-util: optional: true + ts-morph@21.0.1: + resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==} + tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -11896,6 +11933,13 @@ snapshots: '@tootallnate/once@2.0.0': {} + '@ts-morph/common@0.22.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 9.0.5 + mkdirp: 3.0.1 + path-browserify: 1.0.1 + '@tufjs/canonical-json@2.0.0': {} '@tufjs/models@2.0.1': @@ -12846,6 +12890,8 @@ snapshots: co@4.6.0: {} + code-block-writer@12.0.0: {} + coi-serviceworker@0.1.7: {} collect-v8-coverage@1.0.3: {} @@ -12875,6 +12921,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@11.1.0: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -14393,7 +14441,7 @@ snapshots: jest-diff@29.7.0: dependencies: - chalk: 4.1.0 + chalk: 4.1.2 diff-sequences: 29.6.3 jest-get-type: 29.6.3 pretty-format: 29.7.0 @@ -15543,6 +15591,8 @@ snapshots: mkdirp@1.0.4: {} + mkdirp@3.0.1: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -15730,7 +15780,7 @@ snapshots: '@yarnpkg/parsers': 3.0.2 '@zkochan/js-yaml': 0.0.7 axios: 1.13.2 - chalk: 4.1.0 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 cliui: 8.0.1 @@ -15812,7 +15862,7 @@ snapshots: ora@5.3.0: dependencies: bl: 4.1.0 - chalk: 4.1.0 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 is-interactive: 1.0.0 @@ -17045,6 +17095,11 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.28.5) jest-util: 29.7.0 + ts-morph@21.0.1: + dependencies: + '@ts-morph/common': 0.22.0 + code-block-writer: 12.0.0 + tsconfig-paths@4.2.0: dependencies: json5: 2.2.3