fix(build): 修复 Web 构建组件注册和用户脚本打包问题 (#302)

* refactor(build): 重构 Web 构建管线,支持配置驱动的 Import Maps

- 重构 WebBuildPipeline 支持 split-bundles 和 single-bundle 两种构建模式
- 使用 module.json 的 isCore 字段识别核心模块,消除硬编码列表
- 动态生成 Import Map,从模块清单的 name 字段获取包名映射
- 动态扫描 module.json 文件,不再依赖固定模块列表
- 添加 HTTP 服务器启动脚本 (start-server.bat/sh) 支持 ESM 模块
- 更新 BuildSettingsPanel UI 支持新的构建模式选项
- 添加多语言支持 (zh/en/es)

* fix(build): 修复 Web 构建组件注册和用户脚本打包问题

主要修复:
- 修复组件反序列化时找不到类型的问题
- @ECSComponent 装饰器现在自动注册到 ComponentRegistry
- 添加未使用装饰器的组件警告
- 构建管线自动扫描用户脚本(无需入口文件)

架构改进:
- 解决 Decorators ↔ ComponentRegistry 循环依赖
- 新建 ComponentTypeUtils.ts 作为底层无依赖模块
- 移除冗余的防御性 register 调用
- 统一 ComponentType 定义位置

* refactor(build): 统一 WASM 配置架构,移除硬编码

- 新增 wasmConfig 统一配置替代 wasmPaths/wasmBindings
- wasmConfig.files 支持多候选源路径和明确目标路径
- wasmConfig.runtimePath 指定运行时加载路径
- 重构 _copyWasmFiles 使用统一配置
- HTML 生成使用配置中的 runtimePath
- 移除 physics-rapier2d 的冗余 WASM 配置(由 rapier2d 负责)
- IBuildFileSystem 新增 deleteFile 方法

* feat(build): 单文件构建模式完善和场景配置驱动

## 主要改动

### 单文件构建(single-file mode)
- 修复 WASM 初始化问题,支持 initSync 同步初始化
- 配置驱动的 WASM 识别,通过 wasmConfig.isEngineCore 标识核心引擎模块
- 从 wasmConfig.files 动态获取 JS 绑定路径,消除硬编码

### 场景配置
- 构建验证:必须选择至少一个场景才能构建
- 自动扫描:项目加载时扫描 scenes 目录
- 抽取 _filterScenesByWhitelist 公共方法统一过滤逻辑

### 构建面板优化
- availableScenes prop 传递场景列表
- 场景复选框可点击切换启用状态
- 移除动态 import,使用 prop 传入数据

* chore(build): 补充构建相关的辅助改动

- 添加 BuildFileSystemService 的 listFilesByExtension 优化
- 更新 module.json 添加 externalDependencies 配置
- BrowserRuntime 支持 wasmModule 参数传递
- GameRuntime 添加 loadSceneFromData 方法
- Rust 构建命令更新
- 国际化文案更新

* feat(build): 持久化构建设置到项目配置

## 设计架构

### ProjectService 扩展
- 新增 BuildSettingsConfig 接口定义构建配置字段
- ProjectConfig 添加 buildSettings 字段
- 新增 getBuildSettings / updateBuildSettings 方法

### BuildSettingsPanel
- 组件挂载时从 projectService 加载已保存配置
- 设置变化时自动保存(500ms 防抖)
- 场景选择状态与项目配置同步

### 配置保存位置
保存在项目的 ecs-editor.config.json 中:
- scenes: 选中的场景列表
- buildMode: 构建模式
- companyName/productName/version: 产品信息
- developmentBuild/sourceMap: 构建选项

* fix(editor): Ctrl+S 仅在主编辑区域触发保存场景

- 模态窗口打开时跳过(构建设置、设置、关于等)
- 焦点在 input/textarea/contenteditable 时跳过

* fix(tests): 修复 ECS 测试中 Component 注册问题

- 为所有测试 Component 类添加 @ECSComponent 装饰器
- 移除 beforeEach 中的 ComponentRegistry.reset() 调用
- 将内联 Component 类移到文件顶层以支持装饰器
- 更新测试预期值匹配新的组件类型名称
- 添加缺失的 HierarchyComponent 导入

所有 1388 个测试现已通过。
This commit is contained in:
YHH
2025-12-10 18:23:29 +08:00
committed by GitHub
parent 1b0d38edce
commit a716d8006c
67 changed files with 3671 additions and 1455 deletions
+5 -4
View File
@@ -13,22 +13,23 @@
* - 导入设置 * - 导入设置
*/ */
// Meta file management // Meta file management | 元数据文件管理
export { export {
AssetMetaManager, AssetMetaManager,
type IAssetMeta, type IAssetMeta,
type IImportSettings, type IImportSettings,
type IMetaFileSystem, type IMetaFileSystem,
generateGUID,
getMetaFilePath, getMetaFilePath,
inferAssetType, inferAssetType,
getDefaultImportSettings, getDefaultImportSettings,
createAssetMeta, createAssetMeta,
serializeAssetMeta, serializeAssetMeta,
parseAssetMeta, parseAssetMeta
isValidGUID
} from './meta/AssetMetaFile'; } from './meta/AssetMetaFile';
// Re-export utilities from asset-system | 从 asset-system 重导出工具函数
export { generateGUID, isValidGUID } from '@esengine/asset-system';
// Asset packing // Asset packing
export { export {
AssetPacker, AssetPacker,
@@ -13,7 +13,11 @@
* - 标签:用户定义的标签 * - 标签:用户定义的标签
*/ */
import { AssetGUID, AssetType } from '@esengine/asset-system'; import {
AssetGUID,
AssetType,
generateGUID
} from '@esengine/asset-system';
/** /**
* Meta file content structure * Meta file content structure
@@ -68,23 +72,6 @@ export interface IImportSettings {
[key: string]: unknown; [key: string]: unknown;
} }
/**
* Generate a new UUID v4
* 生成新的 UUID v4
*/
export function generateGUID(): AssetGUID {
// Use crypto.randomUUID if available (modern browsers/Node 19+)
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback implementation
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/** /**
* Get meta file path for an asset * Get meta file path for an asset
@@ -228,14 +215,6 @@ export function parseAssetMeta(json: string): IAssetMeta {
return meta; return meta;
} }
/**
* Validate GUID format (UUID v4)
* 验证 GUID 格式 (UUID v4)
*/
export function isValidGUID(guid: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(guid);
}
/** /**
* Asset Meta File Manager * Asset Meta File Manager
@@ -15,7 +15,8 @@ import {
IRuntimeBundleInfo, IRuntimeBundleInfo,
IRuntimeAssetLocation, IRuntimeAssetLocation,
IAssetToPack, IAssetToPack,
IBundlePackOptions IBundlePackOptions,
hashBuffer
} from '@esengine/asset-system'; } from '@esengine/asset-system';
import { IAssetMeta } from '../meta/AssetMetaFile'; import { IAssetMeta } from '../meta/AssetMetaFile';
@@ -129,7 +130,7 @@ export class AssetPacker {
catalogBundles[bundleName] = { catalogBundles[bundleName] = {
url: `assets/${bundleName}.bundle`, url: `assets/${bundleName}.bundle`,
size: packed.data.byteLength, size: packed.data.byteLength,
hash: await this._hashBuffer(packed.data), hash: await hashBuffer(packed.data),
// 预加载核心资产包(可通过配置扩展) | Preload core bundles (extensible via config) // 预加载核心资产包(可通过配置扩展) | Preload core bundles (extensible via config)
preload: options.preloadBundles?.includes(bundleName) ?? preload: options.preloadBundles?.includes(bundleName) ??
(bundleName === 'core' || bundleName === 'main') (bundleName === 'core' || bundleName === 'main')
@@ -323,7 +324,7 @@ export class AssetPacker {
const manifest: IBundleManifest = { const manifest: IBundleManifest = {
name, name,
version: '1.0', version: '1.0',
hash: await this._hashBuffer(bundleData.buffer), hash: await hashBuffer(bundleData.buffer),
compression: 'none', compression: 'none',
size: bundleData.byteLength, size: bundleData.byteLength,
assets: assetInfos, assets: assetInfos,
@@ -338,27 +339,6 @@ export class AssetPacker {
}; };
} }
/**
* Hash a buffer using SHA-256
* 使用 SHA-256 哈希缓冲区
*/
private async _hashBuffer(buffer: ArrayBuffer): Promise<string> {
// Use Web Crypto API if available
if (typeof crypto !== 'undefined' && crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
}
// Fallback: simple hash
const view = new Uint8Array(buffer);
let hash = 0;
for (let i = 0; i < view.length; i++) {
hash = ((hash << 5) - hash) + view[i];
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(16, '0');
}
} }
/** /**
@@ -120,7 +120,7 @@ export class AssetManager implements IAssetManager {
* 可在构造后调用以加载目录条目。 * 可在构造后调用以加载目录条目。
*/ */
initializeFromCatalog(catalog: IAssetCatalog): void { initializeFromCatalog(catalog: IAssetCatalog): void {
catalog.entries.forEach((entry, guid) => { for (const [guid, entry] of Object.entries(catalog.entries)) {
const metadata: IAssetMetadata = { const metadata: IAssetMetadata = {
guid, guid,
path: entry.path, path: entry.path,
@@ -137,7 +137,7 @@ export class AssetManager implements IAssetManager {
this._database.addAsset(metadata); this._database.addAsset(metadata);
this._pathToGuid.set(entry.path, guid); this._pathToGuid.set(entry.path, guid);
}); }
} }
/** /**
+7
View File
@@ -55,6 +55,13 @@ export type { IResourceLoader } from './services/SceneResourceManager';
// Utils // Utils
export { UVHelper } from './utils/UVHelper'; export { UVHelper } from './utils/UVHelper';
export {
isValidGUID,
generateGUID,
hashBuffer,
hashString,
hashFileInfo
} from './utils/AssetUtils';
// Default instance // Default instance
import { AssetManager } from './core/AssetManager'; import { AssetManager } from './core/AssetManager';
+139 -15
View File
@@ -357,40 +357,164 @@ export interface IAssetLoadProgress {
progress: number; progress: number;
} }
/**
* Asset loading strategy
* 资产加载策略
*
* - 'file': Load assets directly via HTTP (development, simple builds)
* - 'bundle': Load assets from binary bundles (optimized production builds)
*
* - 'file': 通过 HTTP 直接加载资产(开发模式、简单构建)
* - 'bundle': 从二进制包加载资产(优化的生产构建)
*/
export type AssetLoadStrategy = 'file' | 'bundle';
/** /**
* Asset catalog entry for runtime lookups * Asset catalog entry for runtime lookups
* 运行时查找的资产目录条目 * 运行时查找的资产目录条目
*
* This is a unified format supporting both file-based and bundle-based loading.
* 这是一个统一格式,同时支持基于文件和基于包的加载。
*/ */
export interface IAssetCatalogEntry { export interface IAssetCatalogEntry {
/** 资产GUID */ /** 资产 GUID / Asset GUID */
guid: AssetGUID; guid: AssetGUID;
/** 资产路径 */
/** 资产相对路径 / Asset relative path (e.g., 'assets/textures/player.png') */
path: string; path: string;
/** 资产类型 */
/** 资产类型 / Asset type */
type: AssetType; type: AssetType;
/** 所在包名称 / Bundle containing this asset */
bundleName?: string; /** 文件大小(字节) / File size in bytes */
/** 可用变体 / Available variants */
variants?: IAssetVariant[];
/** 大小(字节) / Size in bytes */
size: number; size: number;
/** 内容哈希 / Content hash */
/** 内容哈希(用于缓存校验) / Content hash for cache validation */
hash: string; hash: string;
// ===== Bundle mode fields (optional) =====
// ===== Bundle 模式字段(可选)=====
/** 所在包名称(仅 bundle 模式) / Bundle name (bundle mode only) */
bundle?: string;
/** 包内偏移(仅 bundle 模式) / Offset within bundle (bundle mode only) */
offset?: number;
// ===== Optional metadata =====
// ===== 可选元数据 =====
/** 可用变体 / Available variants (platform/quality specific) */
variants?: IAssetVariant[];
}
/**
* Asset bundle info for runtime loading
* 运行时加载的资产包信息
*/
export interface IAssetBundleInfo {
/** 包 URL(相对于 catalog / Bundle URL relative to catalog */
url: string;
/** 包大小(字节) / Bundle size in bytes */
size: number;
/** 内容哈希 / Content hash for integrity check */
hash: string;
/** 是否预加载 / Whether to preload this bundle */
preload?: boolean;
/** 压缩类型 / Compression type */
compression?: 'none' | 'gzip' | 'brotli';
/** 依赖的其他包 / Dependencies on other bundles */
dependencies?: string[];
} }
/** /**
* Runtime asset catalog * Runtime asset catalog
* 运行时资产目录 * 运行时资产目录
*
* This is the canonical format for asset catalogs in ESEngine.
* Both WebBuildPipeline and AssetPacker generate this format.
* 这是 ESEngine 中资产目录的标准格式。
* WebBuildPipeline 和 AssetPacker 都生成此格式。
*
* @example File mode (development/simple builds)
* ```json
* {
* "version": "1.0.0",
* "createdAt": 1702185600000,
* "loadStrategy": "file",
* "entries": {
* "550e8400-e29b-41d4-a716-446655440000": {
* "guid": "550e8400-e29b-41d4-a716-446655440000",
* "path": "assets/textures/player.png",
* "type": "texture",
* "size": 12345,
* "hash": "abc123"
* }
* }
* }
* ```
*
* @example Bundle mode (optimized production)
* ```json
* {
* "version": "1.0.0",
* "createdAt": 1702185600000,
* "loadStrategy": "bundle",
* "entries": {
* "550e8400-e29b-41d4-a716-446655440000": {
* "guid": "550e8400-e29b-41d4-a716-446655440000",
* "path": "assets/textures/player.png",
* "type": "texture",
* "size": 12345,
* "hash": "abc123",
* "bundle": "textures",
* "offset": 1024
* }
* },
* "bundles": {
* "textures": {
* "url": "bundles/textures.bundle",
* "size": 1048576,
* "hash": "def456",
* "preload": true
* }
* }
* }
* ```
*/ */
export interface IAssetCatalog { export interface IAssetCatalog {
/** 版本号 */ /** 目录版本号 / Catalog version */
version: string; version: string;
/** 创建时间戳 / Creation timestamp */
/** 创建时间戳 / Creation timestamp (Unix ms) */
createdAt: number; createdAt: number;
/** 所有目录条目 / All catalog entries */
entries: Map<AssetGUID, IAssetCatalogEntry>; /**
/** 此目录中的包 / Bundles in this catalog */ * 加载策略 / Loading strategy
bundles: Map<string, IAssetBundleManifest>; * - 'file': 直接 HTTP 加载
* - 'bundle': 从二进制包加载
*/
loadStrategy: AssetLoadStrategy;
/**
* 资产条目(GUID 到条目的映射)
* Asset entries (GUID to entry mapping)
*
* Uses Record for JSON serialization compatibility.
* 使用 Record 以兼容 JSON 序列化。
*/
entries: Record<AssetGUID, IAssetCatalogEntry>;
/**
* 包信息(仅 bundle 模式)
* Bundle info (bundle mode only)
*/
bundles?: Record<string, IAssetBundleInfo>;
} }
/** /**
@@ -0,0 +1,119 @@
/**
* Asset Utilities
* 资产工具函数
*
* Provides common utilities for asset management:
* - GUID validation and generation
* - Content hashing
* 提供资产管理的通用工具:
* - GUID 验证和生成
* - 内容哈希
*/
import type { AssetGUID } from '../types/AssetTypes';
// ============================================================================
// GUID Utilities
// GUID 工具
// ============================================================================
/**
* UUID v4 regex pattern
* UUID v4 正则表达式
*/
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
/**
* Check if a string is a valid UUID v4 format
* 检查字符串是否为有效的 UUID v4 格式
*/
export function isValidGUID(guid: string): boolean {
return UUID_REGEX.test(guid);
}
/**
* Generate a new UUID v4
* 生成新的 UUID v4
*
* Uses crypto.randomUUID() if available, otherwise falls back to manual generation.
* 如果可用则使用 crypto.randomUUID(),否则回退到手动生成。
*/
export function generateGUID(): AssetGUID {
// Use native crypto if available (Node.js, modern browsers)
// 如果可用则使用原生 crypto(Node.js、现代浏览器)
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback: manual UUID v4 generation
// 回退:手动生成 UUID v4
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// ============================================================================
// Hash Utilities
// 哈希工具
// ============================================================================
/**
* Compute SHA-256 hash of an ArrayBuffer
* 计算 ArrayBuffer 的 SHA-256 哈希
*
* Returns first 16 hex characters of the hash.
* 返回哈希的前 16 个十六进制字符。
*
* @param buffer - The buffer to hash
* @returns Hash string (16 hex characters)
*/
export async function hashBuffer(buffer: ArrayBuffer): Promise<string> {
// Use Web Crypto API if available
// 如果可用则使用 Web Crypto API
if (typeof crypto !== 'undefined' && crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
}
// Fallback: simple DJB2 hash
// 回退:简单的 DJB2 哈希
const view = new Uint8Array(buffer);
let hash = 5381;
for (let i = 0; i < view.length; i++) {
hash = ((hash << 5) + hash) ^ view[i];
}
return Math.abs(hash).toString(16).padStart(16, '0');
}
/**
* Compute hash of a string
* 计算字符串的哈希
*
* @param str - The string to hash
* @returns Hash string (8 hex characters)
*/
export function hashString(str: string): string {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
}
return Math.abs(hash).toString(16).padStart(8, '0');
}
/**
* Compute content hash from file path and size
* 从文件路径和大小计算内容哈希
*
* This is a lightweight hash for quick comparison, not cryptographically secure.
* 这是一个用于快速比较的轻量级哈希,不具有加密安全性。
*
* @param path - File path
* @param size - File size in bytes
* @returns Hash string (8 hex characters)
*/
export function hashFileInfo(path: string, size: number): string {
return hashString(`${path}:${size}`);
}
+1 -5
View File
@@ -10,11 +10,7 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.mjs", "import": "./dist/index.mjs",
"require": "./dist/index.cjs", "require": "./dist/index.cjs"
"development": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
} }
}, },
"files": [ "files": [
@@ -271,9 +271,6 @@ export class ArchetypeSystem {
private generateArchetypeId(componentTypes: ComponentType[]): ArchetypeId { private generateArchetypeId(componentTypes: ComponentType[]): ArchetypeId {
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO); const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const type of componentTypes) { for (const type of componentTypes) {
if (!ComponentRegistry.isRegistered(type)) {
ComponentRegistry.register(type);
}
const bitMask = ComponentRegistry.getBitMask(type); const bitMask = ComponentRegistry.getBitMask(type);
BitMask64Utils.orInPlace(mask, bitMask); BitMask64Utils.orInPlace(mask, bitMask);
} }
@@ -2,11 +2,12 @@ import { Component } from '../Component';
import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility'; import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility';
import { SoAStorage, SupportedTypedArray } from './SoAStorage'; import { SoAStorage, SupportedTypedArray } from './SoAStorage';
import { createLogger } from '../../Utils/Logger'; import { createLogger } from '../../Utils/Logger';
import { getComponentTypeName } from '../Decorators'; import { getComponentTypeName, ComponentType } from '../Decorators';
import { ComponentRegistry, ComponentType } from './ComponentStorage/ComponentRegistry'; import { ComponentRegistry } from './ComponentStorage/ComponentRegistry';
// 导出核心类型 // 导出核心类型
export { ComponentRegistry, ComponentType }; export { ComponentRegistry };
export type { ComponentType };
/** /**
@@ -20,11 +21,6 @@ export class ComponentStorage<T extends Component> {
constructor(componentType: ComponentType<T>) { constructor(componentType: ComponentType<T>) {
this.componentType = componentType; this.componentType = componentType;
// 确保组件类型已注册
if (!ComponentRegistry.isRegistered(componentType)) {
ComponentRegistry.register(componentType);
}
} }
/** /**
@@ -1,12 +1,11 @@
import { Component } from '../../Component'; import { Component } from '../../Component';
import { BitMask64Utils, BitMask64Data } from '../../Utils/BigIntCompatibility'; import { BitMask64Utils, BitMask64Data } from '../../Utils/BigIntCompatibility';
import { createLogger } from '../../../Utils/Logger'; import { createLogger } from '../../../Utils/Logger';
import { getComponentTypeName } from '../../Decorators'; import {
ComponentType,
/** getComponentTypeName,
* 组件类型定义 hasECSComponentDecorator
*/ } from './ComponentTypeUtils';
export type ComponentType<T extends Component = Component> = new (...args: any[]) => T;
/** /**
* 组件注册表 * 组件注册表
@@ -29,14 +28,33 @@ export class ComponentRegistry {
*/ */
private static hotReloadEnabled = false; private static hotReloadEnabled = false;
/**
* 已警告过的组件类型集合,避免重复警告
* Set of warned component types to avoid duplicate warnings
*/
private static warnedComponents = new Set<Function>();
/** /**
* 注册组件类型并分配位掩码 * 注册组件类型并分配位掩码
* Register component type and allocate bitmask
*
* @param componentType 组件类型 * @param componentType 组件类型
* @returns 分配的位索引 * @returns 分配的位索引
*/ */
public static register<T extends Component>(componentType: ComponentType<T>): number { public static register<T extends Component>(componentType: ComponentType<T>): number {
const typeName = getComponentTypeName(componentType); const typeName = getComponentTypeName(componentType);
// 检查是否使用了 @ECSComponent 装饰器
// Check if @ECSComponent decorator is used
if (!hasECSComponentDecorator(componentType) && !this.warnedComponents.has(componentType)) {
this.warnedComponents.add(componentType);
console.warn(
`[ComponentRegistry] Component "${typeName}" is missing @ECSComponent decorator. ` +
`This may cause issues with serialization and code minification. ` +
`Please add: @ECSComponent('${typeName}')`
);
}
if (this.componentTypes.has(componentType)) { if (this.componentTypes.has(componentType)) {
const existingIndex = this.componentTypes.get(componentType)!; const existingIndex = this.componentTypes.get(componentType)!;
return existingIndex; return existingIndex;
@@ -324,6 +342,7 @@ export class ComponentRegistry {
this.componentNameToType.clear(); this.componentNameToType.clear();
this.componentNameToId.clear(); this.componentNameToId.clear();
this.maskCache.clear(); this.maskCache.clear();
this.warnedComponents.clear();
this.nextBitIndex = 0; this.nextBitIndex = 0;
this.hotReloadEnabled = false; this.hotReloadEnabled = false;
} }
@@ -0,0 +1,83 @@
/**
* Component Type Utilities
* 组件类型工具函数
*
* This module contains low-level utilities for component type handling.
* It has NO dependencies on other ECS modules to avoid circular imports.
*
* 此模块包含组件类型处理的底层工具函数。
* 它不依赖其他 ECS 模块,以避免循环导入。
*/
import type { Component } from '../../Component';
/**
* 组件类型定义
* Component type definition
*/
export type ComponentType<T extends Component = Component> = new (...args: any[]) => T;
/**
* 存储组件类型名称的 Symbol 键
* Symbol key for storing component type name
*/
export const COMPONENT_TYPE_NAME = Symbol('ComponentTypeName');
/**
* 存储组件依赖的 Symbol 键
* Symbol key for storing component dependencies
*/
export const COMPONENT_DEPENDENCIES = Symbol('ComponentDependencies');
/**
* 检查组件是否使用了 @ECSComponent 装饰器
* Check if component has @ECSComponent decorator
*
* @param componentType 组件构造函数
* @returns 是否有装饰器
*/
export function hasECSComponentDecorator(componentType: ComponentType): boolean {
return !!(componentType as any)[COMPONENT_TYPE_NAME];
}
/**
* 获取组件类型的名称,优先使用装饰器指定的名称
* Get component type name, preferring decorator-specified name
*
* @param componentType 组件构造函数
* @returns 组件类型名称
*/
export function getComponentTypeName(componentType: ComponentType): string {
// 优先使用装饰器指定的名称
// Prefer decorator-specified name
const decoratorName = (componentType as any)[COMPONENT_TYPE_NAME];
if (decoratorName) {
return decoratorName;
}
// 回退到 constructor.name
// Fallback to constructor.name
return componentType.name || 'UnknownComponent';
}
/**
* 从组件实例获取类型名称
* Get type name from component instance
*
* @param component 组件实例
* @returns 组件类型名称
*/
export function getComponentInstanceTypeName(component: Component): string {
return getComponentTypeName(component.constructor as ComponentType);
}
/**
* 获取组件的依赖列表
* Get component dependencies
*
* @param componentType 组件构造函数
* @returns 依赖的组件名称列表
*/
export function getComponentDependencies(componentType: ComponentType): string[] | undefined {
return (componentType as any)[COMPONENT_DEPENDENCIES];
}
+1 -5
View File
@@ -821,13 +821,9 @@ export class QuerySystem {
return cached; return cached;
} }
// 使用ComponentRegistry而不是ComponentTypeManager,确保bitIndex一致 // 使用ComponentRegistry确保bitIndex一致
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO); const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const type of componentTypes) { for (const type of componentTypes) {
// 确保组件已注册
if (!ComponentRegistry.isRegistered(type)) {
ComponentRegistry.register(type);
}
const bitMask = ComponentRegistry.getBitMask(type); const bitMask = ComponentRegistry.getBitMask(type);
BitMask64Utils.orInPlace(mask, bitMask); BitMask64Utils.orInPlace(mask, bitMask);
} }
@@ -1,36 +1,48 @@
/**
* Type Decorators for ECS Components and Systems
* ECS 组件和系统的类型装饰器
*
* Provides decorators to mark component/system types with stable names
* that survive code minification.
*
* 提供装饰器为组件/系统类型标记稳定的名称,使其在代码混淆后仍然有效。
*/
import type { Component } from '../Component'; import type { Component } from '../Component';
import type { EntitySystem } from '../Systems'; import type { EntitySystem } from '../Systems';
import { ComponentType } from '../../Types'; import { ComponentRegistry } from '../Core/ComponentStorage/ComponentRegistry';
import {
/** COMPONENT_TYPE_NAME,
* 存储组件类型名称的Symbol键 COMPONENT_DEPENDENCIES
*/ } from '../Core/ComponentStorage/ComponentTypeUtils';
export const COMPONENT_TYPE_NAME = Symbol('ComponentTypeName');
/**
* 存储组件依赖的Symbol键
*/
export const COMPONENT_DEPENDENCIES = Symbol('ComponentDependencies');
/** /**
* 存储系统类型名称的Symbol键 * 存储系统类型名称的Symbol键
* Symbol key for storing system type name
*/ */
export const SYSTEM_TYPE_NAME = Symbol('SystemTypeName'); export const SYSTEM_TYPE_NAME = Symbol('SystemTypeName');
/** /**
* 组件装饰器配置选项 * 组件装饰器配置选项
* Component decorator options
*/ */
export interface ComponentOptions { export interface ComponentOptions {
/** 依赖的其他组件名称列表 */ /** 依赖的其他组件名称列表 | List of required component names */
requires?: string[]; requires?: string[];
} }
/** /**
* 组件类型装饰器 * 组件类型装饰器
* 用于为组件类指定固定的类型名称,避免在代码混淆后失效 * Component type decorator
* *
* @param typeName 组件类型名称 * 用于为组件类指定固定的类型名称,避免在代码混淆后失效。
* @param options 组件配置选项 * 装饰器执行时会自动注册到 ComponentRegistry,使组件可以通过名称反序列化。
*
* Assigns a stable type name to component classes that survives minification.
* The decorator automatically registers to ComponentRegistry, enabling deserialization by name.
*
* @param typeName 组件类型名称 | Component type name
* @param options 组件配置选项 | Component options
* @example * @example
* ```typescript * ```typescript
* @ECSComponent('Position') * @ECSComponent('Position')
@@ -39,7 +51,7 @@ export interface ComponentOptions {
* y: number = 0; * y: number = 0;
* } * }
* *
* // 带依赖声明 * // 带依赖声明 | With dependency declaration
* @ECSComponent('SpriteAnimator', { requires: ['Sprite'] }) * @ECSComponent('SpriteAnimator', { requires: ['Sprite'] })
* class SpriteAnimatorComponent extends Component { * class SpriteAnimatorComponent extends Component {
* // ... * // ...
@@ -53,48 +65,52 @@ export function ECSComponent(typeName: string, options?: ComponentOptions) {
} }
// 在构造函数上存储类型名称 // 在构造函数上存储类型名称
// Store type name on constructor
(target as any)[COMPONENT_TYPE_NAME] = typeName; (target as any)[COMPONENT_TYPE_NAME] = typeName;
// 存储依赖关系 // 存储依赖关系
// Store dependencies
if (options?.requires) { if (options?.requires) {
(target as any)[COMPONENT_DEPENDENCIES] = options.requires; (target as any)[COMPONENT_DEPENDENCIES] = options.requires;
} }
// 自动注册到 ComponentRegistry,使组件可以通过名称查找
// Auto-register to ComponentRegistry, enabling lookup by name
ComponentRegistry.register(target);
return target; return target;
}; };
} }
/** /**
* 获取组件的依赖列表 * System 元数据配置
*/ * System metadata configuration
export function getComponentDependencies(componentType: ComponentType): string[] | undefined {
return (componentType as any)[COMPONENT_DEPENDENCIES];
}
/**
* System元数据配置
*/ */
export interface SystemMetadata { export interface SystemMetadata {
/** /**
* 更新顺序(数值越小越先执行,默认0) * 更新顺序(数值越小越先执行,默认0)
* Update order (lower values execute first, default 0)
*/ */
updateOrder?: number; updateOrder?: number;
/** /**
* 是否默认启用(默认true) * 是否默认启用(默认true)
* Whether enabled by default (default true)
*/ */
enabled?: boolean; enabled?: boolean;
} }
/** /**
* 系统类型装饰器 * 系统类型装饰器
* 用于为系统类指定固定的类型名称,避免在代码混淆后失效 * System type decorator
* *
* @param typeName 系统类型名称 * 用于为系统类指定固定的类型名称,避免在代码混淆后失效。
* @param metadata 系统元数据配置 * Assigns a stable type name to system classes that survives minification.
*
* @param typeName 系统类型名称 | System type name
* @param metadata 系统元数据配置 | System metadata configuration
* @example * @example
* ```typescript * ```typescript
* // 基本使用
* @ECSSystem('Movement') * @ECSSystem('Movement')
* class MovementSystem extends EntitySystem { * class MovementSystem extends EntitySystem {
* protected process(entities: Entity[]): void { * protected process(entities: Entity[]): void {
@@ -102,16 +118,9 @@ export interface SystemMetadata {
* } * }
* } * }
* *
* // 配置更新顺序和依赖注入
* @Injectable()
* @ECSSystem('Physics', { updateOrder: 10 }) * @ECSSystem('Physics', { updateOrder: 10 })
* class PhysicsSystem extends EntitySystem { * class PhysicsSystem extends EntitySystem {
* @InjectProperty(CollisionSystem) * // ...
* private collision!: CollisionSystem;
*
* constructor() {
* super(Matcher.empty().all(Transform, RigidBody));
* }
* } * }
* ``` * ```
*/ */
@@ -122,9 +131,11 @@ export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
} }
// 在构造函数上存储类型名称 // 在构造函数上存储类型名称
// Store type name on constructor
(target as any)[SYSTEM_TYPE_NAME] = typeName; (target as any)[SYSTEM_TYPE_NAME] = typeName;
// 存储元数据 // 存储元数据
// Store metadata
if (metadata) { if (metadata) {
(target as any).__systemMetadata__ = metadata; (target as any).__systemMetadata__ = metadata;
} }
@@ -134,67 +145,37 @@ export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
} }
/** /**
* 获取System的元数据 * 获取 System 的元数据
* Get System metadata
*/ */
export function getSystemMetadata(systemType: new (...args: any[]) => EntitySystem): SystemMetadata | undefined { export function getSystemMetadata(systemType: new (...args: any[]) => EntitySystem): SystemMetadata | undefined {
return (systemType as any).__systemMetadata__; return (systemType as any).__systemMetadata__;
} }
/**
* 获取组件类型的名称,优先使用装饰器指定的名称
*
* @param componentType 组件构造函数
* @returns 组件类型名称
*/
export function getComponentTypeName(
componentType: ComponentType
): string {
// 优先使用装饰器指定的名称
const decoratorName = (componentType as any)[COMPONENT_TYPE_NAME];
if (decoratorName) {
return decoratorName;
}
// 回退到constructor.name
return componentType.name || 'UnknownComponent';
}
/** /**
* 获取系统类型的名称,优先使用装饰器指定的名称 * 获取系统类型的名称,优先使用装饰器指定的名称
* Get system type name, preferring decorator-specified name
* *
* @param systemType 系统构造函数 * @param systemType 系统构造函数 | System constructor
* @returns 系统类型名称 * @returns 系统类型名称 | System type name
*/ */
export function getSystemTypeName<T extends EntitySystem>( export function getSystemTypeName<T extends EntitySystem>(
systemType: new (...args: any[]) => T systemType: new (...args: any[]) => T
): string { ): string {
// 优先使用装饰器指定的名称
const decoratorName = (systemType as any)[SYSTEM_TYPE_NAME]; const decoratorName = (systemType as any)[SYSTEM_TYPE_NAME];
if (decoratorName) { if (decoratorName) {
return decoratorName; return decoratorName;
} }
// 回退到constructor.name
return systemType.name || 'UnknownSystem'; return systemType.name || 'UnknownSystem';
} }
/**
* 从组件实例获取类型名称
*
* @param component 组件实例
* @returns 组件类型名称
*/
export function getComponentInstanceTypeName(component: Component): string {
return getComponentTypeName(component.constructor as ComponentType);
}
/** /**
* 从系统实例获取类型名称 * 从系统实例获取类型名称
* Get type name from system instance
* *
* @param system 系统实例 * @param system 系统实例 | System instance
* @returns 系统类型名称 * @returns 系统类型名称 | System type name
*/ */
export function getSystemInstanceTypeName(system: EntitySystem): string { export function getSystemInstanceTypeName(system: EntitySystem): string {
return getSystemTypeName(system.constructor as new (...args: any[]) => EntitySystem); return getSystemTypeName(system.constructor as new (...args: any[]) => EntitySystem);
} }
+35 -6
View File
@@ -1,19 +1,37 @@
// ============================================================================
// Component Type Utilities (from ComponentTypeUtils - no circular deps)
// 组件类型工具(来自 ComponentTypeUtils - 无循环依赖)
// ============================================================================
export {
COMPONENT_TYPE_NAME,
COMPONENT_DEPENDENCIES,
getComponentTypeName,
getComponentInstanceTypeName,
getComponentDependencies,
hasECSComponentDecorator
} from '../Core/ComponentStorage/ComponentTypeUtils';
export type { ComponentType } from '../Core/ComponentStorage/ComponentTypeUtils';
// ============================================================================
// Type Decorators (ECSComponent, ECSSystem)
// 类型装饰器
// ============================================================================
export { export {
ECSComponent, ECSComponent,
ECSSystem, ECSSystem,
getComponentTypeName,
getSystemTypeName, getSystemTypeName,
getComponentInstanceTypeName,
getSystemInstanceTypeName, getSystemInstanceTypeName,
getSystemMetadata, getSystemMetadata,
getComponentDependencies,
COMPONENT_TYPE_NAME,
COMPONENT_DEPENDENCIES,
SYSTEM_TYPE_NAME SYSTEM_TYPE_NAME
} from './TypeDecorators'; } from './TypeDecorators';
export type { SystemMetadata, ComponentOptions } from './TypeDecorators'; export type { SystemMetadata, ComponentOptions } from './TypeDecorators';
// ============================================================================
// Entity Reference Decorator
// 实体引用装饰器
// ============================================================================
export { export {
EntityRef, EntityRef,
getEntityRefMetadata, getEntityRefMetadata,
@@ -23,6 +41,10 @@ export {
export type { EntityRefMetadata } from './EntityRefDecorator'; export type { EntityRefMetadata } from './EntityRefDecorator';
// ============================================================================
// Property Decorator
// 属性装饰器
// ============================================================================
export { export {
Property, Property,
getPropertyMetadata, getPropertyMetadata,
@@ -30,4 +52,11 @@ export {
PROPERTY_METADATA PROPERTY_METADATA
} from './PropertyDecorator'; } from './PropertyDecorator';
export type { PropertyOptions, PropertyType, PropertyControl, PropertyAction, AssetType, EnumOption } from './PropertyDecorator'; export type {
PropertyOptions,
PropertyType,
PropertyControl,
PropertyAction,
AssetType,
EnumOption
} from './PropertyDecorator';
+2 -5
View File
@@ -368,11 +368,8 @@ export class Entity {
private addComponentInternal<T extends Component>(component: T): T { private addComponentInternal<T extends Component>(component: T): T {
const componentType = component.constructor as ComponentType<T>; const componentType = component.constructor as ComponentType<T>;
if (!ComponentRegistry.isRegistered(componentType)) { // 更新位掩码(组件已通过 @ECSComponent 装饰器自动注册)
ComponentRegistry.register(componentType); // Update bitmask (component already registered via @ECSComponent decorator)
}
// 更新位掩码
const componentMask = ComponentRegistry.getBitMask(componentType); const componentMask = ComponentRegistry.getBitMask(componentType);
BitMask64Utils.orInPlace(this._componentMask, componentMask); BitMask64Utils.orInPlace(this._componentMask, componentMask);
-4
View File
@@ -672,10 +672,6 @@ export class Scene implements IScene {
* @param system 系统 | System * @param system 系统 | System
*/ */
private addSystemToComponentIndex(componentType: ComponentType, system: EntitySystem): void { private addSystemToComponentIndex(componentType: ComponentType, system: EntitySystem): void {
if (!ComponentRegistry.isRegistered(componentType)) {
ComponentRegistry.register(componentType);
}
const componentId = ComponentRegistry.getBitIndex(componentType); const componentId = ComponentRegistry.getBitIndex(componentType);
let systems = this._componentIdToSystems.get(componentId); let systems = this._componentIdToSystems.get(componentId);
@@ -85,11 +85,6 @@ export class ComponentSparseSet {
const componentType = component.constructor as ComponentType; const componentType = component.constructor as ComponentType;
entityComponents.add(componentType); entityComponents.add(componentType);
// 确保组件类型已注册
if (!ComponentRegistry.isRegistered(componentType)) {
ComponentRegistry.register(componentType);
}
// 获取组件位掩码并合并 // 获取组件位掩码并合并
const bitMask = ComponentRegistry.getBitMask(componentType); const bitMask = ComponentRegistry.getBitMask(componentType);
BitMask64Utils.orInPlace(componentMask, bitMask); BitMask64Utils.orInPlace(componentMask, bitMask);
-8
View File
@@ -49,14 +49,6 @@ export interface ISystemBase {
lateUpdate?(): void; lateUpdate?(): void;
} }
/**
* 组件类型定义
*
* 用于类型安全的组件操作
* 支持任意构造函数签名,提供更好的类型安全性
*/
export type ComponentType<T extends IComponent = IComponent> = new (...args: any[]) => T;
/** /**
* 事件总线接口 * 事件总线接口
* 提供类型安全的事件发布订阅机制 * 提供类型安全的事件发布订阅机制
@@ -1,8 +1,10 @@
import { Component } from '../../src/ECS/Component'; import { Component } from '../../src/ECS/Component';
import { Entity } from '../../src/ECS/Entity'; import { Entity } from '../../src/ECS/Entity';
import { Scene } from '../../src/ECS/Scene'; import { Scene } from '../../src/ECS/Scene';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试组件 // 测试组件
@ECSComponent('ComponentTest_TestComponent')
class TestComponent extends Component { class TestComponent extends Component {
public value: number = 100; public value: number = 100;
public onAddedCalled = false; public onAddedCalled = false;
@@ -17,6 +19,7 @@ class TestComponent extends Component {
} }
} }
@ECSComponent('ComponentTest_AnotherTestComponent')
class AnotherTestComponent extends Component { class AnotherTestComponent extends Component {
public name: string = 'test'; public name: string = 'test';
} }
@@ -4,8 +4,10 @@ import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene'; import { Scene } from '../../../src/ECS/Scene';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher'; import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件 // 测试组件
@ECSComponent('CmdBuf_HealthComponent')
class HealthComponent extends Component { class HealthComponent extends Component {
public value: number = 100; public value: number = 100;
@@ -16,10 +18,12 @@ class HealthComponent extends Component {
} }
} }
@ECSComponent('CmdBuf_MarkerComponent')
class MarkerComponent extends Component { class MarkerComponent extends Component {
public marked: boolean = true; public marked: boolean = true;
} }
@ECSComponent('CmdBuf_VelocityComponent')
class VelocityComponent extends Component { class VelocityComponent extends Component {
public vx: number = 0; public vx: number = 0;
public vy: number = 0; public vy: number = 0;
@@ -3,9 +3,10 @@ import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component'; import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher'; import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage'; import { ECSComponent } from '../../../src/ECS/Decorators';
// 简单的测试组件 // 简单的测试组件
@ECSComponent('MinSysInit_HealthComponent')
class HealthComponent extends Component { class HealthComponent extends Component {
public health: number; public health: number;
@@ -34,7 +35,6 @@ describe('MinimalSystemInit - 最小化系统初始化测试', () => {
let scene: Scene; let scene: Scene;
beforeEach(() => { beforeEach(() => {
ComponentRegistry.reset();
scene = new Scene(); scene = new Scene();
}); });
@@ -52,7 +52,6 @@ describe('MinimalSystemInit - 最小化系统初始化测试', () => {
entity.addComponent(new HealthComponent(100)); entity.addComponent(new HealthComponent(100));
console.log('[Test] Entity created with HealthComponent'); console.log('[Test] Entity created with HealthComponent');
console.log('[Test] ComponentRegistry registered types:', ComponentRegistry.getRegisteredCount());
// 2. 验证QuerySystem能查询到实体 // 2. 验证QuerySystem能查询到实体
const queryResult = scene.querySystem.queryAll(HealthComponent); const queryResult = scene.querySystem.queryAll(HealthComponent);
@@ -3,9 +3,10 @@ import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component'; import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher'; import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage'; import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件 // 测试组件
@ECSComponent('MultiSysInit_PositionComponent')
class PositionComponent extends Component { class PositionComponent extends Component {
public x: number; public x: number;
public y: number; public y: number;
@@ -18,6 +19,7 @@ class PositionComponent extends Component {
} }
} }
@ECSComponent('MultiSysInit_VelocityComponent')
class VelocityComponent extends Component { class VelocityComponent extends Component {
public vx: number; public vx: number;
public vy: number; public vy: number;
@@ -30,6 +32,7 @@ class VelocityComponent extends Component {
} }
} }
@ECSComponent('MultiSysInit_HealthComponent')
class HealthComponent extends Component { class HealthComponent extends Component {
public health: number; public health: number;
@@ -71,7 +74,6 @@ describe('MultiSystemInit - 多系统初始化测试', () => {
let scene: Scene; let scene: Scene;
beforeEach(() => { beforeEach(() => {
ComponentRegistry.reset();
scene = new Scene(); scene = new Scene();
}); });
@@ -91,7 +93,6 @@ describe('MultiSystemInit - 多系统初始化测试', () => {
entity.addComponent(new HealthComponent(100)); entity.addComponent(new HealthComponent(100));
console.log('[Test] Entity created with Position, Velocity, Health'); console.log('[Test] Entity created with Position, Velocity, Health');
console.log('[Test] ComponentRegistry registered types:', ComponentRegistry.getRegisteredCount());
// 2. 验证QuerySystem能查询到实体 // 2. 验证QuerySystem能查询到实体
const movementQuery = scene.querySystem.queryAll(PositionComponent, VelocityComponent); const movementQuery = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
@@ -3,8 +3,10 @@ import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component'; import { Component } from '../../../src/ECS/Component';
import { ComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage'; import { ComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage';
import { Scene } from '../../../src/ECS/Scene'; import { Scene } from '../../../src/ECS/Scene';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件 // 测试组件
@ECSComponent('QuerySys_PositionComponent')
class PositionComponent extends Component { class PositionComponent extends Component {
public x: number; public x: number;
public y: number; public y: number;
@@ -17,6 +19,7 @@ class PositionComponent extends Component {
} }
} }
@ECSComponent('QuerySys_VelocityComponent')
class VelocityComponent extends Component { class VelocityComponent extends Component {
public vx: number; public vx: number;
public vy: number; public vy: number;
@@ -29,6 +32,7 @@ class VelocityComponent extends Component {
} }
} }
@ECSComponent('QuerySys_HealthComponent')
class HealthComponent extends Component { class HealthComponent extends Component {
public health: number; public health: number;
public maxHealth: number; public maxHealth: number;
@@ -41,6 +45,7 @@ class HealthComponent extends Component {
} }
} }
@ECSComponent('QuerySys_RenderComponent')
class RenderComponent extends Component { class RenderComponent extends Component {
public visible: boolean; public visible: boolean;
public layer: number; public layer: number;
@@ -53,6 +58,7 @@ class RenderComponent extends Component {
} }
} }
@ECSComponent('QuerySys_AIComponent')
class AIComponent extends Component { class AIComponent extends Component {
public behavior: string; public behavior: string;
@@ -63,6 +69,7 @@ class AIComponent extends Component {
} }
} }
@ECSComponent('QuerySys_PhysicsComponent')
class PhysicsComponent extends Component { class PhysicsComponent extends Component {
public mass: number; public mass: number;
@@ -3,7 +3,9 @@ import { ReferenceTracker } from '../../../src/ECS/Core/ReferenceTracker';
import { Component } from '../../../src/ECS/Component'; import { Component } from '../../../src/ECS/Component';
import { Entity } from '../../../src/ECS/Entity'; import { Entity } from '../../../src/ECS/Entity';
import { Scene } from '../../../src/ECS/Scene'; import { Scene } from '../../../src/ECS/Scene';
import { ECSComponent } from '../../../src/ECS/Decorators';
@ECSComponent('RefTrackerTestComponent')
class TestComponent extends Component { class TestComponent extends Component {
public target: Entity | null = null; public target: Entity | null = null;
} }
@@ -3,6 +3,7 @@ import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component'; import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher'; import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ECSComponent } from '../../../src/ECS/Decorators';
/** /**
* System初始化测试套件 * System初始化测试套件
@@ -15,6 +16,7 @@ import { Matcher } from '../../../src/ECS/Utils/Matcher';
*/ */
// 测试组件 // 测试组件
@ECSComponent('SysInit_PositionComponent')
class PositionComponent extends Component { class PositionComponent extends Component {
public x: number; public x: number;
public y: number; public y: number;
@@ -27,6 +29,7 @@ class PositionComponent extends Component {
} }
} }
@ECSComponent('SysInit_VelocityComponent')
class VelocityComponent extends Component { class VelocityComponent extends Component {
public vx: number; public vx: number;
public vy: number; public vy: number;
@@ -39,6 +42,7 @@ class VelocityComponent extends Component {
} }
} }
@ECSComponent('SysInit_HealthComponent')
class HealthComponent extends Component { class HealthComponent extends Component {
public health: number; public health: number;
@@ -49,6 +53,7 @@ class HealthComponent extends Component {
} }
} }
@ECSComponent('SysInit_TagComponent')
class TagComponent extends Component { class TagComponent extends Component {
public tag: string; public tag: string;
@@ -59,6 +64,7 @@ class TagComponent extends Component {
} }
} }
@ECSComponent('SysInit_TestComponent')
class TestComponent extends Component { class TestComponent extends Component {
public value: number; public value: number;
@@ -7,8 +7,10 @@ import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Component } from '../../../src/ECS/Component'; import { Component } from '../../../src/ECS/Component';
import { Matcher } from '../../../src/ECS/Utils/Matcher'; import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { Entity } from '../../../src/ECS/Entity'; import { Entity } from '../../../src/ECS/Entity';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试用组件 // 测试用组件
@ECSComponent('WorldCore_TestComponent')
class TestComponent extends Component { class TestComponent extends Component {
public value: number = 0; public value: number = 0;
+7 -2
View File
@@ -1,8 +1,10 @@
import { Entity } from '../../src/ECS/Entity'; import { Entity } from '../../src/ECS/Entity';
import { Component } from '../../src/ECS/Component'; import { Component } from '../../src/ECS/Component';
import { Scene } from '../../src/ECS/Scene'; import { Scene } from '../../src/ECS/Scene';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试组件类 // 测试组件类
@ECSComponent('EntityTest_PositionComponent')
class TestPositionComponent extends Component { class TestPositionComponent extends Component {
public x: number = 0; public x: number = 0;
public y: number = 0; public y: number = 0;
@@ -15,6 +17,7 @@ class TestPositionComponent extends Component {
} }
} }
@ECSComponent('EntityTest_HealthComponent')
class TestHealthComponent extends Component { class TestHealthComponent extends Component {
public health: number = 100; public health: number = 100;
@@ -25,6 +28,7 @@ class TestHealthComponent extends Component {
} }
} }
@ECSComponent('EntityTest_VelocityComponent')
class TestVelocityComponent extends Component { class TestVelocityComponent extends Component {
public vx: number = 0; public vx: number = 0;
public vy: number = 0; public vy: number = 0;
@@ -37,6 +41,7 @@ class TestVelocityComponent extends Component {
} }
} }
@ECSComponent('EntityTest_RenderComponent')
class TestRenderComponent extends Component { class TestRenderComponent extends Component {
public visible: boolean = true; public visible: boolean = true;
@@ -267,8 +272,8 @@ describe('Entity - 组件缓存优化测试', () => {
expect(debugInfo.name).toBe('TestEntity'); expect(debugInfo.name).toBe('TestEntity');
expect(debugInfo.id).toBeGreaterThanOrEqual(0); expect(debugInfo.id).toBeGreaterThanOrEqual(0);
expect(debugInfo.componentCount).toBe(2); expect(debugInfo.componentCount).toBe(2);
expect(debugInfo.componentTypes).toContain('TestPositionComponent'); expect(debugInfo.componentTypes).toContain('EntityTest_PositionComponent');
expect(debugInfo.componentTypes).toContain('TestHealthComponent'); expect(debugInfo.componentTypes).toContain('EntityTest_HealthComponent');
expect(debugInfo.cacheBuilt).toBe(true); expect(debugInfo.cacheBuilt).toBe(true);
}); });
}); });
@@ -6,20 +6,23 @@ import { Matcher } from '../../src/ECS/Utils/Matcher';
import { Injectable, InjectProperty } from '../../src/Core/DI'; import { Injectable, InjectProperty } from '../../src/Core/DI';
import { Core } from '../../src/Core'; import { Core } from '../../src/Core';
import type { IService } from '../../src/Core/ServiceContainer'; import type { IService } from '../../src/Core/ServiceContainer';
import { ECSSystem } from '../../src/ECS/Decorators'; import { ECSSystem, ECSComponent } from '../../src/ECS/Decorators';
@ECSComponent('DI_Transform')
class Transform extends Component { class Transform extends Component {
constructor(public x: number = 0, public y: number = 0) { constructor(public x: number = 0, public y: number = 0) {
super(); super();
} }
} }
@ECSComponent('DI_Velocity')
class Velocity extends Component { class Velocity extends Component {
constructor(public vx: number = 0, public vy: number = 0) { constructor(public vx: number = 0, public vy: number = 0) {
super(); super();
} }
} }
@ECSComponent('DI_Health')
class Health extends Component { class Health extends Component {
constructor(public value: number = 100) { constructor(public value: number = 100) {
super(); super();
@@ -3,8 +3,10 @@ import { Component } from '../../src/ECS/Component';
import { Scene } from '../../src/ECS/Scene'; import { Scene } from '../../src/ECS/Scene';
import { SceneManager } from '../../src/ECS/SceneManager'; import { SceneManager } from '../../src/ECS/SceneManager';
import { EEntityLifecyclePolicy } from '../../src/ECS/Core/EntityLifecyclePolicy'; import { EEntityLifecyclePolicy } from '../../src/ECS/Core/EntityLifecyclePolicy';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试组件 // 测试组件
@ECSComponent('Persistent_PositionComponent')
class PositionComponent extends Component { class PositionComponent extends Component {
public x: number; public x: number;
public y: number; public y: number;
@@ -16,6 +18,7 @@ class PositionComponent extends Component {
} }
} }
@ECSComponent('Persistent_PlayerComponent')
class PlayerComponent extends Component { class PlayerComponent extends Component {
public name: string; public name: string;
public score: number; public score: number;
@@ -27,6 +30,7 @@ class PlayerComponent extends Component {
} }
} }
@ECSComponent('Persistent_EnemyComponent')
class EnemyComponent extends Component { class EnemyComponent extends Component {
public type: string; public type: string;
+6 -1
View File
@@ -3,8 +3,10 @@ import { Entity } from '../../src/ECS/Entity';
import { Component } from '../../src/ECS/Component'; import { Component } from '../../src/ECS/Component';
import { EntitySystem } from '../../src/ECS/Systems/EntitySystem'; import { EntitySystem } from '../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../src/ECS/Utils/Matcher'; import { Matcher } from '../../src/ECS/Utils/Matcher';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试组件 // 测试组件
@ECSComponent('SceneTest_PositionComponent')
class PositionComponent extends Component { class PositionComponent extends Component {
public x: number; public x: number;
public y: number; public y: number;
@@ -17,6 +19,7 @@ class PositionComponent extends Component {
} }
} }
@ECSComponent('SceneTest_VelocityComponent')
class VelocityComponent extends Component { class VelocityComponent extends Component {
public vx: number; public vx: number;
public vy: number; public vy: number;
@@ -29,6 +32,7 @@ class VelocityComponent extends Component {
} }
} }
@ECSComponent('SceneTest_HealthComponent')
class HealthComponent extends Component { class HealthComponent extends Component {
public health: number; public health: number;
@@ -39,6 +43,7 @@ class HealthComponent extends Component {
} }
} }
@ECSComponent('SceneTest_RenderComponent')
class RenderComponent extends Component { class RenderComponent extends Component {
public visible: boolean; public visible: boolean;
@@ -387,7 +392,7 @@ describe('Scene - 场景管理系统测试', () => {
entity.addComponent(new PositionComponent(10, 20)); entity.addComponent(new PositionComponent(10, 20));
expect(componentAddedEvent).toBeDefined(); expect(componentAddedEvent).toBeDefined();
expect(componentAddedEvent.componentType).toBe('PositionComponent'); expect(componentAddedEvent.componentType).toBe('SceneTest_PositionComponent');
}); });
}); });
@@ -2,6 +2,7 @@ import { SceneSerializer } from '../../../src/ECS/Serialization/SceneSerializer'
import { Scene } from '../../../src/ECS/Scene'; import { Scene } from '../../../src/ECS/Scene';
import { Component } from '../../../src/ECS/Component'; import { Component } from '../../../src/ECS/Component';
import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem'; import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem';
import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent';
import { ECSComponent } from '../../../src/ECS/Decorators'; import { ECSComponent } from '../../../src/ECS/Decorators';
import { ComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage'; import { ComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage';
import { Serializable, Serialize } from '../../../src/ECS/Serialization'; import { Serializable, Serialize } from '../../../src/ECS/Serialization';
@@ -37,10 +38,6 @@ describe('SceneSerializer', () => {
let componentRegistry: Map<string, ComponentType>; let componentRegistry: Map<string, ComponentType>;
beforeEach(() => { beforeEach(() => {
ComponentRegistry.reset();
ComponentRegistry.register(PositionComponent);
ComponentRegistry.register(VelocityComponent);
scene = new Scene({ name: 'SceneSerializerTestScene' }); scene = new Scene({ name: 'SceneSerializerTestScene' });
componentRegistry = ComponentRegistry.getAllComponentNames() as Map<string, ComponentType>; componentRegistry = ComponentRegistry.getAllComponentNames() as Map<string, ComponentType>;
@@ -5,8 +5,10 @@ import { Scene } from '../../../src/ECS/Scene';
import { Matcher } from '../../../src/ECS/Utils/Matcher'; import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { GlobalEventBus } from '../../../src/ECS/Core/EventBus'; import { GlobalEventBus } from '../../../src/ECS/Core/EventBus';
import { TypeSafeEventSystem } from '../../../src/ECS/Core/EventSystem'; import { TypeSafeEventSystem } from '../../../src/ECS/Core/EventSystem';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件 // 测试组件
@ECSComponent('EntitySysTest_TestComponent')
class TestComponent extends Component { class TestComponent extends Component {
public value: number = 0; public value: number = 0;
@@ -17,6 +19,45 @@ class TestComponent extends Component {
} }
} }
// 额外测试组件 - 用于内联测试
@ECSComponent('EntitySysTest_TagComponent2')
class TagComponent2 extends Component {}
@ECSComponent('EntitySysTest_NonExistent')
class NonExistentComponent extends Component {}
@ECSComponent('EntitySysTest_ClickComponent')
class ClickComponent extends Component {
public element: string;
constructor(element: string = '') {
super();
this.element = element;
}
}
@ECSComponent('EntitySysTest_A')
class AComponent extends Component {}
@ECSComponent('EntitySysTest_B')
class BComponent extends Component {}
@ECSComponent('EntitySysTest_C')
class CComponent extends Component {
public aId: number;
public bId: number;
constructor(aId: number = 0, bId: number = 0) {
super();
this.aId = aId;
this.bId = bId;
}
}
@ECSComponent('EntitySysTest_D')
class DComponent extends Component {}
@ECSComponent('EntitySysTest_TagComponent')
class TagComponent extends TestComponent {}
// 测试事件 // 测试事件
interface TestEvent { interface TestEvent {
id: number; id: number;
@@ -441,13 +482,8 @@ describe('EntitySystem', () => {
}); });
it('在系统 process 中添加组件时应立即触发其他系统的 onAdded', () => { it('在系统 process 中添加组件时应立即触发其他系统的 onAdded', () => {
// 使用独立的场景,避免 beforeEach 创建的实体干扰
// Use independent scene to avoid interference from beforeEach entities
const testScene = new Scene(); const testScene = new Scene();
// 组件定义
class TagComponent extends TestComponent {}
// SystemA: 匹配 TestComponent + TagComponent // SystemA: 匹配 TestComponent + TagComponent
class SystemA extends EntitySystem { class SystemA extends EntitySystem {
public onAddedEntities: Entity[] = []; public onAddedEntities: Entity[] = [];
@@ -499,12 +535,8 @@ describe('EntitySystem', () => {
}); });
it('同一帧内添加后移除组件,onAdded 和 onRemoved 都应触发', () => { it('同一帧内添加后移除组件,onAdded 和 onRemoved 都应触发', () => {
// 使用独立的场景,避免 beforeEach 创建的实体干扰
// Use independent scene to avoid interference from beforeEach entities
const testScene = new Scene(); const testScene = new Scene();
class TagComponent extends TestComponent {}
class TrackingSystemWithTag extends EntitySystem { class TrackingSystemWithTag extends EntitySystem {
public onAddedEntities: Entity[] = []; public onAddedEntities: Entity[] = [];
public onRemovedEntities: Entity[] = []; public onRemovedEntities: Entity[] = [];
@@ -585,9 +617,8 @@ describe('EntitySystem', () => {
// Use independent scene to avoid interference from beforeEach entities // Use independent scene to avoid interference from beforeEach entities
const testScene = new Scene(); const testScene = new Scene();
// 使用独立的组件类,避免继承带来的问题 // 使用文件顶部定义的 TagComponent2
// Use independent component class to avoid inheritance issues // Use TagComponent2 defined at file top
class TagComponent2 extends Component {}
class SystemA extends EntitySystem { class SystemA extends EntitySystem {
public onAddedEntities: Entity[] = []; public onAddedEntities: Entity[] = [];
@@ -747,8 +778,7 @@ describe('EntitySystem', () => {
}); });
it('requireComponent 应该在组件不存在时抛出错误', () => { it('requireComponent 应该在组件不存在时抛出错误', () => {
class NonExistentComponent extends Component {} // 使用文件顶部定义的 NonExistentComponent
expect(() => { expect(() => {
helperSystem.testRequireComponent(entity, NonExistentComponent); helperSystem.testRequireComponent(entity, NonExistentComponent);
}).toThrow(); }).toThrow();
@@ -834,13 +864,7 @@ describe('EntitySystem', () => {
// 使用独立场景 | Use independent scene // 使用独立场景 | Use independent scene
const testScene = new Scene(); const testScene = new Scene();
class ClickComponent extends Component { // 使用文件顶部定义的 ClickComponent
public element: string;
constructor(element: string) {
super();
this.element = element;
}
}
const testEntity = testScene.createEntity('panel'); const testEntity = testScene.createEntity('panel');
@@ -858,13 +882,7 @@ describe('EntitySystem', () => {
// 使用独立场景 | Use independent scene // 使用独立场景 | Use independent scene
const testScene = new Scene(); const testScene = new Scene();
class ClickComponent extends Component { // 使用文件顶部定义的 ClickComponent
public element: string;
constructor(element: string) {
super();
this.element = element;
}
}
// 添加一个监听该组件的系统 | Add a system that listens to this component // 添加一个监听该组件的系统 | Add a system that listens to this component
class ClickSystem extends EntitySystem { class ClickSystem extends EntitySystem {
@@ -903,13 +921,7 @@ describe('EntitySystem', () => {
// 使用独立场景 | Use independent scene // 使用独立场景 | Use independent scene
const testScene = new Scene(); const testScene = new Scene();
class ClickComponent extends Component { // 使用文件顶部定义的 ClickComponent
public element: string;
constructor(element: string) {
super();
this.element = element;
}
}
// 这个系统在 onAdded 中移除组件(模拟可能的用户代码) // 这个系统在 onAdded 中移除组件(模拟可能的用户代码)
// This system removes component in onAdded (simulating possible user code) // This system removes component in onAdded (simulating possible user code)
@@ -950,25 +962,14 @@ describe('EntitySystem', () => {
// 模拟 lawn-mower-demo 的场景 | Simulate lawn-mower-demo scenario // 模拟 lawn-mower-demo 的场景 | Simulate lawn-mower-demo scenario
const testScene = new Scene(); const testScene = new Scene();
// 组件定义 | Component definitions // 使用顶层已装饰的组件类 | Use top-level decorated component classes
class A extends Component {} // AComponent, BComponent, CComponent, DComponent
class B extends Component {}
class C extends Component {
public aId: number;
public bId: number;
constructor(aId: number, bId: number) {
super();
this.aId = aId;
this.bId = bId;
}
}
class D extends Component {}
// ASystem: 匹配 A + D | Matches A + D // ASystem: 匹配 A + D | Matches A + D
class ASystem extends EntitySystem { class ASystem extends EntitySystem {
public onAddedEntities: Entity[] = []; public onAddedEntities: Entity[] = [];
constructor() { constructor() {
super(Matcher.all(A, D)); super(Matcher.all(AComponent, DComponent));
} }
protected override onAdded(entity: Entity): void { protected override onAdded(entity: Entity): void {
console.log('ASystem onAdded:', entity.name); console.log('ASystem onAdded:', entity.name);
@@ -980,7 +981,7 @@ describe('EntitySystem', () => {
class BSystem extends EntitySystem { class BSystem extends EntitySystem {
public onAddedEntities: Entity[] = []; public onAddedEntities: Entity[] = [];
constructor() { constructor() {
super(Matcher.all(B, D)); super(Matcher.all(BComponent, DComponent));
} }
protected override onAdded(entity: Entity): void { protected override onAdded(entity: Entity): void {
console.log('BSystem onAdded:', entity.name); console.log('BSystem onAdded:', entity.name);
@@ -992,21 +993,21 @@ describe('EntitySystem', () => {
// CSystem: Adds D component to A and B entities in process // CSystem: Adds D component to A and B entities in process
class CSystem extends EntitySystem { class CSystem extends EntitySystem {
constructor() { constructor() {
super(Matcher.all(C)); super(Matcher.all(CComponent));
} }
protected override process(entities: readonly Entity[]): void { protected override process(entities: readonly Entity[]): void {
for (const entity of entities) { for (const entity of entities) {
const c = entity.getComponent(C); const c = entity.getComponent(CComponent);
if (c) { if (c) {
const a = this.scene!.findEntityById(c.aId); const a = this.scene!.findEntityById(c.aId);
if (a && !a.hasComponent(D)) { if (a && !a.hasComponent(DComponent)) {
console.log('CSystem: Adding D to Entity A'); console.log('CSystem: Adding D to Entity A');
a.addComponent(new D()); a.addComponent(new DComponent());
} }
const b = this.scene!.findEntityById(c.bId); const b = this.scene!.findEntityById(c.bId);
if (b && !b.hasComponent(D)) { if (b && !b.hasComponent(DComponent)) {
console.log('CSystem: Adding D to Entity B'); console.log('CSystem: Adding D to Entity B');
b.addComponent(new D()); b.addComponent(new DComponent());
} }
} }
} }
@@ -1015,17 +1016,17 @@ describe('EntitySystem', () => {
// DSystem: 在 lateProcess 中移除 D 组件 // DSystem: 在 lateProcess 中移除 D 组件
// DSystem: Removes D component in lateProcess // DSystem: Removes D component in lateProcess
class DSystem extends EntitySystem { class DSystemImpl extends EntitySystem {
public lateProcessEntities: Entity[] = []; public lateProcessEntities: Entity[] = [];
constructor() { constructor() {
super(Matcher.all(D)); super(Matcher.all(DComponent));
} }
protected override lateProcess(entities: readonly Entity[]): void { protected override lateProcess(entities: readonly Entity[]): void {
console.log('DSystem lateProcess, entities count:', entities.length); console.log('DSystem lateProcess, entities count:', entities.length);
for (const entity of entities) { for (const entity of entities) {
console.log('DSystem removing D from:', entity.name); console.log('DSystem removing D from:', entity.name);
this.lateProcessEntities.push(entity); this.lateProcessEntities.push(entity);
const d = entity.getComponent(D); const d = entity.getComponent(DComponent);
if (d) { if (d) {
entity.removeComponent(d); entity.removeComponent(d);
} }
@@ -1038,7 +1039,7 @@ describe('EntitySystem', () => {
const aSystem = new ASystem(); const aSystem = new ASystem();
const bSystem = new BSystem(); const bSystem = new BSystem();
const cSystem = new CSystem(); const cSystem = new CSystem();
const dSystem = new DSystem(); const dSystem = new DSystemImpl();
testScene.addSystem(aSystem); testScene.addSystem(aSystem);
testScene.addSystem(bSystem); testScene.addSystem(bSystem);
@@ -1047,13 +1048,13 @@ describe('EntitySystem', () => {
// 创建实体 | Create entities // 创建实体 | Create entities
const entity1 = testScene.createEntity('Entity1'); const entity1 = testScene.createEntity('Entity1');
entity1.addComponent(new A()); entity1.addComponent(new AComponent());
const entity2 = testScene.createEntity('Entity2'); const entity2 = testScene.createEntity('Entity2');
entity2.addComponent(new B()); entity2.addComponent(new BComponent());
const entity3 = testScene.createEntity('Entity3'); const entity3 = testScene.createEntity('Entity3');
entity3.addComponent(new C(entity1.id, entity2.id)); entity3.addComponent(new CComponent(entity1.id, entity2.id));
// 执行一帧 | Execute one frame // 执行一帧 | Execute one frame
testScene.update(); testScene.update();
@@ -1071,8 +1072,8 @@ describe('EntitySystem', () => {
// D 组件应该在 lateProcess 中被移除 // D 组件应该在 lateProcess 中被移除
// D component should be removed in lateProcess // D component should be removed in lateProcess
expect(entity1.hasComponent(D)).toBe(false); expect(entity1.hasComponent(DComponent)).toBe(false);
expect(entity2.hasComponent(D)).toBe(false); expect(entity2.hasComponent(DComponent)).toBe(false);
testScene.removeSystem(aSystem); testScene.removeSystem(aSystem);
testScene.removeSystem(bSystem); testScene.removeSystem(bSystem);
@@ -2,26 +2,31 @@ import { ComponentSparseSet } from '../../../src/ECS/Utils/ComponentSparseSet';
import { Entity } from '../../../src/ECS/Entity'; import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component'; import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene'; import { Scene } from '../../../src/ECS/Scene';
import { ECSComponent } from '../../../src/ECS/Decorators';
// 测试组件类 // 测试组件类
@ECSComponent('SparseSet_PositionComponent')
class PositionComponent extends Component { class PositionComponent extends Component {
constructor(public x: number = 0, public y: number = 0) { constructor(public x: number = 0, public y: number = 0) {
super(); super();
} }
} }
@ECSComponent('SparseSet_VelocityComponent')
class VelocityComponent extends Component { class VelocityComponent extends Component {
constructor(public dx: number = 0, public dy: number = 0) { constructor(public dx: number = 0, public dy: number = 0) {
super(); super();
} }
} }
@ECSComponent('SparseSet_HealthComponent')
class HealthComponent extends Component { class HealthComponent extends Component {
constructor(public health: number = 100, public maxHealth: number = 100) { constructor(public health: number = 100, public maxHealth: number = 100) {
super(); super();
} }
} }
@ECSComponent('SparseSet_RenderComponent')
class RenderComponent extends Component { class RenderComponent extends Component {
constructor(public visible: boolean = true) { constructor(public visible: boolean = true) {
super(); super();
+3
View File
@@ -5,8 +5,10 @@ import { Entity } from '../../src/ECS/Entity';
import { Component } from '../../src/ECS/Component'; import { Component } from '../../src/ECS/Component';
import { Matcher } from '../../src/ECS/Utils/Matcher'; import { Matcher } from '../../src/ECS/Utils/Matcher';
import { IService } from '../../src/Core/ServiceContainer'; import { IService } from '../../src/Core/ServiceContainer';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试用组件 // 测试用组件
@ECSComponent('WorldTest_TestComponent')
class TestComponent extends Component { class TestComponent extends Component {
public value: number = 0; public value: number = 0;
@@ -16,6 +18,7 @@ class TestComponent extends Component {
} }
} }
@ECSComponent('WorldTest_PlayerComponent')
class PlayerComponent extends Component { class PlayerComponent extends Component {
public playerId: string; public playerId: string;
@@ -1,8 +1,10 @@
import { WorldManager, IWorldManagerConfig } from '../../src/ECS/WorldManager'; import { WorldManager, IWorldManagerConfig } from '../../src/ECS/WorldManager';
import { IWorldConfig, World } from '../../src/ECS/World'; import { IWorldConfig, World } from '../../src/ECS/World';
import { Component } from '../../src/ECS/Component'; import { Component } from '../../src/ECS/Component';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试用组件 // 测试用组件
@ECSComponent('WorldMgr_TestComponent')
class TestComponent extends Component { class TestComponent extends Component {
public value: number = 0; public value: number = 0;
@@ -5,14 +5,16 @@ import { Component } from '../../src/ECS/Component';
import { Matcher } from '../../src/ECS/Utils/Matcher'; import { Matcher } from '../../src/ECS/Utils/Matcher';
import { DebugPlugin } from '../../src/Plugins/DebugPlugin'; import { DebugPlugin } from '../../src/Plugins/DebugPlugin';
import { Injectable } from '../../src/Core/DI'; import { Injectable } from '../../src/Core/DI';
import { ECSSystem } from '../../src/ECS/Decorators'; import { ECSSystem, ECSComponent } from '../../src/ECS/Decorators';
import { EntitySystem } from '../../src/ECS/Systems/EntitySystem'; import { EntitySystem } from '../../src/ECS/Systems/EntitySystem';
@ECSComponent('Debug_HealthComponent')
class HealthComponent extends Component { class HealthComponent extends Component {
public health: number = 100; public health: number = 100;
public maxHealth: number = 100; public maxHealth: number = 100;
} }
@ECSComponent('Debug_PositionComponent')
class PositionComponent extends Component { class PositionComponent extends Component {
public x: number = 0; public x: number = 0;
public y: number = 0; public y: number = 0;
@@ -5,7 +5,7 @@ import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem';
import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent'; import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent';
import { ECSComponent } from '../../../src/ECS/Decorators'; import { ECSComponent } from '../../../src/ECS/Decorators';
@ECSComponent('TestPosition') @ECSComponent('EDC_Position')
class PositionComponent extends Component { class PositionComponent extends Component {
public x: number = 0; public x: number = 0;
public y: number = 0; public y: number = 0;
@@ -17,12 +17,38 @@ class PositionComponent extends Component {
} }
} }
@ECSComponent('TestVelocity') @ECSComponent('EDC_Velocity')
class VelocityComponent extends Component { class VelocityComponent extends Component {
public vx: number = 0; public vx: number = 0;
public vy: number = 0; public vy: number = 0;
} }
@ECSComponent('EDC_ComponentWithPrivate')
class ComponentWithPrivate extends Component {
public publicValue: number = 1;
private _privateValue: number = 2;
}
@ECSComponent('EDC_ComponentWithNested')
class ComponentWithNested extends Component {
public nested = { value: 42 };
}
@ECSComponent('EDC_ComponentWithArray')
class ComponentWithArray extends Component {
public items = [{ id: 1 }, { id: 2 }];
}
@ECSComponent('EDC_ComponentWithLongString')
class ComponentWithLongString extends Component {
public longText = 'x'.repeat(300);
}
@ECSComponent('EDC_ComponentWithLargeArray')
class ComponentWithLargeArray extends Component {
public items = Array.from({ length: 20 }, (_, i) => i);
}
describe('EntityDataCollector', () => { describe('EntityDataCollector', () => {
let collector: EntityDataCollector; let collector: EntityDataCollector;
let scene: Scene; let scene: Scene;
@@ -266,7 +292,7 @@ describe('EntityDataCollector', () => {
const details = collector.extractComponentDetails([component]); const details = collector.extractComponentDetails([component]);
expect(details.length).toBe(1); expect(details.length).toBe(1);
expect(details[0].typeName).toBe('TestPosition'); expect(details[0].typeName).toBe('EDC_Position');
expect(details[0].properties.x).toBe(100); expect(details[0].properties.x).toBe(100);
expect(details[0].properties.y).toBe(200); expect(details[0].properties.y).toBe(200);
}); });
@@ -277,11 +303,6 @@ describe('EntityDataCollector', () => {
}); });
test('should skip private properties', () => { test('should skip private properties', () => {
class ComponentWithPrivate extends Component {
public publicValue: number = 1;
private _privateValue: number = 2;
}
const component = new ComponentWithPrivate(); const component = new ComponentWithPrivate();
const details = collector.extractComponentDetails([component]); const details = collector.extractComponentDetails([component]);
@@ -340,10 +361,6 @@ describe('EntityDataCollector', () => {
}); });
test('should expand object at path', () => { test('should expand object at path', () => {
class ComponentWithNested extends Component {
public nested = { value: 42 };
}
const entity = scene.createEntity('Entity'); const entity = scene.createEntity('Entity');
entity.addComponent(new ComponentWithNested()); entity.addComponent(new ComponentWithNested());
@@ -354,10 +371,6 @@ describe('EntityDataCollector', () => {
}); });
test('should handle array index in path', () => { test('should handle array index in path', () => {
class ComponentWithArray extends Component {
public items = [{ id: 1 }, { id: 2 }];
}
const entity = scene.createEntity('Entity'); const entity = scene.createEntity('Entity');
entity.addComponent(new ComponentWithArray()); entity.addComponent(new ComponentWithArray());
@@ -380,10 +393,6 @@ describe('EntityDataCollector', () => {
}); });
test('should handle entity with long string properties', () => { test('should handle entity with long string properties', () => {
class ComponentWithLongString extends Component {
public longText = 'x'.repeat(300);
}
const entity = scene.createEntity('Entity'); const entity = scene.createEntity('Entity');
entity.addComponent(new ComponentWithLongString()); entity.addComponent(new ComponentWithLongString());
@@ -393,10 +402,6 @@ describe('EntityDataCollector', () => {
}); });
test('should handle entity with large arrays', () => { test('should handle entity with large arrays', () => {
class ComponentWithLargeArray extends Component {
public items = Array.from({ length: 20 }, (_, i) => i);
}
const entity = scene.createEntity('Entity'); const entity = scene.createEntity('Entity');
entity.addComponent(new ComponentWithLargeArray()); entity.addComponent(new ComponentWithLargeArray());
+15 -1
View File
@@ -17,5 +17,19 @@
"other": ["EngineBridge", "EngineRenderSystem", "CameraSystem"] "other": ["EngineBridge", "EngineRenderSystem", "CameraSystem"]
}, },
"requiresWasm": true, "requiresWasm": true,
"outputPath": "dist/index.js" "outputPath": "dist/index.js",
"wasmConfig": {
"files": [
{
"src": "engine/pkg/es_engine.js",
"dst": "libs/es-engine/es_engine.js"
},
{
"src": "engine/pkg/es_engine_bg.wasm",
"dst": "libs/es-engine/es_engine_bg.wasm"
}
],
"runtimePath": "libs/es-engine/es_engine.js",
"isEngineCore": true
}
} }
@@ -146,6 +146,18 @@ pub struct BundleOptions {
pub project_root: String, pub project_root: String,
/// Define replacements | 宏定义替换 /// Define replacements | 宏定义替换
pub define: Option<std::collections::HashMap<String, String>>, pub define: Option<std::collections::HashMap<String, String>>,
/// Module alias mappings (e.g., @esengine/ecs-framework -> /path/to/module)
/// 模块别名映射(例如 @esengine/ecs-framework -> /path/to/module
pub alias: Option<std::collections::HashMap<String, String>>,
/// Global name for IIFE format (assigns exports to window.{globalName})
/// IIFE 格式的全局变量名(将导出赋值给 window.{globalName}
pub global_name: Option<String>,
/// Files to inject at the start of bundle (esbuild --inject)
/// 在打包开始时注入的文件(esbuild --inject
pub inject: Option<Vec<String>>,
/// Banner code to prepend to bundle
/// 添加到打包文件开头的代码
pub banner: Option<String>,
} }
/// Bundle result. /// Bundle result.
@@ -172,9 +184,11 @@ pub async fn bundle_scripts(options: BundleOptions) -> Result<BundleResult, Stri
let esbuild_path = find_esbuild(&options.project_root)?; let esbuild_path = find_esbuild(&options.project_root)?;
// Build output file path | 构建输出文件路径 // Build output file path | 构建输出文件路径
// Note: Don't use .with_extension() as it replaces the last dot-segment
// 注意:不要使用 .with_extension(),因为它会替换最后一个点分段
// e.g., "esengine.core" would become "esengine.js" instead of "esengine.core.js"
let output_file = Path::new(&options.output_dir) let output_file = Path::new(&options.output_dir)
.join(&options.bundle_name) .join(format!("{}.js", &options.bundle_name));
.with_extension("js");
// Ensure output directory exists | 确保输出目录存在 // Ensure output directory exists | 确保输出目录存在
if let Some(parent) = output_file.parent() { if let Some(parent) = output_file.parent() {
@@ -190,6 +204,9 @@ pub async fn bundle_scripts(options: BundleOptions) -> Result<BundleResult, Stri
args.push(format!("--format={}", options.format)); args.push(format!("--format={}", options.format));
args.push("--platform=browser".to_string()); args.push("--platform=browser".to_string());
args.push("--target=es2020".to_string()); args.push("--target=es2020".to_string());
// Show detailed warnings instead of just count
// 显示详细警告而不仅仅是数量
args.push("--log-level=warning".to_string());
if options.source_map { if options.source_map {
args.push("--sourcemap".to_string()); args.push("--sourcemap".to_string());
@@ -210,6 +227,37 @@ pub async fn bundle_scripts(options: BundleOptions) -> Result<BundleResult, Stri
} }
} }
// Add alias mappings | 添加别名映射
if let Some(ref aliases) = options.alias {
for (from, to) in aliases {
args.push(format!("--alias:{}={}", from, to));
}
}
// Add global name for IIFE format | 为 IIFE 格式添加全局变量名
if let Some(ref global_name) = options.global_name {
args.push(format!("--global-name={}", global_name));
}
// Add inject files | 添加注入文件
if let Some(ref inject_files) = options.inject {
for file in inject_files {
args.push(format!("--inject:{}", file));
}
}
// Add banner | 添加 banner
if let Some(ref banner) = options.banner {
args.push(format!("--banner:js={}", banner));
}
// Log esbuild command for debugging
println!("[esbuild] bundle_name: {}", options.bundle_name);
println!("[esbuild] format: {}", options.format);
println!("[esbuild] output_file: {}", output_file.display());
println!("[esbuild] entry_points: {:?}", options.entry_points);
println!("[esbuild] args: {:?}", args);
// Run esbuild | 运行 esbuild // Run esbuild | 运行 esbuild
let output = Command::new(&esbuild_path) let output = Command::new(&esbuild_path)
.args(&args) .args(&args)
@@ -224,12 +272,24 @@ pub async fn bundle_scripts(options: BundleOptions) -> Result<BundleResult, Stri
.ok(); .ok();
// Parse warnings from stderr | 从 stderr 解析警告 // Parse warnings from stderr | 从 stderr 解析警告
// esbuild outputs warnings to stderr even on success
// esbuild 即使成功也会将警告输出到 stderr
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
let warnings: Vec<String> = stderr let mut warnings: Vec<String> = Vec::new();
.lines()
.filter(|l| l.contains("warning")) if !stderr.is_empty() {
.map(|l| l.to_string()) println!("[esbuild] stderr output:\n{}", stderr);
.collect();
// Collect all non-empty lines as warnings
// esbuild warning format varies, so collect everything
// 收集所有非空行作为警告,因为 esbuild 警告格式多变
for line in stderr.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
warnings.push(trimmed.to_string());
}
}
}
Ok(BundleResult { Ok(BundleResult {
success: true, success: true,
@@ -426,8 +486,20 @@ fn list_files_recursive(
{ {
let entry = entry.map_err(|e| format!("Failed to read entry | 读取条目失败: {}", e))?; let entry = entry.map_err(|e| format!("Failed to read entry | 读取条目失败: {}", e))?;
let entry_path = entry.path(); let entry_path = entry.path();
let file_name = entry.file_name();
let file_name_str = file_name.to_string_lossy();
if entry_path.is_dir() { if entry_path.is_dir() {
// Skip node_modules, hidden directories, and other large directories
// 跳过 node_modules、隐藏目录和其他大型目录
if file_name_str.starts_with('.')
|| file_name_str == "node_modules"
|| file_name_str == "target"
|| file_name_str == ".git"
{
continue;
}
if recursive { if recursive {
list_files_recursive(&entry_path, extensions, recursive, files)?; list_files_recursive(&entry_path, extensions, recursive, files)?;
} }
@@ -49,8 +49,11 @@ pub struct ModuleIndexEntry {
/// Get the engine modules directory path. /// Get the engine modules directory path.
/// 获取引擎模块目录路径。 /// 获取引擎模块目录路径。
/// ///
/// Uses compile-time CARGO_MANIFEST_DIR in dev mode to locate dist/engine. /// In dev mode: First tries dist/engine, then falls back to packages/ source directory.
/// 在开发模式下使用编译时的 CARGO_MANIFEST_DIR 来定位 dist/engine /// 在开发模式下:首先尝试 dist/engine,然后回退到 packages/ 源目录
///
/// In production: Uses the bundled resource directory.
/// 在生产模式下:使用打包的资源目录。
#[allow(unused_variables)] #[allow(unused_variables)]
fn get_engine_modules_path(app: &AppHandle) -> Result<PathBuf, String> { fn get_engine_modules_path(app: &AppHandle) -> Result<PathBuf, String> {
// In development mode, use compile-time path // In development mode, use compile-time path
@@ -59,30 +62,45 @@ fn get_engine_modules_path(app: &AppHandle) -> Result<PathBuf, String> {
{ {
// CARGO_MANIFEST_DIR is set at compile time, pointing to src-tauri // CARGO_MANIFEST_DIR is set at compile time, pointing to src-tauri
// CARGO_MANIFEST_DIR 在编译时设置,指向 src-tauri // CARGO_MANIFEST_DIR 在编译时设置,指向 src-tauri
let dev_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
// Try dist/engine first (if modules have been copied with actual content)
// 首先尝试 dist/engine(如果模块已复制且包含实际内容)
let dist_engine_path = manifest_dir
.parent() .parent()
.map(|p| p.join("dist/engine")) .map(|p| p.join("dist/engine"))
.unwrap_or_else(|| PathBuf::from("dist/engine")); .unwrap_or_else(|| PathBuf::from("dist/engine"));
if dev_path.exists() { // Check if dist/engine has actual module content (not just empty directories)
println!("[modules] Using dev path: {:?}", dev_path); // 检查 dist/engine 是否有实际模块内容(而不仅是空目录)
return Ok(dev_path); let dist_core_output = dist_engine_path.join("core/dist/index.mjs");
if dist_core_output.exists() {
println!("[modules] Using dist/engine path: {:?}", dist_engine_path);
return Ok(dist_engine_path);
} }
// Fallback: try current working directory // Fallback: use packages/ source directory directly (dev mode without copy)
// 回退:尝试当前工作目录 // 回退:直接使用 packages/ 源目录(开发模式无需复制)
let cwd_path = std::env::current_dir() // This allows building without running copy-modules first
.map(|p| p.join("dist/engine")) // 这样可以在不运行 copy-modules 的情况下进行构建
.unwrap_or_else(|_| PathBuf::from("dist/engine")); let packages_path = manifest_dir
.parent() // editor-app
.and_then(|p| p.parent()) // packages
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("packages"));
if cwd_path.exists() { // Verify packages directory has module.json files
println!("[modules] Using cwd path: {:?}", cwd_path); // 验证 packages 目录包含 module.json 文件
return Ok(cwd_path); let core_module = packages_path.join("core/module.json");
if core_module.exists() {
println!("[modules] Using packages source path: {:?}", packages_path);
return Ok(packages_path);
} }
return Err(format!( return Err(format!(
"Engine modules directory not found in dev mode. Tried: {:?}, {:?}. Run 'pnpm copy-modules' first.", "Engine modules directory not found in dev mode. Tried: {:?}, {:?}. \
dev_path, cwd_path Either run 'pnpm copy-modules' or ensure packages/ directory exists.",
dist_engine_path, packages_path
)); ));
} }
+41 -2
View File
@@ -90,6 +90,7 @@ function App() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [loadingMessage, setLoadingMessage] = useState(''); const [loadingMessage, setLoadingMessage] = useState('');
const [currentProjectPath, setCurrentProjectPath] = useState<string | null>(null); const [currentProjectPath, setCurrentProjectPath] = useState<string | null>(null);
const [availableScenes, setAvailableScenes] = useState<string[]>([]);
const [pluginManager, setPluginManager] = useState<PluginManager | null>(null); const [pluginManager, setPluginManager] = useState<PluginManager | null>(null);
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null); const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
const [messageHub, setMessageHub] = useState<MessageHub | null>(null); const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
@@ -101,6 +102,7 @@ function App() {
const [notification, setNotification] = useState<INotification | null>(null); const [notification, setNotification] = useState<INotification | null>(null);
const [dialog, setDialog] = useState<IDialogExtended | null>(null); const [dialog, setDialog] = useState<IDialogExtended | null>(null);
const [buildService, setBuildService] = useState<BuildService | null>(null); const [buildService, setBuildService] = useState<BuildService | null>(null);
const [projectServiceState, setProjectServiceState] = useState<ProjectService | null>(null);
const [commandManager] = useState(() => new CommandManager()); const [commandManager] = useState(() => new CommandManager());
const { t, locale, changeLocale } = useLocale(); const { t, locale, changeLocale } = useLocale();
@@ -156,7 +158,26 @@ function App() {
const handleKeyDown = async (e: KeyboardEvent) => { const handleKeyDown = async (e: KeyboardEvent) => {
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
switch (e.key.toLowerCase()) { switch (e.key.toLowerCase()) {
case 's': case 's': {
// Skip if any modal/dialog is open
// 如果有模态窗口/对话框打开则跳过
const hasModalOpen = showBuildSettings || showSettings || showAbout ||
showPluginGenerator || showPortManager || showAdvancedProfiler ||
errorDialog || confirmDialog;
if (hasModalOpen) {
return;
}
// Skip if focus is in an input/textarea/contenteditable element
// 如果焦点在输入框/文本域/可编辑元素中则跳过
const activeEl = document.activeElement;
const isInInput = activeEl instanceof HTMLInputElement ||
activeEl instanceof HTMLTextAreaElement ||
activeEl?.getAttribute('contenteditable') === 'true';
if (isInInput) {
return;
}
e.preventDefault(); e.preventDefault();
if (sceneManager) { if (sceneManager) {
try { try {
@@ -169,6 +190,7 @@ function App() {
} }
} }
break; break;
}
case 'r': case 'r':
e.preventDefault(); e.preventDefault();
handleReloadPlugins(); handleReloadPlugins();
@@ -182,7 +204,9 @@ function App() {
return () => { return () => {
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keydown', handleKeyDown);
}; };
}, [sceneManager, locale, currentProjectPath, pluginManager]); }, [sceneManager, locale, currentProjectPath, pluginManager,
showBuildSettings, showSettings, showAbout, showPluginGenerator,
showPortManager, showAdvancedProfiler, errorDialog, confirmDialog]);
useEffect(() => { useEffect(() => {
if (messageHub) { if (messageHub) {
@@ -377,6 +401,7 @@ function App() {
return; return;
} }
setProjectServiceState(projectService);
await projectService.openProject(projectPath); await projectService.openProject(projectPath);
// 注意:插件配置会在引擎初始化后加载和激活 // 注意:插件配置会在引擎初始化后加载和激活
@@ -397,6 +422,18 @@ function App() {
settings.addRecentProject(projectPath); settings.addRecentProject(projectPath);
setCurrentProjectPath(projectPath); setCurrentProjectPath(projectPath);
// Scan for available scenes in project
// 扫描项目中可用的场景
try {
const sceneFiles = await TauriAPI.scanDirectory(`${projectPath}/scenes`, '*.ecs');
const sceneNames = sceneFiles.map(f => `scenes/${f.split(/[\\/]/).pop()}`);
setAvailableScenes(sceneNames);
console.log('[App] Found scenes:', sceneNames);
} catch (e) {
console.warn('[App] Failed to scan scenes:', e);
}
// 设置 projectLoaded 为 true,触发主界面渲染(包括 Viewport) // 设置 projectLoaded 为 true,触发主界面渲染(包括 Viewport)
setProjectLoaded(true); setProjectLoaded(true);
@@ -1025,6 +1062,8 @@ function App() {
projectPath={currentProjectPath || undefined} projectPath={currentProjectPath || undefined}
buildService={buildService || undefined} buildService={buildService || undefined}
sceneManager={sceneManager || undefined} sceneManager={sceneManager || undefined}
projectService={projectServiceState || undefined}
availableScenes={availableScenes}
/> />
)} )}
@@ -135,6 +135,8 @@ export class ServiceRegistry {
for (const comp of standardComponents) { for (const comp of standardComponents) {
// Register to editor registry for UI // Register to editor registry for UI
// 组件已通过 @ECSComponent 装饰器自动注册到 CoreComponentRegistry
// Components are auto-registered to CoreComponentRegistry via @ECSComponent decorator
componentRegistry.register({ componentRegistry.register({
name: comp.editorName, name: comp.editorName,
type: comp.type, type: comp.type,
@@ -142,9 +144,6 @@ export class ServiceRegistry {
description: comp.description, description: comp.description,
icon: comp.icon icon: comp.icon
}); });
// Register to core registry for serialization/deserialization
CoreComponentRegistry.register(comp.type as any);
} }
// Enable hot reload for editor environment // Enable hot reload for editor environment
@@ -11,9 +11,9 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { import {
Monitor, Apple, Smartphone, Globe, Server, Gamepad2, Monitor, Apple, Smartphone, Globe, Server, Gamepad2,
Plus, Minus, ChevronDown, ChevronRight, Settings, Plus, Minus, ChevronDown, ChevronRight, Settings,
Package, Loader2, CheckCircle, XCircle, AlertTriangle, X Package, Loader2, CheckCircle, XCircle, AlertTriangle, X, Copy, Check
} from 'lucide-react'; } from 'lucide-react';
import type { BuildService, BuildProgress, BuildConfig, WebBuildConfig, WeChatBuildConfig, SceneManagerService } from '@esengine/editor-core'; import type { BuildService, BuildProgress, BuildConfig, WebBuildConfig, WeChatBuildConfig, SceneManagerService, ProjectService, BuildSettingsConfig } from '@esengine/editor-core';
import { BuildPlatform, BuildStatus } from '@esengine/editor-core'; import { BuildPlatform, BuildStatus } from '@esengine/editor-core';
import { useLocale } from '../hooks/useLocale'; import { useLocale } from '../hooks/useLocale';
import '../styles/BuildSettingsPanel.css'; import '../styles/BuildSettingsPanel.css';
@@ -63,7 +63,8 @@ interface BuildSettings {
developmentBuild: boolean; developmentBuild: boolean;
sourceMap: boolean; sourceMap: boolean;
compressionMethod: 'Default' | 'LZ4' | 'LZ4HC'; compressionMethod: 'Default' | 'LZ4' | 'LZ4HC';
bundleModules: boolean; /** Web build mode | Web 构建模式 */
buildMode: 'split-bundles' | 'single-bundle' | 'single-file';
} }
// ==================== Constants | 常量 ==================== // ==================== Constants | 常量 ====================
@@ -87,7 +88,7 @@ const DEFAULT_SETTINGS: BuildSettings = {
developmentBuild: false, developmentBuild: false,
sourceMap: false, sourceMap: false,
compressionMethod: 'Default', compressionMethod: 'Default',
bundleModules: false, buildMode: 'split-bundles',
}; };
// ==================== Status Key Mapping | 状态键映射 ==================== // ==================== Status Key Mapping | 状态键映射 ====================
@@ -105,12 +106,85 @@ const buildStatusKeys: Record<BuildStatus, string> = {
[BuildStatus.Cancelled]: 'buildSettings.cancelled' [BuildStatus.Cancelled]: 'buildSettings.cancelled'
}; };
// ==================== Build Error Display Component | 构建错误显示组件 ====================
/**
* Format and display build errors in a readable way.
*
*/
function BuildErrorDisplay({ error }: { error: string }) {
const { t } = useLocale();
const [isExpanded, setIsExpanded] = useState(false);
const [copied, setCopied] = useState(false);
// Extract first line as summary | 提取第一行作为摘要
const lines = error.split('\n');
const firstErrorMatch = error.match(/X \[ERROR\][^\n]*/);
const firstLine = lines[0] || '';
const matchedError = firstErrorMatch?.[0] || '';
const summary = matchedError
? matchedError.slice(0, 100) + (matchedError.length > 100 ? '...' : '')
: firstLine.slice(0, 100) + (firstLine.length > 100 ? '...' : '');
// Check if error is long (needs expansion) | 检查错误是否很长(需要展开)
const isLongError = error.length > 200 || lines.length > 3;
// Copy error to clipboard | 复制错误到剪贴板
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(error);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (e) {
console.error('Failed to copy:', e);
}
};
return (
<div className="build-result-error">
<div className="build-error-header">
<AlertTriangle size={16} />
<span className="build-error-summary">{summary}</span>
<button
className="build-error-copy-btn"
onClick={handleCopy}
title={t('buildSettings.copyError')}
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
</div>
{isLongError && (
<>
<button
className="build-error-expand-btn"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? t('buildSettings.collapse') : t('buildSettings.showDetails')}
<ChevronDown
size={14}
className={isExpanded ? 'rotated' : ''}
/>
</button>
{isExpanded && (
<pre className="build-error-details">{error}</pre>
)}
</>
)}
</div>
);
}
// ==================== Props | 属性 ==================== // ==================== Props | 属性 ====================
interface BuildSettingsPanelProps { interface BuildSettingsPanelProps {
projectPath?: string; projectPath?: string;
buildService?: BuildService; buildService?: BuildService;
sceneManager?: SceneManagerService; sceneManager?: SceneManagerService;
projectService?: ProjectService;
/** Available scenes in the project | 项目中可用的场景列表 */
availableScenes?: string[];
onBuild?: (profile: BuildProfile, settings: BuildSettings) => void; onBuild?: (profile: BuildProfile, settings: BuildSettings) => void;
onClose?: () => void; onClose?: () => void;
} }
@@ -121,6 +195,8 @@ export function BuildSettingsPanel({
projectPath, projectPath,
buildService, buildService,
sceneManager, sceneManager,
projectService,
availableScenes,
onBuild, onBuild,
onClose onClose
}: BuildSettingsPanelProps) { }: BuildSettingsPanelProps) {
@@ -232,9 +308,11 @@ export function BuildSettingsPanel({
const webConfig: WebBuildConfig = { const webConfig: WebBuildConfig = {
...baseConfig, ...baseConfig,
platform: BuildPlatform.Web, platform: BuildPlatform.Web,
format: 'iife', buildMode: settings.buildMode,
bundleModules: settings.bundleModules, generateHtml: true,
generateHtml: true minify: !settings.developmentBuild,
generateAssetCatalog: true,
assetLoadingStrategy: 'on-demand'
}; };
buildConfig = webConfig; buildConfig = webConfig;
} else if (platform === BuildPlatform.WeChatMiniGame) { } else if (platform === BuildPlatform.WeChatMiniGame) {
@@ -280,6 +358,78 @@ export function BuildSettingsPanel({
} }
}, [selectedProfile, settings, projectPath, buildService, onBuild, getPlatformEnum]); }, [selectedProfile, settings, projectPath, buildService, onBuild, getPlatformEnum]);
// Load saved build settings from project config
// 从项目配置加载已保存的构建设置
useEffect(() => {
if (!projectService) return;
const savedSettings = projectService.getBuildSettings();
if (savedSettings) {
setSettings(prev => ({
...prev,
scriptingDefines: savedSettings.scriptingDefines || [],
companyName: savedSettings.companyName || prev.companyName,
productName: savedSettings.productName || prev.productName,
version: savedSettings.version || prev.version,
developmentBuild: savedSettings.developmentBuild ?? prev.developmentBuild,
sourceMap: savedSettings.sourceMap ?? prev.sourceMap,
compressionMethod: savedSettings.compressionMethod || prev.compressionMethod,
buildMode: savedSettings.buildMode || prev.buildMode
}));
}
}, [projectService]);
// Initialize scenes from availableScenes prop and saved settings
// 从 availableScenes prop 和已保存设置初始化场景列表
useEffect(() => {
if (availableScenes && availableScenes.length > 0) {
const savedSettings = projectService?.getBuildSettings();
const savedScenes = savedSettings?.scenes || [];
setSettings(prev => ({
...prev,
scenes: availableScenes.map(path => ({
path,
enabled: savedScenes.length > 0 ? savedScenes.includes(path) : true
}))
}));
}
}, [availableScenes, projectService]);
// Auto-save build settings when changed
// 设置变化时自动保存
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (!projectService) return;
// Debounce save to avoid too many writes
// 防抖保存,避免频繁写入
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
saveTimeoutRef.current = setTimeout(() => {
const configToSave: BuildSettingsConfig = {
scenes: settings.scenes.filter(s => s.enabled).map(s => s.path),
scriptingDefines: settings.scriptingDefines,
companyName: settings.companyName,
productName: settings.productName,
version: settings.version,
developmentBuild: settings.developmentBuild,
sourceMap: settings.sourceMap,
compressionMethod: settings.compressionMethod,
buildMode: settings.buildMode
};
projectService.updateBuildSettings(configToSave);
}, 500);
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, [settings, projectService]);
// Monitor build progress from service | 从服务监控构建进度 // Monitor build progress from service | 从服务监控构建进度
useEffect(() => { useEffect(() => {
if (!buildService || !isBuilding) { if (!buildService || !isBuilding) {
@@ -475,11 +625,24 @@ export function BuildSettingsPanel({
<div className="build-settings-field-content"> <div className="build-settings-field-content">
<div className="build-settings-scene-list"> <div className="build-settings-scene-list">
{settings.scenes.length === 0 ? ( {settings.scenes.length === 0 ? (
<div className="build-settings-empty-list"></div> <div className="build-settings-empty-list">
{t('buildSettings.noScenesFound')}
</div>
) : ( ) : (
settings.scenes.map((scene, index) => ( settings.scenes.map((scene, index) => (
<div key={index} className="build-settings-scene-item"> <div key={index} className="build-settings-scene-item">
<input type="checkbox" checked={scene.enabled} readOnly /> <input
type="checkbox"
checked={scene.enabled}
onChange={e => {
setSettings(prev => ({
...prev,
scenes: prev.scenes.map((s, i) =>
i === index ? { ...s, enabled: e.target.checked } : s
)
}));
}}
/>
<span>{scene.path}</span> <span>{scene.path}</span>
</div> </div>
)) ))
@@ -582,18 +745,25 @@ export function BuildSettingsPanel({
</select> </select>
</div> </div>
<div className="build-settings-form-row"> <div className="build-settings-form-row">
<label>{t('buildSettings.bundleModules')}</label> <label>{t('buildSettings.buildMode')}</label>
<div className="build-settings-toggle-group"> <div className="build-settings-toggle-group">
<input <select
type="checkbox" value={settings.buildMode}
checked={settings.bundleModules}
onChange={e => setSettings(prev => ({ onChange={e => setSettings(prev => ({
...prev, ...prev,
bundleModules: e.target.checked buildMode: e.target.value as 'split-bundles' | 'single-bundle' | 'single-file'
}))} }))}
/> >
<option value="split-bundles">{t('buildSettings.splitBundles')}</option>
<option value="single-bundle">{t('buildSettings.singleBundle')}</option>
<option value="single-file">{t('buildSettings.singleFile')}</option>
</select>
<span className="build-settings-hint"> <span className="build-settings-hint">
{settings.bundleModules ? t('buildSettings.bundleModulesHint') : t('buildSettings.separateModulesHint')} {settings.buildMode === 'split-bundles'
? t('buildSettings.splitBundlesHint')
: settings.buildMode === 'single-bundle'
? t('buildSettings.singleBundleHint')
: t('buildSettings.singleFileHint')}
</span> </span>
</div> </div>
</div> </div>
@@ -749,10 +919,7 @@ export function BuildSettingsPanel({
{/* Error Message | 错误消息 */} {/* Error Message | 错误消息 */}
{buildResult.error && ( {buildResult.error && (
<div className="build-result-error"> <BuildErrorDisplay error={buildResult.error} />
<AlertTriangle size={16} />
<span>{buildResult.error}</span>
</div>
)} )}
{/* Warnings | 警告 */} {/* Warnings | 警告 */}
@@ -7,7 +7,7 @@
*/ */
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import type { BuildService, SceneManagerService } from '@esengine/editor-core'; import type { BuildService, SceneManagerService, ProjectService } from '@esengine/editor-core';
import { BuildSettingsPanel } from './BuildSettingsPanel'; import { BuildSettingsPanel } from './BuildSettingsPanel';
import { useLocale } from '../hooks/useLocale'; import { useLocale } from '../hooks/useLocale';
import '../styles/BuildSettingsWindow.css'; import '../styles/BuildSettingsWindow.css';
@@ -16,6 +16,9 @@ interface BuildSettingsWindowProps {
projectPath?: string; projectPath?: string;
buildService?: BuildService; buildService?: BuildService;
sceneManager?: SceneManagerService; sceneManager?: SceneManagerService;
projectService?: ProjectService;
/** Available scenes in the project | 项目中可用的场景列表 */
availableScenes?: string[];
onClose: () => void; onClose: () => void;
} }
@@ -23,6 +26,8 @@ export function BuildSettingsWindow({
projectPath, projectPath,
buildService, buildService,
sceneManager, sceneManager,
projectService,
availableScenes,
onClose onClose
}: BuildSettingsWindowProps) { }: BuildSettingsWindowProps) {
const { t } = useLocale(); const { t } = useLocale();
@@ -45,6 +50,8 @@ export function BuildSettingsWindow({
projectPath={projectPath} projectPath={projectPath}
buildService={buildService} buildService={buildService}
sceneManager={sceneManager} sceneManager={sceneManager}
projectService={projectService}
availableScenes={availableScenes}
onClose={onClose} onClose={onClose}
/> />
</div> </div>
+11 -4
View File
@@ -793,9 +793,13 @@ export const en: Translations = {
developmentBuild: 'Development Build', developmentBuild: 'Development Build',
sourceMap: 'Source Map', sourceMap: 'Source Map',
compressionMethod: 'Compression Method', compressionMethod: 'Compression Method',
bundleModules: 'Bundle Modules', buildMode: 'Build Mode',
bundleModulesHint: 'Merge all modules into single file', splitBundles: 'Split Bundles (Recommended)',
separateModulesHint: 'Keep modules as separate files', singleBundle: 'Single Bundle',
singleFile: 'Single File (Playable Ads)',
splitBundlesHint: 'Core runtime + plugins loaded on demand. Best for production games.',
singleBundleHint: 'All code in one JS file. Suitable for simple deployment.',
singleFileHint: 'Everything inlined into one HTML file. Best for playable ads.',
playerSettingsOverrides: 'Player Settings Overrides', playerSettingsOverrides: 'Player Settings Overrides',
companyName: 'Company Name', companyName: 'Company Name',
productName: 'Product Name', productName: 'Product Name',
@@ -819,7 +823,10 @@ export const en: Translations = {
outputPath: 'Output Path', outputPath: 'Output Path',
duration: 'Duration', duration: 'Duration',
selectPlatform: 'Select a platform or build profile', selectPlatform: 'Select a platform or build profile',
settings: 'Settings' settings: 'Settings',
copyError: 'Copy error',
showDetails: 'Show details',
collapse: 'Collapse'
}, },
// ======================================== // ========================================
+7 -3
View File
@@ -793,9 +793,13 @@ export const es: Translations = {
developmentBuild: 'Compilación de Desarrollo', developmentBuild: 'Compilación de Desarrollo',
sourceMap: 'Source Map', sourceMap: 'Source Map',
compressionMethod: 'Método de Compresión', compressionMethod: 'Método de Compresión',
bundleModules: 'Empaquetar Módulos', buildMode: 'Modo de Compilación',
bundleModulesHint: 'Combinar todos los módulos en un solo archivo', splitBundles: 'Paquetes Separados (Recomendado)',
separateModulesHint: 'Mantener módulos como archivos separados', singleBundle: 'Paquete Único',
singleFile: 'Archivo Único (Anuncios Jugables)',
splitBundlesHint: 'Runtime core + plugins cargados bajo demanda. Mejor para juegos de producción.',
singleBundleHint: 'Todo el código en un archivo JS. Adecuado para despliegue simple.',
singleFileHint: 'Todo incrustado en un archivo HTML. Mejor para anuncios jugables.',
playerSettingsOverrides: 'Sobrescrituras de Configuración del Jugador', playerSettingsOverrides: 'Sobrescrituras de Configuración del Jugador',
companyName: 'Nombre de Empresa', companyName: 'Nombre de Empresa',
productName: 'Nombre del Producto', productName: 'Nombre del Producto',
+11 -4
View File
@@ -793,9 +793,13 @@ export const zh: Translations = {
developmentBuild: '开发版本', developmentBuild: '开发版本',
sourceMap: 'Source Map', sourceMap: 'Source Map',
compressionMethod: '压缩方式', compressionMethod: '压缩方式',
bundleModules: '打包模块', buildMode: '构建模式',
bundleModulesHint: '合并所有模块为单文件', splitBundles: '分包模式(推荐)',
separateModulesHint: '保持模块为独立文件', singleBundle: '单包模式',
singleFile: '单文件模式(可玩广告)',
splitBundlesHint: '核心运行时 + 插件按需加载,适合正式游戏',
singleBundleHint: '所有代码打包到一个 JS 文件,适合简单部署',
singleFileHint: '所有内容内联到一个 HTML 文件,适合可玩广告',
playerSettingsOverrides: '玩家设置覆盖', playerSettingsOverrides: '玩家设置覆盖',
companyName: '公司名称', companyName: '公司名称',
productName: '产品名称', productName: '产品名称',
@@ -819,7 +823,10 @@ export const zh: Translations = {
outputPath: '输出路径', outputPath: '输出路径',
duration: '耗时', duration: '耗时',
selectPlatform: '请选择平台或构建配置', selectPlatform: '请选择平台或构建配置',
settings: '设置' settings: '设置',
copyError: '复制错误信息',
showDetails: '显示详情',
collapse: '收起'
}, },
// ======================================== // ========================================
@@ -31,6 +31,37 @@ export interface BundleOptions {
projectRoot: string; projectRoot: string;
/** Define replacements | 宏定义替换 */ /** Define replacements | 宏定义替换 */
define?: Record<string, string>; define?: Record<string, string>;
/**
* Module alias mappings.
*
*
* Maps package names to actual file paths for bundling.
* Used in single-bundle mode to resolve @esengine/* imports.
*
* @esengine/*
*/
alias?: Record<string, string>;
/**
* Global name for IIFE format.
* IIFE
*
* Assigns exports to window.{globalName}.
* window.{globalName}
*/
globalName?: string;
/**
* Files to inject at the start of bundle.
*
*
* Used to inject shims that map external imports to global variables.
* shim
*/
inject?: string[];
/**
* Banner code to prepend to bundle.
*
*/
banner?: string;
} }
/** /**
@@ -237,6 +268,16 @@ export class BuildFileSystemService {
async readBinaryFileAsBase64(path: string): Promise<string> { async readBinaryFileAsBase64(path: string): Promise<string> {
return await invoke('read_binary_file_as_base64', { path }); return await invoke('read_binary_file_as_base64', { path });
} }
/**
* Delete a file.
*
*
* @param path - File path |
*/
async deleteFile(path: string): Promise<void> {
await invoke('delete_file', { path });
}
} }
// Singleton instance | 单例实例 // Singleton instance | 单例实例
@@ -835,19 +835,113 @@
.build-result-error { .build-result-error {
display: flex; display: flex;
align-items: flex-start; flex-direction: column;
gap: 6px; gap: 8px;
padding: 8px 10px; padding: 10px 12px;
background: rgba(239, 68, 68, 0.1); background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3); border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 2px; border-radius: 4px;
color: #ef4444; color: #ef4444;
font-size: 11px; font-size: 12px;
} }
.build-result-error svg { .build-error-header {
display: flex;
align-items: flex-start;
gap: 8px;
}
.build-error-header svg {
flex-shrink: 0; flex-shrink: 0;
margin-top: 1px; margin-top: 2px;
}
.build-error-summary {
flex: 1;
word-break: break-word;
line-height: 1.4;
}
.build-error-copy-btn {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 3px;
color: #ccc;
cursor: pointer;
transition: all 0.15s ease;
}
.build-error-copy-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
.build-error-expand-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 3px;
color: #aaa;
font-size: 11px;
cursor: pointer;
transition: all 0.15s ease;
align-self: flex-start;
}
.build-error-expand-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #ccc;
}
.build-error-expand-btn svg {
transition: transform 0.2s ease;
}
.build-error-expand-btn svg.rotated {
transform: rotate(180deg);
}
.build-error-details {
max-height: 200px;
overflow: auto;
padding: 10px;
margin: 0;
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
color: #f87171;
}
.build-error-details::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.build-error-details::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.build-error-details::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.build-error-details::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
} }
.build-result-warnings { .build-result-warnings {
@@ -108,26 +108,118 @@ export interface BuildConfig {
disabledModules?: string[]; disabledModules?: string[];
} }
/**
* Web build mode.
* Web
*/
export type WebBuildMode =
/** Split bundles: Core + plugins loaded on demand, best for production games
* + */
| 'split-bundles'
/** Single bundle: All code in one JS file, suitable for simple deployment
* JS */
| 'single-bundle'
/** Single file: Everything inlined into one HTML file, best for playable ads
* HTML 广 */
| 'single-file';
/**
* Inline configuration for single-file builds.
*
*
* Single-file mode inlines EVERYTHING into one HTML file by default.
* These options allow disabling specific inlining for debugging purposes.
* HTML
*
*/
export interface InlineConfig {
/**
* Inline JavaScript into HTML as <script> tag content.
* JS HTML <script>
* Default: true
*/
inlineJs?: boolean;
/**
* Inline WASM files as Base64.
* WASM Base64
* Default: true
*/
inlineWasm?: boolean;
/**
* Inline asset files (images, audio, fonts) as Base64 data URLs.
* Base64 data URL
* Default: true
*/
inlineAssets?: boolean;
/**
* Inline scene JSON files.
* JSON
* Default: true
*/
inlineScenes?: boolean;
}
/** /**
* Web platform build configuration. * Web platform build configuration.
* Web * Web
*/ */
export interface WebBuildConfig extends BuildConfig { export interface WebBuildConfig extends BuildConfig {
platform: BuildPlatform.Web; platform: BuildPlatform.Web;
/** Output format | 输出格式 */
format: 'iife' | 'esm';
/** /**
* Whether to bundle all modules into a single JS file. * Build mode.
* JS *
* - true: Bundle into one runtime.browser.js (smaller total size, single request) * - 'split-bundles': Core + plugins loaded on demand, best for production (default)
* - false: Keep modules separate (better caching, parallel loading) * - 'single-bundle': All code in one JS file, suitable for simple deployment
* - 'single-file': Everything inlined into one HTML, best for playable ads
*/
buildMode: WebBuildMode;
/**
* Inline configuration for single-file builds.
*
* Only used when buildMode is 'single-file'.
*/
inlineConfig?: InlineConfig;
/**
* Whether to minify output.
*
* Default: true for release builds
*/
minify?: boolean;
/**
* Whether to generate HTML file.
* HTML
*/
generateHtml: boolean;
/**
* HTML template path.
* HTML
*/
htmlTemplate?: string;
/**
* Asset loading strategy.
*
* - 'preload': Load all assets before game starts (best for small games)
* - 'on-demand': Load assets when needed (best for large games)
* Default: 'on-demand'
*/
assetLoadingStrategy?: 'preload' | 'on-demand';
/**
* Whether to generate asset catalog.
*
* Default: true * Default: true
*/ */
bundleModules: boolean; generateAssetCatalog?: boolean;
/** Whether to generate HTML file | 是否生成 HTML 文件 */
generateHtml: boolean;
/** HTML template path | HTML 模板路径 */
htmlTemplate?: string;
} }
/** /**
@@ -12,6 +12,8 @@ export {
type BuildProgress, type BuildProgress,
type BuildConfig, type BuildConfig,
type WebBuildConfig, type WebBuildConfig,
type WebBuildMode,
type InlineConfig,
type WeChatBuildConfig, type WeChatBuildConfig,
type BuildResult, type BuildResult,
type BuildStep, type BuildStep,
File diff suppressed because it is too large Load Diff
@@ -49,6 +49,34 @@ export interface ModuleSettings {
disabledModules: string[]; disabledModules: string[];
} }
/**
*
* Build Settings Configuration
*
* Persisted build settings for the project.
*
*/
export interface BuildSettingsConfig {
/** Selected scenes for build | 构建选中的场景 */
scenes?: string[];
/** Scripting defines | 脚本定义 */
scriptingDefines?: string[];
/** Company name | 公司名 */
companyName?: string;
/** Product name | 产品名 */
productName?: string;
/** Version | 版本号 */
version?: string;
/** Development build | 开发构建 */
developmentBuild?: boolean;
/** Source map | 源码映射 */
sourceMap?: boolean;
/** Compression method | 压缩方式 */
compressionMethod?: 'Default' | 'LZ4' | 'LZ4HC';
/** Web build mode | Web 构建模式 */
buildMode?: 'split-bundles' | 'single-bundle' | 'single-file';
}
export interface ProjectConfig { export interface ProjectConfig {
projectType?: ProjectType; projectType?: ProjectType;
/** User scripts directory (default: 'scripts') | 用户脚本目录(默认:'scripts' */ /** User scripts directory (default: 'scripts') | 用户脚本目录(默认:'scripts' */
@@ -65,6 +93,8 @@ export interface ProjectConfig {
plugins?: PluginSettings; plugins?: PluginSettings;
/** Module settings | 模块配置 */ /** Module settings | 模块配置 */
modules?: ModuleSettings; modules?: ModuleSettings;
/** Build settings | 构建配置 */
buildSettings?: BuildSettingsConfig;
} }
@Injectable() @Injectable()
@@ -521,6 +551,34 @@ export class ProjectService implements IService {
return !disabled.includes(moduleId); return !disabled.includes(moduleId);
} }
// ==================== Build Settings ====================
/**
*
* Get build settings
*/
public getBuildSettings(): BuildSettingsConfig | null {
return this.projectConfig?.buildSettings || null;
}
/**
*
* Update build settings
*
* @param settings - Build settings to update (partial)
*/
public async updateBuildSettings(settings: Partial<BuildSettingsConfig>): Promise<void> {
const current = this.projectConfig?.buildSettings || {};
await this.updateConfig({
buildSettings: {
...current,
...settings
}
});
await this.messageHub.publish('project:buildSettingsChanged', { settings });
logger.info('Build settings saved');
}
public dispose(): void { public dispose(): void {
this.currentProject = null; this.currentProject = null;
this.projectConfig = null; this.projectConfig = null;
+1
View File
@@ -20,6 +20,7 @@
"clean": "rimraf dist" "clean": "rimraf dist"
}, },
"dependencies": { "dependencies": {
"@esengine/asset-system": "workspace:*",
"@tauri-apps/api": "^2.2.0", "@tauri-apps/api": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-fs": "^2.4.2",
+4 -1
View File
@@ -34,5 +34,8 @@
] ]
}, },
"requiresWasm": false, "requiresWasm": false,
"outputPath": "dist/index.js" "outputPath": "dist/index.js",
"coreServiceExports": ["createServiceToken", "PluginServiceRegistry"],
"userScriptEntries": ["index.ts", "main.ts", "game.ts", "index.js", "main.js"],
"userScriptExternals": ["@esengine/ecs-framework", "@esengine/core", "@esengine/engine-core", "@esengine/asset-system"]
} }
+94 -12
View File
@@ -143,26 +143,63 @@ export interface ModuleManifest {
wasmSize?: number; wasmSize?: number;
/** /**
* WASM file paths relative to module's node_modules or package root. * Unified WASM configuration for all WASM-related modules.
* WASM node_modules * WASM WASM
* *
* Can be glob patterns like "*.wasm" or specific paths. * This replaces the legacy wasmPaths, runtimeWasmPath, and wasmBindings fields.
* glob "*.wasm" * wasmPathsruntimeWasmPath wasmBindings
*
* Example: ["@dimforge/rapier2d-compat/*.wasm"]
*/ */
wasmPaths?: string[]; wasmConfig?: {
/**
* List of WASM files to copy during build.
* WASM
*
* Each entry specifies source location and output destination.
*
*/
files: Array<{
/**
* Source file path relative to engine modules directory.
*
*
* Supports multiple candidate paths (first existing one is used).
* 使
*
* Example: ["rapier2d/pkg/rapier_wasm2d_bg.wasm", "rapier2d/rapier_wasm2d_bg.wasm"]
*/
src: string | string[];
/** /**
* Runtime WASM path relative to game output root. * Destination path relative to build output directory.
* WASM *
* *
* Build pipeline copies WASM to this location. * Example: "wasm/rapier_wasm2d_bg.wasm" or "libs/es-engine/es_engine_bg.wasm"
* 线 WASM */
dst: string;
}>;
/**
* Runtime WASM path for dynamic loading (used by JS code at runtime).
* WASM JS 使
*
* This is the path that runtime code uses to fetch the WASM file.
* WASM
* *
* Example: "wasm/rapier_wasm2d_bg.wasm" * Example: "wasm/rapier_wasm2d_bg.wasm"
*/ */
runtimeWasmPath?: string; runtimePath?: string;
/**
* Whether this is the core engine WASM module.
* WASM
*
* The core engine WASM (e.g., es_engine) must be initialized first
* before the runtime can start. Only one module should have this flag.
* WASM es_engine
*
*/
isEngineCore?: boolean;
};
// ==================== Build Configuration ==================== // ==================== Build Configuration ====================
// ==================== 构建配置 ==================== // ==================== 构建配置 ====================
@@ -193,4 +230,49 @@ export interface ModuleManifest {
* Example: ["chunk-*.js", "worker.js"] * Example: ["chunk-*.js", "worker.js"]
*/ */
includes?: string[]; includes?: string[];
// ==================== Build Pipeline Configuration ====================
// ==================== 构建管线配置 ====================
/**
* Core service exports that should be explicitly exported first.
*
*
* Used to avoid naming conflicts when re-exporting modules.
*
*
* Example: ["createServiceToken", "PluginServiceRegistry"]
*/
coreServiceExports?: string[];
/**
* Whether this module is the runtime entry point (provides default export).
*
*
* Only one module should have this set to true (typically platform-web).
* true platform-web
*/
isRuntimeEntry?: boolean;
/**
* Standard entry file names to search for user scripts.
*
*
* Build pipeline will try these files in order.
* 线
*
* Example: ["index.ts", "main.ts", "game.ts"]
*/
userScriptEntries?: string[];
/**
* Additional external packages that user scripts might import.
*
*
* These packages will be marked as external during bundling.
*
*
* Example: ["@esengine/ecs-framework", "@esengine/core"]
*/
userScriptExternals?: string[];
} }
+3
View File
@@ -26,6 +26,9 @@
"engine-core", "engine-core",
"asset-system" "asset-system"
], ],
"externalDependencies": [
"@esengine/physics-rapier2d"
],
"exports": { "exports": {
"components": [ "components": [
"ParticleSystemComponent" "ParticleSystemComponent"
-5
View File
@@ -38,11 +38,6 @@
"PhysicsSystem2D" "PhysicsSystem2D"
] ]
}, },
"requiresWasm": true,
"wasmPaths": [
"rapier_wasm2d_bg.wasm"
],
"runtimeWasmPath": "wasm/rapier_wasm2d_bg.wasm",
"outputPath": "dist/index.js", "outputPath": "dist/index.js",
"pluginExport": "PhysicsPlugin", "pluginExport": "PhysicsPlugin",
"includes": ["chunk-*.js"] "includes": ["chunk-*.js"]
+2 -1
View File
@@ -15,5 +15,6 @@
"dependencies": ["core", "runtime-core"], "dependencies": ["core", "runtime-core"],
"exports": {}, "exports": {},
"outputPath": "dist/index.mjs", "outputPath": "dist/index.mjs",
"requiresWasm": false "requiresWasm": false,
"isRuntimeEntry": true
} }
+22 -26
View File
@@ -18,7 +18,7 @@ import {
BrowserFileSystemService, BrowserFileSystemService,
type IPlugin type IPlugin
} from '@esengine/runtime-core'; } from '@esengine/runtime-core';
import { assetManager as globalAssetManager, type IAssetManager, type IAssetCatalog, type IAssetCatalogEntry } from '@esengine/asset-system'; import { assetManager as globalAssetManager, type IAssetManager } from '@esengine/asset-system';
import { BrowserAssetReader } from './BrowserAssetReader'; import { BrowserAssetReader } from './BrowserAssetReader';
/** /**
@@ -145,36 +145,21 @@ export class BrowserRuntime {
this._runtime.assetManager.setReader(this._assetReader); this._runtime.assetManager.setReader(this._assetReader);
} }
// Initialize AssetManager with catalog data from BrowserFileSystemService // Initialize AssetManager with catalog from BrowserFileSystemService
// 使用 BrowserFileSystemService 的 catalog 数据初始化 AssetManager // 使用 BrowserFileSystemService 的 catalog 初始化 AssetManager
// Catalog format is now unified - no conversion needed
// 目录格式已统一 - 无需转换
if (this._fileSystem?.catalog) { if (this._fileSystem?.catalog) {
const browserCatalog = this._fileSystem.catalog; const catalog = this._fileSystem.catalog;
const assetCatalog: IAssetCatalog = {
version: browserCatalog.version,
createdAt: browserCatalog.createdAt,
entries: new Map<string, IAssetCatalogEntry>(),
bundles: new Map()
};
// Convert browser catalog entries to IAssetCatalog format // Initialize GLOBAL assetManager singleton (used by particle and other modules)
// 将浏览器 catalog 条目转换为 IAssetCatalog 格式 // 初始化全局 assetManager 单例(被 particle 等模块使用)
for (const [guid, entry] of Object.entries(browserCatalog.entries)) { globalAssetManager.initializeFromCatalog(catalog);
assetCatalog.entries.set(guid, {
guid: entry.guid,
path: entry.path,
type: entry.type,
size: entry.size,
hash: entry.hash
});
}
// Initialize GLOBAL assetManager singleton (this is what particle module uses)
// 初始化全局 assetManager 单例(particle 模块使用的就是这个)
globalAssetManager.initializeFromCatalog(assetCatalog);
// Also initialize runtime's assetManager if available // Also initialize runtime's assetManager if available
// 如果可用,也初始化运行时的 assetManager
if (this._runtime.assetManager) { if (this._runtime.assetManager) {
this._runtime.assetManager.initializeFromCatalog(assetCatalog); this._runtime.assetManager.initializeFromCatalog(catalog);
} }
} }
} }
@@ -198,6 +183,17 @@ export class BrowserRuntime {
await this._runtime.loadSceneFromUrl(sceneUrl); await this._runtime.loadSceneFromUrl(sceneUrl);
} }
/**
* Load a scene from data object (for single-file mode)
*
*/
async loadSceneFromData(sceneData: unknown): Promise<void> {
if (!this._runtime) {
throw new Error('Runtime not initialized. Call initialize() first.');
}
await this._runtime.loadSceneFromData(sceneData);
}
/** /**
* Start the game loop * Start the game loop
* *
+8 -2
View File
@@ -24,9 +24,15 @@
"other": ["RAPIER"] "other": ["RAPIER"]
}, },
"requiresWasm": true, "requiresWasm": true,
"wasmPaths": [ "wasmConfig": {
"pkg/rapier_wasm2d_bg.wasm" "files": [
{
"src": ["rapier2d/pkg/rapier_wasm2d_bg.wasm", "rapier2d/rapier_wasm2d_bg.wasm"],
"dst": "wasm/rapier_wasm2d_bg.wasm"
}
], ],
"runtimePath": "wasm/rapier_wasm2d_bg.wasm"
},
"outputPath": "dist/index.js", "outputPath": "dist/index.js",
"isExternalDependency": true "isExternalDependency": true
} }
+9
View File
@@ -641,6 +641,15 @@ export class GameRuntime {
await this.loadScene(sceneJson); await this.loadScene(sceneJson);
} }
/**
*
* Load scene from data object (for single-file mode)
*/
async loadSceneFromData(sceneData: unknown): Promise<void> {
const sceneJson = JSON.stringify(sceneData);
await this.loadScene(sceneJson);
}
/** /**
* *
* Resize viewport * Resize viewport
+9 -2
View File
@@ -66,11 +66,18 @@ export {
export { export {
BrowserFileSystemService, BrowserFileSystemService,
createBrowserFileSystem, createBrowserFileSystem,
type AssetCatalog,
type AssetCatalogEntry,
type BrowserFileSystemOptions type BrowserFileSystemOptions
} from './services/BrowserFileSystemService'; } from './services/BrowserFileSystemService';
// Re-export catalog types from asset-system (canonical source)
// 从 asset-system 重新导出目录类型(规范来源)
export type {
IAssetCatalog,
IAssetCatalogEntry,
IAssetBundleInfo,
AssetLoadStrategy
} from '@esengine/asset-system';
// Re-export Input System from engine-core for convenience // Re-export Input System from engine-core for convenience
export { export {
Input, Input,
@@ -9,28 +9,16 @@
* Uses asset catalog to resolve GUIDs to actual URLs. * Uses asset catalog to resolve GUIDs to actual URLs.
*/ */
/** import type {
* Asset catalog entry IAssetCatalog,
*/ IAssetCatalogEntry,
export interface AssetCatalogEntry { IAssetBundleInfo,
guid: string; AssetLoadStrategy
path: string; } from '@esengine/asset-system';
type: string;
size: number;
hash: string;
}
/**
* Asset catalog loaded from JSON
*/
export interface AssetCatalog {
version: string;
createdAt: number;
entries: Record<string, AssetCatalogEntry>;
}
/** /**
* Browser file system service options * Browser file system service options
*
*/ */
export interface BrowserFileSystemOptions { export interface BrowserFileSystemOptions {
/** Base URL for assets (e.g., '/assets' or 'https://cdn.example.com/assets') */ /** Base URL for assets (e.g., '/assets' or 'https://cdn.example.com/assets') */
@@ -43,15 +31,19 @@ export interface BrowserFileSystemOptions {
/** /**
* Browser File System Service * Browser File System Service
*
* *
* Provides file system-like API for browser environments * Provides file system-like API for browser environments
* by fetching files over HTTP. * by fetching files over HTTP. Supports both file-based and bundle-based loading.
* API HTTP fetch
*
*/ */
export class BrowserFileSystemService { export class BrowserFileSystemService {
private _baseUrl: string; private _baseUrl: string;
private _catalogUrl: string; private _catalogUrl: string;
private _catalog: AssetCatalog | null = null; private _catalog: IAssetCatalog | null = null;
private _cache = new Map<string, string>(); private _cache = new Map<string, string>();
private _bundleCache = new Map<string, ArrayBuffer>();
private _enableCache: boolean; private _enableCache: boolean;
private _initialized = false; private _initialized = false;
@@ -63,6 +55,7 @@ export class BrowserFileSystemService {
/** /**
* Initialize service and load catalog * Initialize service and load catalog
*
*/ */
async initialize(): Promise<void> { async initialize(): Promise<void> {
if (this._initialized) return; if (this._initialized) return;
@@ -70,24 +63,62 @@ export class BrowserFileSystemService {
try { try {
await this._loadCatalog(); await this._loadCatalog();
this._initialized = true; this._initialized = true;
console.log('[BrowserFileSystem] Initialized with',
Object.keys(this._catalog?.entries ?? {}).length, 'assets'); const strategy = this._catalog?.loadStrategy ?? 'file';
const assetCount = Object.keys(this._catalog?.entries ?? {}).length;
console.log(`[BrowserFileSystem] Initialized: ${assetCount} assets, strategy=${strategy}`);
} catch (error) { } catch (error) {
console.warn('[BrowserFileSystem] Failed to load catalog:', error); console.warn('[BrowserFileSystem] Failed to load catalog:', error);
// Continue without catalog - will use path-based loading // Continue without catalog - will use path-based loading
// 无目录时继续,使用基于路径的加载
this._initialized = true; this._initialized = true;
} }
} }
/** /**
* Load asset catalog * Load asset catalog
*
*/ */
private async _loadCatalog(): Promise<void> { private async _loadCatalog(): Promise<void> {
const response = await fetch(this._catalogUrl); const response = await fetch(this._catalogUrl);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch catalog: ${response.status}`); throw new Error(`Failed to fetch catalog: ${response.status}`);
} }
this._catalog = await response.json();
const rawCatalog = await response.json();
// Normalize catalog format (handle legacy format without loadStrategy)
// 规范化目录格式(处理没有 loadStrategy 的旧格式)
this._catalog = this._normalizeCatalog(rawCatalog);
}
/**
* Normalize catalog to ensure it has all required fields
*
*/
private _normalizeCatalog(raw: Record<string, unknown>): IAssetCatalog {
// Determine load strategy
// 确定加载策略
let loadStrategy: AssetLoadStrategy = 'file';
if (raw.loadStrategy === 'bundle' || raw.bundles) {
loadStrategy = 'bundle';
}
return {
version: (raw.version as string) ?? '1.0.0',
createdAt: (raw.createdAt as number) ?? Date.now(),
loadStrategy,
entries: (raw.entries as Record<string, IAssetCatalogEntry>) ?? {},
bundles: (raw.bundles as Record<string, IAssetBundleInfo>) ?? undefined
};
}
/**
* Get current load strategy
*
*/
get loadStrategy(): AssetLoadStrategy {
return this._catalog?.loadStrategy ?? 'file';
} }
/** /**
@@ -237,8 +268,9 @@ export class BrowserFileSystemService {
/** /**
* Get asset metadata from catalog * Get asset metadata from catalog
*
*/ */
getAssetMetadata(guidOrPath: string): AssetCatalogEntry | null { getAssetMetadata(guidOrPath: string): IAssetCatalogEntry | null {
if (!this._catalog) return null; if (!this._catalog) return null;
// Try as GUID // Try as GUID
@@ -258,8 +290,9 @@ export class BrowserFileSystemService {
/** /**
* Get all assets of a specific type * Get all assets of a specific type
*
*/ */
getAssetsByType(type: string): AssetCatalogEntry[] { getAssetsByType(type: string): IAssetCatalogEntry[] {
if (!this._catalog) return []; if (!this._catalog) return [];
return Object.values(this._catalog.entries) return Object.values(this._catalog.entries)
@@ -268,6 +301,7 @@ export class BrowserFileSystemService {
/** /**
* Clear cache * Clear cache
*
*/ */
clearCache(): void { clearCache(): void {
this._cache.clear(); this._cache.clear();
@@ -275,8 +309,9 @@ export class BrowserFileSystemService {
/** /**
* Get catalog * Get catalog
*
*/ */
get catalog(): AssetCatalog | null { get catalog(): IAssetCatalog | null {
return this._catalog; return this._catalog;
} }
+3
View File
@@ -25,6 +25,9 @@
"sprite", "sprite",
"asset-system" "asset-system"
], ],
"externalDependencies": [
"@esengine/physics-rapier2d"
],
"exports": { "exports": {
"components": [ "components": [
"TilemapComponent" "TilemapComponent"
+3
View File
@@ -834,6 +834,9 @@ importers:
packages/editor-runtime: packages/editor-runtime:
dependencies: dependencies:
'@esengine/asset-system':
specifier: workspace:*
version: link:../asset-system
'@tauri-apps/api': '@tauri-apps/api':
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.9.0 version: 2.9.0