Files
esengine/packages/engine/platform-web/src/BrowserRuntime.ts
YHH 155411e743 refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
2025-12-26 14:50:35 +08:00

428 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Browser Runtime
* 浏览器运行时
*
* Lightweight runtime for web game builds.
* Uses dynamic plugin loading via import maps.
*
* 轻量级 Web 游戏运行时。
* 通过 import maps 动态加载插件。
*/
import { Core } from '@esengine/ecs-framework';
import {
GameRuntime,
createGameRuntime,
BrowserPlatformAdapter,
runtimePluginManager,
BrowserFileSystemService,
RuntimeSceneManager,
RuntimeSceneManagerToken,
type IRuntimePlugin,
type IRuntimeSceneManager
} from '@esengine/runtime-core';
import { isValidGUID, setGlobalAssetFileLoader, type IAssetManager, type IAssetFileLoader } from '@esengine/asset-system';
import { BrowserAssetReader } from './BrowserAssetReader';
import { WebInputSubsystem } from './subsystems/WebInputSubsystem';
/**
* Runtime configuration
* 运行时配置
*/
export interface RuntimeConfig {
/** Canvas element ID */
canvasId: string;
/** Canvas width (defaults to window.innerWidth) */
width?: number;
/** Canvas height (defaults to window.innerHeight) */
height?: number;
/** Asset catalog file URL (defaults to '/asset-catalog.json') */
assetCatalogUrl?: string;
/** Asset base URL (defaults to '/assets') */
assetBaseUrl?: string;
}
/**
* Browser Runtime
* 浏览器运行时
*
* Main entry point for running games in browser.
* Supports dynamic plugin registration.
*/
export class BrowserRuntime {
private _runtime: GameRuntime | null = null;
private _canvasId: string;
private _width: number;
private _height: number;
private _assetCatalogUrl: string;
private _assetBaseUrl: string;
private _fileSystem: BrowserFileSystemService | null = null;
private _assetReader: BrowserAssetReader | null = null;
private _sceneManager: RuntimeSceneManager | null = null;
private _initialized = false;
constructor(config: RuntimeConfig) {
this._canvasId = config.canvasId;
this._width = config.width ?? window.innerWidth;
this._height = config.height ?? window.innerHeight;
this._assetCatalogUrl = config.assetCatalogUrl ?? '/asset-catalog.json';
this._assetBaseUrl = config.assetBaseUrl ?? '/assets';
}
/**
* Register a plugin dynamically
* 动态注册插件
*
* Call this before initialize() to register plugins.
*/
registerPlugin(plugin: IRuntimePlugin): void {
if (plugin) {
runtimePluginManager.register(plugin);
runtimePluginManager.enable(plugin.manifest.id);
console.log(`[Runtime] Registered plugin: ${plugin.manifest.id}`);
}
}
/**
* Register multiple plugins
* 注册多个插件
*/
registerPlugins(plugins: IRuntimePlugin[]): void {
for (const plugin of plugins) {
this.registerPlugin(plugin);
}
}
/**
* Initialize the runtime
* 初始化运行时
*
* @param wasmModule - Optional WASM module (from es_engine.js)
*/
async initialize(wasmModule?: unknown): Promise<void> {
if (this._initialized) {
console.warn('[Runtime] Already initialized');
return;
}
// Initialize browser file system service
this._fileSystem = new BrowserFileSystemService({
baseUrl: this._assetBaseUrl,
catalogUrl: this._assetCatalogUrl,
enableCache: true
});
await this._fileSystem.initialize();
// Initialize asset reader
this._assetReader = new BrowserAssetReader(this._assetBaseUrl);
// Create browser platform adapter
// 创建输入子系统,用于处理键盘、鼠标、触摸事件
// Create input subsystem for keyboard, mouse, touch events
// 使用 'direct' 模式,因为独立 Web 构建不使用 Tauri 代理
// Use 'direct' mode since standalone web builds don't use Tauri proxy
const platform = new BrowserPlatformAdapter({
wasmModule: wasmModule ?? undefined,
inputSubsystemFactory: () => new WebInputSubsystem(),
assetBaseUrl: this._assetBaseUrl,
pathResolveMode: 'direct'
});
// Create game runtime
this._runtime = createGameRuntime({
platform,
canvasId: this._canvasId,
width: this._width,
height: this._height,
autoStartRenderLoop: false
});
await this._runtime.initialize();
// Register file system service
const IFileSystemServiceKey = Symbol.for('IFileSystemService');
if (!Core.services.isRegistered(IFileSystemServiceKey)) {
Core.services.registerInstance(IFileSystemServiceKey, this._fileSystem);
}
// Set asset reader for AssetManager
// 设置资产读取器
if (this._assetReader && this._runtime.assetManager) {
this._runtime.assetManager.setReader(this._assetReader);
// Initialize AssetManager with catalog from BrowserFileSystemService
// 使用 BrowserFileSystemService 的 catalog 初始化 AssetManager
// Catalog format is now unified - no conversion needed
// 目录格式已统一 - 无需转换
if (this._fileSystem?.catalog) {
const catalog = this._fileSystem.catalog;
this._runtime.assetManager.initializeFromCatalog(catalog);
}
// Set global asset file loader for UI atlas and other subsystems
// 设置全局资产文件加载器供 UI 图集和其他子系统使用
const assetFileLoader: IAssetFileLoader = {
loadImage: (assetPath: string) => this._assetReader!.loadImage(assetPath),
loadText: (assetPath: string) => this._assetReader!.readText(assetPath),
loadBinary: (assetPath: string) => this._assetReader!.readBinary(assetPath),
exists: (assetPath: string) => this._assetReader!.exists(assetPath)
};
setGlobalAssetFileLoader(assetFileLoader);
}
// Disable editor mode (hides grid, gizmos, axis indicator)
// 禁用编辑器模式隐藏网格、gizmos、坐标轴指示器
this._runtime.setEditorMode(false);
// Set up asset path resolver for rendering system
// 为渲染系统设置资产路径解析器
this._setupAssetPathResolver();
// Initialize scene manager
// 初始化场景管理器
this._initializeSceneManager();
this._initialized = true;
console.log('[Runtime] Initialized');
}
/**
* Initialize the runtime scene manager
* 初始化运行时场景管理器
*/
private _initializeSceneManager(): void {
if (!this._runtime) return;
// Create scene manager with scene loader
// 使用场景加载器创建场景管理器
this._sceneManager = new RuntimeSceneManager(
(url: string) => this._runtime!.loadSceneFromUrl(url),
'./scenes'
);
// Auto-discover scenes from catalog
// 从目录自动发现场景
// scenes 是运行时扩展字段,不在 IAssetCatalog 接口中
// scenes is a runtime extension field, not in IAssetCatalog interface
const catalog = this._fileSystem?.catalog as { scenes?: Array<{ name: string; path: string }> } | null;
if (catalog?.scenes) {
const scenes = catalog.scenes.map((scene) => ({
name: scene.name,
path: `./scenes/${scene.name}.ecs`
}));
this._sceneManager.registerScenes(scenes);
}
// Register scene manager as a service
// 注册场景管理器为服务
const serviceRegistry = this._runtime.getServiceRegistry();
if (serviceRegistry) {
serviceRegistry.register(RuntimeSceneManagerToken, this._sceneManager);
}
// Also register in Core.services for global access (systems can access it)
// 同时注册到 Core.services 供全局访问(系统可以访问)
// RuntimeSceneManager 实现了 IService 接口(有 dispose 方法)
// RuntimeSceneManager implements IService interface (has dispose method)
const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager');
if (!Core.services.isRegistered(GlobalSceneManagerKey)) {
Core.services.registerInstance(GlobalSceneManagerKey, this._sceneManager);
}
console.log('[Runtime] Scene manager initialized');
}
/**
* Set up asset path resolver for the render system
* 为渲染系统设置资产路径解析器
*
* This enables GUID-to-URL resolution for textures in Web runtime.
* 这使得 Web 运行时能够将 GUID 解析为纹理 URL。
*/
private _setupAssetPathResolver(): void {
const renderSystem = this._runtime?.renderSystem;
const assetManager = this._runtime?.assetManager;
if (!renderSystem || !assetManager) return;
const assetBaseUrl = this._assetBaseUrl;
renderSystem.setAssetPathResolver((guidOrPath: string): string => {
// Skip if already a valid URL
// 如果已经是有效的 URL 则跳过
if (!guidOrPath || guidOrPath.startsWith('http') || guidOrPath.startsWith('data:') || guidOrPath.startsWith('/')) {
return guidOrPath;
}
// Check if this is a GUID using the unified validation function
// 使用统一的验证函数检查是否为 GUID
if (isValidGUID(guidOrPath)) {
// Get metadata from runtime's asset manager
// 从运行时资产管理器获取元数据
const metadata = assetManager.getDatabase().getMetadata(guidOrPath);
if (metadata?.path) {
// Construct full URL: baseUrl + relative path
// 构建完整 URLbaseUrl + 相对路径
let relativePath = metadata.path.replace(/\\/g, '/');
// Remove 'assets/' prefix if present to avoid duplication
// 移除 'assets/' 前缀以避免重复assetBaseUrl 已包含 /assets
if (relativePath.startsWith('assets/')) {
relativePath = relativePath.substring(7);
}
return `${assetBaseUrl}/${relativePath}`;
}
// GUID not found in catalog, return original
// 目录中未找到 GUID返回原值
console.warn(`[BrowserRuntime] GUID not found in asset catalog: ${guidOrPath}`);
return guidOrPath;
}
// Not a GUID, treat as relative path
// 不是 GUID视为相对路径
return `${assetBaseUrl}/${guidOrPath}`;
});
console.log('[Runtime] Asset path resolver configured');
}
/**
* Load a scene from URL
* 从 URL 加载场景
*
* @param sceneUrl 场景 URL 或名称 | Scene URL or name
*/
async loadScene(sceneUrl: string): Promise<void> {
if (!this._runtime) {
throw new Error('Runtime not initialized. Call initialize() first.');
}
// Use scene manager if available for proper tracking
// 如果可用,使用场景管理器进行正确跟踪
if (this._sceneManager) {
await this._sceneManager.loadSceneByPath(sceneUrl);
} else {
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(): void {
this._runtime?.start();
}
/**
* Stop the game loop
* 停止游戏循环
*/
stop(): void {
this._runtime?.stop();
}
/**
* Handle window resize
* 处理窗口大小变化
*/
handleResize(width: number, height: number): void {
this._runtime?.resize(width, height);
}
/**
* Get the underlying GameRuntime
* 获取底层 GameRuntime
*/
get gameRuntime(): GameRuntime | null {
return this._runtime;
}
/**
* Get the asset manager
* 获取资产管理器
*
* @returns The asset manager instance, or null if not initialized
*/
get assetManager(): IAssetManager | null {
return this._runtime?.assetManager ?? null;
}
/**
* Get the scene manager
* 获取场景管理器
*
* Use this to load scenes, check available scenes, and listen to scene events.
* 使用它来加载场景、检查可用场景和监听场景事件。
*
* @example
* ```typescript
* // Load a scene by name
* await runtime.sceneManager?.loadScene('Level1');
*
* // Get list of available scenes
* const scenes = runtime.sceneManager?.availableScenes;
*
* // Listen to scene load events
* runtime.sceneManager?.onLoadComplete((sceneName) => {
* console.log(`Scene loaded: ${sceneName}`);
* });
* ```
*
* @returns The scene manager instance, or null if not initialized
*/
get sceneManager(): IRuntimeSceneManager | null {
return this._sceneManager;
}
/**
* Check if runtime is initialized
* 检查运行时是否已初始化
*/
get isInitialized(): boolean {
return this._initialized;
}
}
/**
* Create a browser runtime instance
* 创建浏览器运行时实例
*/
export function create(config: RuntimeConfig): BrowserRuntime {
return new BrowserRuntime(config);
}
/**
* Default export for convenient usage
* 默认导出,便于使用
*
* Usage in game HTML:
* ```js
* const ECSRuntime = (await import('@esengine/platform-web')).default;
* const runtime = ECSRuntime.create({ canvasId: 'game-canvas' });
* ```
*/
export default {
create,
BrowserRuntime,
BrowserAssetReader,
// Re-export useful types from dependencies
Core,
GameRuntime,
createGameRuntime,
BrowserPlatformAdapter,
runtimePluginManager
};