Files
esengine/packages/ecs-engine-bindgen/src/core/EngineBridge.ts

807 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Main bridge between TypeScript ECS and Rust Engine.
* TypeScript ECS与Rust引擎之间的主桥接层。
*/
import type { SpriteRenderData, TextureLoadRequest, EngineStats, CameraConfig } from '../types';
import type { IEngineBridge } from '@esengine/asset-system';
import type { GameEngine } from '../wasm/es_engine';
/**
* 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 implements IEngineBridge {
private engine: GameEngine | null = null;
private config: Required<EngineBridgeConfig>;
private initialized = false;
// Path resolver for converting file paths to URLs
// 用于将文件路径转换为URL的路径解析器
private pathResolver: ((path: string) => string) | null = null;
// Pre-allocated typed arrays for batch submission
// 预分配的类型数组用于批量提交
private transformBuffer: Float32Array;
private textureIdBuffer: Uint32Array;
private uvBuffer: Float32Array;
private colorBuffer: Uint32Array;
private materialIdBuffer: 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);
this.materialIdBuffer = 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(/* @vite-ignore */ 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;
}
/**
* Get engine instance (throws if not initialized)
* 获取引擎实例(未初始化时抛出异常)
*/
private getEngine(): GameEngine {
if (!this.engine) {
throw new Error('Engine not initialized. Call initialize() first.');
}
return this.engine;
}
/**
* 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.getEngine().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;
// Material ID (0 = default) | 材质ID0 = 默认)
this.materialIdBuffer[i] = sprite.materialId ?? 0;
}
// Submit to engine (single WASM call) | 提交到引擎单次WASM调用
this.getEngine().submitSpriteBatch(
this.transformBuffer.subarray(0, count * 7),
this.textureIdBuffer.subarray(0, count),
this.uvBuffer.subarray(0, count * 4),
this.colorBuffer.subarray(0, count),
this.materialIdBuffer.subarray(0, count)
);
this.stats.spriteCount = count;
}
/**
* Render the current frame.
* 渲染当前帧。
*/
render(): void {
if (!this.initialized) return;
const startTime = performance.now();
this.getEngine().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;
}
}
/**
* Render sprites as overlay without clearing the screen.
* 渲染精灵作为叠加层,不清除屏幕。
*
* This is used for UI rendering on top of world content.
* 用于在世界内容上渲染 UI。
*/
renderOverlay(): void {
if (!this.initialized) return;
this.getEngine().renderOverlay();
}
/**
* Load a texture.
* 加载纹理。
*
* @param id - Texture ID | 纹理ID
* @param url - Image URL | 图片URL
*/
loadTexture(id: number, url: string): Promise<void> {
if (!this.initialized) return Promise.resolve();
this.getEngine().loadTexture(id, url);
// Currently synchronous, but return Promise for interface compatibility
// 目前是同步的但返回Promise以兼容接口
return Promise.resolve();
}
/**
* Load multiple textures.
* 加载多个纹理。
*
* @param requests - Texture load requests | 纹理加载请求
*/
async loadTextures(requests: Array<{ id: number; url: string }>): Promise<void> {
for (const req of requests) {
await this.loadTexture(req.id, req.url);
}
}
/**
* Load texture by path, returning texture ID.
* 按路径加载纹理返回纹理ID。
*
* @param path - Image path/URL | 图片路径/URL
* @returns Texture ID | 纹理ID
*/
loadTextureByPath(path: string): number {
if (!this.initialized) return 0;
return this.getEngine().loadTextureByPath(path);
}
/**
* Get texture ID by path.
* 按路径获取纹理ID。
*
* @param path - Image path | 图片路径
* @returns Texture ID or undefined | 纹理ID或undefined
*/
getTextureIdByPath(path: string): number | undefined {
if (!this.initialized) return undefined;
return this.getEngine().getTextureIdByPath(path);
}
/**
* Set path resolver for converting file paths to URLs.
* 设置路径解析器用于将文件路径转换为URL。
*
* @param resolver - Function to resolve paths | 解析路径的函数
*/
setPathResolver(resolver: (path: string) => string): void {
this.pathResolver = resolver;
}
/**
* Get or load texture by path.
* 按路径获取或加载纹理。
*
* @param path - Image path/URL | 图片路径/URL
* @returns Texture ID | 纹理ID
*/
getOrLoadTextureByPath(path: string): number {
if (!this.initialized) return 0;
// Resolve path if resolver is set
// 如果设置了解析器,则解析路径
const resolvedPath = this.pathResolver ? this.pathResolver(path) : path;
return this.getEngine().getOrLoadTextureByPath(resolvedPath);
}
/**
* Unload texture from GPU.
* 从GPU卸载纹理。
*
* @param id - Texture ID | 纹理ID
*/
unloadTexture(id: number): void {
if (!this.initialized) return;
// TODO: Implement in Rust engine
// TODO: 在Rust引擎中实现
console.warn('unloadTexture not yet implemented in engine');
}
/**
* Get texture information.
* 获取纹理信息。
*
* @param id - Texture ID | 纹理ID
*/
getTextureInfo(id: number): { width: number; height: number } | null {
if (!this.initialized) return null;
// TODO: Implement in Rust engine
// TODO: 在Rust引擎中实现
// Return default values for now / 暂时返回默认值
return { width: 64, height: 64 };
}
/**
* Check if a key is pressed.
* 检查按键是否按下。
*
* @param keyCode - Key code | 键码
*/
isKeyDown(keyCode: string): boolean {
if (!this.initialized) return false;
return this.getEngine().isKeyDown(keyCode);
}
/**
* Update input state (call once per frame).
* 更新输入状态(每帧调用一次)。
*/
updateInput(): void {
if (!this.initialized) return;
this.getEngine().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;
const engine = this.getEngine();
if (engine.resize) {
engine.resize(width, height);
}
}
/**
* Set camera position, zoom, and rotation.
* 设置相机位置、缩放和旋转。
*
* @param config - Camera configuration | 相机配置
*/
setCamera(config: CameraConfig): void {
if (!this.initialized) return;
this.getEngine().setCamera(config.x, config.y, config.zoom, config.rotation);
}
/**
* Get camera state.
* 获取相机状态。
*/
getCamera(): CameraConfig {
if (!this.initialized) {
return { x: 0, y: 0, zoom: 1, rotation: 0 };
}
const state = this.getEngine().getCamera();
return {
x: state[0],
y: state[1],
zoom: state[2],
rotation: state[3]
};
}
/**
* Set grid visibility.
* 设置网格可见性。
*/
setShowGrid(show: boolean): void {
if (!this.initialized) return;
this.getEngine().setShowGrid(show);
}
/**
* Set clear color (background color).
* 设置清除颜色(背景颜色)。
*
* @param r - Red component (0.0-1.0) | 红色分量
* @param g - Green component (0.0-1.0) | 绿色分量
* @param b - Blue component (0.0-1.0) | 蓝色分量
* @param a - Alpha component (0.0-1.0) | 透明度分量
*/
setClearColor(r: number, g: number, b: number, a: number): void {
if (!this.initialized) return;
this.getEngine().setClearColor(r, g, b, a);
}
/**
* Add a rectangle gizmo outline.
* 添加矩形Gizmo边框。
*
* @param x - Center X position | 中心X位置
* @param y - Center Y position | 中心Y位置
* @param width - Rectangle width | 矩形宽度
* @param height - Rectangle height | 矩形高度
* @param rotation - Rotation in radians | 旋转角度(弧度)
* @param originX - Origin X (0-1) | 原点X (0-1)
* @param originY - Origin Y (0-1) | 原点Y (0-1)
* @param r - Red (0-1) | 红色
* @param g - Green (0-1) | 绿色
* @param b - Blue (0-1) | 蓝色
* @param a - Alpha (0-1) | 透明度
* @param showHandles - Whether to show transform handles | 是否显示变换手柄
*/
addGizmoRect(
x: number,
y: number,
width: number,
height: number,
rotation: number,
originX: number,
originY: number,
r: number,
g: number,
b: number,
a: number,
showHandles: boolean = true
): void {
if (!this.initialized) return;
this.getEngine().addGizmoRect(x, y, width, height, rotation, originX, originY, r, g, b, a, showHandles);
}
/**
* Add a circle outline gizmo (native rendering).
* 添加圆形边框Gizmo原生渲染
*/
addGizmoCircle(
x: number,
y: number,
radius: number,
r: number,
g: number,
b: number,
a: number
): void {
if (!this.initialized) return;
this.getEngine().addGizmoCircle(x, y, radius, r, g, b, a);
}
/**
* Add a line gizmo (native rendering).
* 添加线条Gizmo原生渲染
*/
addGizmoLine(
points: number[],
r: number,
g: number,
b: number,
a: number,
closed: boolean
): void {
if (!this.initialized) return;
this.getEngine().addGizmoLine(new Float32Array(points), r, g, b, a, closed);
}
/**
* Add a capsule outline gizmo (native rendering).
* 添加胶囊边框Gizmo原生渲染
*/
addGizmoCapsule(
x: number,
y: number,
radius: number,
halfHeight: number,
rotation: number,
r: number,
g: number,
b: number,
a: number
): void {
if (!this.initialized) return;
this.getEngine().addGizmoCapsule(x, y, radius, halfHeight, rotation, r, g, b, a);
}
/**
* Set transform tool mode.
* 设置变换工具模式。
*
* @param mode - 0=Select, 1=Move, 2=Rotate, 3=Scale
*/
setTransformMode(mode: number): void {
if (!this.initialized) return;
this.getEngine().setTransformMode(mode);
}
/**
* Set gizmo visibility.
* 设置辅助工具可见性。
*/
setShowGizmos(show: boolean): void {
if (!this.initialized) return;
this.getEngine().setShowGizmos(show);
}
// ===== Multi-viewport API =====
// ===== 多视口 API =====
/**
* Register a new viewport.
* 注册新视口。
*
* @param id - Unique viewport identifier | 唯一视口标识符
* @param canvasId - HTML canvas element ID | HTML canvas元素ID
*/
registerViewport(id: string, canvasId: string): void {
if (!this.initialized) return;
this.getEngine().registerViewport(id, canvasId);
}
/**
* Unregister a viewport.
* 注销视口。
*/
unregisterViewport(id: string): void {
if (!this.initialized) return;
this.getEngine().unregisterViewport(id);
}
/**
* Set the active viewport.
* 设置活动视口。
*/
setActiveViewport(id: string): boolean {
if (!this.initialized) return false;
return this.getEngine().setActiveViewport(id);
}
/**
* Set camera for a specific viewport.
* 为特定视口设置相机。
*/
setViewportCamera(viewportId: string, config: CameraConfig): void {
if (!this.initialized) return;
this.getEngine().setViewportCamera(viewportId, config.x, config.y, config.zoom, config.rotation);
}
/**
* Get camera for a specific viewport.
* 获取特定视口的相机。
*/
getViewportCamera(viewportId: string): CameraConfig | null {
if (!this.initialized) return null;
const state = this.getEngine().getViewportCamera(viewportId);
if (!state) return null;
return {
x: state[0],
y: state[1],
zoom: state[2],
rotation: state[3]
};
}
/**
* Set viewport configuration.
* 设置视口配置。
*/
setViewportConfig(viewportId: string, showGrid: boolean, showGizmos: boolean): void {
if (!this.initialized) return;
this.getEngine().setViewportConfig(viewportId, showGrid, showGizmos);
}
/**
* Resize a specific viewport.
* 调整特定视口大小。
*/
resizeViewport(viewportId: string, width: number, height: number): void {
if (!this.initialized) return;
this.getEngine().resizeViewport(viewportId, width, height);
}
/**
* Render to a specific viewport.
* 渲染到特定视口。
*/
renderToViewport(viewportId: string): void {
if (!this.initialized) return;
this.getEngine().renderToViewport(viewportId);
}
/**
* Get all registered viewport IDs.
* 获取所有已注册的视口ID。
*/
getViewportIds(): string[] {
if (!this.initialized) return [];
return this.getEngine().getViewportIds();
}
// ===== Screen Space Mode API =====
// ===== 屏幕空间模式 API =====
// Saved world space camera state
// 保存的世界空间相机状态
private savedWorldCamera: CameraConfig | null = null;
/**
* Push screen space rendering mode.
* 进入屏幕空间渲染模式。
*
* Saves the current world camera and switches to a fixed orthographic projection
* centered at (0, 0) with the specified canvas size.
* 保存当前世界相机并切换到以 (0, 0) 为中心的固定正交投影。
*
* @param canvasWidth - UI canvas width (design resolution) | UI 画布宽度(设计分辨率)
* @param canvasHeight - UI canvas height (design resolution) | UI 画布高度(设计分辨率)
*/
pushScreenSpaceMode(canvasWidth: number, canvasHeight: number): void {
if (!this.initialized) return;
// Save current world camera state
// 保存当前世界相机状态
this.savedWorldCamera = this.getCamera();
// Switch to screen space camera:
// - Position at origin (0, 0)
// - Zoom = 1 (1 pixel = 1 world unit)
// - No rotation
// 切换到屏幕空间相机:
// - 位置在原点 (0, 0)
// - 缩放 = 11 像素 = 1 世界单位)
// - 无旋转
//
// For screen space UI, we want the camera to show exactly canvasWidth x canvasHeight pixels
// centered at (0, 0). This means the visible area is:
// X: [-canvasWidth/2, canvasWidth/2]
// Y: [-canvasHeight/2, canvasHeight/2]
// 对于屏幕空间 UI我们希望相机精确显示 canvasWidth x canvasHeight 像素
// 以 (0, 0) 为中心。这意味着可见区域是:
// X: [-canvasWidth/2, canvasWidth/2]
// Y: [-canvasHeight/2, canvasHeight/2]
// Get current viewport size to calculate proper zoom
// 获取当前视口尺寸以计算正确的缩放
// Note: This assumes canvas.width/height match actual rendering size
// 注意:这假设 canvas.width/height 与实际渲染尺寸匹配
const canvas = document.getElementById(this.config.canvasId) as HTMLCanvasElement;
if (canvas) {
// Calculate zoom so that canvasWidth x canvasHeight fits exactly in the viewport
// 计算缩放使 canvasWidth x canvasHeight 正好适合视口
// zoom = viewport_size / world_visible_size
// For UI, we want 1 UI unit = 1 pixel on screen when canvas matches viewport
// 对于 UI当画布与视口匹配时我们希望 1 UI 单位 = 1 屏幕像素
const viewportWidth = canvas.width;
const viewportHeight = canvas.height;
// Calculate zoom based on the design canvas size vs actual viewport
// 根据设计画布尺寸与实际视口计算缩放
// This scales UI to fit the viewport while maintaining aspect ratio
const zoomX = viewportWidth / canvasWidth;
const zoomY = viewportHeight / canvasHeight;
// Use minimum to ensure entire canvas is visible (letterbox if needed)
// 使用最小值确保整个画布可见(如需要则显示黑边)
const zoom = Math.min(zoomX, zoomY);
this.setCamera({
x: 0,
y: 0,
zoom: zoom,
rotation: 0
});
} else {
// Fallback: use zoom = 1
// 回退:使用 zoom = 1
this.setCamera({
x: 0,
y: 0,
zoom: 1,
rotation: 0
});
}
}
/**
* Pop screen space rendering mode.
* 退出屏幕空间渲染模式。
*
* Restores the previously saved world camera.
* 恢复之前保存的世界相机。
*/
popScreenSpaceMode(): void {
if (!this.initialized) return;
// Restore world camera
// 恢复世界相机
if (this.savedWorldCamera) {
this.setCamera(this.savedWorldCamera);
this.savedWorldCamera = null;
}
}
/**
* Dispose the bridge and release resources.
* 销毁桥接并释放资源。
*/
dispose(): void {
this.engine = null;
this.initialized = false;
}
}