feat: 预制体系统与架构改进 (#303)

* feat(prefab): 实现预制体系统和编辑器 UX 改进

## 预制体系统
- 新增 PrefabSerializer: 预制体序列化/反序列化
- 新增 PrefabInstanceComponent: 追踪预制体实例来源和修改
- 新增 PrefabService: 预制体核心服务
- 新增 PrefabLoader: 预制体资产加载器
- 新增预制体命令: Create/Instantiate/Apply/Revert/BreakLink

## 预制体编辑模式
- 支持双击 .prefab 文件进入编辑模式
- 预制体编辑模式工具栏 (保存/退出)
- 预制体实例指示器和操作菜单

## 编辑器 UX 改进
- SceneHierarchy 快捷键: F2 重命名, Ctrl+D 复制, ↑↓ 导航
- 支持双击实体名称内联编辑
- 删除实体时显示子节点数量警告
- 右键菜单添加重命名/复制选项及快捷键提示
- 布局持久化和重置功能

## Bug 修复
- 修复 editor-runtime 组件类重复导致的 TransformComponent 不识别问题
- 修复 .prefab-name 样式覆盖导致预制体工具栏文字不可见
- 修复 Inspector 资源字段高度不正确问题

* feat(editor): 改进编辑器 UX 交互体验

- ContentBrowser: 加载动画 spinner、搜索高亮、改进空状态设计
- SceneHierarchy: 选中项自动滚动到视图、搜索清除按钮
- PropertyInspector: 输入框本地状态管理、Enter/Escape 键处理
- EntityInspector: 组件折叠状态持久化、属性搜索清除按钮
- Viewport: 变换操作实时数值显示
- 国际化: 添加相关文本 (en/zh)

* fix(build): 修复 Web 构建资产加载和编辑器 UX 改进

构建系统修复:
- 修复 asset-catalog.json 字段名不匹配 (entries vs assets)
- 修复 BrowserFileSystemService 支持两种目录格式
- 修复 bundle 策略检测逻辑 (空对象判断)
- 修复 module.json 中 assetExtensions 声明和类型推断

行为树修复:
- 修复 BehaviorTreeExecutionSystem 使用 loadAsset 替代 loadAssetByPath
- 修复 BehaviorTreeAssetType 常量与 module.json 类型名一致 (behavior-tree)

编辑器 UX 改进:
- 构建完成对话框添加"打开文件夹"按钮
- 构建完成对话框样式优化 (圆形图标背景、按钮布局)
- SceneHierarchy 响应式布局 (窄窗口自动隐藏 Type 列)
- SceneHierarchy 隐藏滚动条

错误追踪:
- 添加全局错误处理器写入日志文件 (%TEMP%/esengine-editor-crash.log)
- 添加 append_to_log Tauri 命令

* feat(render): 修复 UI 渲染和点击特效系统

## UI 渲染修复
- 修复 GUID 验证 bug,使用统一的 isValidGUID() 函数
- 修复 UI 渲染顺序随机问题,Rust 端使用 IndexMap 替代 HashMap
- Web 运行时添加 assetPathResolver 支持 GUID 解析
- UIInteractableComponent.blockEvents 默认值改为 false

## 点击特效系统
- 新增 ClickFxComponent 和 ClickFxSystem
- 支持在点击位置播放粒子效果
- 支持多种触发模式和粒子轮换

## Camera 系统重构
- CameraSystem 从 ecs-engine-bindgen 移至 camera 包
- 新增 CameraManager 统一管理相机

## 编辑器改进
- 改进属性面板 UI 交互
- 粒子编辑器面板优化
- Transform 命令系统

* feat(render): 实现 Sorting Layer 系统和 Overlay 渲染层

- 新增 SortingLayerManager 管理排序层级 (Background, Default, Foreground, UI, Overlay)
- 实现 ISortable 接口,统一 Sprite、UI、Particle 的排序属性
- 修复粒子 Overlay 层被 UI 遮挡问题:添加独立的 Overlay Pass 在 UI 之后渲染
- 更新粒子资产格式:从 sortingOrder 改为 sortingLayer + orderInLayer
- 更新粒子编辑器面板支持新的排序属性
- 优化 UI 渲染系统使用新的排序层级

* feat(ci): 集成 SignPath 代码签名服务

- 添加 SignPath 自动签名工作流(Windows)
- 配置 release-editor.yml 支持代码签名
- 将构建改为草稿模式,等待签名完成后发布
- 添加证书文件到 .gitignore 防止泄露

* fix(asset): 修复 Web 构建资产路径解析和全局单例移除

## 资产路径修复
- 修复 Tauri 本地服务器 `/asset?path=...` 路径解析,正确与 root 目录连接
- BrowserPathResolver 支持两种模式:
  - 'proxy': 使用 /asset?path=... 格式(编辑器 Run in Browser)
  - 'direct': 使用直接路径 /assets/path.png(独立 Web 构建)
- BrowserRuntime 使用 'direct' 模式,无需 Tauri 代理

## 架构改进 - 移除全局单例
- 移除 globalAssetManager 导出,改用 AssetManagerToken 依赖注入
- 移除 globalPathResolver 导出,改用 PathResolutionService
- 移除 globalPathResolutionService 导出
- ParticleUpdateSystem/ClickFxSystem 通过 setAssetManager() 注入依赖
- EngineService 使用 new AssetManager() 替代全局实例

## 新增服务
- PathResolutionService: 统一路径解析接口
- RuntimeModeService: 运行时模式查询服务
- SerializationContext: EntityRef 序列化上下文

## 其他改进
- 完善 ServiceToken 注释说明本地定义的意图
- 导出 BrowserPathResolveMode 类型

* fix(build): 添加 world-streaming composite 设置修复类型检查

* fix(build): 移除 world-streaming 引用避免 composite 冲突

* fix(build): 将 const enum 改为 enum 兼容 isolatedModules

* fix(build): 添加缺失的 IAssetManager 导入
This commit is contained in:
YHH
2025-12-13 19:44:08 +08:00
committed by GitHub
parent a716d8006c
commit beaa1d09de
258 changed files with 17725 additions and 3030 deletions

View File

@@ -4,7 +4,7 @@
*/
import type { SpriteRenderData, TextureLoadRequest, EngineStats, CameraConfig } from '../types';
import type { IEngineBridge } from '@esengine/asset-system';
import type { ITextureEngineBridge } from '@esengine/asset-system';
import type { GameEngine } from '../wasm/es_engine';
/**
@@ -43,7 +43,7 @@ export interface EngineBridgeConfig {
* bridge.render();
* ```
*/
export class EngineBridge implements IEngineBridge {
export class EngineBridge implements ITextureEngineBridge {
private engine: GameEngine | null = null;
private config: Required<EngineBridgeConfig>;
private initialized = false;
@@ -468,6 +468,41 @@ export class EngineBridge implements IEngineBridge {
};
}
/**
* Convert screen coordinates to world coordinates.
* 将屏幕坐标转换为世界坐标。
*
* Screen coordinates: (0,0) at top-left of canvas, Y-down
* World coordinates: Y-up, camera position at center of view
*
* @param screenX - Screen X coordinate (relative to canvas left edge)
* @param screenY - Screen Y coordinate (relative to canvas top edge)
* @returns World coordinates { x, y }
*/
screenToWorld(screenX: number, screenY: number): { x: number; y: number } {
if (!this.initialized) {
return { x: screenX, y: screenY };
}
const result = this.getEngine().screenToWorld(screenX, screenY);
return { x: result[0], y: result[1] };
}
/**
* Convert world coordinates to screen coordinates.
* 将世界坐标转换为屏幕坐标。
*
* @param worldX - World X coordinate
* @param worldY - World Y coordinate
* @returns Screen coordinates { x, y } (relative to canvas)
*/
worldToScreen(worldX: number, worldY: number): { x: number; y: number } {
if (!this.initialized) {
return { x: worldX, y: worldY };
}
const result = this.getEngine().worldToScreen(worldX, worldY);
return { x: result[0], y: result[1] };
}
/**
* Set grid visibility.
* 设置网格可见性。
@@ -817,6 +852,37 @@ export class EngineBridge implements IEngineBridge {
}
}
// ===== Texture Cache API =====
// ===== 纹理缓存 API =====
/**
* Clear the texture path cache.
* 清除纹理路径缓存。
*
* This should be called when restoring scene snapshots to ensure
* textures are reloaded with correct IDs.
* 在恢复场景快照时应调用此方法以确保纹理使用正确的ID重新加载。
*/
clearTexturePathCache(): void {
if (!this.initialized) return;
this.getEngine().clearTexturePathCache();
}
/**
* Clear all textures and reset state.
* 清除所有纹理并重置状态。
*
* This removes all loaded textures from GPU memory and resets
* the ID counter. Use with caution as all texture references
* will become invalid.
* 这会从GPU内存中移除所有已加载的纹理并重置ID计数器。
* 请谨慎使用,因为所有纹理引用都将变得无效。
*/
clearAllTextures(): void {
if (!this.initialized) return;
this.getEngine().clearAllTextures();
}
/**
* Dispose the bridge and release resources.
* 销毁桥接并释放资源。

View File

@@ -22,5 +22,4 @@ export { RenderBatcher } from './core/RenderBatcher';
export { SpriteRenderHelper } from './core/SpriteRenderHelper';
export type { ITransformComponent } from './core/SpriteRenderHelper';
export { EngineRenderSystem, type TransformComponentType, type IUIRenderDataProvider, type GizmoDataProviderFn, type HasGizmoProviderFn, type ProviderRenderData, type AssetPathResolverFn } from './systems/EngineRenderSystem';
export { CameraSystem } from './systems/CameraSystem';
export * from './types';

View File

@@ -1,52 +0,0 @@
/**
* Camera System
* 相机系统
*/
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
import { CameraComponent } from '@esengine/camera';
import type { EngineBridge } from '../core/EngineBridge';
@ECSSystem('Camera', { updateOrder: -100 })
export class CameraSystem extends EntitySystem {
private bridge: EngineBridge;
private lastAppliedCameraId: number | null = null;
constructor(bridge: EngineBridge) {
// Match entities with CameraComponent
super(Matcher.empty().all(CameraComponent));
this.bridge = bridge;
}
protected override onBegin(): void {
// Will process cameras in process()
}
protected override process(entities: readonly Entity[]): void {
// Use first enabled camera
for (const entity of entities) {
if (!entity.enabled) continue;
const camera = entity.getComponent(CameraComponent);
if (!camera) continue;
// Only apply if camera changed
if (this.lastAppliedCameraId !== entity.id) {
this.applyCamera(camera);
this.lastAppliedCameraId = entity.id;
}
// Only use first active camera
break;
}
}
private applyCamera(camera: CameraComponent): void {
// Apply background color
const bgColor = camera.backgroundColor || '#000000';
const r = parseInt(bgColor.slice(1, 3), 16) / 255;
const g = parseInt(bgColor.slice(3, 5), 16) / 255;
const b = parseInt(bgColor.slice(5, 7), 16) / 255;
this.bridge.setClearColor(r, g, b, 1.0);
}
}

View File

@@ -4,7 +4,7 @@
*/
import { EntitySystem, Matcher, Entity, ComponentType, ECSSystem, Component, Core } from '@esengine/ecs-framework';
import { TransformComponent } from '@esengine/engine-core';
import { TransformComponent, sortingLayerManager } from '@esengine/engine-core';
import { Color } from '@esengine/ecs-framework-math';
import { SpriteComponent } from '@esengine/sprite';
import { CameraComponent } from '@esengine/camera';
@@ -24,10 +24,29 @@ export interface ProviderRenderData {
uvs: Float32Array;
colors: Uint32Array;
tileCount: number;
/** Sorting order for render ordering | 渲染排序顺序 */
sortingOrder: number;
/** Texture path for loading (optional, used if textureId is 0) */
texturePath?: string;
/**
* 排序层名称
* Sorting layer name
*
* 决定渲染的大类顺序。默认为 'Default'。
* Determines the major render order category. Defaults to 'Default'.
*/
sortingLayer: string;
/**
* 层内排序顺序
* Order within the sorting layer
*/
orderInLayer: number;
/** 纹理 GUID如果 textureId 为 0 则使用)| Texture GUID (used if textureId is 0) */
textureGuid?: string;
/**
* 是否在屏幕空间渲染
* Whether to render in screen space
*
* 覆盖 sortingLayer 的 bScreenSpace 设置,用于粒子等需要动态指定渲染空间的场景。
* Overrides sortingLayer's bScreenSpace setting, for particles that need dynamic render space.
*/
bScreenSpace?: boolean;
}
/**
@@ -244,32 +263,73 @@ export class EngineRenderSystem extends EntitySystem {
* Process all matched entities.
* 处理所有匹配的实体。
*
* Rendering is done in two passes:
* 1. World Pass: World sprites, tilemaps, gizmos (affected by world camera)
* 2. UI Pass: Screen space UI (independent orthographic projection, overlaid on world)
* Rendering pipeline:
* 渲染管线:
*
* 渲染分两个阶段进行:
* 1. 世界阶段:世界 Sprite、瓦片地图、Gizmo受世界相机影响
* 2. UI 阶段:屏幕空间 UI独立正交投影叠加在世界之上
* 1. World Space Pass: Background → Default → Foreground → WorldOverlay
* 世界空间阶段:背景 → 默认 → 前景 → 世界覆盖层
*
* 2. Screen Space Pass (Preview Mode Only): UI → ScreenOverlay → Modal
* 屏幕空间阶段仅预览模式UI → 屏幕覆盖层 → 模态层
*
* @param entities - Entities to process | 要处理的实体
*/
protected override process(entities: readonly Entity[]): void {
// Clear and reuse map for gizmo drawing
// 清空并重用映射用于绘制gizmo
// 清空并重用映射用于绘制 gizmo
this.entityRenderMap.clear();
// Collect all render items separated by render space
// 按渲染空间分离收集所有渲染项
const worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }> = [];
const screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }> = [];
// Collect sprites from entities (all in world space)
// 收集实体的 sprites都在世界空间
this.collectEntitySprites(entities, worldSpaceItems);
// Collect render data from providers (e.g., tilemap, particle)
// 收集渲染数据提供者的数据(如瓦片地图、粒子)
this.collectProviderRenderData(worldSpaceItems, screenSpaceItems);
// Collect UI render data
// 收集 UI 渲染数据
if (this.uiRenderDataProvider) {
const uiRenderData = this.uiRenderDataProvider.getRenderData();
for (const data of uiRenderData) {
const uiSprites = this.convertProviderDataToSprites(data);
if (uiSprites.length > 0) {
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
// UI always goes to screen space in preview mode, world space in editor mode
// UI 在预览模式下始终在屏幕空间,编辑器模式下在世界空间
if (this.previewMode) {
screenSpaceItems.push({ sortKey, sprites: uiSprites });
} else {
worldSpaceItems.push({ sortKey, sprites: uiSprites });
}
}
}
}
// ===== Pass 1: World Space Rendering =====
// ===== 阶段 1世界空间渲染 =====
// This includes world sprites, tilemaps, and world space UI
// 包括世界 Sprite、瓦片地图和世界空间 UI
this.renderWorldSpacePass(worldSpaceItems);
// Collect all render items with sorting order
// 收集所有渲染项及其排序顺序
const renderItems: Array<{ sortingOrder: number; sprites: SpriteRenderData[] }> = [];
// ===== Pass 2: Screen Space Rendering (Preview Mode Only) =====
// ===== 阶段 2屏幕空间渲染仅预览模式=====
if (this.previewMode && screenSpaceItems.length > 0) {
this.renderScreenSpacePass(screenSpaceItems);
}
}
// Collect sprites from entities
// 收集实体的 sprites
/**
* Collect sprites from matched entities.
* 收集匹配实体的 sprites。
*/
private collectEntitySprites(
entities: readonly Entity[],
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
): void {
for (const entity of entities) {
const sprite = entity.getComponent(SpriteComponent);
const transform = entity.getComponent(this.transformType) as unknown as ITransformComponent | null;
@@ -278,7 +338,7 @@ export class EngineRenderSystem extends EntitySystem {
continue;
}
// Calculate UV with flip | 计算带翻转的UV
// Calculate UV with flip | 计算带翻转的 UV
const uv: [number, number, number, number] = [0, 0, 1, 1];
if (sprite.flipX || sprite.flipY) {
if (sprite.flipX) {
@@ -296,40 +356,30 @@ export class EngineRenderSystem extends EntitySystem {
? transform.worldRotation.z
: (typeof transform.rotation === 'number' ? transform.rotation : transform.rotation.z);
// Convert hex color string to packed RGBA | 将十六进制颜色字符串转换为打包的RGBA
// Convert hex color string to packed RGBA | 将十六进制颜色字符串转换为打包的 RGBA
const color = Color.packHexAlpha(sprite.color, sprite.alpha);
// Get texture ID from sprite component
// 从精灵组件获取纹理ID
// Use Rust engine's path-based texture loading for automatic caching
// 使用Rust引擎的基于路径的纹理加载实现自动缓存
// 从精灵组件获取纹理 ID
let textureId = 0;
const textureSource = sprite.getTextureSource();
if (textureSource) {
// Resolve GUID to path if resolver is available
// 如果有解析器,将 GUID 解析为路径
const texturePath = this.assetPathResolver
? this.assetPathResolver(textureSource)
: textureSource;
const texturePath = this.resolveAssetPath(textureSource);
textureId = this.bridge.getOrLoadTextureByPath(texturePath);
}
// Get material ID from GUID (0 = default if not found or no GUID specified)
// 从 GUID 获取材质 ID0 = 默认,如果未找到或未指定 GUID
// Get material ID from GUID
// 从 GUID 获取材质 ID
const materialGuidOrPath = sprite.materialGuid;
const materialPath = materialGuidOrPath && this.assetPathResolver
? this.assetPathResolver(materialGuidOrPath)
const materialPath = materialGuidOrPath
? this.resolveAssetPath(materialGuidOrPath)
: materialGuidOrPath;
const materialId = materialPath
? getMaterialManager().getMaterialIdByPath(materialPath)
: 0;
// Collect material overrides if any
// 收集材质覆盖(如果有)
const hasOverrides = sprite.hasOverrides();
// Pass actual display dimensions (sprite size * world transform scale)
// 传递实际显示尺寸sprite尺寸 * 世界变换缩放)
const renderData: SpriteRenderData = {
x: pos.x,
y: pos.y,
@@ -342,27 +392,41 @@ export class EngineRenderSystem extends EntitySystem {
uv,
color,
materialId,
// Only include overrides if there are any
// 仅在有覆盖时包含
...(hasOverrides ? { materialOverrides: sprite.materialOverrides } : {})
};
renderItems.push({ sortingOrder: sprite.sortingOrder, sprites: [renderData] });
const sortKey = sortingLayerManager.getSortKey(sprite.sortingLayer, sprite.orderInLayer);
worldSpaceItems.push({ sortKey, sprites: [renderData] });
this.entityRenderMap.set(entity.id, renderData);
}
}
// Collect render data from providers (e.g., tilemap)
/**
* Collect render data from providers (tilemap, particle, etc.).
* 收集渲染数据提供者的数据(瓦片地图、粒子等)。
*/
private collectProviderRenderData(
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>,
screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
): void {
for (const provider of this.renderDataProviders) {
const renderDataList = provider.getRenderData();
for (const data of renderDataList) {
// Get texture ID - load from path if needed
// Determine render space: explicit flag > layer config
// 确定渲染空间:显式标志 > 层配置
const bScreenSpace = data.bScreenSpace ?? sortingLayerManager.isScreenSpace(data.sortingLayer);
// Get texture ID - load from GUID if needed
// 获取纹理 ID - 如果需要从 GUID 加载
let textureId = data.textureIds[0] || 0;
if (textureId === 0 && data.texturePath) {
textureId = this.bridge.getOrLoadTextureByPath(data.texturePath);
if (textureId === 0 && data.textureGuid) {
const resolvedPath = this.resolveAssetPath(data.textureGuid);
textureId = this.bridge.getOrLoadTextureByPath(resolvedPath);
}
// Convert tilemap render data to sprites
const tilemapSprites: SpriteRenderData[] = [];
// Convert render data to sprites
// 转换渲染数据为 sprites
const sprites: SpriteRenderData[] = [];
for (let i = 0; i < data.tileCount; i++) {
const tOffset = i * 7;
const uvOffset = i * 4;
@@ -380,34 +444,38 @@ export class EngineRenderSystem extends EntitySystem {
color: data.colors[i]
};
tilemapSprites.push(renderData);
sprites.push(renderData);
}
if (tilemapSprites.length > 0) {
renderItems.push({ sortingOrder: data.sortingOrder, sprites: tilemapSprites });
if (sprites.length > 0) {
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
// Route to appropriate render space
// 路由到适当的渲染空间
if (this.previewMode && bScreenSpace) {
screenSpaceItems.push({ sortKey, sprites });
} else {
worldSpaceItems.push({ sortKey, sprites });
}
}
}
}
}
// Collect UI render data if in editor mode (renders in world space)
// 如果在编辑器模式,收集 UI 渲染数据(在世界空间渲染)
if (!this.previewMode && this.uiRenderDataProvider) {
const uiRenderData = this.uiRenderDataProvider.getRenderData();
for (const data of uiRenderData) {
const uiSprites = this.convertProviderDataToSprites(data);
if (uiSprites.length > 0) {
renderItems.push({ sortingOrder: data.sortingOrder, sprites: uiSprites });
}
}
}
// Sort by sortingOrder (lower values render first, appear behind)
// 按 sortingOrder 排序(值越小越先渲染,显示在后面)
renderItems.sort((a, b) => a.sortingOrder - b.sortingOrder);
/**
* Render world space content.
* 渲染世界空间内容。
*/
private renderWorldSpacePass(
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
): void {
// Sort by sortKey (lower values render first, appear behind)
// 按 sortKey 排序(值越小越先渲染,显示在后面)
worldSpaceItems.sort((a, b) => a.sortKey - b.sortKey);
// Submit all sprites in sorted order
// 按排序顺序提交所有 sprites
for (const item of renderItems) {
for (const item of worldSpaceItems) {
for (const sprite of item.sprites) {
this.batcher.addSprite(sprite);
}
@@ -418,93 +486,53 @@ export class EngineRenderSystem extends EntitySystem {
this.bridge.submitSprites(sprites);
}
// Draw gizmos for all entities with IGizmoProvider components
// 为所有具有 IGizmoProvider 组件的实体绘制 Gizmo
// Draw gizmos
// 绘制 Gizmo
if (this.showGizmos) {
this.drawComponentGizmos();
}
// Draw gizmos for selected entities (always, even if no sprites)
// 为选中的实体绘制Gizmo始终绘制即使没有精灵
if (this.showGizmos && this.selectedEntityIds.size > 0) {
this.drawSelectedEntityGizmos();
}
// Draw camera frustum gizmos
// 绘制相机视锥体 gizmo
if (this.showGizmos) {
this.drawCameraFrustums();
}
// Draw UI canvas boundary
// 绘制 UI 画布边界
if (this.showGizmos && this.showUICanvasBoundary && this.uiCanvasWidth > 0 && this.uiCanvasHeight > 0) {
this.drawUICanvasBoundary();
}
// ===== World Pass: Render world content =====
// ===== 世界阶段:渲染世界内容 =====
// Render world content
// 渲染世界内容
this.bridge.render();
// ===== Pass 2: Screen Space UI Rendering (Preview Mode Only) =====
// ===== 阶段 2屏幕空间 UI 渲染(仅预览模式)=====
// UI is rendered on top of world content with independent projection
// UI 使用独立投影渲染在世界内容之上
// Only in preview mode - in editor mode, UI is rendered in world space above
// 仅在预览模式 - 在编辑器模式UI 在上面的世界空间渲染
if (this.previewMode) {
this.renderScreenSpaceUI();
}
}
/**
* Render screen space UI with fixed orthographic projection.
* 使用固定正交投影渲染屏幕空间 UI
*
* Screen space UI is rendered with an independent orthographic projection
* based on the UI canvas size, not affected by the world camera.
* 屏幕空间 UI 使用基于 UI 画布尺寸的独立正交投影渲染,不受世界相机影响。
* Render screen space content (UI, ScreenOverlay, Modal).
* 渲染屏幕空间内容UI、屏幕覆盖层、模态层
*/
private renderScreenSpaceUI(): void {
if (!this.uiRenderDataProvider) {
return;
}
// Get all UI render data (now only screen space)
// 获取所有 UI 渲染数据(现在只有屏幕空间)
const uiRenderData = this.uiRenderDataProvider.getRenderData();
if (uiRenderData.length === 0) {
return;
}
private renderScreenSpacePass(
screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
): void {
// Sort by sortKey
// 按 sortKey 排序
screenSpaceItems.sort((a, b) => a.sortKey - b.sortKey);
// Switch to screen space projection
// 切换到屏幕空间投影
// Use UI canvas size for the orthographic projection
// 使用 UI 画布尺寸进行正交投影
const canvasWidth = this.uiCanvasWidth > 0 ? this.uiCanvasWidth : 1920;
const canvasHeight = this.uiCanvasHeight > 0 ? this.uiCanvasHeight : 1080;
// Save current camera state and switch to screen space mode
// 保存当前相机状态并切换到屏幕空间模式
this.bridge.pushScreenSpaceMode(canvasWidth, canvasHeight);
// Clear batcher for screen space content
// 清空批处理器用于屏幕空间内容
this.batcher.clear();
// Collect screen space UI render items
const screenSpaceItems: Array<{ sortingOrder: number; sprites: SpriteRenderData[] }> = [];
for (const data of uiRenderData) {
const uiSprites = this.convertProviderDataToSprites(data);
if (uiSprites.length > 0) {
screenSpaceItems.push({ sortingOrder: data.sortingOrder, sprites: uiSprites });
}
}
// Sort by sortingOrder
screenSpaceItems.sort((a, b) => a.sortingOrder - b.sortingOrder);
// Submit screen space UI sprites
// Submit screen space sprites
// 提交屏幕空间 sprites
for (const item of screenSpaceItems) {
for (const sprite of item.sprites) {
this.batcher.addSprite(sprite);
@@ -529,10 +557,11 @@ export class EngineRenderSystem extends EntitySystem {
* 将提供者渲染数据转换为 Sprite 渲染数据数组。
*/
private convertProviderDataToSprites(data: ProviderRenderData): SpriteRenderData[] {
// Get texture ID - load from path if needed
// Get texture ID - load from GUID if needed
// 获取纹理 ID - 如果需要从 GUID 加载
let textureId = data.textureIds[0] || 0;
if (textureId === 0 && data.texturePath) {
textureId = this.bridge.getOrLoadTextureByPath(data.texturePath);
if (textureId === 0 && data.textureGuid) {
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(data.textureGuid));
}
const sprites: SpriteRenderData[] = [];
@@ -1209,4 +1238,17 @@ export class EngineRenderSystem extends EntitySystem {
getAssetPathResolver(): AssetPathResolverFn | null {
return this.assetPathResolver;
}
/**
* Resolve asset GUID or path to actual file path.
* 将资产 GUID 或路径解析为实际文件路径。
*
* @param guidOrPath - Asset GUID or path | 资产 GUID 或路径
* @returns Resolved path or original value | 解析后的路径或原值
*/
private resolveAssetPath(guidOrPath: string): string {
return this.assetPathResolver
? this.assetPathResolver(guidOrPath)
: guidOrPath;
}
}

View File

@@ -1,125 +1,37 @@
/**
* ecs-engine-bindgen 服务令牌
* ecs-engine-bindgen service tokens
*
* 定义渲染系统和引擎桥接相关的服务令牌和接口。
* 谁定义接口,谁导出 Token。
*
* Defines service tokens and interfaces for render system and engine bridge.
* Who defines the interface, who exports the Token.
*
* @example
* ```typescript
* // 消费方导入 Token | Consumer imports Token
* import { RenderSystemToken, type IRenderSystem } from '@esengine/ecs-engine-bindgen';
*
* // 获取服务 | Get service
* const renderSystem = context.services.get(RenderSystemToken);
* if (renderSystem) {
* renderSystem.addRenderDataProvider(myProvider);
* }
* ```
*/
import { createServiceToken } from '@esengine/engine-core';
import type { EngineBridge } from './core/EngineBridge';
import { createServiceToken } from '@esengine/ecs-framework';
import { EngineBridgeToken as CoreEngineBridgeToken, type IEngineBridge as CoreIEngineBridge } from '@esengine/engine-core';
import type { IRenderDataProvider as InternalIRenderDataProvider } from './systems/EngineRenderSystem';
// ============================================================================
// 共享渲染接口 | Shared Render Interfaces
// ============================================================================
// 从 engine-core 重新导出 | Re-export from engine-core
export { CoreEngineBridgeToken as EngineBridgeToken };
export type { CoreIEngineBridge as IEngineBridge };
/**
* 渲染数据提供者接口
* Render data provider interface
*
* 由各模块的渲染系统实现,用于向主渲染系统提供渲染数据。
* Implemented by render systems of various modules, used to provide render data to main render system.
*/
export type IRenderDataProvider = InternalIRenderDataProvider;
/**
* 渲染系统接口
* Render system interface
*
* 跨模块共享的渲染系统契约。
* Cross-module shared render system contract.
*/
export interface IRenderSystem {
/**
* 注册渲染数据提供者
* Register a render data provider
*
* @param provider 渲染数据提供者 | Render data provider
*/
addRenderDataProvider(provider: IRenderDataProvider): void;
/**
* 移除渲染数据提供者
* Remove a render data provider
*
* @param provider 渲染数据提供者 | Render data provider
*/
removeRenderDataProvider(provider: IRenderDataProvider): void;
}
/**
* 引擎桥接接口
* Engine bridge interface
*
* WASM 引擎桥接契约。
* WASM engine bridge contract.
*/
export interface IEngineBridge {
/**
* 加载纹理
* Load texture
*/
loadTexture(id: number, url: string): Promise<void>;
}
/**
* 引擎集成接口
* Engine integration interface
*
* 纹理加载等引擎集成功能。
* Engine integration features like texture loading.
*/
export interface IEngineIntegration {
/**
* 为组件加载纹理
* Load texture for component
*/
/** 通过相对路径加载纹理(用户脚本使用)| Load texture by relative path (for user scripts) */
loadTextureForComponent(texturePath: string): Promise<number>;
/** 通过 GUID 加载纹理(内部引用使用)| Load texture by GUID (for internal references) */
loadTextureByGuid(guid: string): Promise<number>;
}
// ============================================================================
// 服务令牌 | Service Tokens
// ============================================================================
/**
* 渲染系统服务令牌
* Render system service token
*
* 用于获取渲染系统实例。
* For getting render system instance.
*/
export const RenderSystemToken = createServiceToken<IRenderSystem>('renderSystem');
/**
* 引擎桥接服务令牌
* Engine bridge service token
*
* 用于获取 WASM 引擎桥接实例。
* For getting WASM engine bridge instance.
*/
export const EngineBridgeToken = createServiceToken<IEngineBridge>('engineBridge');
/**
* 引擎集成服务令牌
* Engine integration service token
*
* 用于获取引擎集成实例(纹理加载等)。
* For getting engine integration instance (texture loading, etc.).
*/
export const EngineIntegrationToken = createServiceToken<IEngineIntegration>('engineIntegration');

View File

@@ -153,6 +153,18 @@ export class GameEngine {
* 调整特定视口大小。
*/
resizeViewport(viewport_id: string, width: number, height: number): void;
/**
* Convert screen coordinates to world coordinates.
* 将屏幕坐标转换为世界坐标。
*
* # Arguments | 参数
* * `screen_x` - Screen X coordinate (0 = left edge of canvas)
* * `screen_y` - Screen Y coordinate (0 = top edge of canvas)
*
* # Returns | 返回
* Array of [world_x, world_y] | 数组 [world_x, world_y]
*/
screenToWorld(screen_x: number, screen_y: number): Float32Array;
/**
* Set clear color (background color).
* 设置清除颜色(背景颜色)。
@@ -175,6 +187,18 @@ export class GameEngine {
* 设置辅助工具可见性。
*/
setShowGizmos(show: boolean): void;
/**
* Convert world coordinates to screen coordinates.
* 将世界坐标转换为屏幕坐标。
*
* # Arguments | 参数
* * `world_x` - World X coordinate
* * `world_y` - World Y coordinate
*
* # Returns | 返回
* Array of [screen_x, screen_y] | 数组 [screen_x, screen_y]
*/
worldToScreen(world_x: number, world_y: number): Float32Array;
/**
* Add a circle gizmo outline.
* 添加圆形Gizmo边框。
@@ -214,6 +238,17 @@ export class GameEngine {
* 设置材质的vec4 uniform也用于颜色
*/
setMaterialVec4(material_id: number, name: string, x: number, y: number, z: number, w: number): boolean;
/**
* Clear all textures and reset state.
* 清除所有纹理并重置状态。
*
* This removes all loaded textures from GPU memory and resets
* the ID counter. Use with caution as all texture references
* will become invalid.
* 这会从GPU内存中移除所有已加载的纹理并重置ID计数器。
* 请谨慎使用,因为所有纹理引用都将变得无效。
*/
clearAllTextures(): void;
/**
* Render to a specific viewport.
* 渲染到特定视口。
@@ -317,6 +352,15 @@ export class GameEngine {
* * `blend_mode` - 0=None, 1=Alpha, 2=Additive, 3=Multiply, 4=Screen, 5=PremultipliedAlpha
*/
setMaterialBlendMode(material_id: number, blend_mode: number): boolean;
/**
* Clear the texture path cache.
* 清除纹理路径缓存。
*
* This should be called when restoring scene snapshots to ensure
* textures are reloaded with correct IDs.
* 在恢复场景快照时应调用此方法以确保纹理使用正确的ID重新加载。
*/
clearTexturePathCache(): void;
/**
* Create a new game engine instance.
* 创建新的游戏引擎实例。
@@ -375,6 +419,8 @@ export interface InitOutput {
readonly gameengine_addGizmoLine: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
readonly gameengine_addGizmoRect: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number) => void;
readonly gameengine_clear: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_clearAllTextures: (a: number) => void;
readonly gameengine_clearTexturePathCache: (a: number) => void;
readonly gameengine_compileShader: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
readonly gameengine_compileShaderWithId: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
readonly gameengine_createMaterial: (a: number, b: number, c: number, d: number, e: number) => number;
@@ -401,6 +447,7 @@ export interface InitOutput {
readonly gameengine_renderToViewport: (a: number, b: number, c: number) => [number, number];
readonly gameengine_resize: (a: number, b: number, c: number) => void;
readonly gameengine_resizeViewport: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_screenToWorld: (a: number, b: number, c: number) => [number, number];
readonly gameengine_setActiveViewport: (a: number, b: number, c: number) => number;
readonly gameengine_setCamera: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_setClearColor: (a: number, b: number, c: number, d: number, e: number) => void;
@@ -420,9 +467,10 @@ export interface InitOutput {
readonly gameengine_unregisterViewport: (a: number, b: number, c: number) => void;
readonly gameengine_updateInput: (a: number) => void;
readonly gameengine_width: (a: number) => number;
readonly gameengine_worldToScreen: (a: number, b: number, c: number) => [number, number];
readonly init: () => void;
readonly wasm_bindgen__convert__closures_____invoke__hdbeb4a641c76f980: (a: number, b: number) => void;
readonly wasm_bindgen__closure__destroy__h201da39d82f7cf6e: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__hc746ced83e8f2609: (a: number, b: number) => void;
readonly wasm_bindgen__closure__destroy__hebcd2828f83f27ed: (a: number, b: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;