feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)

* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架

* feat: 增强编辑器UI功能与跨平台支持

* fix: 修复CI测试和类型检查问题

* fix: 修复CI问题并提高测试覆盖率

* fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
YHH
2025-11-21 10:03:18 +08:00
committed by GitHub
parent 8b9616837d
commit a768b890fd
107 changed files with 10221 additions and 477 deletions

View 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;
}
}