Feature/tilemap editor (#237)
* feat: 添加 Tilemap 编辑器插件和组件生命周期支持 * feat(editor-core): 添加声明式插件注册 API * feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor * feat: 添加 editor-runtime SDK 和插件系统改进 * fix(ci): 修复SceneResourceManager里变量未使用问题
This commit is contained in:
@@ -86,11 +86,9 @@ export class AssetPathResolver {
|
||||
// 应用自定义转换器(如果提供)
|
||||
if (this.config.pathTransformer) {
|
||||
path = this.config.pathTransformer(path);
|
||||
// Re-validate after transformation
|
||||
const postTransform = PathValidator.validate(path);
|
||||
if (!postTransform.valid) {
|
||||
throw new Error(`Path transformer produced invalid path: ${postTransform.reason}`);
|
||||
}
|
||||
// Transformer output is trusted (may be absolute path or asset:// URL)
|
||||
// 转换器输出是可信的(可能是绝对路径或 asset:// URL)
|
||||
return path;
|
||||
}
|
||||
|
||||
// Platform-specific resolution
|
||||
|
||||
@@ -9,6 +9,7 @@ export * from './types/AssetTypes';
|
||||
// Interfaces
|
||||
export * from './interfaces/IAssetLoader';
|
||||
export * from './interfaces/IAssetManager';
|
||||
export * from './interfaces/IResourceComponent';
|
||||
|
||||
// Core
|
||||
export { AssetManager } from './core/AssetManager';
|
||||
@@ -30,6 +31,13 @@ export { BinaryLoader } from './loaders/BinaryLoader';
|
||||
export { EngineIntegration } from './integration/EngineIntegration';
|
||||
export type { IEngineBridge } from './integration/EngineIntegration';
|
||||
|
||||
// Services
|
||||
export { SceneResourceManager } from './services/SceneResourceManager';
|
||||
export type { IResourceLoader } from './services/SceneResourceManager';
|
||||
|
||||
// Utils
|
||||
export { UVHelper } from './utils/UVHelper';
|
||||
|
||||
// Default instance
|
||||
import { AssetManager } from './core/AssetManager';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { AssetManager } from '../core/AssetManager';
|
||||
import { AssetGUID } from '../types/AssetTypes';
|
||||
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
||||
import { globalPathResolver } from '../core/AssetPathResolver';
|
||||
|
||||
/**
|
||||
* Engine bridge interface
|
||||
@@ -63,24 +64,35 @@ export class EngineIntegration {
|
||||
/**
|
||||
* Load texture for component
|
||||
* 为组件加载纹理
|
||||
*
|
||||
* 统一的路径解析入口:相对路径会被转换为 Tauri 可用的 asset:// URL
|
||||
* Unified path resolution entry: relative paths will be converted to Tauri-compatible asset:// URLs
|
||||
*/
|
||||
async loadTextureForComponent(texturePath: string): Promise<number> {
|
||||
// 检查是否已有纹理ID / Check if texture ID exists
|
||||
// 检查缓存(使用原始路径作为键)
|
||||
// Check cache (using original path as key)
|
||||
const existingId = this._pathToTextureId.get(texturePath);
|
||||
if (existingId) {
|
||||
return existingId;
|
||||
}
|
||||
|
||||
// 通过资产系统加载 / Load through asset system
|
||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(texturePath);
|
||||
// 使用 globalPathResolver 转换路径
|
||||
// Use globalPathResolver to transform the path
|
||||
const resolvedPath = globalPathResolver.resolve(texturePath);
|
||||
|
||||
// 通过资产系统加载(使用解析后的路径)
|
||||
// Load through asset system (using resolved path)
|
||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(resolvedPath);
|
||||
const textureAsset = result.asset;
|
||||
|
||||
// 如果有引擎桥接,上传到GPU / Upload to GPU if bridge exists
|
||||
// 如果有引擎桥接,上传到GPU(使用解析后的路径)
|
||||
// Upload to GPU if bridge exists (using resolved path)
|
||||
if (this._engineBridge && textureAsset.data) {
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, texturePath);
|
||||
await this._engineBridge.loadTexture(textureAsset.textureId, resolvedPath);
|
||||
}
|
||||
|
||||
// 缓存映射 / Cache mapping
|
||||
// 缓存映射(使用原始路径作为键,避免重复解析)
|
||||
// Cache mapping (using original path as key to avoid re-resolving)
|
||||
this._pathToTextureId.set(texturePath, textureAsset.textureId);
|
||||
|
||||
return textureAsset.textureId;
|
||||
@@ -150,6 +162,25 @@ export class EngineIntegration {
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量加载资源(通用方法,支持 IResourceLoader 接口)
|
||||
* Load resources in batch (generic method for IResourceLoader interface)
|
||||
*
|
||||
* @param paths 资源路径数组 / Array of resource paths
|
||||
* @param type 资源类型 / Resource type
|
||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
||||
*/
|
||||
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
|
||||
// 目前只支持纹理 / Currently only supports textures
|
||||
if (type === 'texture') {
|
||||
return this.loadTexturesBatch(paths);
|
||||
}
|
||||
|
||||
// 其他资源类型暂未实现 / Other resource types not yet implemented
|
||||
console.warn(`[EngineIntegration] Resource type '${type}' not yet supported`);
|
||||
return new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload texture
|
||||
* 卸载纹理
|
||||
|
||||
62
packages/asset-system/src/interfaces/IResourceComponent.ts
Normal file
62
packages/asset-system/src/interfaces/IResourceComponent.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 资源组件接口 - 用于依赖运行时资源的组件(纹理、音频等)
|
||||
* Interface for components that depend on runtime resources (textures, audio, etc.)
|
||||
*
|
||||
* 实现此接口的组件可以参与 SceneResourceManager 管理的集中式资源加载
|
||||
* Components implementing this interface can participate in centralized resource loading managed by SceneResourceManager
|
||||
*/
|
||||
|
||||
/**
|
||||
* 资源引用 - 包含路径和运行时 ID
|
||||
* Resource reference with path and runtime ID
|
||||
*/
|
||||
export interface ResourceReference {
|
||||
/** 资源路径(例如 "assets/sprites/player.png")/ Asset path (e.g., "assets/sprites/player.png") */
|
||||
path: string;
|
||||
/** 引擎分配的运行时资源 ID(例如 GPU 上的纹理 ID)/ Runtime resource ID assigned by engine (e.g., texture ID on GPU) */
|
||||
runtimeId?: number;
|
||||
/** 资源类型标识符 / Resource type identifier */
|
||||
type: 'texture' | 'audio' | 'font' | 'data';
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源组件接口
|
||||
* Resource component interface
|
||||
*
|
||||
* 实现此接口的组件可以在场景启动前由 SceneResourceManager 集中加载资源
|
||||
* Components implementing this interface can have their resources loaded centrally by SceneResourceManager before the scene starts
|
||||
*/
|
||||
export interface IResourceComponent {
|
||||
/**
|
||||
* 获取此组件需要的所有资源引用
|
||||
* Get all resource references needed by this component
|
||||
*
|
||||
* 在场景加载期间调用以收集资源路径
|
||||
* Called during scene loading to collect resource paths
|
||||
*/
|
||||
getResourceReferences(): ResourceReference[];
|
||||
|
||||
/**
|
||||
* 设置已加载资源的运行时 ID
|
||||
* Set runtime IDs for loaded resources
|
||||
*
|
||||
* 在 SceneResourceManager 加载资源后调用
|
||||
* Called after resources are loaded by SceneResourceManager
|
||||
*
|
||||
* @param pathToId 资源路径到运行时 ID 的映射 / Map of resource paths to runtime IDs
|
||||
*/
|
||||
setResourceIds(pathToId: Map<string, number>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型守卫 - 检查组件是否实现了 IResourceComponent
|
||||
* Type guard to check if a component implements IResourceComponent
|
||||
*/
|
||||
export function isResourceComponent(component: any): component is IResourceComponent {
|
||||
return (
|
||||
component !== null &&
|
||||
typeof component === 'object' &&
|
||||
typeof component.getResourceReferences === 'function' &&
|
||||
typeof component.setResourceIds === 'function'
|
||||
);
|
||||
}
|
||||
155
packages/asset-system/src/services/SceneResourceManager.ts
Normal file
155
packages/asset-system/src/services/SceneResourceManager.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 场景资源管理器 - 集中式场景资源加载
|
||||
* 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>>;
|
||||
}
|
||||
|
||||
export class SceneResourceManager {
|
||||
private resourceLoader: IResourceLoader | null = null;
|
||||
|
||||
/**
|
||||
* 设置资源加载器实现
|
||||
* 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
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
// 从组件收集所有资源引用 / 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);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 将资源 ID 分配回组件 / Assign resource IDs back to components
|
||||
this.assignResourceIds(scene, allResourceIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从场景实体收集所有资源引用
|
||||
* 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
|
||||
*
|
||||
* @param scene 要卸载资源的场景 / The scene to unload resources for
|
||||
*/
|
||||
async unloadSceneResources(_scene: Scene): Promise<void> {
|
||||
// TODO: 实现资源卸载 / Implement resource unloading
|
||||
// 需要跟踪资源引用计数,仅在不再使用时卸载
|
||||
// Need to track resource reference counts and only unload when no longer used
|
||||
console.log('[SceneResourceManager] Scene resource unloading not yet implemented');
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,10 @@ export enum AssetType {
|
||||
AnimationClip = 'animation',
|
||||
/** 行为树 */
|
||||
BehaviorTree = 'behaviortree',
|
||||
/** 瓦片地图 */
|
||||
Tilemap = 'tilemap',
|
||||
/** 瓦片集 */
|
||||
Tileset = 'tileset',
|
||||
/** JSON数据 */
|
||||
Json = 'json',
|
||||
/** 文本 */
|
||||
|
||||
81
packages/asset-system/src/utils/UVHelper.ts
Normal file
81
packages/asset-system/src/utils/UVHelper.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* UV Coordinate Helper
|
||||
* UV 坐标辅助工具
|
||||
*
|
||||
* 引擎使用图像坐标系:
|
||||
* Engine uses image coordinate system:
|
||||
* - 原点 (0, 0) 在左上角 | Origin at top-left
|
||||
* - V 轴向下增长 | V-axis increases downward
|
||||
* - UV 格式:[u0, v0, u1, v1] 其中 v0 < v1
|
||||
*/
|
||||
export class UVHelper {
|
||||
/**
|
||||
* Calculate UV coordinates for a texture region
|
||||
* 计算纹理区域的 UV 坐标
|
||||
*/
|
||||
static calculateUV(
|
||||
imageRect: { x: number; y: number; width: number; height: number },
|
||||
textureSize: { width: number; height: number }
|
||||
): [number, number, number, number] {
|
||||
const { x, y, width, height } = imageRect;
|
||||
const { width: tw, height: th } = textureSize;
|
||||
|
||||
return [
|
||||
x / tw, // u0
|
||||
y / th, // v0
|
||||
(x + width) / tw, // u1
|
||||
(y + height) / th // v1
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate UV coordinates for a tile in a tileset
|
||||
* 计算 tileset 中某个 tile 的 UV 坐标
|
||||
*/
|
||||
static calculateTileUV(
|
||||
tileIndex: number,
|
||||
tilesetInfo: {
|
||||
columns: number;
|
||||
tileWidth: number;
|
||||
tileHeight: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
margin?: number;
|
||||
spacing?: number;
|
||||
}
|
||||
): [number, number, number, number] | null {
|
||||
if (tileIndex < 0) return null;
|
||||
|
||||
const {
|
||||
columns,
|
||||
tileWidth,
|
||||
tileHeight,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
margin = 0,
|
||||
spacing = 0
|
||||
} = tilesetInfo;
|
||||
|
||||
const col = tileIndex % columns;
|
||||
const row = Math.floor(tileIndex / columns);
|
||||
const x = margin + col * (tileWidth + spacing);
|
||||
const y = margin + row * (tileHeight + spacing);
|
||||
|
||||
return this.calculateUV(
|
||||
{ x, y, width: tileWidth, height: tileHeight },
|
||||
{ width: imageWidth, height: imageHeight }
|
||||
);
|
||||
}
|
||||
|
||||
static validateUV(uv: [number, number, number, number]): boolean {
|
||||
const [u0, v0, u1, v1] = uv;
|
||||
return u0 >= 0 && u0 <= 1 && u1 >= 0 && u1 <= 1 &&
|
||||
v0 >= 0 && v0 <= 1 && v1 >= 0 && v1 <= 1 &&
|
||||
u0 < u1 && v0 < v1;
|
||||
}
|
||||
|
||||
static debugPrint(uv: [number, number, number, number], label?: string): void {
|
||||
const prefix = label ? `[${label}] ` : '';
|
||||
console.log(`${prefix}UV: [${uv.map(n => n.toFixed(4)).join(', ')}]`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user