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
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,291 @@
/**
* 插件系统核心类型定义
* Plugin system core type definitions
*
* 这是运行时插件类型的唯一定义源。
* 编辑器类型在 editor-core 中扩展这些类型。
*
* This is the single source of truth for runtime plugin types.
* Editor types extend these in editor-core.
*
* @see docs/architecture/plugin-system-design.md
*/
import type { IComponentRegistry, IScene, ServiceContainer } from '@esengine/ecs-framework';
import { PluginServiceRegistry } from '@esengine/ecs-framework';
import { TransformComponent } from './TransformComponent';
import type { ModuleManifest } from './ModuleManifest';
// 导入 engine-core 特有的服务令牌
// Import engine-core specific service tokens
import {
TransformTypeToken,
CanvasElementToken,
TextureServiceToken,
DynamicAtlasServiceToken,
CoordinateServiceToken,
RenderConfigServiceToken,
type ITextureService,
type IDynamicAtlasService,
type ICoordinateService,
type IRenderConfigService
} from './PluginServiceRegistry';
// 导出 engine-core 特有的服务令牌 | Export engine-core specific service tokens
export {
TransformTypeToken,
CanvasElementToken,
TextureServiceToken,
DynamicAtlasServiceToken,
CoordinateServiceToken,
RenderConfigServiceToken,
type ITextureService,
type IDynamicAtlasService,
type ICoordinateService,
type IRenderConfigService
};
// ============================================================================
// 编辑器模块基础接口 | Editor Module Base Interface
// ============================================================================
/**
* 编辑器模块基础接口
* Base editor module interface
*
* 定义编辑器模块的核心生命周期方法。
* 完整的 IEditorModuleLoader 接口在 editor-core 中扩展此接口。
*
* Defines core lifecycle methods for editor modules.
* Full IEditorModuleLoader interface extends this in editor-core.
*/
export interface IEditorModuleBase {
/**
* 安装编辑器模块
* Install editor module
*/
install(services: ServiceContainer): Promise<void>;
/**
* 卸载编辑器模块
* Uninstall editor module
*/
uninstall?(): Promise<void>;
/**
* 编辑器就绪回调
* Editor ready callback
*/
onEditorReady?(): void | Promise<void>;
/**
* 项目打开回调
* Project open callback
*/
onProjectOpen?(projectPath: string): void | Promise<void>;
/**
* 项目关闭回调
* Project close callback
*/
onProjectClose?(): void | Promise<void>;
/**
* 场景加载回调
* Scene loaded callback
*/
onSceneLoaded?(scenePath: string): void;
/**
* 场景保存前回调
* Before scene save callback
*/
onSceneSaving?(scenePath: string): boolean | void;
/**
* 设置语言
* Set locale
*/
setLocale?(locale: string): void;
}
// ============================================================================
// 加载阶段 | Loading Phase
// ============================================================================
/**
* 加载阶段 - 控制插件模块的加载顺序
* Loading phase - controls the loading order of plugin modules
*/
export type LoadingPhase =
| 'earliest' // 最早加载(核心模块) | Earliest (core modules)
| 'preDefault' // 默认之前 | Before default
| 'default' // 默认阶段 | Default phase
| 'postDefault' // 默认之后 | After default
| 'postEngine'; // 引擎初始化后 | After engine init
// ============================================================================
// 系统上下文 | System Context
// ============================================================================
/**
* 系统创建上下文
* System creation context
*
* 包含运行时配置和插件服务注册表。
* Contains runtime configuration and plugin service registry.
*
* @example
* ```typescript
* // 导入需要的 Token | Import needed tokens
* import { Physics2DQueryToken } from '@esengine/physics-rapier2d';
* import { AssetManagerToken } from '@esengine/asset-system';
*
* // 注册服务 | Register service
* context.services.register(Physics2DQueryToken, physicsSystem);
*
* // 获取服务(可选)| Get service (optional)
* const physics = context.services.get(Physics2DQueryToken);
*
* // 获取服务(必需)| Get service (required)
* const physics = context.services.require(Physics2DQueryToken);
* ```
*/
export interface SystemContext {
/** 是否为编辑器模式 | Is editor mode */
readonly isEditor: boolean;
/**
* 插件服务注册表
* Plugin service registry
*
* 用于跨插件共享服务。
* For sharing services between plugins.
*/
readonly services: PluginServiceRegistry;
}
// ============================================================================
// 运行时模块 | Runtime Module
// ============================================================================
/**
* 运行时模块接口
* Runtime module interface
*/
export interface IRuntimeModule {
/**
* 注册组件到 ComponentRegistry
* Register components to ComponentRegistry
*/
registerComponents?(registry: IComponentRegistry): void;
/**
* 注册服务到 ServiceContainer
* Register services to ServiceContainer
*/
registerServices?(services: ServiceContainer): void;
/**
* 为场景创建系统
* Create systems for scene
*/
createSystems?(scene: IScene, context: SystemContext): void;
/**
* 所有系统创建完成后调用
* Called after all systems are created
*/
onSystemsCreated?(scene: IScene, context: SystemContext): void;
/**
* 模块初始化完成回调
* Module initialization complete callback
*/
onInitialize?(): Promise<void>;
/**
* 模块销毁回调
* Module destroy callback
*/
onDestroy?(): void;
}
// ============================================================================
// 插件接口 | Plugin Interface
// ============================================================================
/**
* 插件接口
* Plugin interface
*
* 这是所有插件包导出的统一类型。
* 使用泛型允许编辑器模块使用更具体的类型。
*
* This is the unified type that all plugin packages export.
* Uses generics to allow editor modules to use more specific types.
*
* @example
* ```typescript
* // 纯运行时插件 | Pure runtime plugin
* const MyPlugin: IRuntimePlugin = {
* manifest,
* runtimeModule: new MyRuntimeModule()
* };
*
* // 编辑器插件(在 editor-core 中定义 IEditorPlugin
* // Editor plugin (IEditorPlugin defined in editor-core)
* const MyEditorPlugin: IEditorPlugin = {
* manifest,
* runtimeModule: new MyRuntimeModule(),
* editorModule: new MyEditorModule()
* };
* ```
*/
export interface IRuntimePlugin<TEditorModule = unknown> {
/** 模块清单 | Module manifest */
readonly manifest: ModuleManifest;
/** 运行时模块(可选) | Runtime module (optional) */
readonly runtimeModule?: IRuntimeModule;
/**
* 编辑器模块(可选)
* Editor module (optional)
*
* 泛型参数允许 editor-core 使用 IEditorModuleLoader 类型。
* Generic parameter allows editor-core to use IEditorModuleLoader type.
*/
readonly editorModule?: TEditorModule;
}
// ============================================================================
// Engine Core 插件 | Engine Core Plugin
// ============================================================================
class EngineRuntimeModule implements IRuntimeModule {
registerComponents(registry: IComponentRegistry): void {
registry.register(TransformComponent);
}
}
const manifest: ModuleManifest = {
id: 'engine-core',
name: '@esengine/engine-core',
displayName: 'Engine Core',
description: 'Transform 等核心组件',
version: '1.0.0',
category: 'Core',
icon: 'Box',
isCore: true,
defaultEnabled: true,
isEngineModule: true,
dependencies: ['core', 'math'],
exports: {
components: ['TransformComponent', 'HierarchyComponent'],
systems: ['TransformSystem', 'HierarchySystem']
}
};
export const EnginePlugin: IRuntimePlugin = {
manifest,
runtimeModule: new EngineRuntimeModule()
};

View File

@@ -0,0 +1,2 @@
// Re-export from ecs-framework
export { HierarchyComponent } from '@esengine/ecs-framework';

View File

@@ -0,0 +1,2 @@
// Re-export from ecs-framework
export { HierarchySystem } from '@esengine/ecs-framework';

View File

