feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)

* feat(platform-common): 添加WASM加载器和环境检测API

* feat(rapier2d): 新增Rapier2D WASM绑定包

* feat(physics-rapier2d): 添加跨平台WASM加载器

* feat(asset-system): 添加运行时资产目录和bundle格式

* feat(asset-system-editor): 新增编辑器资产管理包

* feat(editor-core): 添加构建系统和模块管理

* feat(editor-app): 重构浏览器预览使用import maps

* feat(platform-web): 添加BrowserRuntime和资产读取

* feat(engine): 添加材质系统和着色器管理

* feat(material): 新增材质系统和着色器编辑器

* feat(tilemap): 增强tilemap编辑器和动画系统

* feat(modules): 添加module.json配置

* feat(core): 添加module.json和类型定义更新

* chore: 更新依赖和构建配置

* refactor(plugins): 更新插件模板使用ModuleManifest

* chore: 添加第三方依赖库

* chore: 移除BehaviourTree-ai和ecs-astar子模块

* docs: 更新README和文档主题样式

* fix: 修复Rust文档测试和添加rapier2d WASM绑定

* fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题

* feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea)

* fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖

* fix: 添加缺失的包依赖修复CI构建

* fix: 修复CodeQL检测到的代码问题

* fix: 修复构建错误和缺失依赖

* fix: 修复类型检查错误

* fix(material-system): 修复tsconfig配置支持TypeScript项目引用

* fix(editor-core): 修复Rollup构建配置添加tauri external

* fix: 修复CodeQL检测到的代码问题

* fix: 修复CodeQL检测到的代码问题
This commit is contained in:
YHH
2025-12-03 22:15:22 +08:00
committed by GitHub
parent caf7622aa0
commit 63f006ab62
496 changed files with 77601 additions and 4067 deletions

View File

@@ -0,0 +1,19 @@
{
"id": "platform-web",
"name": "@esengine/platform-web",
"displayName": "Platform Web",
"description": "Web platform runtime | Web 平台运行时",
"version": "1.0.0",
"category": "Platform",
"icon": "Globe",
"tags": ["platform", "web", "browser", "runtime"],
"isCore": true,
"defaultEnabled": true,
"isEngineModule": true,
"canContainContent": false,
"platforms": ["web"],
"dependencies": ["core", "runtime-core"],
"exports": {},
"outputPath": "dist/index.mjs",
"requiresWasm": false
}

View File

