feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)
* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 * feat: 增强编辑器UI功能与跨平台支持 * fix: 修复CI测试和类型检查问题 * fix: 修复CI问题并提高测试覆盖率 * fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
335
packages/ecs-engine-bindgen/src/core/EngineBridge.ts
Normal file
335
packages/ecs-engine-bindgen/src/core/EngineBridge.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Main bridge between TypeScript ECS and Rust Engine.
|
||||
* TypeScript ECS与Rust引擎之间的主桥接层。
|
||||
*/
|
||||
|
||||
import type { SpriteRenderData, TextureLoadRequest, EngineStats } from '../types';
|
||||
|
||||
/**
|
||||
* Engine bridge configuration.
|
||||
* 引擎桥接配置。
|
||||
*/
|
||||
export interface EngineBridgeConfig {
|
||||
/** Canvas element ID. | Canvas元素ID。 */
|
||||
canvasId: string;
|
||||
/** Initial canvas width. | 初始画布宽度。 */
|
||||
width?: number;
|
||||
/** Initial canvas height. | 初始画布高度。 */
|
||||
height?: number;
|
||||
/** Maximum sprites per batch. | 每批次最大精灵数。 */
|
||||
maxSprites?: number;
|
||||
/** Enable debug mode. | 启用调试模式。 */
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge for communication between ECS Framework and Rust Engine.
|
||||
* ECS框架与Rust引擎之间的通信桥接。
|
||||
*
|
||||
* This class manages data transfer between the TypeScript ECS layer
|
||||
* and the WebAssembly-based Rust rendering engine.
|
||||
* 此类管理TypeScript ECS层与基于WebAssembly的Rust渲染引擎之间的数据传输。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const bridge = new EngineBridge({ canvasId: 'game-canvas' });
|
||||
* await bridge.initialize();
|
||||
*
|
||||
* // In game loop | 在游戏循环中
|
||||
* bridge.clear(0, 0, 0, 1);
|
||||
* bridge.submitSprites(spriteDataArray);
|
||||
* bridge.render();
|
||||
* ```
|
||||
*/
|
||||
export class EngineBridge {
|
||||
private engine: any; // GameEngine from WASM
|
||||
private config: Required<EngineBridgeConfig>;
|
||||
private initialized = false;
|
||||
|
||||
// Pre-allocated typed arrays for batch submission
|
||||
// 预分配的类型数组用于批量提交
|
||||
private transformBuffer: Float32Array;
|
||||
private textureIdBuffer: Uint32Array;
|
||||
private uvBuffer: Float32Array;
|
||||
private colorBuffer: Uint32Array;
|
||||
|
||||
// Statistics | 统计信息
|
||||
private stats: EngineStats = {
|
||||
fps: 0,
|
||||
drawCalls: 0,
|
||||
spriteCount: 0,
|
||||
frameTime: 0
|
||||
};
|
||||
|
||||
private lastFrameTime = 0;
|
||||
private frameCount = 0;
|
||||
private fpsAccumulator = 0;
|
||||
|
||||
/**
|
||||
* Create a new engine bridge.
|
||||
* 创建新的引擎桥接。
|
||||
*
|
||||
* @param config - Bridge configuration | 桥接配置
|
||||
*/
|
||||
constructor(config: EngineBridgeConfig) {
|
||||
this.config = {
|
||||
canvasId: config.canvasId,
|
||||
width: config.width ?? 800,
|
||||
height: config.height ?? 600,
|
||||
maxSprites: config.maxSprites ?? 10000,
|
||||
debug: config.debug ?? false
|
||||
};
|
||||
|
||||
// Pre-allocate buffers | 预分配缓冲区
|
||||
const maxSprites = this.config.maxSprites;
|
||||
this.transformBuffer = new Float32Array(maxSprites * 7); // x, y, rot, sx, sy, ox, oy
|
||||
this.textureIdBuffer = new Uint32Array(maxSprites);
|
||||
this.uvBuffer = new Float32Array(maxSprites * 4); // u0, v0, u1, v1
|
||||
this.colorBuffer = new Uint32Array(maxSprites);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the engine bridge with WASM module.
|
||||
* 使用WASM模块初始化引擎桥接。
|
||||
*
|
||||
* @param wasmModule - Pre-imported WASM module | 预导入的WASM模块
|
||||
*/
|
||||
async initializeWithModule(wasmModule: any): Promise<void> {
|
||||
if (this.initialized) {
|
||||
console.warn('EngineBridge already initialized | EngineBridge已初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize WASM | 初始化WASM
|
||||
if (wasmModule.default) {
|
||||
await wasmModule.default();
|
||||
}
|
||||
|
||||
// Create engine instance | 创建引擎实例
|
||||
this.engine = new wasmModule.GameEngine(this.config.canvasId);
|
||||
this.initialized = true;
|
||||
|
||||
if (this.config.debug) {
|
||||
console.log('EngineBridge initialized | EngineBridge初始化完成');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize engine: ${error} | 引擎初始化失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the engine bridge.
|
||||
* 初始化引擎桥接。
|
||||
*
|
||||
* Loads the WASM module and creates the engine instance.
|
||||
* 加载WASM模块并创建引擎实例。
|
||||
*
|
||||
* @param wasmPath - Path to WASM package | WASM包路径
|
||||
* @deprecated Use initializeWithModule instead | 请使用 initializeWithModule 代替
|
||||
*/
|
||||
async initialize(wasmPath = '@esengine/engine'): Promise<void> {
|
||||
if (this.initialized) {
|
||||
console.warn('EngineBridge already initialized | EngineBridge已初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic import of WASM module | 动态导入WASM模块
|
||||
const wasmModule = await import(/* webpackIgnore: true */ wasmPath);
|
||||
await this.initializeWithModule(wasmModule);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize engine: ${error} | 引擎初始化失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if bridge is initialized.
|
||||
* 检查桥接是否已初始化。
|
||||
*/
|
||||
get isInitialized(): boolean {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canvas width.
|
||||
* 获取画布宽度。
|
||||
*/
|
||||
get width(): number {
|
||||
return this.engine?.width ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canvas height.
|
||||
* 获取画布高度。
|
||||
*/
|
||||
get height(): number {
|
||||
return this.engine?.height ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the screen.
|
||||
* 清除屏幕。
|
||||
*
|
||||
* @param r - Red (0-1) | 红色
|
||||
* @param g - Green (0-1) | 绿色
|
||||
* @param b - Blue (0-1) | 蓝色
|
||||
* @param a - Alpha (0-1) | 透明度
|
||||
*/
|
||||
clear(r: number, g: number, b: number, a: number): void {
|
||||
if (!this.initialized) return;
|
||||
this.engine.clear(r, g, b, a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit sprite data for rendering.
|
||||
* 提交精灵数据进行渲染。
|
||||
*
|
||||
* @param sprites - Array of sprite render data | 精灵渲染数据数组
|
||||
*/
|
||||
submitSprites(sprites: SpriteRenderData[]): void {
|
||||
if (!this.initialized || sprites.length === 0) return;
|
||||
|
||||
const count = Math.min(sprites.length, this.config.maxSprites);
|
||||
|
||||
// Fill typed arrays | 填充类型数组
|
||||
for (let i = 0; i < count; i++) {
|
||||
const sprite = sprites[i];
|
||||
const tOffset = i * 7;
|
||||
const uvOffset = i * 4;
|
||||
|
||||
// Transform data | 变换数据
|
||||
this.transformBuffer[tOffset] = sprite.x;
|
||||
this.transformBuffer[tOffset + 1] = sprite.y;
|
||||
this.transformBuffer[tOffset + 2] = sprite.rotation;
|
||||
this.transformBuffer[tOffset + 3] = sprite.scaleX;
|
||||
this.transformBuffer[tOffset + 4] = sprite.scaleY;
|
||||
this.transformBuffer[tOffset + 5] = sprite.originX;
|
||||
this.transformBuffer[tOffset + 6] = sprite.originY;
|
||||
|
||||
// Texture ID | 纹理ID
|
||||
this.textureIdBuffer[i] = sprite.textureId;
|
||||
|
||||
// UV coordinates | UV坐标
|
||||
this.uvBuffer[uvOffset] = sprite.uv[0];
|
||||
this.uvBuffer[uvOffset + 1] = sprite.uv[1];
|
||||
this.uvBuffer[uvOffset + 2] = sprite.uv[2];
|
||||
this.uvBuffer[uvOffset + 3] = sprite.uv[3];
|
||||
|
||||
// Color | 颜色
|
||||
this.colorBuffer[i] = sprite.color;
|
||||
}
|
||||
|
||||
// Submit to engine (single WASM call) | 提交到引擎(单次WASM调用)
|
||||
this.engine.submitSpriteBatch(
|
||||
this.transformBuffer.subarray(0, count * 7),
|
||||
this.textureIdBuffer.subarray(0, count),
|
||||
this.uvBuffer.subarray(0, count * 4),
|
||||
this.colorBuffer.subarray(0, count)
|
||||
);
|
||||
|
||||
this.stats.spriteCount = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the current frame.
|
||||
* 渲染当前帧。
|
||||
*/
|
||||
render(): void {
|
||||
if (!this.initialized) return;
|
||||
|
||||
const startTime = performance.now();
|
||||
this.engine.render();
|
||||
const endTime = performance.now();
|
||||
|
||||
// Update statistics | 更新统计信息
|
||||
this.stats.frameTime = endTime - startTime;
|
||||
this.stats.drawCalls = 1; // Currently single batch | 当前单批次
|
||||
|
||||
// Calculate FPS | 计算FPS
|
||||
this.frameCount++;
|
||||
this.fpsAccumulator += endTime - this.lastFrameTime;
|
||||
this.lastFrameTime = endTime;
|
||||
|
||||
if (this.fpsAccumulator >= 1000) {
|
||||
this.stats.fps = this.frameCount;
|
||||
this.frameCount = 0;
|
||||
this.fpsAccumulator = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a texture.
|
||||
* 加载纹理。
|
||||
*
|
||||
* @param id - Texture ID | 纹理ID
|
||||
* @param url - Image URL | 图片URL
|
||||
*/
|
||||
loadTexture(id: number, url: string): void {
|
||||
if (!this.initialized) return;
|
||||
this.engine.loadTexture(id, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load multiple textures.
|
||||
* 加载多个纹理。
|
||||
*
|
||||
* @param requests - Texture load requests | 纹理加载请求
|
||||
*/
|
||||
loadTextures(requests: TextureLoadRequest[]): void {
|
||||
for (const req of requests) {
|
||||
this.loadTexture(req.id, req.url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key is pressed.
|
||||
* 检查按键是否按下。
|
||||
*
|
||||
* @param keyCode - Key code | 键码
|
||||
*/
|
||||
isKeyDown(keyCode: string): boolean {
|
||||
if (!this.initialized) return false;
|
||||
return this.engine.isKeyDown(keyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update input state (call once per frame).
|
||||
* 更新输入状态(每帧调用一次)。
|
||||
*/
|
||||
updateInput(): void {
|
||||
if (!this.initialized) return;
|
||||
this.engine.updateInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engine statistics.
|
||||
* 获取引擎统计信息。
|
||||
*/
|
||||
getStats(): EngineStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the viewport.
|
||||
* 调整视口大小。
|
||||
*
|
||||
* @param width - New width | 新宽度
|
||||
* @param height - New height | 新高度
|
||||
*/
|
||||
resize(width: number, height: number): void {
|
||||
if (!this.initialized) return;
|
||||
if (this.engine.resize) {
|
||||
this.engine.resize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the bridge and release resources.
|
||||
* 销毁桥接并释放资源。
|
||||
*/
|
||||
dispose(): void {
|
||||
this.engine = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user