@@ -0,0 +1,534 @@
/**
* 输入管理器 - 统一管理所有输入状态
* Input Manager - Unified input state management
*
* 提供简单易用的 API 供游戏代码查询输入状态。
* Provides simple API for game code to query input state.
*
* @example
* ```typescript
* // 在游戏系统中使用 | Use in game system
* class PlayerMovementSystem extends EntitySystem {
* protected process(entities: readonly Entity[]): void {
* const input = Input;
*
* for (const entity of entities) {
* const transform = entity.getComponent(TransformComponent);
*
* // 键盘移动 | Keyboard movement
* if (input.isKeyDown('KeyW') || input.isKeyDown('ArrowUp')) {
* transform.position.y -= 5;
* }
* if (input.isKeyDown('KeyS') || input.isKeyDown('ArrowDown')) {
* transform.position.y += 5;
* }
*
* // 鼠标点击 | Mouse click
* if (input.isMouseButtonJustPressed(MouseButton.Left)) {
* console.log('Clicked at:', input.mousePosition);
* }
* }
* }
* }
* ```
*/
import { MouseButton } from '@esengine/platform-common';
import type { KeyboardEventInfo, MouseEventInfo, WheelEventInfo, TouchInfo } from '@esengine/platform-common';
import type { IVector2 } from '@esengine/ecs-framework-math';
/**
* 按键状态
* Key state
*/
export interface KeyState {
/** 是否按下 | Is pressed */
pressed: boolean;
/** 本帧刚按下 | Just pressed this frame */
justPressed: boolean;
/** 本帧刚释放 | Just released this frame */
justReleased: boolean;
}
/**
* 鼠标按钮状态
* Mouse button state
*/
export interface MouseButtonState {
/** 是否按下 | Is pressed */
pressed: boolean;
/** 本帧刚按下 | Just pressed this frame */
justPressed: boolean;
/** 本帧刚释放 | Just released this frame */
justReleased: boolean;
}
/**
* 输入管理器类
* Input Manager class
*/
export class InputManager {
// ========== 键盘状态 | Keyboard state ==========
private _keyStates: Map<string, KeyState> = new Map();
private _keysJustPressed: Set<string> = new Set();
private _keysJustReleased: Set<string> = new Set();
// ========== 鼠标状态 | Mouse state ==========
private _mousePosition: IVector2 = { x: 0, y: 0 };
private _mouseMovement: IVector2 = { x: 0, y: 0 };
private _mouseButtonStates: Map<MouseButton, MouseButtonState> = new Map();
private _mouseButtonsJustPressed: Set<MouseButton> = new Set();
private _mouseButtonsJustReleased: Set<MouseButton> = new Set();
private _scrollDelta: IVector2 = { x: 0, y: 0 };
// ========== 触摸状态 | Touch state ==========
private _touches: Map<number, TouchInfo> = new Map();
private _touchesJustStarted: Set<number> = new Set();
private _touchesJustEnded: Set<number> = new Set();
// ========== 修饰键状态 | Modifier key state ==========
private _altKey: boolean = false;
private _ctrlKey: boolean = false;
private _shiftKey: boolean = false;
private _metaKey: boolean = false;
constructor() {
// 初始化鼠标按钮状态 | Initialize mouse button states
this._mouseButtonStates.set(MouseButton.Left, { pressed: false, justPressed: false, justReleased: false });
this._mouseButtonStates.set(MouseButton.Middle, { pressed: false, justPressed: false, justReleased: false });
this._mouseButtonStates.set(MouseButton.Right, { pressed: false, justPressed: false, justReleased: false });
}
// ========== 键盘 API | Keyboard API ==========
/**
* 检查按键是否按下
* Check if a key is pressed
*
* @param code 按键代码 (如 'KeyW', 'Space', 'ArrowUp') | Key code
*/
isKeyDown(code: string): boolean {
return this._keyStates.get(code)?.pressed ?? false;
}
/**
* 检查按键是否本帧刚按下
* Check if a key was just pressed this frame
*
* @param code 按键代码 | Key code
*/
isKeyJustPressed(code: string): boolean {
return this._keyStates.get(code)?.justPressed ?? false;
}
/**
* 检查按键是否本帧刚释放
* Check if a key was just released this frame
*
* @param code 按键代码 | Key code
*/
isKeyJustReleased(code: string): boolean {
return this._keyStates.get(code)?.justReleased ?? false;
}
/**
* 获取所有按下的按键
* Get all pressed keys
*/
getPressedKeys(): string[] {
const result: string[] = [];
this._keyStates.forEach((state, code) => {
if (state.pressed) {
result.push(code);
}
});
return result;
}
// ========== 鼠标 API | Mouse API ==========
/**
* 获取鼠标位置(屏幕坐标)
* Get mouse position (screen coordinates)
*/
get mousePosition(): Readonly<IVector2> {
return this._mousePosition;
}
/**
* 获取鼠标本帧移动量
* Get mouse movement this frame
*/
get mouseMovement(): Readonly<IVector2> {
return this._mouseMovement;
}
/**
* 获取滚轮滚动量
* Get scroll delta
*/
get scrollDelta(): Readonly<IVector2> {
return this._scrollDelta;
}
/**
* 检查鼠标按钮是否按下
* Check if a mouse button is pressed
*
* @param button 鼠标按钮 | Mouse button
*/
isMouseButtonDown(button: MouseButton): boolean {
return this._mouseButtonStates.get(button)?.pressed ?? false;
}
/**
* 检查鼠标按钮是否本帧刚按下
* Check if a mouse button was just pressed this frame
*
* @param button 鼠标按钮 | Mouse button
*/
isMouseButtonJustPressed(button: MouseButton): boolean {
return this._mouseButtonStates.get(button)?.justPressed ?? false;
}
/**
* 检查鼠标按钮是否本帧刚释放
* Check if a mouse button was just released this frame
*
* @param button 鼠标按钮 | Mouse button
*/
isMouseButtonJustReleased(button: MouseButton): boolean {
return this._mouseButtonStates.get(button)?.justReleased ?? false;
}
// ========== 触摸 API | Touch API ==========
/**
* 获取所有触摸点
* Get all touch points
*/
get touches(): ReadonlyMap<number, TouchInfo> {
return this._touches;
}
/**
* 获取触摸点数量
* Get touch count
*/
get touchCount(): number {
return this._touches.size;
}
/**
* 获取指定触摸点
* Get a specific touch point
*
* @param identifier 触摸点标识符 | Touch identifier
*/
getTouch(identifier: number): TouchInfo | undefined {
return this._touches.get(identifier);
}
/**
* 检查是否有触摸
* Check if there are any touches
*/
get isTouching(): boolean {
return this._touches.size > 0;
}
/**
* 检查触摸点是否本帧刚开始
* Check if a touch just started this frame
*
* @param identifier 触摸点标识符 | Touch identifier
*/
isTouchJustStarted(identifier: number): boolean {
return this._touchesJustStarted.has(identifier);
}
/**
* 检查触摸点是否本帧刚结束
* Check if a touch just ended this frame
*
* @param identifier 触摸点标识符 | Touch identifier
*/
isTouchJustEnded(identifier: number): boolean {
return this._touchesJustEnded.has(identifier);
}
// ========== 修饰键 API | Modifier keys API ==========
/** Alt 键是否按下 | Is Alt key pressed */
get altKey(): boolean { return this._altKey; }
/** Ctrl 键是否按下 | Is Ctrl key pressed */
get ctrlKey(): boolean { return this._ctrlKey; }
/** Shift 键是否按下 | Is Shift key pressed */
get shiftKey(): boolean { return this._shiftKey; }
/** Meta 键是否按下 (Windows/Command) | Is Meta key pressed */
get metaKey(): boolean { return this._metaKey; }
// ========== 内部更新方法 | Internal update methods ==========
/**
* 处理键盘按下事件
* Handle key down event
* @internal
*/
handleKeyDown(event: KeyboardEventInfo): void {
let state = this._keyStates.get(event.code);
if (!state) {
state = { pressed: false, justPressed: false, justReleased: false };
this._keyStates.set(event.code, state);
}
if (!state.pressed && !event.repeat) {
state.justPressed = true;
this._keysJustPressed.add(event.code);
}
state.pressed = true;
// 更新修饰键 | Update modifier keys
this._altKey = event.altKey;
this._ctrlKey = event.ctrlKey;
this._shiftKey = event.shiftKey;
this._metaKey = event.metaKey;
}
/**
* 处理键盘释放事件
* Handle key up event
* @internal
*/
handleKeyUp(event: KeyboardEventInfo): void {
let state = this._keyStates.get(event.code);
if (!state) {
state = { pressed: false, justPressed: false, justReleased: false };
this._keyStates.set(event.code, state);
}
if (state.pressed) {
state.justReleased = true;
this._keysJustReleased.add(event.code);
}
state.pressed = false;
// 更新修饰键 | Update modifier keys
this._altKey = event.altKey;
this._ctrlKey = event.ctrlKey;
this._shiftKey = event.shiftKey;
this._metaKey = event.metaKey;
}
/**
* 处理鼠标移动事件
* Handle mouse move event
* @internal
*/
handleMouseMove(event: MouseEventInfo): void {
this._mousePosition.x = event.x;
this._mousePosition.y = event.y;
this._mouseMovement.x += event.movementX;
this._mouseMovement.y += event.movementY;
}
/**
* 处理鼠标按下事件
* Handle mouse down event
* @internal
*/
handleMouseDown(event: MouseEventInfo): void {
const button = event.button as MouseButton;
let state = this._mouseButtonStates.get(button);
if (!state) {
state = { pressed: false, justPressed: false, justReleased: false };
this._mouseButtonStates.set(button, state);
}
if (!state.pressed) {
state.justPressed = true;
this._mouseButtonsJustPressed.add(button);
}
state.pressed = true;
this._mousePosition.x = event.x;
this._mousePosition.y = event.y;
}
/**
* 处理鼠标释放事件
* Handle mouse up event
* @internal
*/
handleMouseUp(event: MouseEventInfo): void {
const button = event.button as MouseButton;
let state = this._mouseButtonStates.get(button);
if (!state) {
state = { pressed: false, justPressed: false, justReleased: false };
this._mouseButtonStates.set(button, state);
}
if (state.pressed) {
state.justReleased = true;
this._mouseButtonsJustReleased.add(button);
}
state.pressed = false;
this._mousePosition.x = event.x;
this._mousePosition.y = event.y;
}
/**
* 处理鼠标滚轮事件
* Handle mouse wheel event
* @internal
*/
handleWheel(event: WheelEventInfo): void {
this._scrollDelta.x += event.deltaX;
this._scrollDelta.y += event.deltaY;
}
/**
* 处理触摸开始事件
* Handle touch start event
* @internal
*/
handleTouchStart(touches: TouchInfo[]): void {
for (const touch of touches) {
this._touches.set(touch.identifier, touch);
this._touchesJustStarted.add(touch.identifier);
}
}
/**
* 处理触摸移动事件
* Handle touch move event
* @internal
*/
handleTouchMove(touches: TouchInfo[]): void {
for (const touch of touches) {
this._touches.set(touch.identifier, touch);
}
}
/**
* 处理触摸结束事件
* Handle touch end event
* @internal
*/
handleTouchEnd(touches: TouchInfo[]): void {
for (const touch of touches) {
this._touches.delete(touch.identifier);
this._touchesJustEnded.add(touch.identifier);
}
}
/**
* 帧末清理临时状态
* Clear temporary state at end of frame
* @internal
*/
endFrame(): void {
// 清理键盘帧状态 | Clear keyboard frame state
for (const code of this._keysJustPressed) {
const state = this._keyStates.get(code);
if (state) {
state.justPressed = false;
}
}
this._keysJustPressed.clear();
for (const code of this._keysJustReleased) {
const state = this._keyStates.get(code);
if (state) {
state.justReleased = false;
}
}
this._keysJustReleased.clear();
// 清理鼠标帧状态 | Clear mouse frame state
for (const button of this._mouseButtonsJustPressed) {
const state = this._mouseButtonStates.get(button);
if (state) {
state.justPressed = false;
}
}
this._mouseButtonsJustPressed.clear();
for (const button of this._mouseButtonsJustReleased) {
const state = this._mouseButtonStates.get(button);
if (state) {
state.justReleased = false;
}
}
this._mouseButtonsJustReleased.clear();
// 清理鼠标移动和滚动 | Clear mouse movement and scroll
this._mouseMovement.x = 0;
this._mouseMovement.y = 0;
this._scrollDelta.x = 0;
this._scrollDelta.y = 0;
// 清理触摸帧状态 | Clear touch frame state
this._touchesJustStarted.clear();
this._touchesJustEnded.clear();
}
/**
* 重置所有输入状态
* Reset all input state
*/
reset(): void {
this._keyStates.clear();
this._keysJustPressed.clear();
this._keysJustReleased.clear();
this._mousePosition = { x: 0, y: 0 };
this._mouseMovement = { x: 0, y: 0 };
this._scrollDelta = { x: 0, y: 0 };
this._mouseButtonStates.forEach(state => {
state.pressed = false;
state.justPressed = false;
state.justReleased = false;
});
this._mouseButtonsJustPressed.clear();
this._mouseButtonsJustReleased.clear();
this._touches.clear();
this._touchesJustStarted.clear();
this._touchesJustEnded.clear();
this._altKey = false;
this._ctrlKey = false;
this._shiftKey = false;
this._metaKey = false;
}
}
/**
* 全局输入管理器实例
* Global input manager instance
*
* 游戏代码可以直接使用这个实例查询输入状态。
* Game code can use this instance directly to query input state.
*
* @example
* ```typescript
* import { Input, MouseButton } from '@esengine/engine-core';
*
* // 检查按键
* if (Input.isKeyDown('KeyW')) {
* // 向上移动
* }
*
* // 检查鼠标
* if (Input.isMouseButtonJustPressed(MouseButton.Left)) {
* console.log('点击位置:', Input.mousePosition);
* }
* ```
*/
export const Input = new InputManager();

