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:
@@ -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.
|
||||
* 销毁桥接并释放资源。
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 获取材质 ID(0 = 默认,如果未找到或未指定 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user