Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级 * feat(core): 层级系统重构与UI变换矩阵修复 * refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题 * fix(physics): 修复跨包组件类引用问题 * feat: 统一运行时架构与浏览器运行支持 * feat(asset): 实现浏览器运行时资产加载系统 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误 * test: 补齐核心模块测试用例,修复CI构建配置 * fix: 修复测试用例中的类型错误和断言问题 * fix: 修复 turbo build:npm 任务的依赖顺序问题 * fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
@@ -1,410 +0,0 @@
|
||||
/**
|
||||
* 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 runtime modules
|
||||
// 注意:这些模块需要从各自包的 runtime 入口点导入,以避免编辑器依赖(React 等)
|
||||
import { UIRuntimeModule, UIRenderDataProvider, UIInputSystem } from '@esengine/ui/runtime';
|
||||
import { TilemapRuntimeModule, TilemapRenderingSystem } from '@esengine/tilemap/runtime';
|
||||
import { BehaviorTreeRuntimeModule, BehaviorTreeExecutionSystem } from '@esengine/behavior-tree/runtime';
|
||||
import { PhysicsRuntimeModule, Physics2DSystem } from '@esengine/physics-rapier2d/runtime';
|
||||
|
||||
/**
|
||||
* 运行时系统集合
|
||||
*/
|
||||
export interface RuntimeSystems {
|
||||
cameraSystem: CameraSystem;
|
||||
animatorSystem?: SpriteAnimatorSystem;
|
||||
tilemapSystem?: TilemapRenderingSystem;
|
||||
behaviorTreeSystem?: BehaviorTreeExecutionSystem;
|
||||
physicsSystem?: Physics2DSystem;
|
||||
renderSystem: EngineRenderSystem;
|
||||
uiRenderProvider?: UIRenderDataProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时配置
|
||||
*/
|
||||
export interface RuntimeModuleConfig {
|
||||
/** 启用的插件 ID 列表,不指定则启用所有已注册插件 */
|
||||
enabledPlugins?: string[];
|
||||
/** 是否为编辑器模式 */
|
||||
isEditor?: boolean;
|
||||
/** Canvas ID 用于 UI 输入绑定 */
|
||||
canvasId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时插件管理器(简化版,用于独立运行时)
|
||||
* 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 {
|
||||
// Phase 1: 创建所有系统
|
||||
// Phase 1: Create all systems
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: 连接跨插件依赖
|
||||
// Phase 2: Wire cross-plugin dependencies
|
||||
for (const [id, plugin] of this.plugins) {
|
||||
if (!this.enabledPlugins.has(id)) continue;
|
||||
const runtimeModule = plugin.runtimeModule;
|
||||
if (runtimeModule?.onSystemsCreated) {
|
||||
try {
|
||||
runtimeModule.onSystemsCreated(scene, context);
|
||||
} catch (e) {
|
||||
console.error(`Failed to wire dependencies 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' }]
|
||||
};
|
||||
|
||||
const physicsDescriptor: PluginDescriptor = {
|
||||
id: '@esengine/physics-rapier2d',
|
||||
name: 'Rapier 2D Physics',
|
||||
version: '1.0.0',
|
||||
category: 'physics',
|
||||
enabledByDefault: true,
|
||||
isEnginePlugin: true,
|
||||
modules: [{ name: 'PhysicsRuntime', type: 'runtime', entry: './src/runtime.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);
|
||||
}
|
||||
|
||||
try {
|
||||
runtimePluginManager.register(createRuntimeOnlyPlugin(physicsDescriptor, new PhysicsRuntimeModule()));
|
||||
} catch (e) {
|
||||
console.error('[RuntimeSystems] Failed to register PhysicsRuntimeModule:', 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);
|
||||
|
||||
// 绑定 UIInputSystem 到 canvas(用于 UI 交互)
|
||||
// Bind UIInputSystem to canvas (for UI interaction)
|
||||
if (config?.canvasId && context.uiInputSystem) {
|
||||
const canvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
(context.uiInputSystem as UIInputSystem).bindToCanvas(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
/**
|
||||
* Web/H5 平台适配器包
|
||||
* @esengine/platform-web
|
||||
*
|
||||
* Web/H5 平台适配器 - 仅包含平台差异代码
|
||||
* 通用运行时逻辑在 @esengine/runtime-core
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// 引擎桥接
|
||||
export { EngineBridge } from './EngineBridge';
|
||||
export type { EngineBridgeConfig } from './EngineBridge';
|
||||
|
||||
// 子系统
|
||||
// Web 平台子系统
|
||||
export { WebCanvasSubsystem } from './subsystems/WebCanvasSubsystem';
|
||||
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';
|
||||
// Web 特定系统
|
||||
export { Canvas2DRenderSystem } from './systems/Canvas2DRenderSystem';
|
||||
|
||||
// 工具
|
||||
export function isWebPlatform(): boolean {
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
||||
}
|
||||
|
||||
export function getWebCanvas(canvasId: string): HTMLCanvasElement | null {
|
||||
return document.getElementById(canvasId) as HTMLCanvasElement | null;
|
||||
}
|
||||
|
||||
export function createWebCanvas(width: number, height: number): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@@ -1,123 +1,196 @@
|
||||
/**
|
||||
* Browser Runtime Entry Point
|
||||
* 浏览器运行时入口
|
||||
*
|
||||
* 使用统一的 GameRuntime 架构,静态导入所有插件
|
||||
* Uses the unified GameRuntime architecture with static plugin imports
|
||||
*/
|
||||
|
||||
import { Core, Scene, SceneSerializer } from '@esengine/ecs-framework';
|
||||
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';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import {
|
||||
GameRuntime,
|
||||
createGameRuntime,
|
||||
BrowserPlatformAdapter,
|
||||
runtimePluginManager,
|
||||
BrowserFileSystemService
|
||||
} from '@esengine/runtime-core';
|
||||
|
||||
interface RuntimeConfig {
|
||||
// 静态导入所有运行时插件(与编辑器保持一致)
|
||||
// Static import all runtime plugins (consistent with editor)
|
||||
import { EnginePlugin } from '@esengine/engine-core';
|
||||
import { CameraPlugin } from '@esengine/camera';
|
||||
import { SpritePlugin } from '@esengine/sprite';
|
||||
import { AudioPlugin } from '@esengine/audio';
|
||||
import { UIPlugin } from '@esengine/ui';
|
||||
import { TilemapPlugin } from '@esengine/tilemap';
|
||||
import { BehaviorTreePlugin } from '@esengine/behavior-tree';
|
||||
// 使用 runtime 子路径导入,包含 WASM 依赖
|
||||
import { PhysicsPlugin } from '@esengine/physics-rapier2d/runtime';
|
||||
|
||||
// 预注册所有插件(在 GameRuntime 初始化前)
|
||||
// Pre-register all plugins (before GameRuntime initialization)
|
||||
const ALL_PLUGINS = [
|
||||
EnginePlugin,
|
||||
CameraPlugin,
|
||||
SpritePlugin,
|
||||
AudioPlugin,
|
||||
UIPlugin,
|
||||
TilemapPlugin,
|
||||
BehaviorTreePlugin,
|
||||
PhysicsPlugin,
|
||||
];
|
||||
|
||||
// 注册并启用所有插件(浏览器运行时默认启用所有功能)
|
||||
for (const plugin of ALL_PLUGINS) {
|
||||
if (plugin) {
|
||||
runtimePluginManager.register(plugin);
|
||||
// 确保所有插件都启用(覆盖 enabledByDefault: false)
|
||||
runtimePluginManager.enable(plugin.descriptor.id);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RuntimeConfig {
|
||||
canvasId: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
/** 项目配置文件 URL / Project config file URL */
|
||||
projectConfigUrl?: string;
|
||||
/** 资产目录文件 URL / Asset catalog file URL */
|
||||
assetCatalogUrl?: string;
|
||||
/** 资产基础 URL / Asset base URL */
|
||||
assetBaseUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器项目配置文件格式
|
||||
* Editor project config file format (ecs-editor.config.json)
|
||||
*/
|
||||
interface EditorProjectConfig {
|
||||
projectType?: string;
|
||||
plugins?: {
|
||||
enabledPlugins: string[];
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser Runtime Wrapper
|
||||
* 浏览器运行时包装器
|
||||
*/
|
||||
class BrowserRuntime {
|
||||
private bridge: EngineBridge;
|
||||
private systems: RuntimeSystems | null = null;
|
||||
private animationId: number | null = null;
|
||||
private assetManager: AssetManager;
|
||||
private engineIntegration: EngineIntegration;
|
||||
private canvasId: string;
|
||||
private _runtime: GameRuntime | null = null;
|
||||
private _canvasId: string;
|
||||
private _configUrl?: string;
|
||||
private _assetCatalogUrl?: string;
|
||||
private _assetBaseUrl?: string;
|
||||
private _fileSystem: BrowserFileSystemService | null = null;
|
||||
|
||||
constructor(config: RuntimeConfig) {
|
||||
this.canvasId = config.canvasId;
|
||||
if (!Core.Instance) {
|
||||
Core.create();
|
||||
this._canvasId = config.canvasId;
|
||||
this._configUrl = config.projectConfigUrl;
|
||||
this._assetCatalogUrl = config.assetCatalogUrl ?? '/asset-catalog.json';
|
||||
this._assetBaseUrl = config.assetBaseUrl ?? '/assets';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从配置文件 URL 加载插件配置
|
||||
*/
|
||||
private async _loadConfigFromUrl(): Promise<void> {
|
||||
if (!this._configUrl) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(this._configUrl);
|
||||
if (!response.ok) {
|
||||
console.warn(`[BrowserRuntime] Failed to load config from ${this._configUrl}: ${response.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const editorConfig: EditorProjectConfig = await response.json();
|
||||
|
||||
// 如果有插件配置,应用到 runtimePluginManager
|
||||
if (editorConfig.plugins?.enabledPlugins) {
|
||||
runtimePluginManager.loadConfig({ enabledPlugins: editorConfig.plugins.enabledPlugins });
|
||||
console.log('[BrowserRuntime] Loaded plugin config:', editorConfig.plugins.enabledPlugins);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[BrowserRuntime] Error loading config file:', error);
|
||||
}
|
||||
|
||||
if (!Core.scene) {
|
||||
const runtimeScene = new Scene({ name: 'Runtime Scene' });
|
||||
Core.setScene(runtimeScene);
|
||||
}
|
||||
|
||||
this.bridge = new EngineBridge({
|
||||
canvasId: config.canvasId,
|
||||
width: config.width || window.innerWidth,
|
||||
height: config.height || window.innerHeight
|
||||
});
|
||||
|
||||
this.assetManager = new AssetManager();
|
||||
this.engineIntegration = new EngineIntegration(this.assetManager, this.bridge);
|
||||
}
|
||||
|
||||
async initialize(wasmModule: any): Promise<void> {
|
||||
await this.bridge.initializeWithModule(wasmModule);
|
||||
// 从配置文件加载插件配置(如果指定了 URL)
|
||||
await this._loadConfigFromUrl();
|
||||
|
||||
this.bridge.setPathResolver((path: string) => {
|
||||
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/asset?')) {
|
||||
return path;
|
||||
}
|
||||
return `/asset?path=${encodeURIComponent(path)}`;
|
||||
// 初始化浏览器文件系统服务(用于资产加载)
|
||||
// Initialize browser file system service (for asset loading)
|
||||
this._fileSystem = new BrowserFileSystemService({
|
||||
baseUrl: this._assetBaseUrl,
|
||||
catalogUrl: this._assetCatalogUrl,
|
||||
enableCache: true
|
||||
});
|
||||
await this._fileSystem.initialize();
|
||||
|
||||
// 创建浏览器平台适配器
|
||||
const platform = new BrowserPlatformAdapter({
|
||||
wasmModule: wasmModule
|
||||
});
|
||||
|
||||
this.bridge.setShowGrid(false);
|
||||
this.bridge.setShowGizmos(false);
|
||||
|
||||
// 初始化模块系统
|
||||
await initializeRuntime(Core);
|
||||
|
||||
// 创建运行时系统(传入 canvasId 用于 UI 输入绑定)
|
||||
this.systems = createRuntimeSystems(Core.scene!, this.bridge, {
|
||||
canvasId: this.canvasId
|
||||
// 创建统一运行时
|
||||
// 插件已经预注册了,GameRuntime 会检测到并跳过动态加载
|
||||
this._runtime = createGameRuntime({
|
||||
platform,
|
||||
canvasId: this._canvasId,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
autoStartRenderLoop: false
|
||||
});
|
||||
|
||||
await this._runtime.initialize();
|
||||
|
||||
// 注册文件系统服务到 Core.services(必须在 GameRuntime.initialize 之后,因为 Core 在那时才创建)
|
||||
// Register file system service to Core.services (must be after GameRuntime.initialize as Core is created there)
|
||||
const IFileSystemServiceKey = Symbol.for('IFileSystemService');
|
||||
if (!Core.services.isRegistered(IFileSystemServiceKey)) {
|
||||
Core.services.registerInstance(IFileSystemServiceKey, this._fileSystem);
|
||||
}
|
||||
|
||||
// 设置浏览器特定配置
|
||||
this._runtime.setShowGrid(false);
|
||||
this._runtime.setShowGizmos(false);
|
||||
}
|
||||
|
||||
async loadScene(sceneUrl: string): Promise<void> {
|
||||
const response = await fetch(sceneUrl);
|
||||
const sceneJson = await response.text();
|
||||
|
||||
if (!Core.scene) {
|
||||
throw new Error('Core.scene not initialized');
|
||||
if (!this._runtime) {
|
||||
throw new Error('Runtime not initialized');
|
||||
}
|
||||
|
||||
SceneSerializer.deserialize(Core.scene, sceneJson, {
|
||||
strategy: 'replace',
|
||||
preserveIds: true
|
||||
});
|
||||
await this._runtime.loadSceneFromUrl(sceneUrl);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.animationId !== null) return;
|
||||
|
||||
let lastTime = performance.now();
|
||||
const loop = () => {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - lastTime) / 1000;
|
||||
lastTime = currentTime;
|
||||
|
||||
Core.update(deltaTime);
|
||||
|
||||
this.animationId = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
loop();
|
||||
if (!this._runtime) return;
|
||||
this._runtime.start();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.animationId !== null) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
if (!this._runtime) return;
|
||||
this._runtime.stop();
|
||||
}
|
||||
|
||||
handleResize(width: number, height: number): void {
|
||||
this.bridge.resize(width, height);
|
||||
if (!this._runtime) return;
|
||||
this._runtime.resize(width, height);
|
||||
}
|
||||
|
||||
getAssetManager(): AssetManager {
|
||||
return this.assetManager;
|
||||
get assetManager() {
|
||||
return this._runtime?.assetManager ?? null;
|
||||
}
|
||||
|
||||
getEngineIntegration(): EngineIntegration {
|
||||
return this.engineIntegration;
|
||||
get engineIntegration() {
|
||||
return this._runtime?.engineIntegration ?? null;
|
||||
}
|
||||
|
||||
getSystems(): RuntimeSystems | null {
|
||||
return this.systems;
|
||||
get gameRuntime(): GameRuntime | null {
|
||||
return this._runtime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +198,7 @@ export default {
|
||||
create: (config: RuntimeConfig) => new BrowserRuntime(config),
|
||||
BrowserRuntime,
|
||||
Core,
|
||||
TransformComponent,
|
||||
SpriteComponent,
|
||||
SpriteAnimatorComponent,
|
||||
CameraComponent
|
||||
GameRuntime,
|
||||
createGameRuntime,
|
||||
BrowserPlatformAdapter
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, ECSSystem, Core } from '@esengine/ecs-framework';
|
||||
import { TransformComponent, SpriteComponent } from '@esengine/ecs-components';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { SpriteComponent } from '@esengine/sprite';
|
||||
|
||||
@ECSSystem('Canvas2DRender', { updateOrder: 1000 })
|
||||
export class Canvas2DRenderSystem extends EntitySystem {
|
||||
@@ -54,11 +55,12 @@ export class Canvas2DRenderSystem extends EntitySystem {
|
||||
|
||||
this.ctx.save();
|
||||
|
||||
const x = (transform.position.x || 0) + this.canvas.width / 2;
|
||||
const y = this.canvas.height / 2 - (transform.position.y || 0);
|
||||
const width = (sprite.width || 64) * (transform.scale.x || 1);
|
||||
const height = (sprite.height || 64) * (transform.scale.y || 1);
|
||||
const rotation = -(transform.rotation.z || 0) * Math.PI / 180;
|
||||
// 使用世界变换(由 TransformSystem 计算,考虑父级变换)
|
||||
const x = (transform.worldPosition.x || 0) + this.canvas.width / 2;
|
||||
const y = this.canvas.height / 2 - (transform.worldPosition.y || 0);
|
||||
const width = (sprite.width || 64) * (transform.worldScale.x || 1);
|
||||
const height = (sprite.height || 64) * (transform.worldScale.y || 1);
|
||||
const rotation = -(transform.worldRotation.z || 0) * Math.PI / 180;
|
||||
|
||||
this.ctx.translate(x, y);
|
||||
this.ctx.rotate(rotation);
|
||||
|
||||
Reference in New Issue
Block a user