View File

@@ -0,0 +1,264 @@
/**
* 输入系统 - 将平台输入事件连接到 InputManager
* Input System - Connects platform input events to InputManager
*
* 在 ECS 更新循环中运行,负责:
* 1. 在帧开始时已经由事件驱动更新了 InputManager
* 2. 在帧末清理临时状态justPressed, justReleased 等)
*
* Runs in ECS update loop, responsible for:
* 1. InputManager is already updated by events at frame start
* 2. Clear temporary state at frame end (justPressed, justReleased, etc.)
*/
import { EntitySystem, Matcher, ECSSystem } from '@esengine/ecs-framework';
import type { Entity } from '@esengine/ecs-framework';
import type {
IPlatformInputSubsystem,
KeyboardEventInfo,
MouseEventInfo,
WheelEventInfo,
TouchEvent
} from '@esengine/platform-common';
import { Input, InputManager } from './InputManager';
/**
* 输入系统配置
* Input system configuration
*/
export interface InputSystemConfig {
/**
* 输入管理器实例,默认使用全局 Input
* Input manager instance, defaults to global Input
*/
inputManager?: InputManager;
/**
* 是否在编辑器模式下禁用(防止与编辑器输入冲突)
* Whether to disable in editor mode (prevent conflict with editor input)
*/
disableInEditor?: boolean;
}
/**
* 输入系统
* Input System
*
* 处理平台输入事件并更新 InputManager 状态。
* Handles platform input events and updates InputManager state.
*
* @example
* ```typescript
* // 在 GameRuntime 中注册
* const inputSystem = new InputSystem({
* inputSubsystem: webInputSubsystem
* });
* scene.addSystem(inputSystem);
*
* // 在游戏系统中使用
* import { Input, MouseButton } from '@esengine/engine-core';
*
* class PlayerSystem extends EntitySystem {
* protected process(entities: readonly Entity[]): void {
* if (Input.isKeyDown('KeyW')) {
* // 移动玩家
* }
* }
* }
* ```
*/
@ECSSystem('InputSystem', { updateOrder: -1000 }) // 最先更新 | Update first
export class InputSystem extends EntitySystem {
private _inputManager: InputManager;
private _inputSubsystem: IPlatformInputSubsystem | null = null;
private _disableInEditor: boolean;
private _isInitialized: boolean = false;
constructor(config: InputSystemConfig = {}) {
// 不匹配任何实体,只用于生命周期 | Match no entities, only for lifecycle
super(Matcher.nothing());
this._inputManager = config.inputManager ?? Input;
this._disableInEditor = config.disableInEditor ?? false;
}
/**
* 设置平台输入子系统
* Set platform input subsystem
*
* @param subsystem 平台输入子系统 | Platform input subsystem
*/
setInputSubsystem(subsystem: IPlatformInputSubsystem): void {
// 如果已有子系统,先解绑 | Unbind if already has subsystem
if (this._inputSubsystem && this._isInitialized) {
this.unbindEvents();
}
this._inputSubsystem = subsystem;
// 如果已初始化,立即绑定 | Bind immediately if initialized
if (this._isInitialized) {
this.bindEvents();
}
}
/**
* 获取输入管理器
* Get input manager
*/
get inputManager(): InputManager {
return this._inputManager;
}
protected override onInitialize(): void {
this._isInitialized = true;
if (this._inputSubsystem) {
this.bindEvents();
}
}
/**
* 绑定平台输入事件
* Bind platform input events
*/
private bindEvents(): void {
if (!this._inputSubsystem) return;
const sub = this._inputSubsystem;
// 键盘事件 | Keyboard events
if (sub.onKeyDown) {
sub.onKeyDown(this._handleKeyDown);
}
if (sub.onKeyUp) {
sub.onKeyUp(this._handleKeyUp);
}
// 鼠标事件 | Mouse events
if (sub.onMouseMove) {
sub.onMouseMove(this._handleMouseMove);
}
if (sub.onMouseDown) {
sub.onMouseDown(this._handleMouseDown);
}
if (sub.onMouseUp) {
sub.onMouseUp(this._handleMouseUp);
}
if (sub.onWheel) {
sub.onWheel(this._handleWheel);
}
// 触摸事件 | Touch events
sub.onTouchStart(this._handleTouchStart);
sub.onTouchMove(this._handleTouchMove);
sub.onTouchEnd(this._handleTouchEnd);
sub.onTouchCancel(this._handleTouchEnd); // 取消当作结束处理 | Treat cancel as end
}
/**
* 解绑平台输入事件
* Unbind platform input events
*/
private unbindEvents(): void {
if (!this._inputSubsystem) return;
const sub = this._inputSubsystem;
// 键盘事件 | Keyboard events
if (sub.offKeyDown) {
sub.offKeyDown(this._handleKeyDown);
}
if (sub.offKeyUp) {
sub.offKeyUp(this._handleKeyUp);
}
// 鼠标事件 | Mouse events
if (sub.offMouseMove) {
sub.offMouseMove(this._handleMouseMove);
}
if (sub.offMouseDown) {
sub.offMouseDown(this._handleMouseDown);
}
if (sub.offMouseUp) {
sub.offMouseUp(this._handleMouseUp);
}
if (sub.offWheel) {
sub.offWheel(this._handleWheel);
}
// 触摸事件 | Touch events
sub.offTouchStart(this._handleTouchStart);
sub.offTouchMove(this._handleTouchMove);
sub.offTouchEnd(this._handleTouchEnd);
sub.offTouchCancel(this._handleTouchEnd);
}
// ========== 事件处理函数 | Event handlers ==========
// 使用箭头函数保持 this 绑定 | Use arrow functions to preserve this binding
private _handleKeyDown = (event: KeyboardEventInfo): void => {
this._inputManager.handleKeyDown(event);
};
private _handleKeyUp = (event: KeyboardEventInfo): void => {
this._inputManager.handleKeyUp(event);
};
private _handleMouseMove = (event: MouseEventInfo): void => {
this._inputManager.handleMouseMove(event);
};
private _handleMouseDown = (event: MouseEventInfo): void => {
this._inputManager.handleMouseDown(event);
};
private _handleMouseUp = (event: MouseEventInfo): void => {
this._inputManager.handleMouseUp(event);
};
private _handleWheel = (event: WheelEventInfo): void => {
this._inputManager.handleWheel(event);
};
private _handleTouchStart = (event: TouchEvent): void => {
this._inputManager.handleTouchStart(event.changedTouches);
};
private _handleTouchMove = (event: TouchEvent): void => {
this._inputManager.handleTouchMove(event.changedTouches);
};
private _handleTouchEnd = (event: TouchEvent): void => {
this._inputManager.handleTouchEnd(event.changedTouches);
};
// ========== 系统生命周期 | System lifecycle ==========
protected override process(_entities: readonly Entity[]): void {
// 不处理实体,仅用于生命周期 | No entity processing, only for lifecycle
}
protected override lateProcess(_entities: readonly Entity[]): void {
// 在帧末清理临时状态 | Clear temporary state at end of frame
this._inputManager.endFrame();
}
protected override onDestroy(): void {
this.unbindEvents();
this._inputManager.reset();
this._isInitialized = false;
}
/**
* 检查是否应该启用输入
* Check if input should be enabled
*/
protected override onCheckProcessing(): boolean {
// 如果设置了编辑器模式禁用,检查场景是否在编辑器模式
if (this._disableInEditor && this.scene?.isEditorMode) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,41 @@
/**
* 输入系统模块
* Input System Module
*
* 提供统一的输入处理能力,支持键盘、鼠标和触摸输入。
* Provides unified input handling for keyboard, mouse, and touch.
*
* @example
* ```typescript
* import { Input, InputSystem, MouseButton } from '@esengine/engine-core';
*
* // 在游戏代码中查询输入状态
* if (Input.isKeyDown('KeyW')) {
* // 向上移动
* }
*
* if (Input.isMouseButtonJustPressed(MouseButton.Left)) {
* console.log('点击位置:', Input.mousePosition);
* }
*
* // 触摸输入
* if (Input.isTouching) {
* for (const [id, touch] of Input.touches) {
* console.log('触摸点:', id, touch.x, touch.y);
* }
* }
* ```
*/
export { InputManager, Input, type KeyState, type MouseButtonState } from './InputManager';
export { InputSystem, type InputSystemConfig } from './InputSystem';
// 重导出平台公共类型 | Re-export platform common types
export { MouseButton } from '@esengine/platform-common';
export type {
KeyboardEventInfo,
MouseEventInfo,
WheelEventInfo,
TouchInfo,
TouchEvent
} from '@esengine/platform-common';

View File

@@ -0,0 +1,301 @@
/**
* Module Manifest Types.
* 模块清单类型定义。
*
* This is the single source of truth for module/plugin configuration.
* Each engine module should have a module.json file containing this information.
* 这是模块/插件配置的唯一数据源。
* 每个引擎模块应该有一个包含此信息的 module.json 文件。
*/
/**
* Module category for organization in UI.
* 用于 UI 组织的模块分类。
*/
export type ModuleCategory =
| 'Core'
| 'Rendering'
| 'Physics'
| 'AI'
| 'Audio'
| 'Networking'
| 'Other';
/**
* Platform requirements for the module.
* 模块的平台要求。
*/
export type ModulePlatform = 'web' | 'desktop' | 'mobile';
/**
* Module exports definition.
* 模块导出定义。
*/
export interface ModuleExports {
/** Exported component classes | 导出的组件类 */
components?: string[];
/** Exported system classes | 导出的系统类 */
systems?: string[];
/** Exported asset loaders | 导出的资源加载器 */
loaders?: string[];
/** Other exported items | 其他导出项 */
other?: string[];
}
/**
* Module manifest definition (Unified Plugin/Module config).
* 模块清单定义(统一的插件/模块配置)。
*
* This interface matches the structure of module.json files.
* 此接口匹配 module.json 文件的结构。
*/
export interface ModuleManifest {
// ==================== Core Identifiers ====================
/** Unique module identifier | 唯一模块标识符 */
id: string;
/** Package name (npm style) | 包名npm 风格) */
name: string;
/** Display name for UI | UI 显示名称 */
displayName: string;
/** Module description | 模块描述 */
description: string;
/** Module version | 模块版本 */
version: string;
// ==================== Classification ====================
/** Category for grouping in UI | UI 分组分类 */
category: ModuleCategory;
/** Tags for search and filtering | 用于搜索和过滤的标签 */
tags?: string[];
/** Icon name (Lucide icon) | 图标名Lucide 图标) */
icon?: string;
// ==================== Lifecycle ====================
/** Whether this is a core module (cannot be disabled) | 是否为核心模块(不能禁用) */
isCore: boolean;
/** Whether enabled by default in new projects | 新项目中是否默认启用 */
defaultEnabled: boolean;
/** Whether this is an engine built-in module | 是否为引擎内置模块 */
isEngineModule?: boolean;
// ==================== Content ====================
/** Whether this module can contain content/assets | 是否可以包含内容/资源 */
canContainContent?: boolean;
/** Platform requirements | 平台要求 */
platforms?: ModulePlatform[];
// ==================== Dependencies ====================
/** Module IDs this module depends on | 此模块依赖的模块 ID */
dependencies: string[];
/**
* External package dependencies that need to be included in import map.
* 需要包含在 import map 中的外部包依赖。
*
* These are runtime dependencies that are dynamically imported by the module.
* 这些是模块动态导入的运行时依赖。
*
* Example: ["@esengine/rapier2d"]
*/
externalDependencies?: string[];
// ==================== Exports ====================
/** Exported items for dependency checking | 导出项用于依赖检查 */
exports: ModuleExports;
// ==================== Asset Configuration ====================
// ==================== 资产配置 ====================
/**
* Asset file extensions supported by this module.
* 此模块支持的资产文件扩展名。
*
* Used by build pipeline to determine which files to copy and their types.
* 构建管线使用此配置决定复制哪些文件及其类型。
*
* Example:
* ```json
* {
* "assetExtensions": {
* ".particle": "particle",
* ".particle.json": "particle",
* ".btree": "behavior-tree"
* }
* }
* ```
*/
assetExtensions?: Record<string, string>;
// ==================== Editor Integration ====================
/**
* Associated editor package name.
* 关联的编辑器包名。
* e.g., "@esengine/tilemap-editor" for "@esengine/tilemap"
*/
editorPackage?: string;
// ==================== Performance (auto-calculated at build time) ====================
// ==================== 性能(构建时自动计算) ====================
/** JS bundle size in bytes | JS 包大小(字节) */
jsSize?: number;
/** Whether this module requires WASM | 是否需要 WASM */
requiresWasm?: boolean;
/** WASM file size in bytes | WASM 文件大小(字节) */
wasmSize?: number;
/**
* Unified WASM configuration for all WASM-related modules.
* 统一的 WASM 配置,用于所有 WASM 相关模块。
*
* This replaces the legacy wasmPaths, runtimeWasmPath, and wasmBindings fields.
* 此配置替代旧的 wasmPaths、runtimeWasmPath 和 wasmBindings 字段。
*/
wasmConfig?: {
/**
* List of WASM files to copy during build.
* 构建时需要复制的 WASM 文件列表。
*
* Each entry specifies source location and output destination.
* 每个条目指定源位置和输出目标。
*/
files: Array<{
/**
* Source file path relative to engine modules directory.
* 源文件路径,相对于引擎模块目录。
*
* Supports multiple candidate paths (first existing one is used).
* 支持多个候选路径(使用第一个存在的)。
*
* Example: ["rapier2d/pkg/rapier_wasm2d_bg.wasm", "rapier2d/rapier_wasm2d_bg.wasm"]
*/
src: string | string[];
/**
* Destination path relative to build output directory.
* 目标路径,相对于构建输出目录。
*
* Example: "wasm/rapier_wasm2d_bg.wasm" or "libs/es-engine/es_engine_bg.wasm"
*/
dst: string;
}>;
/**
* Runtime WASM path for dynamic loading (used by JS code at runtime).
* 运行时 WASM 路径用于动态加载JS 代码在运行时使用)。
*
* This is the path that runtime code uses to fetch the WASM file.
* 这是运行时代码用来获取 WASM 文件的路径。
*
* Example: "wasm/rapier_wasm2d_bg.wasm"
*/
runtimePath?: string;
/**
* Whether this is the core engine WASM module.
* 是否是核心引擎 WASM 模块。
*
* The core engine WASM (e.g., es_engine) must be initialized first
* before the runtime can start. Only one module should have this flag.
* 核心引擎 WASM如 es_engine必须在运行时启动前首先初始化。
* 只有一个模块应该设置此标志。
*/
isEngineCore?: boolean;
};
// ==================== Build Configuration ====================
// ==================== 构建配置 ====================
/**
* Output path for the built module file.
* 构建后模块文件的输出路径。
*
* Example: "dist/index.mjs"
*/
outputPath?: string;
/**
* Plugin export name for dynamic loading.
* 用于动态加载的插件导出名。
*
* Example: "SpritePlugin"
*/
pluginExport?: string;
/**
* Additional files to include when copying module.
* 复制模块时需要包含的额外文件。
*
* Glob patterns relative to the module's dist directory.
* 相对于模块 dist 目录的 glob 模式。
*
* Example: ["chunk-*.js", "worker.js"]
*/
includes?: string[];
// ==================== Build Pipeline Configuration ====================
// ==================== 构建管线配置 ====================
/**
* Core service exports that should be explicitly exported first.
* 需要显式优先导出的核心服务。
*
* Used to avoid naming conflicts when re-exporting modules.
* 用于避免重新导出模块时的命名冲突。
*
* Example: ["createServiceToken", "PluginServiceRegistry"]
*/
coreServiceExports?: string[];
/**
* Whether this module is the runtime entry point (provides default export).
* 此模块是否为运行时入口点(提供默认导出)。
*
* Only one module should have this set to true (typically platform-web).
* 只有一个模块应该设置为 true通常是 platform-web
*/
isRuntimeEntry?: boolean;
/**
* Standard entry file names to search for user scripts.
* 用于搜索用户脚本的标准入口文件名。
*
* Build pipeline will try these files in order.
* 构建管线将按顺序尝试这些文件。
*
* Example: ["index.ts", "main.ts", "game.ts"]
*/
userScriptEntries?: string[];
/**
* Additional external packages that user scripts might import.
* 用户脚本可能导入的额外外部包。
*
* These packages will be marked as external during bundling.
* 这些包在打包时将被标记为外部依赖。
*
* Example: ["@esengine/ecs-framework", "@esengine/core"]
*/
userScriptExternals?: string[];
}

View File

@@ -0,0 +1,121 @@
/**
* 插件服务令牌engine-core 特定)
* Plugin Service Tokens (engine-core specific)
*
* 核心类型 (PluginServiceRegistry, createServiceToken, ServiceToken) 从 @esengine/ecs-framework 导入。
* 这里只定义 engine-core 特有的服务令牌。
*
* Core types (PluginServiceRegistry, createServiceToken, ServiceToken) are imported from @esengine/ecs-framework.
* This file only defines engine-core specific service tokens.
*/
import { createServiceToken } from '@esengine/ecs-framework';
// Re-export from ecs-framework for backwards compatibility
export { PluginServiceRegistry, createServiceToken, type ServiceToken } from '@esengine/ecs-framework';
// ============================================================================
// engine-core 内部 Token | engine-core Internal Tokens
// ============================================================================
/**
* Transform 组件类型 | Transform component type
*
* 使用 any 类型以允许各模块使用自己的 ITransformComponent 接口定义。
* Using any type to allow modules to use their own ITransformComponent interface definition.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const TransformTypeToken = createServiceToken<new (...args: any[]) => any>('transformType');
/**
* Canvas 元素的服务令牌
* Service token for the canvas element
*/
export const CanvasElementToken = createServiceToken<HTMLCanvasElement>('canvasElement');
// ============================================================================
// 渲染服务接口 | Render Service Interfaces
// ============================================================================
/**
* 纹理服务接口
* Texture service interface
*
* 负责纹理的加载、状态查询和管理。
* Responsible for texture loading, state querying, and management.
*/
export interface ITextureService {
/** 加载纹理 | Load texture */
loadTexture(id: number, url: string): Promise<void>;
/** 获取纹理加载状态 | Get texture loading state */
getTextureState(id: number): string;
/** 检查纹理是否就绪 | Check if texture is ready */
isTextureReady(id: number): boolean;
/** 获取正在加载的纹理数量 | Get loading texture count */
getTextureLoadingCount(): number;
/** 异步加载纹理(等待完成)| Load texture async (wait for completion) */
loadTextureAsync(id: number, url: string): Promise<void>;
/** 等待所有加载中的纹理完成 | Wait for all textures to load */
waitForAllTextures(timeout?: number): Promise<void>;
}
/**
* 动态图集服务接口
* Dynamic atlas service interface
*/
export interface IDynamicAtlasService {
/** 创建空白纹理 | Create blank texture */
createBlankTexture(width: number, height: number): number;
/** 更新纹理区域 | Update texture region */
updateTextureRegion(
id: number,
x: number,
y: number,
width: number,
height: number,
pixels: Uint8Array
): void;
}
/**
* 坐标转换服务接口
* Coordinate transform service interface
*/
export interface ICoordinateService {
/** 屏幕坐标转世界坐标 | Screen to world */
screenToWorld(screenX: number, screenY: number): { x: number; y: number };
/** 世界坐标转屏幕坐标 | World to screen */
worldToScreen(worldX: number, worldY: number): { x: number; y: number };
}
/**
* 渲染配置服务接口
* Render config service interface
*/
export interface IRenderConfigService {
/** 设置清除颜色 | Set clear color */
setClearColor(r: number, g: number, b: number, a: number): void;
}
// ============================================================================
// 服务令牌 | Service Tokens
// ============================================================================
/** 纹理服务令牌 | Texture service token */
export const TextureServiceToken = createServiceToken<ITextureService>('textureService');
/** 动态图集服务令牌 | Dynamic atlas service token */
export const DynamicAtlasServiceToken = createServiceToken<IDynamicAtlasService>('dynamicAtlasService');
/** 坐标转换服务令牌 | Coordinate service token */
export const CoordinateServiceToken = createServiceToken<ICoordinateService>('coordinateService');
/** 渲染配置服务令牌 | Render config service token */
export const RenderConfigServiceToken = createServiceToken<IRenderConfigService>('renderConfigService');

View File

@@ -0,0 +1,347 @@
/**
* 排序层系统 - 控制渲染顺序
* Sorting Layer System - Controls render order
*
* 提供排序层和层内排序功能,用于精确控制 2D 对象的渲染顺序。
* Provides sorting layer and order-in-layer functionality for precise 2D rendering order control.
*
* @example
* ```typescript
* // 获取排序层管理器
* const manager = SortingLayerManager.instance;
*
* // 获取层的全局顺序
* const order = manager.getLayerOrder('UI'); // 200
*
* // 计算排序键(用于渲染排序)
* const sortKey = manager.getSortKey('UI', 10); // 200 * 10000 + 10 = 2000010
* ```
*/
import { createServiceToken } from './PluginServiceRegistry';
/**
* 排序层配置
* Sorting layer configuration
*/
export interface SortingLayerConfig {
/** 层名称 | Layer name */
name: string;
/** 全局顺序(数值越大越靠前)| Global order (higher = rendered later/on top) */
order: number;
/**
* 是否在屏幕空间渲染
* Whether to render in screen space
*
* 屏幕空间层使用固定正交投影,不受世界相机影响。
* Screen space layers use fixed orthographic projection, not affected by world camera.
*/
bScreenSpace?: boolean;
}
/**
* 可排序接口 - 所有可渲染组件应实现此接口
* Sortable interface - All renderable components should implement this
*/
export interface ISortable {
/** 排序层名称 | Sorting layer name */
sortingLayer: string;
/** 层内顺序 | Order within layer */
orderInLayer: number;
}
/**
* 默认排序层
* Default sorting layers
*
* 渲染顺序(从后到前):
* Render order (back to front):
*
* 世界空间 | World Space:
* - Background (-100): 背景
* - Default (0): 默认游戏对象
* - Foreground (100): 前景对象
* - WorldOverlay (150): 世界空间特效(技能、伤害数字等)
*
* 屏幕空间 | Screen Space:
* - UI (200): UI 元素
* - ScreenOverlay (300): 屏幕空间特效点击特效、Toast
* - Modal (400): 模态对话框、全屏遮罩
*/
export const DEFAULT_SORTING_LAYERS: SortingLayerConfig[] = [
// 世界空间层 | World space layers
{ name: 'Background', order: -100 },
{ name: 'Default', order: 0 },
{ name: 'Foreground', order: 100 },
{ name: 'WorldOverlay', order: 150 },
// 屏幕空间层 | Screen space layers
{ name: 'UI', order: 200, bScreenSpace: true },
{ name: 'ScreenOverlay', order: 300, bScreenSpace: true },
{ name: 'Modal', order: 400, bScreenSpace: true },
];
/**
* 排序层名称常量
* Sorting layer name constants
*/
export const SortingLayers = {
Background: 'Background',
Default: 'Default',
Foreground: 'Foreground',
WorldOverlay: 'WorldOverlay',
UI: 'UI',
ScreenOverlay: 'ScreenOverlay',
Modal: 'Modal',
} as const;
export type SortingLayerName = typeof SortingLayers[keyof typeof SortingLayers] | string;
/**
* 排序层管理器接口
* Sorting layer manager interface
*/
export interface ISortingLayerManager {
/**
* 获取所有排序层
* Get all sorting layers
*/
getLayers(): readonly SortingLayerConfig[];
/**
* 获取层的全局顺序
* Get layer's global order
*/
getLayerOrder(layerName: string): number;
/**
* 计算排序键
* Calculate sort key
*
* sortKey = layerOrder * 10000 + orderInLayer
*/
getSortKey(layerName: string, orderInLayer: number): number;
/**
* 添加自定义层
* Add custom layer
*/
addLayer(name: string, order: number, bScreenSpace?: boolean): void;
/**
* 移除自定义层
* Remove custom layer
*/
removeLayer(name: string): boolean;
/**
* 获取层名称列表(按顺序)
* Get layer names in order
*/
getLayerNames(): string[];
/**
* 检查层是否为屏幕空间层
* Check if layer is screen space
*/
isScreenSpace(layerName: string): boolean;
/**
* 获取所有屏幕空间层名称
* Get all screen space layer names
*/
getScreenSpaceLayers(): string[];
/**
* 获取所有世界空间层名称
* Get all world space layer names
*/
getWorldSpaceLayers(): string[];
}
/**
* 排序层管理器
* Sorting Layer Manager
*
* 管理渲染排序层,提供统一的排序键计算。
* Manages render sorting layers and provides unified sort key calculation.
*/
export class SortingLayerManager implements ISortingLayerManager {
private static _instance: SortingLayerManager | null = null;
private _layers: Map<string, SortingLayerConfig> = new Map();
private _sortedLayers: SortingLayerConfig[] = [];
/**
* 获取单例实例
* Get singleton instance
*/
static get instance(): SortingLayerManager {
if (!SortingLayerManager._instance) {
SortingLayerManager._instance = new SortingLayerManager();
}
return SortingLayerManager._instance;
}
constructor(layers?: SortingLayerConfig[]) {
const initialLayers = layers ?? DEFAULT_SORTING_LAYERS;
for (const layer of initialLayers) {
this._layers.set(layer.name, layer);
}
this._updateSortedLayers();
}
/**
* 获取所有排序层(按顺序)
* Get all sorting layers (in order)
*/
getLayers(): readonly SortingLayerConfig[] {
return this._sortedLayers;
}
/**
* 获取层的全局顺序
* Get layer's global order
*
* @param layerName 层名称 | Layer name
* @returns 全局顺序,未找到返回 0 | Global order, returns 0 if not found
*/
getLayerOrder(layerName: string): number {
return this._layers.get(layerName)?.order ?? 0;
}
/**
* 计算排序键
* Calculate sort key
*
* 用于渲染排序,数值越大越后渲染(显示在上面)。
* Used for render sorting, higher value = rendered later (on top).
*
* @param layerName 层名称 | Layer name
* @param orderInLayer 层内顺序 | Order within layer
* @returns 排序键 | Sort key
*/
getSortKey(layerName: string, orderInLayer: number): number {
const layerOrder = this.getLayerOrder(layerName);
// 使用 10000 作为乘数,允许 -9999 到 9999 的 orderInLayer
// Use 10000 as multiplier, allows orderInLayer from -9999 to 9999
return layerOrder * 10000 + orderInLayer;
}
/**
* 添加自定义层
* Add custom layer
*
* @param name 层名称 | Layer name
* @param order 全局顺序 | Global order
* @param bScreenSpace 是否为屏幕空间层 | Whether screen space layer
*/
addLayer(name: string, order: number, bScreenSpace: boolean = false): void {
this._layers.set(name, { name, order, bScreenSpace });
this._updateSortedLayers();
}
/**
* 移除自定义层
* Remove custom layer
*
* 注意:不能移除默认层。
* Note: Cannot remove default layers.
*
* @param name 层名称 | Layer name
* @returns 是否成功移除 | Whether successfully removed
*/
removeLayer(name: string): boolean {
// 检查是否为默认层 | Check if default layer
if (DEFAULT_SORTING_LAYERS.some(l => l.name === name)) {
console.warn(`[SortingLayerManager] Cannot remove default layer: ${name}`);
return false;
}
const result = this._layers.delete(name);
if (result) {
this._updateSortedLayers();
}
return result;
}
/**
* 获取层名称列表(按顺序)
* Get layer names in order
*/
getLayerNames(): string[] {
return this._sortedLayers.map(l => l.name);
}
/**
* 检查层是否存在
* Check if layer exists
*/
hasLayer(name: string): boolean {
return this._layers.has(name);
}
/**
* 检查层是否为屏幕空间层
* Check if layer is screen space
*
* @param layerName 层名称 | Layer name
* @returns 是否为屏幕空间层,未找到返回 false | Whether screen space, returns false if not found
*/
isScreenSpace(layerName: string): boolean {
return this._layers.get(layerName)?.bScreenSpace ?? false;
}
/**
* 获取所有屏幕空间层名称
* Get all screen space layer names
*/
getScreenSpaceLayers(): string[] {
return this._sortedLayers
.filter(l => l.bScreenSpace)
.map(l => l.name);
}
/**
* 获取所有世界空间层名称
* Get all world space layer names
*/
getWorldSpaceLayers(): string[] {
return this._sortedLayers
.filter(l => !l.bScreenSpace)
.map(l => l.name);
}
/**
* 重置为默认层
* Reset to default layers
*/
reset(): void {
this._layers.clear();
for (const layer of DEFAULT_SORTING_LAYERS) {
this._layers.set(layer.name, layer);
}
this._updateSortedLayers();
}
/**
* 更新排序后的层列表
* Update sorted layer list
*/
private _updateSortedLayers(): void {
this._sortedLayers = Array.from(this._layers.values())
.sort((a, b) => a.order - b.order);
}
}
/**
* 排序层管理器服务令牌
* Sorting layer manager service token
*/
export const SortingLayerManagerToken = createServiceToken<ISortingLayerManager>('sortingLayerManager');
/**
* 全局排序层管理器实例
* Global sorting layer manager instance
*/
export const sortingLayerManager = SortingLayerManager.instance;

View File

@@ -0,0 +1,241 @@
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
import type { IVector3 } from '@esengine/ecs-framework-math';
/**
* 3x3 矩阵(用于 2D 变换:旋转 + 缩放)
* 实际存储为 [a, b, c, d, tx, ty] 形式的仿射变换
*
* 3x3 matrix for 2D transforms (rotation + scale).
* Stored as affine transform [a, b, c, d, tx, ty].
*/
export interface Matrix2D {
a: number; // scaleX * cos(rotation)
b: number; // scaleX * sin(rotation)
c: number; // scaleY * -sin(rotation)
d: number; // scaleY * cos(rotation)
tx: number; // translateX
ty: number; // translateY
}
/**
* Reactive Vector3 that automatically sets dirty flag on parent transform.
* 响应式 Vector3在修改时自动设置父变换的脏标记。
*
* @internal
*/
class ReactiveVector3 implements IVector3 {
private _x: number;
private _y: number;
private _z: number;
private _owner: TransformComponent;
constructor(owner: TransformComponent, x: number = 0, y: number = 0, z: number = 0) {
this._owner = owner;
this._x = x;
this._y = y;
this._z = z;
}
get x(): number { return this._x; }
set x(value: number) {
if (this._x !== value) {
this._x = value;
this._owner.markDirty();
}
}
get y(): number { return this._y; }
set y(value: number) {
if (this._y !== value) {
this._y = value;
this._owner.markDirty();
}
}
get z(): number { return this._z; }
set z(value: number) {
if (this._z !== value) {
this._z = value;
this._owner.markDirty();
}
}
/**
* Set all components at once (more efficient than setting individually).
* 一次性设置所有分量(比单独设置更高效)。
*/
set(x: number, y: number, z: number = this._z): void {
const changed = this._x !== x || this._y !== y || this._z !== z;
this._x = x;
this._y = y;
this._z = z;
if (changed) {
this._owner.markDirty();
}
}
/**
* Copy from another vector.
* 从另一个向量复制。
*/
copyFrom(v: IVector3): void {
this.set(v.x, v.y, v.z);
}
/**
* Get raw values without triggering getters (for serialization).
* 获取原始值而不触发 getter用于序列化
*/
toObject(): IVector3 {
return { x: this._x, y: this._y, z: this._z };
}
}
@ECSComponent('Transform')
@Serializable({ version: 1, typeId: 'Transform' })
export class TransformComponent extends Component {
// ===== 内部响应式存储 =====
private _position: ReactiveVector3;
private _rotation: ReactiveVector3;
private _scale: ReactiveVector3;
@Serialize()
@Property({ type: 'vector3', label: 'Position' })
get position(): IVector3 { return this._position; }
set position(value: IVector3) {
if (this._position) {
this._position.copyFrom(value);
} else {
this._position = new ReactiveVector3(this, value.x, value.y, value.z);
}
}
/** 欧拉角,单位:度 | Euler angles in degrees */
@Serialize()
@Property({ type: 'vector3', label: 'Rotation' })
get rotation(): IVector3 { return this._rotation; }
set rotation(value: IVector3) {
if (this._rotation) {
this._rotation.copyFrom(value);
} else {
this._rotation = new ReactiveVector3(this, value.x, value.y, value.z);
}
}
@Serialize()
@Property({ type: 'vector3', label: 'Scale' })
get scale(): IVector3 { return this._scale; }
set scale(value: IVector3) {
if (this._scale) {
this._scale.copyFrom(value);
} else {
this._scale = new ReactiveVector3(this, value.x, value.y, value.z);
}
}
// ===== 世界变换(由 TransformSystem 计算)=====
/** 世界位置(只读,由 TransformSystem 计算) */
worldPosition: IVector3 = { x: 0, y: 0, z: 0 };
/** 世界旋转(只读,由 TransformSystem 计算) */
worldRotation: IVector3 = { x: 0, y: 0, z: 0 };
/** 世界缩放(只读,由 TransformSystem 计算) */
worldScale: IVector3 = { x: 1, y: 1, z: 1 };
/** 本地到世界的 2D 变换矩阵(只读,由 TransformSystem 计算) */
localToWorldMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
/** 变换是否需要更新 | Whether transform needs update */
bDirty: boolean = true;
/**
* Frame counter when last updated (for change detection).
* 上次更新的帧计数器(用于变化检测)。
* @internal
*/
_lastUpdateFrame: number = -1;
/**
* Cached render data version (incremented when transform changes).
* 缓存的渲染数据版本(变换改变时递增)。
* @internal
*/
_version: number = 0;
constructor(x: number = 0, y: number = 0, z: number = 0) {
super();
this._position = new ReactiveVector3(this, x, y, z);
this._rotation = new ReactiveVector3(this, 0, 0, 0);
this._scale = new ReactiveVector3(this, 1, 1, 1);
// 初始化世界变换为本地变换值(在 TransformSystem 更新前使用)
this.worldPosition = { x, y, z };
}
/**
* Mark transform as dirty and increment version.
* 标记变换为脏并递增版本号。
*/
markDirty(): void {
if (!this.bDirty) {
this.bDirty = true;
this._version++;
}
}
/**
* Clear dirty flag (called by TransformSystem after update).
* 清除脏标记(由 TransformSystem 更新后调用)。
*/
clearDirty(frameNumber: number): void {
this.bDirty = false;
this._lastUpdateFrame = frameNumber;
}
setPosition(x: number, y: number, z: number = 0): this {
this._position.set(x, y, z);
return this;
}
setRotation(x: number, y: number, z: number): this {
this._rotation.set(x, y, z);
return this;
}
setScale(x: number, y: number, z: number = 1): this {
this._scale.set(x, y, z);
return this;
}
/**
* 将本地坐标转换为世界坐标
*/
localToWorld(localX: number, localY: number): { x: number; y: number } {
const m = this.localToWorldMatrix;
return {
x: m.a * localX + m.c * localY + m.tx,
y: m.b * localX + m.d * localY + m.ty
};
}
/**
* 将世界坐标转换为本地坐标
*/
worldToLocal(worldX: number, worldY: number): { x: number; y: number } {
const m = this.localToWorldMatrix;
const det = m.a * m.d - m.b * m.c;
if (Math.abs(det) < 1e-10) {
return { x: 0, y: 0 };
}
const invDet = 1 / det;
const dx = worldX - m.tx;
const dy = worldY - m.ty;
return {
x: (m.d * dx - m.c * dy) * invDet,
y: (-m.b * dx + m.a * dy) * invDet
};
}
}

View File

@@ -0,0 +1,146 @@
import { EntitySystem, Matcher, Entity, ECSSystem, HierarchyComponent } from '@esengine/ecs-framework';
import { TransformComponent, Matrix2D } from './TransformComponent';
const DEG_TO_RAD = Math.PI / 180;
/**
* 变换系统
* Transform System - Calculates world transforms based on hierarchy
*
* 根据实体层级关系计算世界变换矩阵。
* 子实体的世界变换 = 父实体世界变换 * 子实体本地变换
*/
@ECSSystem('Transform', { updateOrder: -100 })
export class TransformSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(TransformComponent));
}
protected override process(entities: readonly Entity[]): void {
// 获取所有根实体(没有父级或父级没有 TransformComponent
const rootEntities = entities.filter(e => {
const hierarchy = e.getComponent(HierarchyComponent);
if (!hierarchy || hierarchy.parentId === null) {
return true;
}
const parent = this.scene?.findEntityById(hierarchy.parentId);
return !parent || !parent.hasComponent(TransformComponent);
});
// 从根实体开始递归计算世界变换
for (const entity of rootEntities) {
this.updateWorldTransform(entity, null);
}
}
/**
* 递归更新实体及其子实体的世界变换
*/
private updateWorldTransform(entity: Entity, parentMatrix: Matrix2D | null): void {
const transform = entity.getComponent(TransformComponent);
if (!transform) return;
// 计算本地变换矩阵
const localMatrix = this.calculateLocalMatrix(transform);
// 计算世界变换矩阵
if (parentMatrix) {
// 世界矩阵 = 父矩阵 * 本地矩阵
transform.localToWorldMatrix = this.multiplyMatrices(parentMatrix, localMatrix);
} else {
// 没有父级,本地矩阵就是世界矩阵
transform.localToWorldMatrix = localMatrix;
}
// 从世界矩阵提取世界位置、旋转、缩放
this.decomposeMatrix(transform);
transform.bDirty = false;
// 递归处理子实体
const hierarchy = entity.getComponent(HierarchyComponent);
if (hierarchy && hierarchy.childIds.length > 0) {
for (const childId of hierarchy.childIds) {
const child = this.scene?.findEntityById(childId);
if (child) {
this.updateWorldTransform(child, transform.localToWorldMatrix);
}
}
}
}
/**
* 计算本地变换矩阵
*/
private calculateLocalMatrix(transform: TransformComponent): Matrix2D {
const { position, rotation, scale } = transform;
// 只使用 z 轴旋转2D
const rad = rotation.z * DEG_TO_RAD;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
// 构建仿射变换矩阵: Scale -> Rotate -> Translate
// 顺时针旋转 | Clockwise rotation
// [a c tx] [sx 0 0] [cos sin 0] [1 0 tx]
// [b d ty] = [0 sy 0] * [-sin cos 0] * [0 1 ty]
// [0 0 1] [0 0 1] [0 0 1] [0 0 1]
return {
a: scale.x * cos,
b: -scale.x * sin,
c: scale.y * sin,
d: scale.y * cos,
tx: position.x,
ty: position.y
};
}
/**
* 矩阵乘法: result = a * b
*/
private multiplyMatrices(a: Matrix2D, b: Matrix2D): Matrix2D {
return {
a: a.a * b.a + a.c * b.b,
b: a.b * b.a + a.d * b.b,
c: a.a * b.c + a.c * b.d,
d: a.b * b.c + a.d * b.d,
tx: a.a * b.tx + a.c * b.ty + a.tx,
ty: a.b * b.tx + a.d * b.ty + a.ty
};
}
/**
* 从世界矩阵分解出位置、旋转、缩放
*/
private decomposeMatrix(transform: TransformComponent): void {
const m = transform.localToWorldMatrix;
// 位置直接从矩阵获取
transform.worldPosition.x = m.tx;
transform.worldPosition.y = m.ty;
transform.worldPosition.z = transform.position.z;
// 计算缩放
const scaleX = Math.sqrt(m.a * m.a + m.b * m.b);
const scaleY = Math.sqrt(m.c * m.c + m.d * m.d);
// 检测负缩放(通过行列式符号)
const det = m.a * m.d - m.b * m.c;
const sign = det < 0 ? -1 : 1;
transform.worldScale.x = scaleX;
transform.worldScale.y = scaleY * sign;
transform.worldScale.z = transform.scale.z;
// 计算旋转(从归一化的矩阵)
if (scaleX > 1e-10) {
const rotation = Math.atan2(m.b / scaleX, m.a / scaleX);
transform.worldRotation.z = rotation / DEG_TO_RAD;
} else {
transform.worldRotation.z = 0;
}
transform.worldRotation.x = transform.rotation.x;
transform.worldRotation.y = transform.rotation.y;
}
}

