feat(worker): 添加微信小游戏 Worker 支持和 Worker Generator CLI (#297)
* 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 进行安全可靠的代码转换 - 移除所有可能导致回溯的正则表达式
This commit is contained in:
@@ -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
|
### Features
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
### Features
|
||||||
|
|
||||||
|
|||||||
570
docs/en/guide/worker-system.md
Normal file
570
docs/en/guide/worker-system.md
Normal file
@@ -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<PhysicsData> {
|
||||||
|
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<PhysicsData> {
|
||||||
|
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.
|
||||||
@@ -6,409 +6,198 @@
|
|||||||
|
|
||||||
## 特性支持
|
## 特性支持
|
||||||
|
|
||||||
- ✅ **Worker**: 支持(通过 `wx.createWorker` 创建,需要配置 game.json)
|
| 特性 | 支持情况 | 说明 |
|
||||||
- ❌ **SharedArrayBuffer**: 不支持
|
|------|----------|------|
|
||||||
- ❌ **Transferable Objects**: 不支持(只支持可序列化对象)
|
| **Worker** | ✅ 支持 | 需要使用预编译文件,配置 `workerScriptPath` |
|
||||||
- ✅ **高精度时间**: 使用 `Date.now()` 或 `wx.getPerformance()`
|
| **SharedArrayBuffer** | ❌ 不支持 | 微信小游戏环境不支持 |
|
||||||
- ✅ **设备信息**: 完整的微信小游戏设备信息
|
| **Transferable Objects** | ❌ 不支持 | 只支持可序列化对象 |
|
||||||
|
| **高精度时间** | ✅ 支持 | 使用 `wx.getPerformance()` |
|
||||||
|
| **设备信息** | ✅ 支持 | 完整的微信小游戏设备信息 |
|
||||||
|
|
||||||
## 完整实现
|
## WorkerEntitySystem 使用方式
|
||||||
|
|
||||||
```typescript
|
### 重要:微信小游戏 Worker 限制
|
||||||
import type {
|
|
||||||
IPlatformAdapter,
|
|
||||||
PlatformWorker,
|
|
||||||
WorkerCreationOptions,
|
|
||||||
PlatformConfig,
|
|
||||||
WeChatDeviceInfo
|
|
||||||
} from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
/**
|
微信小游戏的 Worker 有以下限制:
|
||||||
* 微信小游戏平台适配器
|
- **Worker 脚本必须在代码包内**,不能动态生成
|
||||||
* 支持微信小游戏环境
|
- **必须在 `game.json` 中配置** `workers` 目录
|
||||||
*/
|
- **最多只能创建 1 个 Worker**
|
||||||
export class WeChatMiniGameAdapter implements IPlatformAdapter {
|
|
||||||
public readonly name = 'wechat-minigame';
|
|
||||||
public readonly version: string;
|
|
||||||
private systemInfo: any;
|
|
||||||
|
|
||||||
constructor() {
|
因此,使用 `WorkerEntitySystem` 时有两种方式:
|
||||||
// 获取微信小游戏版本信息
|
1. **推荐:使用 CLI 工具自动生成** Worker 文件
|
||||||
this.systemInfo = this.getSystemInfo();
|
2. 手动创建 Worker 文件
|
||||||
this.version = this.systemInfo.version || 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
### 方式一:使用 CLI 工具自动生成(推荐)
|
||||||
* 检查是否支持Worker
|
|
||||||
*/
|
|
||||||
public isWorkerSupported(): boolean {
|
|
||||||
// 微信小游戏支持Worker,通过wx.createWorker创建
|
|
||||||
return typeof wx !== 'undefined' && typeof wx.createWorker === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
我们提供了 `@esengine/worker-generator` 工具,可以自动从你的 TypeScript 代码中提取 `workerProcess` 函数并生成微信小游戏兼容的 Worker 文件。
|
||||||
* 检查是否支持SharedArrayBuffer(不支持)
|
|
||||||
*/
|
|
||||||
public isSharedArrayBufferSupported(): boolean {
|
|
||||||
return false; // 微信小游戏不支持SharedArrayBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
#### 安装
|
||||||
* 获取硬件并发数
|
|
||||||
*/
|
|
||||||
public getHardwareConcurrency(): number {
|
|
||||||
// 微信小游戏官方限制:最多只能创建 1 个 Worker
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
```bash
|
||||||
* 创建Worker
|
pnpm add -D @esengine/worker-generator
|
||||||
* @param script 脚本内容或文件路径
|
# 或
|
||||||
* @param options Worker创建选项
|
npm install --save-dev @esengine/worker-generator
|
||||||
*/
|
```
|
||||||
public createWorker(script: string, options: WorkerCreationOptions = {}): PlatformWorker {
|
|
||||||
if (!this.isWorkerSupported()) {
|
|
||||||
throw new Error('微信小游戏不支持Worker');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
#### 使用
|
||||||
return new WeChatWorker(script, options);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`创建微信Worker失败: ${(error as Error).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
```bash
|
||||||
* 创建SharedArrayBuffer(不支持)
|
# 扫描 src 目录,生成 Worker 文件到 workers 目录
|
||||||
*/
|
npx esengine-worker-gen --src ./src --out ./workers --wechat
|
||||||
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
|
||||||
return null; // 微信小游戏不支持SharedArrayBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
# 查看帮助
|
||||||
* 获取高精度时间戳
|
npx esengine-worker-gen --help
|
||||||
*/
|
```
|
||||||
public getHighResTimestamp(): number {
|
|
||||||
// 尝试使用微信的性能API,否则使用Date.now()
|
|
||||||
if (typeof wx !== 'undefined' && wx.getPerformance) {
|
|
||||||
const performance = wx.getPerformance();
|
|
||||||
return performance.now();
|
|
||||||
}
|
|
||||||
return Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
#### 参数说明
|
||||||
* 获取平台配置
|
|
||||||
*/
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
| 参数 | 说明 | 默认值 |
|
||||||
* 获取微信小游戏设备信息
|
|------|------|--------|
|
||||||
*/
|
| `-s, --src <dir>` | 源代码目录 | `./src` |
|
||||||
public getDeviceInfo(): WeChatDeviceInfo {
|
| `-o, --out <dir>` | 输出目录 | `./workers` |
|
||||||
return {
|
| `-w, --wechat` | 生成微信小游戏兼容代码 | `false` |
|
||||||
// 设备基础信息
|
| `-m, --mapping` | 生成 worker-mapping.json | `true` |
|
||||||
brand: this.systemInfo.brand,
|
| `-t, --tsconfig <path>` | TypeScript 配置文件路径 | 自动查找 |
|
||||||
model: this.systemInfo.model,
|
| `-v, --verbose` | 详细输出 | `false` |
|
||||||
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,
|
|
||||||
|
|
||||||
// 窗口信息
|
#### 示例输出
|
||||||
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,
|
🔧 ESEngine Worker Generator
|
||||||
language: this.systemInfo.language,
|
|
||||||
theme: this.systemInfo.theme,
|
|
||||||
SDKVersion: this.systemInfo.SDKVersion,
|
|
||||||
enableDebug: this.systemInfo.enableDebug,
|
|
||||||
fontSizeSetting: this.systemInfo.fontSizeSetting,
|
|
||||||
host: this.systemInfo.host
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
Source directory: /project/src
|
||||||
* 异步获取完整的平台配置
|
Output directory: /project/workers
|
||||||
*/
|
WeChat mode: Yes
|
||||||
public async getPlatformConfigAsync(): Promise<PlatformConfig> {
|
|
||||||
// 可以在这里添加异步获取设备性能信息的逻辑
|
|
||||||
const baseConfig = this.getPlatformConfig();
|
|
||||||
|
|
||||||
// 尝试获取设备性能信息
|
Scanning for WorkerEntitySystem classes...
|
||||||
try {
|
|
||||||
const benchmarkLevel = await this.getBenchmarkLevel();
|
|
||||||
baseConfig.extensions = {
|
|
||||||
...baseConfig.extensions,
|
|
||||||
benchmarkLevel
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('获取性能基准失败:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseConfig;
|
✓ Found 1 WorkerEntitySystem class(es):
|
||||||
}
|
- PhysicsSystem (src/systems/PhysicsSystem.ts)
|
||||||
|
|
||||||
/**
|
Generating Worker files...
|
||||||
* 检查是否支持Transferable Objects
|
|
||||||
*/
|
|
||||||
private checkTransferableObjectsSupport(): boolean {
|
|
||||||
// 微信小游戏不支持 Transferable Objects
|
|
||||||
// 基础库 2.20.2 之前只支持可序列化的 key-value 对象
|
|
||||||
// 2.20.2 之后支持任意类型数据,但仍然不支持 Transferable Objects
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
✓ Successfully generated 1 Worker file(s):
|
||||||
* 获取系统信息
|
- PhysicsSystem -> workers/physics-system-worker.js
|
||||||
*/
|
|
||||||
private getSystemInfo(): any {
|
|
||||||
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) {
|
|
||||||
try {
|
|
||||||
return wx.getSystemInfoSync();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('获取微信系统信息失败:', error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
📝 Usage:
|
||||||
* 获取内存限制
|
1. Copy the generated files to your project's workers/ directory
|
||||||
*/
|
2. Configure game.json (WeChat): { "workers": "workers" }
|
||||||
private getMemoryLimit(): number {
|
3. In your System constructor, add:
|
||||||
// 微信小游戏通常有内存限制
|
workerScriptPath: 'workers/physics-system-worker.js'
|
||||||
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();
|
|
||||||
|
|
||||||
if (unit === 'GB') {
|
#### 在构建流程中集成
|
||||||
return value * 1024 * 1024 * 1024;
|
|
||||||
} else if (unit === 'MB') {
|
|
||||||
return value * 1024 * 1024;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认限制为512MB
|
```json
|
||||||
return 512 * 1024 * 1024;
|
// package.json
|
||||||
}
|
{
|
||||||
|
"scripts": {
|
||||||
/**
|
"build:workers": "esengine-worker-gen --src ./src --out ./workers --wechat",
|
||||||
* 异步获取设备性能基准
|
"build": "pnpm build:workers && your-build-command"
|
||||||
*/
|
}
|
||||||
private async getBenchmarkLevel(): Promise<number> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用方法
|
### 方式二:手动创建 Worker 文件
|
||||||
|
|
||||||
### 1. 复制代码
|
如果你不想使用 CLI 工具,也可以手动创建 Worker 文件。
|
||||||
|
|
||||||
将上述代码复制到你的项目中,例如 `src/platform/WeChatMiniGameAdapter.ts`。
|
在项目中创建 `workers/entity-worker.js`:
|
||||||
|
|
||||||
### 2. 注册适配器
|
```javascript
|
||||||
|
// workers/entity-worker.js
|
||||||
|
// 微信小游戏 WorkerEntitySystem 通用 Worker 模板
|
||||||
|
|
||||||
```typescript
|
let sharedFloatArray = null;
|
||||||
import { PlatformManager } from '@esengine/ecs-framework';
|
|
||||||
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
|
||||||
|
|
||||||
// 检查是否在微信小游戏环境
|
worker.onMessage(function(e) {
|
||||||
if (typeof wx !== 'undefined') {
|
const { type, id, entities, deltaTime, systemConfig, startIndex, endIndex, sharedBuffer } = e.data;
|
||||||
const wechatAdapter = new WeChatMiniGameAdapter();
|
|
||||||
PlatformManager.getInstance().registerAdapter(wechatAdapter);
|
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
|
```typescript
|
||||||
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
import { WorkerEntitySystem, Matcher, Entity } from '@esengine/ecs-framework';
|
||||||
@@ -426,13 +215,17 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super(Matcher.all(Transform, Velocity), {
|
super(Matcher.all(Transform, Velocity), {
|
||||||
enableWorker: true,
|
enableWorker: true,
|
||||||
workerCount: 1, // 微信小游戏限制只能创建1个Worker
|
workerCount: 1, // 微信小游戏限制只能创建 1 个 Worker
|
||||||
systemConfig: { gravity: 100, friction: 0.95 }
|
workerScriptPath: 'workers/entity-worker.js', // 指定预编译的 Worker 文件
|
||||||
|
systemConfig: {
|
||||||
|
gravity: 100,
|
||||||
|
friction: 0.95
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDefaultEntityDataSize(): number {
|
protected getDefaultEntityDataSize(): number {
|
||||||
return 6; // id, x, y, vx, vy, mass
|
return 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected extractEntityData(entity: Entity): PhysicsData {
|
protected extractEntityData(entity: Entity): PhysicsData {
|
||||||
@@ -450,20 +243,15 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkerEntitySystem 会自动将此函数序列化并写入临时文件
|
// 注意:在微信小游戏中,此方法不会被使用
|
||||||
|
// Worker 的处理逻辑在 workers/entity-worker.js 中的 workerProcess 函数里
|
||||||
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
protected workerProcess(entities: PhysicsData[], deltaTime: number, config: any): PhysicsData[] {
|
||||||
return entities.map(entity => {
|
return entities.map(entity => {
|
||||||
// 应用重力
|
|
||||||
entity.vy += config.gravity * deltaTime;
|
entity.vy += config.gravity * deltaTime;
|
||||||
|
|
||||||
// 更新位置
|
|
||||||
entity.x += entity.vx * deltaTime;
|
entity.x += entity.vx * deltaTime;
|
||||||
entity.y += entity.vy * deltaTime;
|
entity.y += entity.vy * deltaTime;
|
||||||
|
|
||||||
// 应用摩擦力
|
|
||||||
entity.vx *= config.friction;
|
entity.vx *= config.friction;
|
||||||
entity.vy *= config.friction;
|
entity.vy *= config.friction;
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -477,201 +265,219 @@ class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
|||||||
velocity.x = result.vx;
|
velocity.x = result.vx;
|
||||||
velocity.y = result.vy;
|
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
|
```typescript
|
||||||
// 1. 在 game.json 中配置 Worker 路径
|
class PhysicsSystem extends WorkerEntitySystem<PhysicsData> {
|
||||||
/*
|
constructor() {
|
||||||
{
|
super(Matcher.all(Transform, Velocity), {
|
||||||
"workers": "workers"
|
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) {
|
```typescript
|
||||||
// 处理物理计算
|
import type {
|
||||||
const results = entities.map(entity => {
|
IPlatformAdapter,
|
||||||
entity.vy += systemConfig.gravity * deltaTime;
|
PlatformWorker,
|
||||||
entity.x += entity.vx * deltaTime;
|
WorkerCreationOptions,
|
||||||
entity.y += entity.vy * deltaTime;
|
PlatformConfig
|
||||||
return entity;
|
} 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会自动处理)
|
public createSharedArrayBuffer(length: number): SharedArrayBuffer | null {
|
||||||
const adapter = PlatformManager.getInstance().getAdapter();
|
return null;
|
||||||
const worker = adapter.createWorker('workers/physics.js');
|
}
|
||||||
|
|
||||||
|
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
|
```typescript
|
||||||
const manager = PlatformManager.getInstance();
|
import { PlatformManager } from '@esengine/ecs-framework';
|
||||||
if (manager.hasAdapter()) {
|
import { WeChatMiniGameAdapter } from './platform/WeChatMiniGameAdapter';
|
||||||
const adapter = manager.getAdapter();
|
|
||||||
console.log('微信设备信息:', adapter.getDeviceInfo());
|
// 在游戏启动时注册适配器
|
||||||
|
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)
|
- [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.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.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 个 Worker |
|
||||||
- **版本要求**: 需要基础库 1.9.90 及以上版本
|
| 版本要求 | 需要基础库 1.9.90 及以上 |
|
||||||
- **脚本支持**: 不支持 blob URL,只能使用文件路径或写入文件系统
|
| 脚本位置 | 必须在代码包内,不支持动态生成 |
|
||||||
- **文件路径**: Worker 脚本路径必须为绝对路径,但不能以 "/" 开头
|
| 生命周期 | 创建新 Worker 前必须先 terminate() |
|
||||||
- **生命周期**: 创建新 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 时无需此配置,框架会自动将脚本写入临时文件。
|
|
||||||
|
|
||||||
### 内存限制
|
### 内存限制
|
||||||
|
|
||||||
微信小游戏有严格的内存限制:
|
|
||||||
|
|
||||||
- 通常限制在 256MB - 512MB
|
- 通常限制在 256MB - 512MB
|
||||||
- 需要及时释放不用的资源
|
- 需要及时释放不用的资源
|
||||||
- 避免内存泄漏
|
- 建议监听内存警告:
|
||||||
|
|
||||||
### API 限制
|
|
||||||
|
|
||||||
- 不支持 `eval()` 函数
|
|
||||||
- 不支持 `Function` 构造器
|
|
||||||
- DOM API 受限
|
|
||||||
- 文件系统 API 受限
|
|
||||||
|
|
||||||
## 性能优化建议
|
|
||||||
|
|
||||||
### 1. 分帧处理
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class FramedProcessor {
|
wx.onMemoryWarning(() => {
|
||||||
private tasks: (() => void)[] = [];
|
console.warn('收到内存警告,开始清理资源');
|
||||||
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('内存使用率过高,建议清理资源');
|
|
||||||
// 触发垃圾回收或资源清理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 调试技巧
|
## 调试技巧
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 检查微信小游戏环境
|
// 检查 Worker 配置
|
||||||
if (typeof wx !== 'undefined') {
|
const adapter = PlatformManager.getInstance().getAdapter();
|
||||||
const adapter = new WeChatMiniGameAdapter();
|
const config = adapter.getPlatformConfig();
|
||||||
|
|
||||||
console.log('微信版本:', adapter.version);
|
console.log('Worker 支持:', adapter.isWorkerSupported());
|
||||||
console.log('设备信息:', adapter.getDeviceInfo());
|
console.log('最大 Worker 数:', config.maxWorkerCount);
|
||||||
console.log('平台配置:', adapter.getPlatformConfig());
|
console.log('平台限制:', config.limitations);
|
||||||
|
|
||||||
// 检查功能支持
|
|
||||||
console.log('Worker支持:', adapter.isWorkerSupported());
|
|
||||||
console.log('SharedArrayBuffer支持:', adapter.isSharedArrayBufferSupported());
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 微信小游戏特殊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('收到内存警告,开始清理资源');
|
|
||||||
// 清理不必要的资源
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -145,6 +145,8 @@ interface WorkerSystemConfig {
|
|||||||
entityDataSize?: number;
|
entityDataSize?: number;
|
||||||
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
||||||
maxEntities?: number;
|
maxEntities?: number;
|
||||||
|
/** 预编译的Worker脚本路径(用于微信小游戏等不支持动态脚本的平台) */
|
||||||
|
workerScriptPath?: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -605,4 +607,166 @@ public getPerformanceMetrics(): WorkerPerformanceMetrics {
|
|||||||
- SharedArrayBuffer优化
|
- 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<PhysicsData> {
|
||||||
|
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框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。
|
Worker系统为ECS框架提供了强大的并行计算能力,让你能够充分利用现代多核处理器的性能,为复杂的游戏逻辑和计算密集型任务提供了高效的解决方案。
|
||||||
@@ -123,7 +123,9 @@ class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
|
|||||||
enableWorker,
|
enableWorker,
|
||||||
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
|
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
|
||||||
systemConfig: defaultConfig,
|
systemConfig: defaultConfig,
|
||||||
useSharedArrayBuffer: true
|
useSharedArrayBuffer: true,
|
||||||
|
// 微信小游戏等平台需要配置此路径,CLI 工具会根据此路径生成 Worker 文件
|
||||||
|
workerScriptPath: 'workers/physics-worker.js'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
277
examples/core-demos/workers/physics-worker.js
Normal file
277
examples/core-demos/workers/physics-worker.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
6
examples/core-demos/workers/worker-mapping.json
Normal file
6
examples/core-demos/workers/worker-mapping.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"generatedAt": "2025-12-08T08:57:32.415Z",
|
||||||
|
"mappings": {
|
||||||
|
"PhysicsWorkerSystem": "physics-worker.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,20 @@ export interface WorkerSystemConfig {
|
|||||||
entityDataSize?: number;
|
entityDataSize?: number;
|
||||||
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
/** 最大实体数量(用于预分配SharedArrayBuffer) */
|
||||||
maxEntities?: number;
|
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<TEntityData = any> extends EntitySystem {
|
export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem {
|
||||||
protected config: Required<Omit<WorkerSystemConfig, 'systemConfig' | 'entitiesPerWorker'>> & {
|
protected config: Required<Omit<WorkerSystemConfig, 'systemConfig' | 'entitiesPerWorker' | 'workerScriptPath'>> & {
|
||||||
systemConfig?: any;
|
systemConfig?: any;
|
||||||
entitiesPerWorker?: number;
|
entitiesPerWorker?: number;
|
||||||
|
workerScriptPath?: string;
|
||||||
};
|
};
|
||||||
private workerPool: PlatformWorkerPool | null = null;
|
private workerPool: PlatformWorkerPool | null = null;
|
||||||
private isProcessing = false;
|
private isProcessing = false;
|
||||||
@@ -222,7 +237,8 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
|
|||||||
...(config.entitiesPerWorker !== undefined && { entitiesPerWorker: config.entitiesPerWorker }),
|
...(config.entitiesPerWorker !== undefined && { entitiesPerWorker: config.entitiesPerWorker }),
|
||||||
useSharedArrayBuffer: config.useSharedArrayBuffer ?? this.isSharedArrayBufferSupported(),
|
useSharedArrayBuffer: config.useSharedArrayBuffer ?? this.isSharedArrayBufferSupported(),
|
||||||
entityDataSize: config.entityDataSize ?? this.getDefaultEntityDataSize(),
|
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<TEntityData = any> extends EntitySystem
|
|||||||
*/
|
*/
|
||||||
private initializeWorkerPool(): void {
|
private initializeWorkerPool(): void {
|
||||||
try {
|
try {
|
||||||
const script = this.createWorkerScript();
|
|
||||||
|
|
||||||
// 在WorkerEntitySystem中处理平台相关逻辑
|
|
||||||
const workers: PlatformWorker[] = [];
|
|
||||||
const platformConfig = this.platformAdapter.getPlatformConfig();
|
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++) {
|
for (let i = 0; i < this.config.workerCount; i++) {
|
||||||
try {
|
try {
|
||||||
const worker = this.platformAdapter.createWorker(fullScript, {
|
const worker = this.platformAdapter.createWorker(scriptOrPath, {
|
||||||
name: `WorkerEntitySystem-${i}`
|
name: `WorkerEntitySystem-${i}`
|
||||||
});
|
});
|
||||||
workers.push(worker);
|
workers.push(worker);
|
||||||
|
|||||||
137
packages/worker-generator/README.md
Normal file
137
packages/worker-generator/README.md
Normal file
@@ -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<PhysicsData> {
|
||||||
|
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 <dir>` | Source directory to scan | `./src` |
|
||||||
|
| `-o, --out <dir>` | 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>` | 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
|
||||||
56
packages/worker-generator/package.json
Normal file
56
packages/worker-generator/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
172
packages/worker-generator/src/cli.ts
Normal file
172
packages/worker-generator/src/cli.ts
Normal file
@@ -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 <dir>', 'Source directory to scan', './src')
|
||||||
|
.option('-o, --out <dir>', '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>', '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();
|
||||||
325
packages/worker-generator/src/generator.ts
Normal file
325
packages/worker-generator/src/generator.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
41
packages/worker-generator/src/index.ts
Normal file
41
packages/worker-generator/src/index.ts
Normal file
@@ -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';
|
||||||
273
packages/worker-generator/src/parser.ts
Normal file
273
packages/worker-generator/src/parser.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
85
packages/worker-generator/src/types.ts
Normal file
85
packages/worker-generator/src/types.ts
Normal file
@@ -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<string, string>;
|
||||||
|
}
|
||||||
20
packages/worker-generator/tsconfig.json
Normal file
20
packages/worker-generator/tsconfig.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@@ -1563,6 +1563,25 @@ importers:
|
|||||||
specifier: ^5.3.3
|
specifier: ^5.3.3
|
||||||
version: 5.9.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:
|
packages/world-streaming:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@esengine/ecs-framework':
|
'@esengine/ecs-framework':
|
||||||
@@ -4113,6 +4132,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
'@ts-morph/common@0.22.0':
|
||||||
|
resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==}
|
||||||
|
|
||||||
'@tufjs/canonical-json@2.0.0':
|
'@tufjs/canonical-json@2.0.0':
|
||||||
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
|
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
|
||||||
engines: {node: ^16.14.0 || >=18.0.0}
|
engines: {node: ^16.14.0 || >=18.0.0}
|
||||||
@@ -4928,6 +4950,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
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:
|
coi-serviceworker@0.1.7:
|
||||||
resolution: {integrity: sha512-bjSUqEngCPOkErY2vbyWsaIGCNRODYzlNycaREVw5s12/C8SM+RnRUUeX6pZbTtov6C52ZLY/+tvHK+BDxuUuA==}
|
resolution: {integrity: sha512-bjSUqEngCPOkErY2vbyWsaIGCNRODYzlNycaREVw5s12/C8SM+RnRUUeX6pZbTtov6C52ZLY/+tvHK+BDxuUuA==}
|
||||||
|
|
||||||
@@ -4962,6 +4987,10 @@ packages:
|
|||||||
comma-separated-tokens@2.0.3:
|
comma-separated-tokens@2.0.3:
|
||||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||||
|
|
||||||
|
commander@11.1.0:
|
||||||
|
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
commander@2.20.3:
|
commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
@@ -6989,6 +7018,11 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
mkdirp@3.0.1:
|
||||||
|
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
mlly@1.8.0:
|
mlly@1.8.0:
|
||||||
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
||||||
|
|
||||||
@@ -8411,6 +8445,9 @@ packages:
|
|||||||
jest-util:
|
jest-util:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ts-morph@21.0.1:
|
||||||
|
resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==}
|
||||||
|
|
||||||
tsconfig-paths@4.2.0:
|
tsconfig-paths@4.2.0:
|
||||||
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -11896,6 +11933,13 @@ snapshots:
|
|||||||
|
|
||||||
'@tootallnate/once@2.0.0': {}
|
'@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/canonical-json@2.0.0': {}
|
||||||
|
|
||||||
'@tufjs/models@2.0.1':
|
'@tufjs/models@2.0.1':
|
||||||
@@ -12846,6 +12890,8 @@ snapshots:
|
|||||||
|
|
||||||
co@4.6.0: {}
|
co@4.6.0: {}
|
||||||
|
|
||||||
|
code-block-writer@12.0.0: {}
|
||||||
|
|
||||||
coi-serviceworker@0.1.7: {}
|
coi-serviceworker@0.1.7: {}
|
||||||
|
|
||||||
collect-v8-coverage@1.0.3: {}
|
collect-v8-coverage@1.0.3: {}
|
||||||
@@ -12875,6 +12921,8 @@ snapshots:
|
|||||||
|
|
||||||
comma-separated-tokens@2.0.3: {}
|
comma-separated-tokens@2.0.3: {}
|
||||||
|
|
||||||
|
commander@11.1.0: {}
|
||||||
|
|
||||||
commander@2.20.3: {}
|
commander@2.20.3: {}
|
||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
@@ -14393,7 +14441,7 @@ snapshots:
|
|||||||
|
|
||||||
jest-diff@29.7.0:
|
jest-diff@29.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.0
|
chalk: 4.1.2
|
||||||
diff-sequences: 29.6.3
|
diff-sequences: 29.6.3
|
||||||
jest-get-type: 29.6.3
|
jest-get-type: 29.6.3
|
||||||
pretty-format: 29.7.0
|
pretty-format: 29.7.0
|
||||||
@@ -15543,6 +15591,8 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
|
mkdirp@3.0.1: {}
|
||||||
|
|
||||||
mlly@1.8.0:
|
mlly@1.8.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
@@ -15730,7 +15780,7 @@ snapshots:
|
|||||||
'@yarnpkg/parsers': 3.0.2
|
'@yarnpkg/parsers': 3.0.2
|
||||||
'@zkochan/js-yaml': 0.0.7
|
'@zkochan/js-yaml': 0.0.7
|
||||||
axios: 1.13.2
|
axios: 1.13.2
|
||||||
chalk: 4.1.0
|
chalk: 4.1.2
|
||||||
cli-cursor: 3.1.0
|
cli-cursor: 3.1.0
|
||||||
cli-spinners: 2.6.1
|
cli-spinners: 2.6.1
|
||||||
cliui: 8.0.1
|
cliui: 8.0.1
|
||||||
@@ -15812,7 +15862,7 @@ snapshots:
|
|||||||
ora@5.3.0:
|
ora@5.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bl: 4.1.0
|
bl: 4.1.0
|
||||||
chalk: 4.1.0
|
chalk: 4.1.2
|
||||||
cli-cursor: 3.1.0
|
cli-cursor: 3.1.0
|
||||||
cli-spinners: 2.6.1
|
cli-spinners: 2.6.1
|
||||||
is-interactive: 1.0.0
|
is-interactive: 1.0.0
|
||||||
@@ -17045,6 +17095,11 @@ snapshots:
|
|||||||
babel-jest: 29.7.0(@babel/core@7.28.5)
|
babel-jest: 29.7.0(@babel/core@7.28.5)
|
||||||
jest-util: 29.7.0
|
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:
|
tsconfig-paths@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
|
|||||||
Reference in New Issue
Block a user