@@ -1,7 +1,8 @@
{
"name": "@esengine/platform-web",
"version": "1.0.0",
"description": "Web/H5 平台适配器",
"description": "Web/H5 Platform Adapter - Browser runtime and asset loading",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
@@ -10,19 +11,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"
],
"scripts": {
"build": "rollup -c && rollup -c rollup.runtime.config.js",
"build:runtime": "rollup -c rollup.runtime.config.js",
"build": "rollup -c",
"build:npm": "npm run build",
"clean": "rimraf dist",
"type-check": "npx tsc --noEmit",
@@ -33,28 +28,22 @@
"web",
"h5",
"platform",
"adapter"
"runtime",
"browser"
],
"author": "yhh",
"license": "MIT",
"dependencies": {
"@esengine/runtime-core": "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",
"@esengine/platform-common": "workspace:*",
"@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,19 +3,41 @@ import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
// 所有 @esengine/* 包设为 external避免多实例问题
/**
* Platform-web Rollup Configuration
*
* Builds:
* 1. ESM + CJS bundles for editor usage
* 2. TypeScript declarations
*
* All @esengine/* packages are external to avoid bundling.
* Game builds use import maps to resolve modules at runtime.
*/
const external = [
'@esengine/platform-common',
'@esengine/ecs-framework',
'@esengine/runtime-core',
'@esengine/platform-common',
'@esengine/asset-system',
'@esengine/ecs-components',
'@esengine/ecs-engine-bindgen',
'@esengine/tilemap',
'@esengine/ui',
'@esengine/behavior-tree',
'@esengine/ecs-engine-bindgen',
'@esengine/asset-system',
// Editor packages (should never be in runtime)
'@esengine/editor-core',
'@esengine/ui-editor',
'@esengine/tilemap-editor',
'@esengine/behavior-tree-editor',
'@esengine/blueprint-editor',
'@esengine/physics-rapier2d-editor',
// React (editor only)
'react',
'react-dom',
];
export default [
// Main bundle (ESM + CJS)
{
input: 'src/index.ts',
output: [
@@ -32,7 +54,10 @@ export default [
],
external,
plugins: [
resolve(),
resolve({
browser: true,
preferBuiltins: false
}),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
@@ -40,6 +65,7 @@ export default [
})
]
},
// TypeScript declarations
{
input: 'src/index.ts',
output: {

View File

@@ -1,51 +0,0 @@
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',
output: {
file: 'dist/runtime.browser.js',
format: 'iife',
name: 'ECSRuntime',
sourcemap: true,
globals: {},
exports: 'default'
},
// Exclude editor-only packages that contain React
external: [
'react',
'react-dom',
'@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
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production')
}),
resolve({
browser: true,
preferBuiltins: false,
exportConditions: ['import', 'default']
}),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
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,129 @@
/**
* Browser Asset Reader
* 浏览器资产读取器
*
* Implements IAssetReader interface for browser environment.
* Uses fetch API to load assets from web server.
*/
/**
* Browser Asset Reader for loading assets via fetch API
* 通过 fetch API 加载资产的浏览器读取器
*/
export class BrowserAssetReader {
private _baseUrl: string;
private _audioContext: AudioContext | null = null;
constructor(baseUrl: string = '/assets') {
this._baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
}
/**
* Resolve asset path to URL
* 将资产路径解析为 URL
*/
private _resolveUrl(absolutePath: string): string {
// Handle absolute Windows paths (e.g., F:\TowerECS\assets\...)
if (/^[A-Za-z]:[\\/]/.test(absolutePath)) {
const normalized = absolutePath.replace(/\\/g, '/');
const assetsIndex = normalized.toLowerCase().indexOf('/assets/');
if (assetsIndex >= 0) {
return `${this._baseUrl}${normalized.substring(assetsIndex + 7)}`;
}
const filename = normalized.split('/').pop();
return `${this._baseUrl}/${filename}`;
}
// Handle relative paths
if (absolutePath.startsWith('./') || absolutePath.startsWith('../')) {
return absolutePath;
}
if (absolutePath.startsWith('/assets/')) {
return absolutePath;
}
if (absolutePath.startsWith('assets/')) {
return `/${absolutePath}`;
}
return `${this._baseUrl}/${absolutePath}`;
}
/**
* Read text file
* 读取文本文件
*/
async readText(absolutePath: string): Promise<string> {
const url = this._resolveUrl(absolutePath);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load text: ${url} (${response.status})`);
}
return response.text();
}
/**
* Read binary file
* 读取二进制文件
*/
async readBinary(absolutePath: string): Promise<ArrayBuffer> {
const url = this._resolveUrl(absolutePath);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load binary: ${url} (${response.status})`);
}
return response.arrayBuffer();
}
/**
* Load image
* 加载图片
*/
async loadImage(absolutePath: string): Promise<HTMLImageElement> {
const url = this._resolveUrl(absolutePath);
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
img.src = url;
});
}
/**
* Load audio
* 加载音频
*/
async loadAudio(absolutePath: string): Promise<AudioBuffer> {
if (!this._audioContext) {
this._audioContext = new AudioContext();
}
const url = this._resolveUrl(absolutePath);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load audio: ${url} (${response.status})`);
}
const arrayBuffer = await response.arrayBuffer();
return this._audioContext.decodeAudioData(arrayBuffer);
}
/**
* Check if file exists
* 检查文件是否存在
*/
async exists(absolutePath: string): Promise<boolean> {
const url = this._resolveUrl(absolutePath);
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch {
return false;
}
}
/**
* Get base URL
* 获取基础 URL
*/
get baseUrl(): string {
return this._baseUrl;
}
}

View File

@@ -0,0 +1,239 @@
/**
* 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,
type IPlugin
} from '@esengine/runtime-core';
import type { IAssetManager } from '@esengine/asset-system';
import { BrowserAssetReader } from './BrowserAssetReader';
/**
* 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 _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: IPlugin): void {
if (plugin) {
runtimePluginManager.register(plugin);
runtimePluginManager.enable(plugin.manifest.id);
console.log(`[Runtime] Registered plugin: ${plugin.manifest.id}`);
}
}
/**
* Register multiple plugins
* 注册多个插件
*/
registerPlugins(plugins: IPlugin[]): 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
const platform = new BrowserPlatformAdapter({
wasmModule: wasmModule ?? undefined
});
// 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._runtime.assetManager && this._assetReader) {
this._runtime.assetManager.setReader(this._assetReader);
}
// Browser-specific settings (no editor UI)
this._runtime.setShowGrid(false);
this._runtime.setShowGizmos(false);
this._initialized = true;
console.log('[Runtime] Initialized');
}
/**
* Load a scene from URL
* 从 URL 加载场景
*/
async loadScene(sceneUrl: string): Promise<void> {
if (!this._runtime) {
throw new Error('Runtime not initialized. Call initialize() first.');
}
await this._runtime.loadSceneFromUrl(sceneUrl);
}
/**
* 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;
}
/**
* 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
};

View File

@@ -1,29 +1,69 @@
/**
* @esengine/platform-web
*
* Web/H5 平台适配器 - 仅包含平台差异代码
* 通用运行时逻辑在 @esengine/runtime-core
* Web/H5 Platform Adapter
* Web/H5 平台适配器
*
* Provides browser-specific implementations:
* - BrowserRuntime: Main entry point for game builds
* - BrowserAssetReader: Asset loading via fetch API
* - Web subsystems: Canvas, Input, Storage, WASM
*
* @packageDocumentation
*/
// Web 平台子系统
// ============================================
// Runtime (main entry for game builds)
// ============================================
export {
BrowserRuntime,
create,
type RuntimeConfig
} from './BrowserRuntime';
// Default export for convenient usage in game builds
export { default } from './BrowserRuntime';
// Asset reader
export { BrowserAssetReader } from './BrowserAssetReader';
// ============================================
// Web Platform Subsystems
// ============================================
export { WebCanvasSubsystem } from './subsystems/WebCanvasSubsystem';
export { WebInputSubsystem } from './subsystems/WebInputSubsystem';
export { WebStorageSubsystem } from './subsystems/WebStorageSubsystem';
export { WebWASMSubsystem } from './subsystems/WebWASMSubsystem';
// Web 特定系统
// ============================================
// Web-specific Systems
// ============================================
export { Canvas2DRenderSystem } from './systems/Canvas2DRenderSystem';
// ============================================
// Platform Detection Utilities
// ============================================
/**
* Check if running in web browser
* 检查是否在浏览器中运行
*/
export function isWebPlatform(): boolean {
return typeof window !== 'undefined' && typeof document !== 'undefined';
}
/**
* Get canvas element by ID
* 根据 ID 获取 canvas 元素
*/
export function getWebCanvas(canvasId: string): HTMLCanvasElement | null {
return document.getElementById(canvasId) as HTMLCanvasElement | null;
}
/**
* Create a new canvas element
* 创建新的 canvas 元素
*/
export function createWebCanvas(width: number, height: number): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;

View File

@@ -1,204 +0,0 @@
/**
* Browser Runtime Entry Point
* 浏览器运行时入口
*
* 使用统一的 GameRuntime 架构,静态导入所有插件
* Uses the unified GameRuntime architecture with static plugin imports
*/
import { Core } from '@esengine/ecs-framework';
import {
GameRuntime,
createGameRuntime,
BrowserPlatformAdapter,
runtimePluginManager,
BrowserFileSystemService
} from '@esengine/runtime-core';
// 静态导入所有运行时插件(与编辑器保持一致)
// 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 _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;
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);
}
}
async initialize(wasmModule: any): Promise<void> {
// 从配置文件加载插件配置(如果指定了 URL
await this._loadConfigFromUrl();
// 初始化浏览器文件系统服务(用于资产加载)
// 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
});
// 创建统一运行时
// 插件已经预注册了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> {
if (!this._runtime) {
throw new Error('Runtime not initialized');
}
await this._runtime.loadSceneFromUrl(sceneUrl);
}
start(): void {
if (!this._runtime) return;
this._runtime.start();
}
stop(): void {
if (!this._runtime) return;
this._runtime.stop();
}
handleResize(width: number, height: number): void {
if (!this._runtime) return;
this._runtime.resize(width, height);
}
get assetManager() {
return this._runtime?.assetManager ?? null;
}
get engineIntegration() {
return this._runtime?.engineIntegration ?? null;
}
get gameRuntime(): GameRuntime | null {
return this._runtime;
}
}
export default {
create: (config: RuntimeConfig) => new BrowserRuntime(config),
BrowserRuntime,
Core,
GameRuntime,
createGameRuntime,
BrowserPlatformAdapter
};