View File

@@ -0,0 +1,63 @@
export { TransformComponent, type Matrix2D } from './TransformComponent';
export { TransformSystem } from './TransformSystem';
export { HierarchyComponent } from './HierarchyComponent';
export { HierarchySystem } from './HierarchySystem';
export {
EnginePlugin,
// Type exports
type LoadingPhase,
type SystemContext,
type IRuntimeModule,
type IRuntimePlugin,
// Engine-specific service tokens
TransformTypeToken,
CanvasElementToken,
TextureServiceToken,
DynamicAtlasServiceToken,
CoordinateServiceToken,
RenderConfigServiceToken,
// Types
type IEditorModuleBase,
type ITextureService,
type IDynamicAtlasService,
type ICoordinateService,
type IRenderConfigService
} from './EnginePlugin';
// Module Manifest types (unified module/plugin configuration)
export {
type ModuleManifest,
type ModuleCategory,
type ModulePlatform,
type ModuleExports
} from './ModuleManifest';
// Input System (keyboard, mouse, touch)
export {
Input,
InputManager,
InputSystem,
MouseButton,
type InputSystemConfig,
type KeyState,
type MouseButtonState,
type KeyboardEventInfo,
type MouseEventInfo,
type WheelEventInfo,
type TouchInfo,
type TouchEvent
} from './Input';
// Sorting Layer System (render order control)
export {
SortingLayerManager,
sortingLayerManager,
SortingLayerManagerToken,
SortingLayers,
DEFAULT_SORTING_LAYERS,
type ISortingLayerManager,
type ISortable,
type SortingLayerConfig,
type SortingLayerName
} from './SortingLayer';