feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)
* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 * feat: 增强编辑器UI功能与跨平台支持 * fix: 修复CI测试和类型检查问题 * fix: 修复CI问题并提高测试覆盖率 * fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
49
packages/ecs-engine-bindgen/package.json
Normal file
49
packages/ecs-engine-bindgen/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@esengine/ecs-engine-bindgen",
|
||||
"version": "0.1.0",
|
||||
"description": "Bridge layer between ECS Framework and Rust Engine | ECS框架与Rust引擎之间的桥接层",
|
||||
"main": "bin/index.js",
|
||||
"module": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./bin/index.d.ts",
|
||||
"import": "./bin/index.js",
|
||||
"require": "./bin/index.js",
|
||||
"default": "./bin/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc --watch",
|
||||
"clean": "rimraf bin dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/ecs-engine-bindgen"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"game-engine",
|
||||
"bridge",
|
||||
"wasm",
|
||||
"typescript"
|
||||
],
|
||||
"author": "ESEngine Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "file:../core"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"es-engine": "file:../engine/pkg"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0",
|
||||
"rimraf": "^5.0.0"
|
||||
}
|
||||
}
|
||||
161
packages/ecs-engine-bindgen/src/components/SpriteComponent.ts
Normal file
161
packages/ecs-engine-bindgen/src/components/SpriteComponent.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Sprite component for ECS entities.
|
||||
* 用于ECS实体的精灵组件。
|
||||
*/
|
||||
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Sprite component data.
|
||||
* 精灵组件数据。
|
||||
*
|
||||
* Attach this component to entities that should be rendered as sprites.
|
||||
* 将此组件附加到应作为精灵渲染的实体。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const entity = scene.createEntity('player');
|
||||
* entity.addComponent(SpriteComponent);
|
||||
* const sprite = entity.getComponent(SpriteComponent);
|
||||
* sprite.textureId = 1;
|
||||
* sprite.width = 64;
|
||||
* sprite.height = 64;
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('Sprite')
|
||||
export class SpriteComponent extends Component {
|
||||
/**
|
||||
* Texture ID for this sprite.
|
||||
* 此精灵的纹理ID。
|
||||
*/
|
||||
textureId: number = 0;
|
||||
|
||||
/**
|
||||
* Sprite width in pixels.
|
||||
* 精灵宽度(像素)。
|
||||
*/
|
||||
width: number = 0;
|
||||
|
||||
/**
|
||||
* Sprite height in pixels.
|
||||
* 精灵高度(像素)。
|
||||
*/
|
||||
height: number = 0;
|
||||
|
||||
/**
|
||||
* UV coordinates [u0, v0, u1, v1].
|
||||
* UV坐标。
|
||||
* Default is full texture [0, 0, 1, 1].
|
||||
* 默认为完整纹理。
|
||||
*/
|
||||
uv: [number, number, number, number] = [0, 0, 1, 1];
|
||||
|
||||
/**
|
||||
* Packed RGBA color (0xAABBGGRR format for WebGL).
|
||||
* 打包的RGBA颜色。
|
||||
* Default is white (0xFFFFFFFF).
|
||||
* 默认为白色。
|
||||
*/
|
||||
color: number = 0xFFFFFFFF;
|
||||
|
||||
/**
|
||||
* Origin point X (0-1, where 0.5 is center).
|
||||
* 原点X(0-1,0.5为中心)。
|
||||
*/
|
||||
originX: number = 0.5;
|
||||
|
||||
/**
|
||||
* Origin point Y (0-1, where 0.5 is center).
|
||||
* 原点Y(0-1,0.5为中心)。
|
||||
*/
|
||||
originY: number = 0.5;
|
||||
|
||||
/**
|
||||
* Whether sprite is visible.
|
||||
* 精灵是否可见。
|
||||
*/
|
||||
visible: boolean = true;
|
||||
|
||||
/**
|
||||
* Render layer/order (higher = rendered on top).
|
||||
* 渲染层级/顺序(越高越在上面)。
|
||||
*/
|
||||
layer: number = 0;
|
||||
|
||||
/**
|
||||
* Flip sprite horizontally.
|
||||
* 水平翻转精灵。
|
||||
*/
|
||||
flipX: boolean = false;
|
||||
|
||||
/**
|
||||
* Flip sprite vertically.
|
||||
* 垂直翻转精灵。
|
||||
*/
|
||||
flipY: boolean = false;
|
||||
|
||||
/**
|
||||
* Set UV from a sprite atlas region.
|
||||
* 从精灵图集区域设置UV。
|
||||
*
|
||||
* @param x - Region X in pixels | 区域X(像素)
|
||||
* @param y - Region Y in pixels | 区域Y(像素)
|
||||
* @param w - Region width in pixels | 区域宽度(像素)
|
||||
* @param h - Region height in pixels | 区域高度(像素)
|
||||
* @param atlasWidth - Atlas total width | 图集总宽度
|
||||
* @param atlasHeight - Atlas total height | 图集总高度
|
||||
*/
|
||||
setAtlasRegion(
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
atlasWidth: number,
|
||||
atlasHeight: number
|
||||
): void {
|
||||
this.uv = [
|
||||
x / atlasWidth,
|
||||
y / atlasHeight,
|
||||
(x + w) / atlasWidth,
|
||||
(y + h) / atlasHeight
|
||||
];
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set color from RGBA values (0-255).
|
||||
* 从RGBA值设置颜色(0-255)。
|
||||
*
|
||||
* @param r - Red | 红色
|
||||
* @param g - Green | 绿色
|
||||
* @param b - Blue | 蓝色
|
||||
* @param a - Alpha | 透明度
|
||||
*/
|
||||
setColorRGBA(r: number, g: number, b: number, a: number = 255): void {
|
||||
this.color = ((a & 0xFF) << 24) | ((b & 0xFF) << 16) | ((g & 0xFF) << 8) | (r & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set color from hex value (0xRRGGBB or 0xRRGGBBAA).
|
||||
* 从十六进制值设置颜色。
|
||||
*
|
||||
* @param hex - Hex color value | 十六进制颜色值
|
||||
*/
|
||||
setColorHex(hex: number): void {
|
||||
if (hex > 0xFFFFFF) {
|
||||
// 0xRRGGBBAA format
|
||||
const r = (hex >> 24) & 0xFF;
|
||||
const g = (hex >> 16) & 0xFF;
|
||||
const b = (hex >> 8) & 0xFF;
|
||||
const a = hex & 0xFF;
|
||||
this.color = (a << 24) | (b << 16) | (g << 8) | r;
|
||||
} else {
|
||||
// 0xRRGGBB format
|
||||
const r = (hex >> 16) & 0xFF;
|
||||
const g = (hex >> 8) & 0xFF;
|
||||
const b = hex & 0xFF;
|
||||
this.color = (0xFF << 24) | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
108
packages/ecs-engine-bindgen/src/core/RenderBatcher.ts
Normal file
108
packages/ecs-engine-bindgen/src/core/RenderBatcher.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Render batcher for collecting sprite data.
|
||||
* 用于收集精灵数据的渲染批处理器。
|
||||
*/
|
||||
|
||||
import type { SpriteRenderData } from '../types';
|
||||
|
||||
/**
|
||||
* Collects and sorts sprite render data for batch submission.
|
||||
* 收集和排序精灵渲染数据用于批量提交。
|
||||
*
|
||||
* This class is used to collect sprites during the ECS update loop
|
||||
* and then submit them all at once to the engine.
|
||||
* 此类用于在ECS更新循环中收集精灵,然后一次性提交到引擎。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const batcher = new RenderBatcher();
|
||||
*
|
||||
* // During ECS update | 在ECS更新期间
|
||||
* batcher.addSprite({
|
||||
* x: 100, y: 200,
|
||||
* rotation: 0,
|
||||
* scaleX: 1, scaleY: 1,
|
||||
* originX: 0.5, originY: 0.5,
|
||||
* textureId: 1,
|
||||
* uv: [0, 0, 1, 1],
|
||||
* color: 0xFFFFFFFF
|
||||
* });
|
||||
*
|
||||
* // At end of frame | 在帧结束时
|
||||
* bridge.submitSprites(batcher.getSprites());
|
||||
* batcher.clear();
|
||||
* ```
|
||||
*/
|
||||
export class RenderBatcher {
|
||||
private sprites: SpriteRenderData[] = [];
|
||||
private sortByZ = false;
|
||||
|
||||
/**
|
||||
* Create a new render batcher.
|
||||
* 创建新的渲染批处理器。
|
||||
*
|
||||
* @param sortByZ - Whether to sort sprites by Z order | 是否按Z顺序排序精灵
|
||||
*/
|
||||
constructor(sortByZ = false) {
|
||||
this.sortByZ = sortByZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sprite to the batch.
|
||||
* 将精灵添加到批处理。
|
||||
*
|
||||
* @param sprite - Sprite render data | 精灵渲染数据
|
||||
*/
|
||||
addSprite(sprite: SpriteRenderData): void {
|
||||
this.sprites.push(sprite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple sprites to the batch.
|
||||
* 将多个精灵添加到批处理。
|
||||
*
|
||||
* @param sprites - Array of sprite render data | 精灵渲染数据数组
|
||||
*/
|
||||
addSprites(sprites: SpriteRenderData[]): void {
|
||||
this.sprites.push(...sprites);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sprites in the batch.
|
||||
* 获取批处理中的所有精灵。
|
||||
*
|
||||
* @returns Sorted array of sprites | 排序后的精灵数组
|
||||
*/
|
||||
getSprites(): SpriteRenderData[] {
|
||||
// Sort by texture ID for better batching (fewer texture switches)
|
||||
// 按纹理ID排序以获得更好的批处理效果(减少纹理切换)
|
||||
if (!this.sortByZ) {
|
||||
this.sprites.sort((a, b) => a.textureId - b.textureId);
|
||||
}
|
||||
return this.sprites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sprite count.
|
||||
* 获取精灵数量。
|
||||
*/
|
||||
get count(): number {
|
||||
return this.sprites.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all sprites from the batch.
|
||||
* 清除批处理中的所有精灵。
|
||||
*/
|
||||
clear(): void {
|
||||
this.sprites.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if batch is empty.
|
||||
* 检查批处理是否为空。
|
||||
*/
|
||||
get isEmpty(): boolean {
|
||||
return this.sprites.length === 0;
|
||||
}
|
||||
}
|
||||
140
packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts
Normal file
140
packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Sprite render helper utilities.
|
||||
* 精灵渲染辅助工具。
|
||||
*/
|
||||
|
||||
import { Entity, Component } from '@esengine/ecs-framework';
|
||||
import type { EngineBridge } from './EngineBridge';
|
||||
import { RenderBatcher } from './RenderBatcher';
|
||||
import { SpriteComponent } from '../components/SpriteComponent';
|
||||
import type { SpriteRenderData } from '../types';
|
||||
|
||||
/**
|
||||
* Transform component interface.
|
||||
* 变换组件接口。
|
||||
*
|
||||
* Your transform component should implement this interface.
|
||||
* 你的变换组件应该实现此接口。
|
||||
*/
|
||||
export interface ITransformComponent {
|
||||
position: { x: number; y: number };
|
||||
rotation: number;
|
||||
scale: { x: number; y: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for rendering sprites (not an ECS System).
|
||||
* 精灵渲染辅助类(非ECS系统)。
|
||||
*
|
||||
* Use this for manual control over rendering, or use EngineRenderSystem
|
||||
* for automatic ECS integration.
|
||||
* 用于手动控制渲染,或使用EngineRenderSystem进行自动ECS集成。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const bridge = new EngineBridge({ canvasId: 'canvas' });
|
||||
* await bridge.initialize();
|
||||
*
|
||||
* const helper = new SpriteRenderHelper(bridge);
|
||||
*
|
||||
* // In game loop | 在游戏循环中
|
||||
* helper.collectSprites(entities, Transform);
|
||||
* helper.render();
|
||||
* ```
|
||||
*/
|
||||
export class SpriteRenderHelper {
|
||||
private bridge: EngineBridge;
|
||||
private batcher: RenderBatcher;
|
||||
|
||||
/**
|
||||
* Create a new sprite render helper.
|
||||
* 创建新的精灵渲染辅助类。
|
||||
*
|
||||
* @param bridge - Engine bridge instance | 引擎桥接实例
|
||||
*/
|
||||
constructor(bridge: EngineBridge) {
|
||||
this.bridge = bridge;
|
||||
this.batcher = new RenderBatcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect sprite data from entities.
|
||||
* 从实体收集精灵数据。
|
||||
*
|
||||
* @param entities - Entities to process | 要处理的实体
|
||||
* @param transformType - Transform component class | 变换组件类
|
||||
*/
|
||||
collectSprites<T extends Component & ITransformComponent>(
|
||||
entities: Entity[],
|
||||
transformType: new () => T
|
||||
): void {
|
||||
this.batcher.clear();
|
||||
|
||||
for (const entity of entities) {
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
const transform = entity.getComponent(transformType);
|
||||
|
||||
if (!sprite || !transform || !sprite.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate UV with flip | 计算带翻转的UV
|
||||
let uv = sprite.uv;
|
||||
if (sprite.flipX || sprite.flipY) {
|
||||
uv = [...sprite.uv] as [number, number, number, number];
|
||||
if (sprite.flipX) {
|
||||
const temp = uv[0];
|
||||
uv[0] = uv[2];
|
||||
uv[2] = temp;
|
||||
}
|
||||
if (sprite.flipY) {
|
||||
const temp = uv[1];
|
||||
uv[1] = uv[3];
|
||||
uv[3] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
const renderData: SpriteRenderData = {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y,
|
||||
rotation: transform.rotation,
|
||||
scaleX: transform.scale.x,
|
||||
scaleY: transform.scale.y,
|
||||
originX: sprite.originX,
|
||||
originY: sprite.originY,
|
||||
textureId: sprite.textureId,
|
||||
uv,
|
||||
color: sprite.color
|
||||
};
|
||||
|
||||
this.batcher.addSprite(renderData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit batched sprites and render.
|
||||
* 提交批处理的精灵并渲染。
|
||||
*/
|
||||
render(): void {
|
||||
if (!this.batcher.isEmpty) {
|
||||
this.bridge.submitSprites(this.batcher.getSprites());
|
||||
}
|
||||
this.bridge.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of sprites to be rendered.
|
||||
* 获取要渲染的精灵数量。
|
||||
*/
|
||||
get spriteCount(): number {
|
||||
return this.batcher.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the current batch.
|
||||
* 清除当前批处理。
|
||||
*/
|
||||
clear(): void {
|
||||
this.batcher.clear();
|
||||
}
|
||||
}
|
||||
13
packages/ecs-engine-bindgen/src/index.ts
Normal file
13
packages/ecs-engine-bindgen/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* ECS Engine Bindgen - Bridge layer between ECS Framework and Rust Engine.
|
||||
* ECS引擎桥接层 - ECS框架与Rust引擎之间的桥接层。
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { EngineBridge, EngineBridgeConfig } from './core/EngineBridge';
|
||||
export { RenderBatcher } from './core/RenderBatcher';
|
||||
export { SpriteRenderHelper, ITransformComponent } from './core/SpriteRenderHelper';
|
||||
export { EngineRenderSystem, type TransformComponentType } from './systems/EngineRenderSystem';
|
||||
export { SpriteComponent } from './components/SpriteComponent';
|
||||
export * from './types';
|
||||
171
packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts
Normal file
171
packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Engine render system for ECS.
|
||||
* 用于ECS的引擎渲染系统。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, Entity, ComponentType, ECSSystem, Component } from '@esengine/ecs-framework';
|
||||
import type { EngineBridge } from '../core/EngineBridge';
|
||||
import { RenderBatcher } from '../core/RenderBatcher';
|
||||
import { SpriteComponent } from '../components/SpriteComponent';
|
||||
import type { SpriteRenderData } from '../types';
|
||||
import type { ITransformComponent } from '../core/SpriteRenderHelper';
|
||||
|
||||
/**
|
||||
* Type for transform component constructor.
|
||||
* 变换组件构造函数类型。
|
||||
*/
|
||||
export type TransformComponentType = ComponentType & (new (...args: any[]) => Component & ITransformComponent);
|
||||
|
||||
/**
|
||||
* ECS System for rendering sprites using the Rust engine.
|
||||
* 使用Rust引擎渲染精灵的ECS系统。
|
||||
*
|
||||
* This system extends EntitySystem and integrates with the ECS lifecycle.
|
||||
* 此系统扩展EntitySystem并与ECS生命周期集成。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create transform component | 创建变换组件
|
||||
* @ECSComponent('Transform')
|
||||
* class Transform extends Component implements ITransformComponent {
|
||||
* position = { x: 0, y: 0 };
|
||||
* rotation = 0;
|
||||
* scale = { x: 1, y: 1 };
|
||||
* }
|
||||
*
|
||||
* // Initialize bridge | 初始化桥接
|
||||
* const bridge = new EngineBridge({ canvasId: 'canvas' });
|
||||
* await bridge.initialize();
|
||||
*
|
||||
* // Add system to scene | 将系统添加到场景
|
||||
* const renderSystem = new EngineRenderSystem(bridge, Transform);
|
||||
* scene.addSystem(renderSystem);
|
||||
* ```
|
||||
*/
|
||||
@ECSSystem('EngineRender', { updateOrder: 1000 }) // Render system executes last | 渲染系统最后执行
|
||||
export class EngineRenderSystem extends EntitySystem {
|
||||
private bridge: EngineBridge;
|
||||
private batcher: RenderBatcher;
|
||||
private transformType: TransformComponentType;
|
||||
|
||||
/**
|
||||
* Create a new engine render system.
|
||||
* 创建新的引擎渲染系统。
|
||||
*
|
||||
* @param bridge - Engine bridge instance | 引擎桥接实例
|
||||
* @param transformType - Transform component class (must implement ITransformComponent) | 变换组件类(必须实现ITransformComponent)
|
||||
*/
|
||||
constructor(bridge: EngineBridge, transformType: TransformComponentType) {
|
||||
// Match entities with both Sprite and Transform components
|
||||
// 匹配同时具有Sprite和Transform组件的实体
|
||||
super(Matcher.empty().all(SpriteComponent, transformType));
|
||||
|
||||
this.bridge = bridge;
|
||||
this.batcher = new RenderBatcher();
|
||||
this.transformType = transformType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when system is initialized.
|
||||
* 系统初始化时调用。
|
||||
*/
|
||||
public override initialize(): void {
|
||||
super.initialize();
|
||||
this.logger.info('EngineRenderSystem initialized | 引擎渲染系统初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before processing entities.
|
||||
* 处理实体之前调用。
|
||||
*/
|
||||
protected begin(): void {
|
||||
// Clear the batch | 清空批处理
|
||||
this.batcher.clear();
|
||||
|
||||
// Clear screen | 清屏
|
||||
this.bridge.clear(0, 0, 0, 1);
|
||||
|
||||
// Update input | 更新输入
|
||||
this.bridge.updateInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all matched entities.
|
||||
* 处理所有匹配的实体。
|
||||
*
|
||||
* @param entities - Entities to process | 要处理的实体
|
||||
*/
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
const transform = entity.getComponent(this.transformType) as unknown as ITransformComponent | null;
|
||||
|
||||
if (!sprite || !transform || !sprite.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate UV with flip | 计算带翻转的UV
|
||||
let uv = sprite.uv;
|
||||
if (sprite.flipX || sprite.flipY) {
|
||||
uv = [...sprite.uv] as [number, number, number, number];
|
||||
if (sprite.flipX) {
|
||||
[uv[0], uv[2]] = [uv[2], uv[0]];
|
||||
}
|
||||
if (sprite.flipY) {
|
||||
[uv[1], uv[3]] = [uv[3], uv[1]];
|
||||
}
|
||||
}
|
||||
|
||||
const renderData: SpriteRenderData = {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y,
|
||||
rotation: transform.rotation,
|
||||
scaleX: transform.scale.x,
|
||||
scaleY: transform.scale.y,
|
||||
originX: sprite.originX,
|
||||
originY: sprite.originY,
|
||||
textureId: sprite.textureId,
|
||||
uv,
|
||||
color: sprite.color
|
||||
};
|
||||
|
||||
this.batcher.addSprite(renderData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after processing entities.
|
||||
* 处理实体之后调用。
|
||||
*/
|
||||
protected end(): void {
|
||||
// Submit batch and render | 提交批处理并渲染
|
||||
if (!this.batcher.isEmpty) {
|
||||
this.bridge.submitSprites(this.batcher.getSprites());
|
||||
}
|
||||
this.bridge.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of sprites rendered.
|
||||
* 获取渲染的精灵数量。
|
||||
*/
|
||||
get spriteCount(): number {
|
||||
return this.batcher.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engine statistics.
|
||||
* 获取引擎统计信息。
|
||||
*/
|
||||
getStats() {
|
||||
return this.bridge.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a texture.
|
||||
* 加载纹理。
|
||||
*/
|
||||
loadTexture(id: number, url: string): void {
|
||||
this.bridge.loadTexture(id, url);
|
||||
}
|
||||
}
|
||||
72
packages/ecs-engine-bindgen/src/types/index.ts
Normal file
72
packages/ecs-engine-bindgen/src/types/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Type definitions for engine bridge.
|
||||
* 引擎桥接层的类型定义。
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sprite render data for batch submission.
|
||||
* 用于批量提交的精灵渲染数据。
|
||||
*/
|
||||
export interface SpriteRenderData {
|
||||
/** Position X. | X位置。 */
|
||||
x: number;
|
||||
/** Position Y. | Y位置。 */
|
||||
y: number;
|
||||
/** Rotation in radians. | 旋转角度(弧度)。 */
|
||||
rotation: number;
|
||||
/** Scale X. | X缩放。 */
|
||||
scaleX: number;
|
||||
/** Scale Y. | Y缩放。 */
|
||||
scaleY: number;
|
||||
/** Origin X (0-1). | 原点X(0-1)。 */
|
||||
originX: number;
|
||||
/** Origin Y (0-1). | 原点Y(0-1)。 */
|
||||
originY: number;
|
||||
/** Texture ID. | 纹理ID。 */
|
||||
textureId: number;
|
||||
/** UV coordinates [u0, v0, u1, v1]. | UV坐标。 */
|
||||
uv: [number, number, number, number];
|
||||
/** Packed RGBA color. | 打包的RGBA颜色。 */
|
||||
color: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture load request.
|
||||
* 纹理加载请求。
|
||||
*/
|
||||
export interface TextureLoadRequest {
|
||||
/** Unique texture ID. | 唯一纹理ID。 */
|
||||
id: number;
|
||||
/** Image URL. | 图片URL。 */
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Engine statistics.
|
||||
* 引擎统计信息。
|
||||
*/
|
||||
export interface EngineStats {
|
||||
/** Frames per second. | 每秒帧数。 */
|
||||
fps: number;
|
||||
/** Number of draw calls. | 绘制调用次数。 */
|
||||
drawCalls: number;
|
||||
/** Number of sprites rendered. | 渲染的精灵数量。 */
|
||||
spriteCount: number;
|
||||
/** Frame time in milliseconds. | 帧时间(毫秒)。 */
|
||||
frameTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Camera configuration.
|
||||
* 相机配置。
|
||||
*/
|
||||
export interface CameraConfig {
|
||||
/** Camera X position. | 相机X位置。 */
|
||||
x: number;
|
||||
/** Camera Y position. | 相机Y位置。 */
|
||||
y: number;
|
||||
/** Zoom level. | 缩放级别。 */
|
||||
zoom: number;
|
||||
/** Rotation in radians. | 旋转角度(弧度)。 */
|
||||
rotation: number;
|
||||
}
|
||||
22
packages/ecs-engine-bindgen/tsconfig.json
Normal file
22
packages/ecs-engine-bindgen/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "bin", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user