Feature/runtime cdn and plugin loader (#240)

* feat(ui): 完善 UI 布局系统和编辑器可视化工具

* refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统

* fix: 修复 CodeQL 警告并提升测试覆盖率

* refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题

* fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤

* docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明

* fix(ci): 修复 type-check 失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖

* fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖

* fix(ci): platform-web 添加缺失的 behavior-tree 依赖

* fix(lint): 移除正则表达式中不必要的转义字符
This commit is contained in:
YHH
2025-11-27 20:42:46 +08:00
committed by GitHub
parent 71869b1a58
commit 107439d70c
367 changed files with 10661 additions and 12473 deletions

View File

@@ -10,8 +10,13 @@
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./runtime": {
"default": "./dist/runtime.browser.js"
}
},
"unpkg": "dist/runtime.browser.js",
"jsdelivr": "dist/runtime.browser.js",
"files": [
"dist"
],
@@ -32,18 +37,21 @@
],
"author": "yhh",
"license": "MIT",
"dependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/ecs-engine-bindgen": "workspace:*",
"@esengine/ecs-components": "workspace:*",
"@esengine/asset-system": "workspace:*"
},
"peerDependencies": {
"@esengine/platform-common": "workspace:*"
"@esengine/asset-system": "workspace:*",
"@esengine/behavior-tree": "workspace:*",
"@esengine/ecs-components": "workspace:*",
"@esengine/ecs-engine-bindgen": "workspace:*",
"@esengine/ecs-framework": "workspace:*",
"@esengine/platform-common": "workspace:*",
"@esengine/tilemap": "workspace:*",
"@esengine/ui": "workspace:*"
},
"devDependencies": {
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-typescript": "^11.1.6",
"rimraf": "^5.0.0",
"rollup": "^4.42.0",

View File

@@ -3,7 +3,17 @@ import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
const external = ['@esengine/platform-common'];
// 所有 @esengine/* 包设为 external避免多实例问题
const external = [
'@esengine/platform-common',
'@esengine/ecs-framework',
'@esengine/ecs-components',
'@esengine/tilemap',
'@esengine/ui',
'@esengine/behavior-tree',
'@esengine/ecs-engine-bindgen',
'@esengine/asset-system',
];
export default [
{

View File

@@ -1,6 +1,7 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import replace from '@rollup/plugin-replace';
export default {
input: 'src/runtime.ts',
@@ -10,12 +11,25 @@ export default {
name: 'ECSRuntime',
sourcemap: true,
globals: {},
exports: 'default' // Only export the default export
exports: 'default'
},
// Exclude editor-only packages that contain React
external: [
'react',
'react-dom',
'@esengine/editor-core'
],
plugins: [
// Replace process.env.NODE_ENV for browser
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production')
}),
resolve({
browser: true,
preferBuiltins: false
preferBuiltins: false,
// Only resolve main/module fields, not source
mainFields: ['module', 'main']
}),
commonjs(),
typescript({
@@ -23,5 +37,10 @@ export default {
declaration: false,
sourceMap: true
})
]
],
onwarn(warning, warn) {
// Suppress "Unresolved dependencies" warnings for external packages
if (warning.code === 'UNRESOLVED_IMPORT') return;
warn(warning);
}
};

View File

@@ -0,0 +1,364 @@
/**
* Runtime Systems Configuration
* 运行时系统配置
*/
import { Core, ComponentRegistry, ServiceContainer } from '@esengine/ecs-framework';
import type { IScene } from '@esengine/ecs-framework';
import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-engine-bindgen';
import { TransformComponent, SpriteAnimatorSystem, CoreRuntimeModule } from '@esengine/ecs-components';
import type { SystemContext, IPluginLoader, IRuntimeModuleLoader, PluginDescriptor } from '@esengine/ecs-components';
// Import from /runtime entry points to avoid editor dependencies (React, etc.)
import { UIRuntimeModule, UIRenderDataProvider } from '@esengine/ui/runtime';
import { TilemapRuntimeModule, TilemapRenderingSystem } from '@esengine/tilemap/runtime';
import { BehaviorTreeRuntimeModule, BehaviorTreeExecutionSystem } from '@esengine/behavior-tree/runtime';
/**
* 运行时系统集合
*/
export interface RuntimeSystems {
cameraSystem: CameraSystem;
animatorSystem?: SpriteAnimatorSystem;
tilemapSystem?: TilemapRenderingSystem;
behaviorTreeSystem?: BehaviorTreeExecutionSystem;
renderSystem: EngineRenderSystem;
uiRenderProvider?: UIRenderDataProvider;
}
/**
* 运行时配置
*/
export interface RuntimeModuleConfig {
/** 启用的插件 ID 列表,不指定则启用所有已注册插件 */
enabledPlugins?: string[];
/** 是否为编辑器模式 */
isEditor?: boolean;
}
/**
* 运行时插件管理器(简化版,用于独立运行时)
* Runtime Plugin Manager (simplified, for standalone runtime)
*/
class RuntimePluginManager {
private plugins: Map<string, IPluginLoader> = new Map();
private enabledPlugins: Set<string> = new Set();
private initialized = false;
/**
* 注册插件
*/
register(plugin: IPluginLoader): void {
const id = plugin.descriptor.id;
if (this.plugins.has(id)) {
return;
}
this.plugins.set(id, plugin);
// 默认启用
if (plugin.descriptor.enabledByDefault !== false) {
this.enabledPlugins.add(id);
}
}
/**
* 启用插件
*/
enable(pluginId: string): void {
this.enabledPlugins.add(pluginId);
}
/**
* 禁用插件
*/
disable(pluginId: string): void {
this.enabledPlugins.delete(pluginId);
}
/**
* 加载配置
*/
loadConfig(config: { enabledPlugins: string[] }): void {
this.enabledPlugins.clear();
for (const id of config.enabledPlugins) {
this.enabledPlugins.add(id);
}
// 始终启用引擎插件
for (const [id, plugin] of this.plugins) {
if (plugin.descriptor.isEnginePlugin) {
this.enabledPlugins.add(id);
}
}
}
/**
* 初始化运行时(注册组件和服务)
*/
async initializeRuntime(services: ServiceContainer): Promise<void> {
if (this.initialized) {
return;
}
// 注册组件
for (const [id, plugin] of this.plugins) {
if (!this.enabledPlugins.has(id)) {
continue;
}
const runtimeModule = plugin.runtimeModule;
if (runtimeModule) {
try {
runtimeModule.registerComponents(ComponentRegistry);
} catch (e) {
console.error(`Failed to register components for ${id}:`, e);
}
}
}
// 注册服务
for (const [id, plugin] of this.plugins) {
if (!this.enabledPlugins.has(id)) continue;
const runtimeModule = plugin.runtimeModule;
if (runtimeModule?.registerServices) {
try {
runtimeModule.registerServices(services);
} catch (e) {
console.error(`Failed to register services for ${id}:`, e);
}
}
}
// 调用初始化回调
for (const [id, plugin] of this.plugins) {
if (!this.enabledPlugins.has(id)) continue;
const runtimeModule = plugin.runtimeModule;
if (runtimeModule?.onInitialize) {
try {
await runtimeModule.onInitialize();
} catch (e) {
console.error(`Failed to initialize ${id}:`, e);
}
}
}
this.initialized = true;
}
/**
* 为场景创建系统
*/
createSystemsForScene(scene: IScene, context: SystemContext): void {
for (const [id, plugin] of this.plugins) {
if (!this.enabledPlugins.has(id)) continue;
const runtimeModule = plugin.runtimeModule;
if (runtimeModule?.createSystems) {
try {
runtimeModule.createSystems(scene, context);
} catch (e) {
console.error(`Failed to create systems for ${id}:`, e);
}
}
}
}
/**
* 获取所有已注册的插件
*/
getPlugins(): IPluginLoader[] {
return Array.from(this.plugins.values());
}
/**
* 检查插件是否启用
*/
isEnabled(pluginId: string): boolean {
return this.enabledPlugins.has(pluginId);
}
/**
* 重置
*/
reset(): void {
this.plugins.clear();
this.enabledPlugins.clear();
this.initialized = false;
}
}
// 单例运行时插件管理器
const runtimePluginManager = new RuntimePluginManager();
/**
* 创建运行时专用的插件加载器
* Create runtime-only plugin loaders (without editor modules to avoid code splitting issues)
*/
function createRuntimeOnlyPlugin(
descriptor: PluginDescriptor,
runtimeModule: IRuntimeModuleLoader
): IPluginLoader {
return {
descriptor,
runtimeModule,
// No editor module for runtime builds
};
}
// 运行时专用插件描述符 | Runtime-only plugin descriptors
const coreDescriptor: PluginDescriptor = {
id: '@esengine/ecs-components',
name: 'Core Components',
version: '1.0.0',
category: 'core',
enabledByDefault: true,
isEnginePlugin: true,
modules: [{ name: 'CoreRuntime', type: 'runtime', entry: './src/index.ts' }]
};
const uiDescriptor: PluginDescriptor = {
id: '@esengine/ui',
name: 'UI System',
version: '1.0.0',
category: 'ui',
enabledByDefault: true,
isEnginePlugin: true,
modules: [{ name: 'UIRuntime', type: 'runtime', entry: './src/index.ts' }]
};
const tilemapDescriptor: PluginDescriptor = {
id: '@esengine/tilemap',
name: 'Tilemap System',
version: '1.0.0',
category: 'rendering',
enabledByDefault: true,
isEnginePlugin: true,
modules: [{ name: 'TilemapRuntime', type: 'runtime', entry: './src/index.ts' }]
};
const behaviorTreeDescriptor: PluginDescriptor = {
id: '@esengine/behavior-tree',
name: 'Behavior Tree',
version: '1.0.0',
category: 'ai',
enabledByDefault: true,
isEnginePlugin: true,
modules: [{ name: 'BehaviorTreeRuntime', type: 'runtime', entry: './src/index.ts' }]
};
/**
* 注册所有可用插件
* 仅注册插件描述信息,不初始化组件和服务
*/
export function registerAvailablePlugins(): void {
try {
runtimePluginManager.register(createRuntimeOnlyPlugin(coreDescriptor, new CoreRuntimeModule()));
} catch (e) {
console.error('[RuntimeSystems] Failed to register CoreRuntimeModule:', e);
}
try {
runtimePluginManager.register(createRuntimeOnlyPlugin(uiDescriptor, new UIRuntimeModule()));
} catch (e) {
console.error('[RuntimeSystems] Failed to register UIRuntimeModule:', e);
}
try {
runtimePluginManager.register(createRuntimeOnlyPlugin(tilemapDescriptor, new TilemapRuntimeModule()));
} catch (e) {
console.error('[RuntimeSystems] Failed to register TilemapRuntimeModule:', e);
}
try {
runtimePluginManager.register(createRuntimeOnlyPlugin(behaviorTreeDescriptor, new BehaviorTreeRuntimeModule()));
} catch (e) {
console.error('[RuntimeSystems] Failed to register BehaviorTreeRuntimeModule:', e);
}
}
/**
* 初始化运行时(完整流程)
* 用于独立游戏运行时,一次性完成所有初始化
*/
export async function initializeRuntime(
coreInstance: typeof Core,
config?: RuntimeModuleConfig
): Promise<void> {
registerAvailablePlugins();
if (config?.enabledPlugins) {
runtimePluginManager.loadConfig({ enabledPlugins: config.enabledPlugins });
} else {
// 默认启用所有插件
for (const plugin of runtimePluginManager.getPlugins()) {
runtimePluginManager.enable(plugin.descriptor.id);
}
}
await runtimePluginManager.initializeRuntime(coreInstance.services);
}
/**
* 初始化插件(编辑器用)
* 根据项目配置初始化已启用的插件
*
* @param coreInstance Core 实例
* @param enabledPlugins 启用的插件 ID 列表
*/
export async function initializePluginsForProject(
coreInstance: typeof Core,
enabledPlugins: string[]
): Promise<void> {
// 确保插件已注册
registerAvailablePlugins();
// 加载项目的插件配置
runtimePluginManager.loadConfig({ enabledPlugins });
// 初始化插件(注册组件和服务)
await runtimePluginManager.initializeRuntime(coreInstance.services);
}
/**
* 创建运行时系统
*/
export function createRuntimeSystems(
scene: IScene,
bridge: EngineBridge,
config?: RuntimeModuleConfig
): RuntimeSystems {
const isEditor = config?.isEditor ?? false;
const cameraSystem = new CameraSystem(bridge);
scene.addSystem(cameraSystem);
const renderSystem = new EngineRenderSystem(bridge, TransformComponent);
const context: SystemContext = {
core: Core,
engineBridge: bridge,
renderSystem,
isEditor
};
runtimePluginManager.createSystemsForScene(scene, context);
// 注册 UI 渲染提供者到渲染系统
// Register UI render provider to render system
if (context.uiRenderProvider) {
renderSystem.setUIRenderDataProvider(context.uiRenderProvider);
}
// 独立运行时始终使用预览模式(屏幕空间 UI
// Standalone runtime always uses preview mode (screen space UI)
if (!isEditor) {
renderSystem.setPreviewMode(true);
}
scene.addSystem(renderSystem);
return {
cameraSystem,
animatorSystem: context.animatorSystem as SpriteAnimatorSystem | undefined,
tilemapSystem: context.tilemapSystem as TilemapRenderingSystem | undefined,
behaviorTreeSystem: context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined,
renderSystem,
uiRenderProvider: context.uiRenderProvider as UIRenderDataProvider | undefined
};
}

View File

@@ -13,6 +13,15 @@ export { WebInputSubsystem } from './subsystems/WebInputSubsystem';
export { WebStorageSubsystem } from './subsystems/WebStorageSubsystem';
export { WebWASMSubsystem } from './subsystems/WebWASMSubsystem';
// 运行时系统配置
export {
registerAvailablePlugins,
initializeRuntime,
initializePluginsForProject,
createRuntimeSystems
} from './RuntimeSystems';
export type { RuntimeSystems, RuntimeModuleConfig } from './RuntimeSystems';
// 工具
export function isWebPlatform(): boolean {
return typeof window !== 'undefined' && typeof document !== 'undefined';

View File

@@ -1,15 +1,13 @@
/**
* Browser Runtime Entry Point
* 浏览器运行时入口
*
* Uses the same Rust WASM engine as the editor
* 使用与编辑器相同的 Rust WASM 引擎
*/
import { Core, Scene, SceneSerializer } from '@esengine/ecs-framework';
import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-engine-bindgen';
import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, SpriteAnimatorSystem, CameraComponent } from '@esengine/ecs-components';
import { EngineBridge } from '@esengine/ecs-engine-bindgen';
import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, CameraComponent } from '@esengine/ecs-components';
import { AssetManager, EngineIntegration } from '@esengine/asset-system';
import { initializeRuntime, createRuntimeSystems, type RuntimeSystems } from './RuntimeSystems';
interface RuntimeConfig {
canvasId: string;
@@ -19,93 +17,63 @@ interface RuntimeConfig {
class BrowserRuntime {
private bridge: EngineBridge;
private cameraSystem: CameraSystem;
private renderSystem: EngineRenderSystem;
private animatorSystem: SpriteAnimatorSystem;
private systems: RuntimeSystems | null = null;
private animationId: number | null = null;
private assetManager: AssetManager;
private engineIntegration: EngineIntegration;
constructor(config: RuntimeConfig) {
// Initialize Core if not already created
if (!Core.Instance) {
Core.create();
}
// Initialize Core.scene if not already initialized
if (!Core.scene) {
const runtimeScene = new Scene({ name: 'Runtime Scene' });
Core.setScene(runtimeScene);
}
// Initialize Rust WASM engine bridge
this.bridge = new EngineBridge({
canvasId: config.canvasId,
width: config.width || window.innerWidth,
height: config.height || window.innerHeight
});
// Initialize asset system
// 初始化资产系统
this.assetManager = new AssetManager();
this.engineIntegration = new EngineIntegration(this.assetManager, this.bridge);
// Add camera system (updates before render)
this.cameraSystem = new CameraSystem(this.bridge);
Core.scene!.addSystem(this.cameraSystem);
// Add sprite animator system
this.animatorSystem = new SpriteAnimatorSystem();
Core.scene!.addSystem(this.animatorSystem);
// Add render system
this.renderSystem = new EngineRenderSystem(this.bridge, TransformComponent);
Core.scene!.addSystem(this.renderSystem);
}
async initialize(wasmModule: any): Promise<void> {
await this.bridge.initializeWithModule(wasmModule);
// Set path resolver for browser asset proxy
// 设置浏览器资产代理的路径解析器
this.bridge.setPathResolver((path: string) => {
// If already a URL, return as-is
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/asset?')) {
return path;
}
// Use asset proxy endpoint for local file paths
return `/asset?path=${encodeURIComponent(path)}`;
});
// Disable editor tools for game runtime
this.bridge.setShowGrid(false);
this.bridge.setShowGizmos(false);
// 初始化模块系统
await initializeRuntime(Core);
// 创建运行时系统
this.systems = createRuntimeSystems(Core.scene!, this.bridge);
}
async loadScene(sceneUrl: string): Promise<void> {
try {
const response = await fetch(sceneUrl);
const sceneJson = await response.text();
const response = await fetch(sceneUrl);
const sceneJson = await response.text();
if (!Core.scene) {
throw new Error('Core.scene not initialized');
}
SceneSerializer.deserialize(Core.scene, sceneJson, {
strategy: 'replace',
preserveIds: true
});
// Textures are now loaded automatically by EngineRenderSystem
// via Rust engine's path-based texture loading
// 纹理现在由EngineRenderSystem通过Rust引擎的路径加载自动处理
// Auto-play animations are started by SpriteAnimatorSystem.onAdded
// 自动播放动画由SpriteAnimatorSystem.onAdded启动
} catch (error) {
console.error('Failed to load scene:', error);
throw error;
if (!Core.scene) {
throw new Error('Core.scene not initialized');
}
SceneSerializer.deserialize(Core.scene, sceneJson, {
strategy: 'replace',
preserveIds: true
});
}
start(): void {
@@ -117,8 +85,6 @@ class BrowserRuntime {
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// Update Core (includes Time.update and all scenes)
// Texture loading is handled automatically by EngineRenderSystem
Core.update(deltaTime);
this.animationId = requestAnimationFrame(loop);
@@ -145,14 +111,14 @@ class BrowserRuntime {
getEngineIntegration(): EngineIntegration {
return this.engineIntegration;
}
getSystems(): RuntimeSystems | null {
return this.systems;
}
}
// Export everything on a single object for IIFE bundle
export default {
create: (config: RuntimeConfig) => {
const runtime = new BrowserRuntime(config);
return runtime;
},
create: (config: RuntimeConfig) => new BrowserRuntime(config),
BrowserRuntime,
Core,
TransformComponent,

View File

@@ -11,7 +11,7 @@
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,