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:
YHH
2025-11-25 22:23:19 +08:00
committed by GitHub
parent 551ca7805d
commit 3fb6f919f8
166 changed files with 54691 additions and 8674 deletions

View File

@@ -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

View File

@@ -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';

View File

@@ -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
* 卸载纹理

View 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'
);
}

View 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');
}
}

View File

@@ -59,6 +59,10 @@ export enum AssetType {
AnimationClip = 'animation',
/** 行为树 */
BehaviorTree = 'behaviortree',
/** 瓦片地图 */
Tilemap = 'tilemap',
/** 瓦片集 */
Tileset = 'tileset',
/** JSON数据 */
Json = 'json',
/** 文本 */

View 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(', ')}]`);
}
}