Files
esengine/packages/ui/src/systems/SceneLoadTriggerSystem.ts
YHH ed8f6e283b feat: 纹理路径稳定 ID 与架构改进 (#305)
* feat(asset-system): 实现路径稳定 ID 生成器

使用 FNV-1a hash 算法为纹理生成稳定的运行时 ID:
- 新增 _pathIdCache 静态缓存,跨 Play/Stop 循环保持稳定
- 新增 getStableIdForPath() 方法,相同路径永远返回相同 ID
- 修改 loadTextureForComponent/loadTextureByGuid 使用稳定 ID
- clearTextureMappings() 不再清除 _pathIdCache

这解决了 Play/Stop 后纹理 ID 失效的根本问题。

* fix(runtime-core): 移除 Play/Stop 循环中的 clearTextureMappings 调用

使用路径稳定 ID 后,不再需要在快照保存/恢复时清除纹理缓存:
- saveSceneSnapshot() 移除 clearTextureMappings() 调用
- restoreSceneSnapshot() 移除 clearTextureMappings() 调用
- 组件保存的 textureId 在 Play/Stop 后仍然有效

* fix(editor-core): 修复场景切换时的资源泄漏

在 openScene() 加载新场景前先卸载旧场景资源:
- 调用 sceneResourceManager.unloadSceneResources() 释放旧资源
- 使用引用计数机制,仅卸载不再被引用的资源
- 路径稳定 ID 缓存不受影响,保持 ID 稳定性

* fix(runtime-core): 修复 PluginManager 组件注册类型错误

将 ComponentRegistry 类改为 GlobalComponentRegistry 实例:
- registerComponents() 期望 IComponentRegistry 接口实例
- GlobalComponentRegistry 是 ComponentRegistry 的全局实例

* refactor(core): 提取 IComponentRegistry 接口

将组件注册表抽象为接口,支持场景级组件注册:
- 新增 IComponentRegistry 接口定义
- Scene 持有独立的 componentRegistry 实例
- 支持从 GlobalComponentRegistry 克隆
- 各系统支持传入自定义注册表

* refactor(engine-core): 改进插件服务注册机制

- 更新 IComponentRegistry 类型引用
- 优化 PluginServiceRegistry 服务管理

* refactor(modules): 适配新的组件注册接口

更新各模块 RuntimeModule 使用 IComponentRegistry 接口:
- audio, behavior-tree, camera
- sprite, tilemap, world-streaming

* fix(physics-rapier2d): 修复物理插件组件注册

- PhysicsEditorPlugin 添加 runtimeModule 引用
- 适配 IComponentRegistry 接口
- 修复物理组件在场景加载时未注册的问题

* feat(editor-core): 添加 UserCodeService 就绪信号机制

- 新增 waitForReady()/signalReady() API
- 支持等待用户脚本编译完成
- 解决场景加载时组件未注册的时序问题

* fix(editor-app): 在编译完成后调用 signalReady()

确保用户脚本编译完成后发出就绪信号:
- 编译成功后调用 userCodeService.signalReady()
- 编译失败也要发出信号,避免阻塞场景加载

* feat(editor-core): 改进编辑器核心服务

- EntityStoreService 添加调试日志
- AssetRegistryService 优化资产注册
- PluginManager 改进插件管理
- IFileAPI 添加 getFileMtime 接口

* feat(engine): 改进 Rust 纹理管理器

- 支持任意 ID 的纹理加载(非递增)
- 添加纹理状态追踪 API
- 优化纹理缓存清理机制
- 更新 TypeScript 绑定

* feat(ui): 添加场景切换和文本闪烁组件

新增组件:
- SceneLoadTriggerComponent: 场景切换触发器
- TextBlinkComponent: 文本闪烁效果

新增系统:
- SceneLoadTriggerSystem: 处理场景切换逻辑
- TextBlinkSystem: 处理文本闪烁动画

其他改进:
- UIRuntimeModule 适配新组件注册接口
- UI 渲染系统优化

* feat(editor-app): 添加外部文件修改检测

- 新增 ExternalModificationDialog 组件
- TauriFileAPI 支持 getFileMtime
- 场景文件被外部修改时提示用户

* feat(editor-app): 添加渲染调试面板

- 新增 RenderDebugService 和调试面板 UI
- App/ContentBrowser 添加调试日志
- TitleBar/Viewport 优化
- DialogManager 改进

* refactor(editor-app): 编辑器服务和组件优化

- EngineService 改进引擎集成
- EditorEngineSync 同步优化
- AssetFileInspector 改进
- VectorFieldEditors 优化
- InstantiatePrefabCommand 改进

* feat(i18n): 更新国际化翻译

- 添加新功能相关翻译
- 更新中文、英文、西班牙文

* feat(tauri): 添加文件修改时间查询命令

- 新增 get_file_mtime 命令
- 支持检测文件外部修改

* refactor(particle): 粒子系统改进

- 适配新的组件注册接口
- ParticleSystem 优化
- 添加单元测试

* refactor(platform): 平台适配层优化

- BrowserRuntime 改进
- 新增 RuntimeSceneManager 服务
- 导出优化

* refactor(asset-system-editor): 资产元数据改进

- AssetMetaFile 优化
- 导出调整

* fix(asset-system): 移除未使用的 TextureLoader 导入

* fix(tests): 更新测试以使用 GlobalComponentRegistry 实例

修复多个测试文件以适配 ComponentRegistry 从静态类变为实例类的变更:
- ComponentStorage.test.ts: 使用 GlobalComponentRegistry.reset()
- EntitySerializer.test.ts: 使用 GlobalComponentRegistry 实例
- IncrementalSerialization.test.ts: 使用 GlobalComponentRegistry 实例
- SceneSerializer.test.ts: 使用 GlobalComponentRegistry 实例
- ComponentRegistry.extended.test.ts: 使用 GlobalComponentRegistry,同时注册到 scene.componentRegistry
- SystemTypes.test.ts: 在 Scene 创建前注册组件
- QuerySystem.test.ts: mockScene 添加 componentRegistry
2025-12-16 12:46:14 +08:00

163 lines
5.7 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.
/**
* 场景加载触发系统
* Scene Load Trigger System
*
* 处理 SceneLoadTriggerComponent绑定 UIInteractable 点击事件到场景加载。
* Processes SceneLoadTriggerComponent, binds UIInteractable click to scene loading.
*/
import { Entity, EntitySystem, Matcher, ECSSystem, Core } from '@esengine/ecs-framework';
import { SceneLoadTriggerComponent } from '../components/SceneLoadTriggerComponent';
import { UIInteractableComponent } from '../components/UIInteractableComponent';
/**
* 场景加载函数类型(与 RuntimeSceneManager.loadScene 兼容)
* Scene load function type (compatible with RuntimeSceneManager.loadScene)
*/
type SceneLoadFunction = (sceneName: string) => Promise<void>;
/**
* 场景管理器接口(最小化,避免循环依赖)
* Scene manager interface (minimal, avoids circular dependency)
*
* 包含 IService 的 dispose 方法以兼容 ServiceContainer。
* Includes IService's dispose method for ServiceContainer compatibility.
*/
interface ISceneManager {
loadScene(sceneName: string): Promise<void>;
dispose(): void;
}
/**
* 全局场景管理器服务键
* Global scene manager service key
*
* 使用 Symbol.for 确保与 BrowserRuntime 中注册的键一致。
* Uses Symbol.for to match the key registered in BrowserRuntime.
*/
const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager');
/**
* 场景加载触发系统
* Scene Load Trigger System
*
* 自动将 SceneLoadTriggerComponent 的配置连接到 UIInteractable 的点击事件。
* Automatically connects SceneLoadTriggerComponent config to UIInteractable click events.
*/
@ECSSystem('SceneLoadTrigger')
export class SceneLoadTriggerSystem extends EntitySystem {
private _sceneLoader: SceneLoadFunction | null = null;
constructor() {
super(Matcher.empty().all(SceneLoadTriggerComponent, UIInteractableComponent));
}
/**
* 设置场景加载函数
* Set scene load function
*
* 可以直接设置函数,或者系统会尝试从服务注册表获取 RuntimeSceneManager。
* Can set function directly, or system will try to get RuntimeSceneManager from service registry.
*/
public setSceneLoader(loader: SceneLoadFunction): void {
this._sceneLoader = loader;
}
protected override process(entities: readonly Entity[]): void {
// 如果没有设置场景加载器,尝试从服务注册表获取
// If no scene loader set, try to get from service registry
if (!this._sceneLoader) {
this._tryGetSceneManager();
}
for (const entity of entities) {
const trigger = entity.getComponent(SceneLoadTriggerComponent);
const interactable = entity.getComponent(UIInteractableComponent);
if (!trigger || !interactable) continue;
if (!trigger.enabled || !trigger.targetScene) continue;
// 只绑定一次回调
// Only bind callback once
if (trigger._callbackBound) continue;
this._bindClickHandler(entity, trigger, interactable);
}
}
/**
* 尝试从全局服务获取场景管理器
* Try to get scene manager from global services
*/
private _tryGetSceneManager(): void {
try {
// 从 Core.services 获取场景管理器
// Get scene manager from Core.services
// RuntimeSceneManager 实现了 IService 接口
// RuntimeSceneManager implements IService interface
const sceneManager = Core.services.tryResolve<ISceneManager>(GlobalSceneManagerKey);
if (sceneManager?.loadScene) {
this._sceneLoader = (sceneName: string) => sceneManager.loadScene(sceneName);
}
} catch (e) {
// 忽略错误,保持 _sceneLoader 为 null
// Ignore error, keep _sceneLoader as null
}
}
/**
* 绑定点击处理器
* Bind click handler
*/
private _bindClickHandler(
entity: Entity,
trigger: SceneLoadTriggerComponent,
interactable: UIInteractableComponent
): void {
const targetScene = trigger.targetScene;
// 保存原有的 onClick如果有
// Save original onClick (if any)
const originalOnClick = interactable.onClick;
interactable.onClick = () => {
// 调用原有回调
// Call original callback
originalOnClick?.();
// 检查是否启用
// Check if enabled
if (!trigger.enabled) return;
// 禁用(防止重复点击)
// Disable (prevent double clicks)
if (trigger.disableOnClick) {
trigger.enabled = false;
}
// 尝试获取场景加载器(可能在回调绑定后才注册)
// Try to get scene loader (may be registered after callback binding)
if (!this._sceneLoader) {
this._tryGetSceneManager();
}
// 加载场景
// Load scene
if (this._sceneLoader) {
this._sceneLoader(targetScene).catch((error) => {
console.error(`[SceneLoadTriggerSystem] Failed to load scene "${targetScene}":`, error);
// 恢复启用状态
// Restore enabled state
if (trigger.disableOnClick) {
trigger.enabled = true;
}
});
}
// 静默处理:编辑器预览模式下场景切换不可用
// Silent handling: scene switching not available in editor preview mode
};
trigger._callbackBound = true;
}
}