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:
YHH
2025-12-01 22:28:51 +08:00
committed by GitHub
parent 189714c727
commit b42a7b4e43
468 changed files with 18301 additions and 9075 deletions

View File

@@ -37,18 +37,20 @@
],
"author": "yhh",
"license": "MIT",
"peerDependencies": {
"@esengine/asset-system": "workspace:*",
"@esengine/behavior-tree": "workspace:*",
"@esengine/ecs-components": "workspace:*",
"@esengine/ecs-engine-bindgen": "workspace:*",
"@esengine/ecs-framework": "workspace:*",
"@esengine/physics-rapier2d": "workspace:*",
"@esengine/platform-common": "workspace:*",
"@esengine/tilemap": "workspace:*",
"@esengine/ui": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/asset-system": "workspace:*",
"@esengine/platform-common": "workspace:*",
"@esengine/audio": "workspace:*",
"@esengine/behavior-tree": "workspace:*",
"@esengine/camera": "workspace:*",
"@esengine/ecs-engine-bindgen": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/physics-rapier2d": "workspace:*",
"@esengine/runtime-core": "workspace:*",
"@esengine/sprite": "workspace:*",
"@esengine/tilemap": "workspace:*",
"@esengine/ui": "workspace:*",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",

View File

@@ -17,7 +17,13 @@ export default {
external: [
'react',
'react-dom',
'@esengine/editor-core'
'@esengine/editor-core',
// Editor packages should never be imported in runtime
'@esengine/ui-editor',
'@esengine/tilemap-editor',
'@esengine/behavior-tree-editor',
'@esengine/blueprint-editor',
'@esengine/physics-rapier2d-editor'
],
plugins: [
// Replace process.env.NODE_ENV for browser
@@ -28,10 +34,7 @@ export default {
resolve({
browser: true,
preferBuiltins: false,
// Only resolve main/module fields, not source
mainFields: ['module', 'main'],
// Support package.json exports field for subpath imports
exportConditions: ['import', 'module', 'default']
exportConditions: ['import', 'default']
}),
commonjs(),
typescript({

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,5 +20,8 @@
"noEmit": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../platform-common" }
]
}