refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Asset Metadata Service
|
||||
* 资产元数据服务
|
||||
*
|
||||
* Provides global access to asset metadata without requiring asset loading.
|
||||
* This service is independent of the texture loading path, allowing
|
||||
* render systems to query sprite info regardless of how textures are loaded.
|
||||
*
|
||||
* 提供对资产元数据的全局访问,无需加载资产。
|
||||
* 此服务独立于纹理加载路径,允许渲染系统查询 sprite 信息,
|
||||
* 无论纹理是如何加载的。
|
||||
*/
|
||||
|
||||
import { AssetDatabase, ITextureSpriteInfo } from '../core/AssetDatabase';
|
||||
import type { AssetGUID } from '../types/AssetTypes';
|
||||
import type { ITextureEngineBridge } from '../integration/EngineIntegration';
|
||||
|
||||
/**
|
||||
* Global asset database instance
|
||||
* 全局资产数据库实例
|
||||
*/
|
||||
let globalAssetDatabase: AssetDatabase | null = null;
|
||||
|
||||
/**
|
||||
* Global engine bridge instance
|
||||
* 全局引擎桥实例
|
||||
*
|
||||
* Used to query texture dimensions from Rust engine (single source of truth).
|
||||
* 用于从 Rust 引擎查询纹理尺寸(唯一事实来源)。
|
||||
*/
|
||||
let globalEngineBridge: ITextureEngineBridge | null = null;
|
||||
|
||||
/**
|
||||
* Set the global asset database
|
||||
* 设置全局资产数据库
|
||||
*
|
||||
* Should be called during engine initialization.
|
||||
* 应在引擎初始化期间调用。
|
||||
*
|
||||
* @param database - AssetDatabase instance | AssetDatabase 实例
|
||||
*/
|
||||
export function setGlobalAssetDatabase(database: AssetDatabase | null): void {
|
||||
globalAssetDatabase = database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global asset database
|
||||
* 获取全局资产数据库
|
||||
*
|
||||
* @returns AssetDatabase instance or null | AssetDatabase 实例或 null
|
||||
*/
|
||||
export function getGlobalAssetDatabase(): AssetDatabase | null {
|
||||
return globalAssetDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the global engine bridge
|
||||
* 设置全局引擎桥
|
||||
*
|
||||
* The engine bridge is used to query texture dimensions directly from Rust engine.
|
||||
* This is the single source of truth for texture dimensions.
|
||||
* 引擎桥用于直接从 Rust 引擎查询纹理尺寸。
|
||||
* 这是纹理尺寸的唯一事实来源。
|
||||
*
|
||||
* @param bridge - ITextureEngineBridge instance | ITextureEngineBridge 实例
|
||||
*/
|
||||
export function setGlobalEngineBridge(bridge: ITextureEngineBridge | null): void {
|
||||
globalEngineBridge = bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global engine bridge
|
||||
* 获取全局引擎桥
|
||||
*
|
||||
* @returns ITextureEngineBridge instance or null | ITextureEngineBridge 实例或 null
|
||||
*/
|
||||
export function getGlobalEngineBridge(): ITextureEngineBridge | null {
|
||||
return globalEngineBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture sprite info by GUID
|
||||
* 通过 GUID 获取纹理 Sprite 信息
|
||||
*
|
||||
* This is the primary API for render systems to query nine-patch/sprite info.
|
||||
* It combines data from:
|
||||
* - Asset metadata (sliceBorder, pivot) from AssetDatabase
|
||||
* - Texture dimensions (width, height) from Rust engine (single source of truth)
|
||||
*
|
||||
* 这是渲染系统查询九宫格/sprite 信息的主要 API。
|
||||
* 它合并来自:
|
||||
* - AssetDatabase 的资产元数据(sliceBorder, pivot)
|
||||
* - Rust 引擎的纹理尺寸(width, height)(唯一事实来源)
|
||||
*
|
||||
* @param guid - Texture asset GUID | 纹理资产 GUID
|
||||
* @returns Sprite info or undefined | Sprite 信息或 undefined
|
||||
*/
|
||||
export function getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
|
||||
// Get sprite settings from metadata
|
||||
// 从元数据获取 sprite 设置
|
||||
const metadataInfo = globalAssetDatabase?.getTextureSpriteInfo(guid);
|
||||
|
||||
// Get texture dimensions from Rust engine (single source of truth)
|
||||
// 从 Rust 引擎获取纹理尺寸(唯一事实来源)
|
||||
let dimensions: { width: number; height: number } | undefined;
|
||||
|
||||
if (globalEngineBridge?.getTextureInfoByPath && globalAssetDatabase) {
|
||||
// Get asset path from database
|
||||
// 从数据库获取资产路径
|
||||
const metadata = globalAssetDatabase.getMetadata(guid);
|
||||
if (metadata?.path) {
|
||||
const engineInfo = globalEngineBridge.getTextureInfoByPath(metadata.path);
|
||||
if (engineInfo) {
|
||||
dimensions = engineInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no metadata and no dimensions, return undefined
|
||||
// 如果没有元数据也没有尺寸,返回 undefined
|
||||
if (!metadataInfo && !dimensions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Merge the two sources
|
||||
// 合并两个数据源
|
||||
// Prefer engine dimensions (runtime loaded), fallback to metadata dimensions (catalog stored)
|
||||
// 优先使用引擎尺寸(运行时加载),后备使用元数据尺寸(目录存储)
|
||||
return {
|
||||
sliceBorder: metadataInfo?.sliceBorder,
|
||||
pivot: metadataInfo?.pivot,
|
||||
width: dimensions?.width ?? metadataInfo?.width,
|
||||
height: dimensions?.height ?? metadataInfo?.height
|
||||
};
|
||||
}
|
||||
|
||||
// Re-export type for convenience
|
||||
// 为方便起见重新导出类型
|
||||
export type { ITextureSpriteInfo };
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 路径解析服务
|
||||
* Path Resolution Service
|
||||
*
|
||||
* 提供统一的路径解析接口,处理编辑器、Catalog、运行时三层路径转换。
|
||||
* Provides unified path resolution interface for editor, catalog, and runtime path conversion.
|
||||
*
|
||||
* 路径格式约定 | Path Format Convention:
|
||||
* - 编辑器路径 (Editor Path): 绝对路径,如 `C:\Project\assets\textures\bg.png`
|
||||
* - Catalog 路径 (Catalog Path): 相对于 assets 目录,不含 `assets/` 前缀,如 `textures/bg.png`
|
||||
* - 运行时 URL (Runtime URL): 完整 URL,如 `./assets/textures/bg.png` 或 `https://cdn.example.com/assets/textures/bg.png`
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { PathResolutionServiceToken, type IPathResolutionService } from '@esengine/asset-system';
|
||||
*
|
||||
* // 获取服务
|
||||
* const pathService = context.services.get(PathResolutionServiceToken);
|
||||
*
|
||||
* // Catalog 路径转运行时 URL
|
||||
* const url = pathService.catalogToRuntime('textures/bg.png');
|
||||
* // => './assets/textures/bg.png'
|
||||
*
|
||||
* // 编辑器路径转 Catalog 路径
|
||||
* const catalogPath = pathService.editorToCatalog('C:\\Project\\assets\\textures\\bg.png', 'C:\\Project');
|
||||
* // => 'textures/bg.png'
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
|
||||
// ============================================================================
|
||||
// 接口定义 | Interface Definitions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 路径解析服务接口
|
||||
* Path resolution service interface
|
||||
*/
|
||||
export interface IPathResolutionService {
|
||||
/**
|
||||
* 将 Catalog 路径转换为运行时 URL
|
||||
* Convert catalog path to runtime URL
|
||||
*
|
||||
* @param catalogPath Catalog 路径(相对于 assets 目录,不含 assets/ 前缀)
|
||||
* @returns 运行时 URL
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 输入: 'textures/bg.png'
|
||||
* // 输出: './assets/textures/bg.png' (取决于 baseUrl 配置)
|
||||
* pathService.catalogToRuntime('textures/bg.png');
|
||||
* ```
|
||||
*/
|
||||
catalogToRuntime(catalogPath: string): string;
|
||||
|
||||
/**
|
||||
* 将编辑器绝对路径转换为 Catalog 路径
|
||||
* Convert editor absolute path to catalog path
|
||||
*
|
||||
* @param editorPath 编辑器绝对路径
|
||||
* @param projectRoot 项目根目录
|
||||
* @returns Catalog 路径(相对于 assets 目录,不含 assets/ 前缀)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 输入: 'C:\\Project\\assets\\textures\\bg.png', 'C:\\Project'
|
||||
* // 输出: 'textures/bg.png'
|
||||
* pathService.editorToCatalog('C:\\Project\\assets\\textures\\bg.png', 'C:\\Project');
|
||||
* ```
|
||||
*/
|
||||
editorToCatalog(editorPath: string, projectRoot: string): string;
|
||||
|
||||
/**
|
||||
* 设置运行时基础 URL
|
||||
* Set runtime base URL
|
||||
*
|
||||
* @param url 基础 URL(通常为 './assets' 或 CDN URL)
|
||||
*/
|
||||
setBaseUrl(url: string): void;
|
||||
|
||||
/**
|
||||
* 获取当前基础 URL
|
||||
* Get current base URL
|
||||
*/
|
||||
getBaseUrl(): string;
|
||||
|
||||
/**
|
||||
* 规范化路径(统一斜杠方向,移除重复斜杠)
|
||||
* Normalize path (unify slash direction, remove duplicate slashes)
|
||||
*
|
||||
* @param path 输入路径
|
||||
* @returns 规范化后的路径
|
||||
*/
|
||||
normalize(path: string): string;
|
||||
|
||||
/**
|
||||
* 检查路径是否为绝对 URL
|
||||
* Check if path is absolute URL
|
||||
*
|
||||
* @param path 输入路径
|
||||
* @returns 是否为绝对 URL
|
||||
*/
|
||||
isAbsoluteUrl(path: string): boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 服务令牌 | Service Token
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 路径解析服务令牌
|
||||
* Path resolution service token
|
||||
*/
|
||||
export const PathResolutionServiceToken = createServiceToken<IPathResolutionService>('pathResolutionService');
|
||||
|
||||
// ============================================================================
|
||||
// 默认实现 | Default Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 路径解析服务默认实现
|
||||
* Default path resolution service implementation
|
||||
*/
|
||||
export class PathResolutionService implements IPathResolutionService {
|
||||
private _baseUrl: string = './assets';
|
||||
private _assetsDir: string = 'assets';
|
||||
|
||||
/**
|
||||
* 创建路径解析服务
|
||||
* Create path resolution service
|
||||
*
|
||||
* @param baseUrl 基础 URL(默认 './assets')
|
||||
*/
|
||||
constructor(baseUrl?: string) {
|
||||
if (baseUrl !== undefined) {
|
||||
this._baseUrl = baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Catalog 路径转换为运行时 URL
|
||||
* Convert catalog path to runtime URL
|
||||
*/
|
||||
catalogToRuntime(catalogPath: string): string {
|
||||
// 空路径直接返回
|
||||
if (!catalogPath) {
|
||||
return catalogPath;
|
||||
}
|
||||
|
||||
// 已经是绝对 URL 则直接返回
|
||||
if (this.isAbsoluteUrl(catalogPath)) {
|
||||
return catalogPath;
|
||||
}
|
||||
|
||||
// Data URL 直接返回
|
||||
if (catalogPath.startsWith('data:')) {
|
||||
return catalogPath;
|
||||
}
|
||||
|
||||
// 规范化路径
|
||||
let normalized = this.normalize(catalogPath);
|
||||
|
||||
// 移除开头的斜杠
|
||||
normalized = normalized.replace(/^\/+/, '');
|
||||
|
||||
// 如果路径以 'assets/' 开头,移除它(避免重复)
|
||||
// Catalog 路径不应包含 assets/ 前缀
|
||||
if (normalized.startsWith('assets/')) {
|
||||
normalized = normalized.substring(7);
|
||||
}
|
||||
|
||||
// 构建完整 URL
|
||||
const base = this._baseUrl.replace(/\/+$/, ''); // 移除尾部斜杠
|
||||
return `${base}/${normalized}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将编辑器绝对路径转换为 Catalog 路径
|
||||
* Convert editor absolute path to catalog path
|
||||
*/
|
||||
editorToCatalog(editorPath: string, projectRoot: string): string {
|
||||
// 规范化路径
|
||||
let normalizedPath = this.normalize(editorPath);
|
||||
let normalizedRoot = this.normalize(projectRoot);
|
||||
|
||||
// 确保根路径以斜杠结尾
|
||||
if (!normalizedRoot.endsWith('/')) {
|
||||
normalizedRoot += '/';
|
||||
}
|
||||
|
||||
// 移除项目根路径前缀
|
||||
if (normalizedPath.startsWith(normalizedRoot)) {
|
||||
normalizedPath = normalizedPath.substring(normalizedRoot.length);
|
||||
}
|
||||
|
||||
// 移除 assets/ 前缀(如果存在)
|
||||
const assetsPrefix = `${this._assetsDir}/`;
|
||||
if (normalizedPath.startsWith(assetsPrefix)) {
|
||||
normalizedPath = normalizedPath.substring(assetsPrefix.length);
|
||||
}
|
||||
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行时基础 URL
|
||||
* Set runtime base URL
|
||||
*/
|
||||
setBaseUrl(url: string): void {
|
||||
this._baseUrl = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前基础 URL
|
||||
* Get current base URL
|
||||
*/
|
||||
getBaseUrl(): string {
|
||||
return this._baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径
|
||||
* Normalize path
|
||||
*/
|
||||
normalize(path: string): string {
|
||||
return path
|
||||
.replace(/\\/g, '/') // 反斜杠转正斜杠
|
||||
.replace(/\/+/g, '/'); // 移除重复斜杠
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否为绝对 URL
|
||||
* Check if path is absolute URL
|
||||
*/
|
||||
isAbsoluteUrl(path: string): boolean {
|
||||
return /^(https?:\/\/|file:\/\/|asset:\/\/|blob:)/.test(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* 场景资源管理器 - 集中式场景资源加载
|
||||
* SceneResourceManager - Centralized resource loading for scenes
|
||||
*
|
||||
* 扫描场景中所有组件,收集资源引用,批量加载资源,并将运行时 ID 分配回组件
|
||||
* Scans all components in a scene, collects resource references, batch-loads them, and assigns runtime IDs back to components
|
||||
*/
|
||||
|
||||
import type { Scene } from '@esengine/ecs-framework';
|
||||
import { isResourceComponent, type ResourceReference } from '../interfaces/IResourceComponent';
|
||||
|
||||
/**
|
||||
* 资源加载器接口
|
||||
* Resource loader interface
|
||||
*/
|
||||
export interface IResourceLoader {
|
||||
/**
|
||||
* 批量加载资源并返回路径到 ID 的映射
|
||||
* Load a batch of resources and return path-to-ID mapping
|
||||
* @param paths 资源路径数组 / Array of resource paths
|
||||
* @param type 资源类型 / Resource type
|
||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||
*/
|
||||
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
|
||||
|
||||
/**
|
||||
* 卸载纹理资源(可选)
|
||||
* Unload texture resource (optional)
|
||||
*/
|
||||
unloadTexture?(textureId: number): void;
|
||||
|
||||
/**
|
||||
* 卸载音频资源(可选)
|
||||
* Unload audio resource (optional)
|
||||
*/
|
||||
unloadAudio?(audioId: number): void;
|
||||
|
||||
/**
|
||||
* 卸载数据资源(可选)
|
||||
* Unload data resource (optional)
|
||||
*/
|
||||
unloadData?(dataId: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源引用计数条目
|
||||
* Resource reference count entry
|
||||
*/
|
||||
interface ResourceRefCountEntry {
|
||||
/** 资源路径 / Resource path */
|
||||
path: string;
|
||||
/** 资源类型 / Resource type */
|
||||
type: ResourceReference['type'];
|
||||
/** 运行时 ID / Runtime ID */
|
||||
runtimeId: number;
|
||||
/** 使用此资源的场景名称集合 / Set of scene names using this resource */
|
||||
sceneNames: Set<string>;
|
||||
}
|
||||
|
||||
export class SceneResourceManager {
|
||||
private resourceLoader: IResourceLoader | null = null;
|
||||
|
||||
/**
|
||||
* 资源引用计数表
|
||||
* Resource reference count table
|
||||
*
|
||||
* Key: resource path, Value: reference count entry
|
||||
*/
|
||||
private _resourceRefCounts = new Map<string, ResourceRefCountEntry>();
|
||||
|
||||
/**
|
||||
* 场景到其使用的资源路径的映射
|
||||
* Map of scene name to resource paths used by that scene
|
||||
*/
|
||||
private _sceneResources = new Map<string, Set<string>>();
|
||||
|
||||
/**
|
||||
* 设置资源加载器实现
|
||||
* Set the resource loader implementation
|
||||
*
|
||||
* 应由引擎集成层调用
|
||||
* This should be called by the engine integration layer
|
||||
*
|
||||
* @param loader 资源加载器实例 / Resource loader instance
|
||||
*/
|
||||
setResourceLoader(loader: IResourceLoader): void {
|
||||
this.resourceLoader = loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载场景所需的所有资源
|
||||
* Load all resources required by a scene
|
||||
*
|
||||
* 流程 / Process:
|
||||
* 1. 扫描所有实体并从 IResourceComponent 实现中收集资源引用
|
||||
* Scan all entities and collect resource references from IResourceComponent implementations
|
||||
* 2. 按类型分组资源(纹理、音频等)
|
||||
* Group resources by type (texture, audio, etc.)
|
||||
* 3. 批量加载每种资源类型
|
||||
* Batch load each resource type
|
||||
* 4. 将运行时 ID 分配回组件
|
||||
* Assign runtime IDs back to components
|
||||
* 5. 更新引用计数
|
||||
* Update reference counts
|
||||
*
|
||||
* @param scene 要加载资源的场景 / The scene to load resources for
|
||||
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
|
||||
*/
|
||||
async loadSceneResources(scene: Scene): Promise<void> {
|
||||
if (!this.resourceLoader) {
|
||||
console.warn('[SceneResourceManager] No resource loader set, skipping resource loading');
|
||||
return;
|
||||
}
|
||||
|
||||
const sceneName = scene.name;
|
||||
|
||||
// 从组件收集所有资源引用 / Collect all resource references from components
|
||||
const resourceRefs = this.collectResourceReferences(scene);
|
||||
|
||||
if (resourceRefs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 按资源类型分组 / Group by resource type
|
||||
const resourcesByType = new Map<ResourceReference['type'], Set<string>>();
|
||||
for (const ref of resourceRefs) {
|
||||
if (!resourcesByType.has(ref.type)) {
|
||||
resourcesByType.set(ref.type, new Set());
|
||||
}
|
||||
resourcesByType.get(ref.type)!.add(ref.path);
|
||||
}
|
||||
|
||||
// 批量加载每种资源类型 / Load each resource type in batch
|
||||
const allResourceIds = new Map<string, number>();
|
||||
|
||||
for (const [type, paths] of resourcesByType) {
|
||||
const pathsArray = Array.from(paths);
|
||||
|
||||
try {
|
||||
const resourceIds = await this.resourceLoader.loadResourcesBatch(pathsArray, type);
|
||||
|
||||
// 合并到总映射表 / Merge into combined map
|
||||
for (const [path, id] of resourceIds) {
|
||||
allResourceIds.set(path, id);
|
||||
|
||||
// 更新引用计数 / Update reference count
|
||||
this.addResourceReference(path, type, id, sceneName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 将资源 ID 分配回组件 / Assign resource IDs back to components
|
||||
this.assignResourceIds(scene, allResourceIds);
|
||||
|
||||
// 记录场景使用的资源 / Record resources used by scene
|
||||
const scenePaths = new Set<string>();
|
||||
for (const ref of resourceRefs) {
|
||||
scenePaths.add(ref.path);
|
||||
}
|
||||
this._sceneResources.set(sceneName, scenePaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加资源引用
|
||||
* Add resource reference
|
||||
*/
|
||||
private addResourceReference(
|
||||
path: string,
|
||||
type: ResourceReference['type'],
|
||||
runtimeId: number,
|
||||
sceneName: string
|
||||
): void {
|
||||
let entry = this._resourceRefCounts.get(path);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
path,
|
||||
type,
|
||||
runtimeId,
|
||||
sceneNames: new Set()
|
||||
};
|
||||
this._resourceRefCounts.set(path, entry);
|
||||
}
|
||||
entry.sceneNames.add(sceneName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除资源引用
|
||||
* Remove resource reference
|
||||
*
|
||||
* @returns true 如果资源引用计数归零 / true if resource reference count reaches zero
|
||||
*/
|
||||
private removeResourceReference(path: string, sceneName: string): boolean {
|
||||
const entry = this._resourceRefCounts.get(path);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entry.sceneNames.delete(sceneName);
|
||||
|
||||
if (entry.sceneNames.size === 0) {
|
||||
this._resourceRefCounts.delete(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景实体收集所有资源引用
|
||||
* Collect all resource references from scene entities
|
||||
*/
|
||||
private collectResourceReferences(scene: Scene): ResourceReference[] {
|
||||
const refs: ResourceReference[] = [];
|
||||
|
||||
for (const entity of scene.entities.buffer) {
|
||||
for (const component of entity.components) {
|
||||
if (isResourceComponent(component)) {
|
||||
const componentRefs = component.getResourceReferences();
|
||||
refs.push(...componentRefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将已加载的资源 ID 分配回组件
|
||||
* Assign loaded resource IDs back to components
|
||||
*
|
||||
* @param scene 场景 / Scene
|
||||
* @param pathToId 路径到 ID 的映射 / Path to ID mapping
|
||||
*/
|
||||
private assignResourceIds(scene: Scene, pathToId: Map<string, number>): void {
|
||||
for (const entity of scene.entities.buffer) {
|
||||
for (const component of entity.components) {
|
||||
if (isResourceComponent(component)) {
|
||||
component.setResourceIds(pathToId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载场景使用的所有资源
|
||||
* Unload all resources used by a scene
|
||||
*
|
||||
* 在场景销毁时调用,只会卸载不再被其他场景引用的资源
|
||||
* Called when a scene is being destroyed, only unloads resources not referenced by other scenes
|
||||
*
|
||||
* @param scene 要卸载资源的场景 / The scene to unload resources for
|
||||
*/
|
||||
async unloadSceneResources(scene: Scene): Promise<void> {
|
||||
const sceneName = scene.name;
|
||||
|
||||
// 获取场景使用的资源路径 / Get resource paths used by scene
|
||||
const scenePaths = this._sceneResources.get(sceneName);
|
||||
if (!scenePaths) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 要卸载的资源 / Resources to unload
|
||||
const toUnload: ResourceRefCountEntry[] = [];
|
||||
|
||||
// 移除引用并收集需要卸载的资源 / Remove references and collect resources to unload
|
||||
for (const path of scenePaths) {
|
||||
const entry = this._resourceRefCounts.get(path);
|
||||
if (entry) {
|
||||
const shouldUnload = this.removeResourceReference(path, sceneName);
|
||||
if (shouldUnload) {
|
||||
toUnload.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理场景资源记录 / Clean up scene resource record
|
||||
this._sceneResources.delete(sceneName);
|
||||
|
||||
// 卸载不再使用的资源 / Unload resources no longer in use
|
||||
if (this.resourceLoader && toUnload.length > 0) {
|
||||
for (const entry of toUnload) {
|
||||
this.unloadResource(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载单个资源
|
||||
* Unload a single resource
|
||||
*/
|
||||
private unloadResource(entry: ResourceRefCountEntry): void {
|
||||
if (!this.resourceLoader) return;
|
||||
|
||||
switch (entry.type) {
|
||||
case 'texture':
|
||||
if (this.resourceLoader.unloadTexture) {
|
||||
this.resourceLoader.unloadTexture(entry.runtimeId);
|
||||
}
|
||||
break;
|
||||
case 'audio':
|
||||
if (this.resourceLoader.unloadAudio) {
|
||||
this.resourceLoader.unloadAudio(entry.runtimeId);
|
||||
}
|
||||
break;
|
||||
case 'data':
|
||||
if (this.resourceLoader.unloadData) {
|
||||
this.resourceLoader.unloadData(entry.runtimeId);
|
||||
}
|
||||
break;
|
||||
case 'font':
|
||||
// 字体卸载暂未实现 / Font unloading not yet implemented
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源统计信息
|
||||
* Get resource statistics
|
||||
*/
|
||||
getStatistics(): {
|
||||
totalResources: number;
|
||||
trackedScenes: number;
|
||||
resourcesByType: Map<ResourceReference['type'], number>;
|
||||
} {
|
||||
const resourcesByType = new Map<ResourceReference['type'], number>();
|
||||
|
||||
for (const entry of this._resourceRefCounts.values()) {
|
||||
const count = resourcesByType.get(entry.type) || 0;
|
||||
resourcesByType.set(entry.type, count + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
totalResources: this._resourceRefCounts.size,
|
||||
trackedScenes: this._sceneResources.size,
|
||||
resourcesByType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源的引用计数
|
||||
* Get reference count for a resource
|
||||
*/
|
||||
getResourceRefCount(path: string): number {
|
||||
const entry = this._resourceRefCounts.get(path);
|
||||
return entry ? entry.sceneNames.size : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有跟踪数据
|
||||
* Clear all tracking data
|
||||
*/
|
||||
clearAll(): void {
|
||||
this._resourceRefCounts.clear();
|
||||
this._sceneResources.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user