* perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令
ReactiveQuery 快照优化:
- 添加快照机制,避免每帧拷贝数组
- 只在实体列表变化时创建新快照
- 静态场景下多个系统共享同一快照
CommandBuffer 延迟命令系统:
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
- 每个系统拥有独立的 commands 属性
- 命令在帧末统一执行,避免迭代过程中修改实体列表
Scene 更新:
- 在 lateUpdate 后自动刷新所有系统的命令缓冲区
文档:
- 更新系统文档,添加 CommandBuffer 使用说明
* fix(ci): upgrade first-interaction action to v1.3.0
Fix Docker build failure in welcome workflow.
* fix(ci): upgrade pnpm/action-setup to v4 and fix unused import
- Upgrade pnpm/action-setup@v2 to v4 in all workflow files
- Remove unused CommandType import in CommandBuffer.test.ts
* fix(ci): remove duplicate pnpm version specification
* feat(engine-core): 添加统一输入系统
添加完整的输入系统,支持平台抽象:
- IPlatformInputSubsystem: 扩展接口支持键盘/鼠标/滚轮事件
- WebInputSubsystem: 浏览器实现,支持事件绑定/解绑
- InputManager: 全局输入状态管理器(键盘、鼠标、触摸)
- InputSystem: ECS 系统,连接平台事件到 InputManager
- GameRuntime 集成: 自动创建 InputSystem 并绑定平台子系统
使用方式:
```typescript
import { Input, MouseButton } from '@esengine/engine-core';
if (Input.isKeyDown('KeyW')) { /* 移动 */ }
if (Input.isKeyJustPressed('Space')) { /* 跳跃 */ }
if (Input.isMouseButtonDown(MouseButton.Left)) { /* 射击 */ }
```
* fix(runtime-core): 添加缺失的 platform-common 依赖
* fix(runtime-core): 移除 platform-web 依赖避免循环依赖
* fix(runtime-core): 使用工厂函数注入 InputSubsystem 避免循环依赖
- BrowserPlatformAdapter 通过 inputSubsystemFactory 配置接收输入子系统
- 在 IPlatformInputSubsystem 接口添加可选的 dispose 方法
- 移除对 @esengine/platform-web 的直接依赖
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
/**
|
|
* Web 平台输入子系统
|
|
* Web platform input subsystem
|
|
*/
|
|
|
|
import type {
|
|
IPlatformInputSubsystem,
|
|
TouchHandler,
|
|
TouchEvent,
|
|
KeyboardHandler,
|
|
KeyboardEventInfo,
|
|
MouseHandler,
|
|
MouseEventInfo,
|
|
WheelHandler,
|
|
WheelEventInfo
|
|
} from '@esengine/platform-common';
|
|
import { MouseButton } from '@esengine/platform-common';
|
|
|
|
/**
|
|
* Web 平台输入子系统实现
|
|
* Web platform input subsystem implementation
|
|
*/
|
|
export class WebInputSubsystem implements IPlatformInputSubsystem {
|
|
// ========== Touch handlers ==========
|
|
private _touchStartHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
|
|
private _touchMoveHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
|
|
private _touchEndHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
|
|
private _touchCancelHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
|
|
|
|
// ========== Keyboard handlers ==========
|
|
private _keyDownHandlers: Map<KeyboardHandler, (e: globalThis.KeyboardEvent) => void> = new Map();
|
|
private _keyUpHandlers: Map<KeyboardHandler, (e: globalThis.KeyboardEvent) => void> = new Map();
|
|
|
|
// ========== Mouse handlers ==========
|
|
private _mouseMoveHandlers: Map<MouseHandler, (e: globalThis.MouseEvent) => void> = new Map();
|
|
private _mouseDownHandlers: Map<MouseHandler, (e: globalThis.MouseEvent) => void> = new Map();
|
|
private _mouseUpHandlers: Map<MouseHandler, (e: globalThis.MouseEvent) => void> = new Map();
|
|
private _wheelHandlers: Map<WheelHandler, (e: globalThis.WheelEvent) => void> = new Map();
|
|
|
|
// ========== Touch events ==========
|
|
|
|
onTouchStart(handler: TouchHandler): void {
|
|
const nativeHandler = (e: globalThis.TouchEvent) => {
|
|
handler(this.convertTouchEvent(e));
|
|
};
|
|
this._touchStartHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('touchstart', nativeHandler);
|
|
}
|
|
|
|
onTouchMove(handler: TouchHandler): void {
|
|
const nativeHandler = (e: globalThis.TouchEvent) => {
|
|
handler(this.convertTouchEvent(e));
|
|
};
|
|
this._touchMoveHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('touchmove', nativeHandler);
|
|
}
|
|
|
|
onTouchEnd(handler: TouchHandler): void {
|
|
const nativeHandler = (e: globalThis.TouchEvent) => {
|
|
handler(this.convertTouchEvent(e));
|
|
};
|
|
this._touchEndHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('touchend', nativeHandler);
|
|
}
|
|
|
|
onTouchCancel(handler: TouchHandler): void {
|
|
const nativeHandler = (e: globalThis.TouchEvent) => {
|
|
handler(this.convertTouchEvent(e));
|
|
};
|
|
this._touchCancelHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('touchcancel', nativeHandler);
|
|
}
|
|
|
|
offTouchStart(handler: TouchHandler): void {
|
|
const nativeHandler = this._touchStartHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('touchstart', nativeHandler);
|
|
this._touchStartHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
offTouchMove(handler: TouchHandler): void {
|
|
const nativeHandler = this._touchMoveHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('touchmove', nativeHandler);
|
|
this._touchMoveHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
offTouchEnd(handler: TouchHandler): void {
|
|
const nativeHandler = this._touchEndHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('touchend', nativeHandler);
|
|
this._touchEndHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
offTouchCancel(handler: TouchHandler): void {
|
|
const nativeHandler = this._touchCancelHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('touchcancel', nativeHandler);
|
|
this._touchCancelHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
supportsPressure(): boolean {
|
|
return typeof Touch !== 'undefined' && 'force' in Touch.prototype;
|
|
}
|
|
|
|
// ========== Keyboard events ==========
|
|
|
|
onKeyDown(handler: KeyboardHandler): void {
|
|
const nativeHandler = (e: globalThis.KeyboardEvent) => {
|
|
handler(this.convertKeyboardEvent(e));
|
|
};
|
|
this._keyDownHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('keydown', nativeHandler);
|
|
}
|
|
|
|
onKeyUp(handler: KeyboardHandler): void {
|
|
const nativeHandler = (e: globalThis.KeyboardEvent) => {
|
|
handler(this.convertKeyboardEvent(e));
|
|
};
|
|
this._keyUpHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('keyup', nativeHandler);
|
|
}
|
|
|
|
offKeyDown(handler: KeyboardHandler): void {
|
|
const nativeHandler = this._keyDownHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('keydown', nativeHandler);
|
|
this._keyDownHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
offKeyUp(handler: KeyboardHandler): void {
|
|
const nativeHandler = this._keyUpHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('keyup', nativeHandler);
|
|
this._keyUpHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
// ========== Mouse events ==========
|
|
|
|
onMouseMove(handler: MouseHandler): void {
|
|
const nativeHandler = (e: globalThis.MouseEvent) => {
|
|
handler(this.convertMouseEvent(e));
|
|
};
|
|
this._mouseMoveHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('mousemove', nativeHandler);
|
|
}
|
|
|
|
onMouseDown(handler: MouseHandler): void {
|
|
const nativeHandler = (e: globalThis.MouseEvent) => {
|
|
handler(this.convertMouseEvent(e));
|
|
};
|
|
this._mouseDownHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('mousedown', nativeHandler);
|
|
}
|
|
|
|
onMouseUp(handler: MouseHandler): void {
|
|
const nativeHandler = (e: globalThis.MouseEvent) => {
|
|
handler(this.convertMouseEvent(e));
|
|
};
|
|
this._mouseUpHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('mouseup', nativeHandler);
|
|
}
|
|
|
|
onWheel(handler: WheelHandler): void {
|
|
const nativeHandler = (e: globalThis.WheelEvent) => {
|
|
handler(this.convertWheelEvent(e));
|
|
};
|
|
this._wheelHandlers.set(handler, nativeHandler);
|
|
window.addEventListener('wheel', nativeHandler);
|
|
}
|
|
|
|
offMouseMove(handler: MouseHandler): void {
|
|
const nativeHandler = this._mouseMoveHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('mousemove', nativeHandler);
|
|
this._mouseMoveHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
offMouseDown(handler: MouseHandler): void {
|
|
const nativeHandler = this._mouseDownHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('mousedown', nativeHandler);
|
|
this._mouseDownHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
offMouseUp(handler: MouseHandler): void {
|
|
const nativeHandler = this._mouseUpHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('mouseup', nativeHandler);
|
|
this._mouseUpHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
offWheel(handler: WheelHandler): void {
|
|
const nativeHandler = this._wheelHandlers.get(handler);
|
|
if (nativeHandler) {
|
|
window.removeEventListener('wheel', nativeHandler);
|
|
this._wheelHandlers.delete(handler);
|
|
}
|
|
}
|
|
|
|
// ========== Capability queries ==========
|
|
|
|
supportsKeyboard(): boolean {
|
|
return true;
|
|
}
|
|
|
|
supportsMouse(): boolean {
|
|
// 检测是否有鼠标设备 | Check if mouse device exists
|
|
return window.matchMedia('(pointer: fine)').matches;
|
|
}
|
|
|
|
// ========== Event converters ==========
|
|
|
|
private convertTouchEvent(e: globalThis.TouchEvent): TouchEvent {
|
|
const convertTouch = (touch: globalThis.Touch) => ({
|
|
identifier: touch.identifier,
|
|
x: touch.clientX,
|
|
y: touch.clientY,
|
|
force: (touch as any).force
|
|
});
|
|
|
|
return {
|
|
touches: Array.from(e.touches).map(convertTouch),
|
|
changedTouches: Array.from(e.changedTouches).map(convertTouch),
|
|
timeStamp: e.timeStamp
|
|
};
|
|
}
|
|
|
|
private convertKeyboardEvent(e: globalThis.KeyboardEvent): KeyboardEventInfo {
|
|
return {
|
|
code: e.code,
|
|
key: e.key,
|
|
altKey: e.altKey,
|
|
ctrlKey: e.ctrlKey,
|
|
shiftKey: e.shiftKey,
|
|
metaKey: e.metaKey,
|
|
repeat: e.repeat,
|
|
timeStamp: e.timeStamp
|
|
};
|
|
}
|
|
|
|
private convertMouseEvent(e: globalThis.MouseEvent): MouseEventInfo {
|
|
return {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
movementX: e.movementX,
|
|
movementY: e.movementY,
|
|
button: e.button as MouseButton,
|
|
buttons: e.buttons,
|
|
altKey: e.altKey,
|
|
ctrlKey: e.ctrlKey,
|
|
shiftKey: e.shiftKey,
|
|
metaKey: e.metaKey,
|
|
timeStamp: e.timeStamp
|
|
};
|
|
}
|
|
|
|
private convertWheelEvent(e: globalThis.WheelEvent): WheelEventInfo {
|
|
return {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
deltaX: e.deltaX,
|
|
deltaY: e.deltaY,
|
|
deltaZ: e.deltaZ,
|
|
timeStamp: e.timeStamp
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 销毁并移除所有事件监听器
|
|
* Dispose and remove all event listeners
|
|
*/
|
|
dispose(): void {
|
|
// 清理触摸事件 | Clean up touch events
|
|
this._touchStartHandlers.forEach((handler) => {
|
|
window.removeEventListener('touchstart', handler);
|
|
});
|
|
this._touchStartHandlers.clear();
|
|
|
|
this._touchMoveHandlers.forEach((handler) => {
|
|
window.removeEventListener('touchmove', handler);
|
|
});
|
|
this._touchMoveHandlers.clear();
|
|
|
|
this._touchEndHandlers.forEach((handler) => {
|
|
window.removeEventListener('touchend', handler);
|
|
});
|
|
this._touchEndHandlers.clear();
|
|
|
|
this._touchCancelHandlers.forEach((handler) => {
|
|
window.removeEventListener('touchcancel', handler);
|
|
});
|
|
this._touchCancelHandlers.clear();
|
|
|
|
// 清理键盘事件 | Clean up keyboard events
|
|
this._keyDownHandlers.forEach((handler) => {
|
|
window.removeEventListener('keydown', handler);
|
|
});
|
|
this._keyDownHandlers.clear();
|
|
|
|
this._keyUpHandlers.forEach((handler) => {
|
|
window.removeEventListener('keyup', handler);
|
|
});
|
|
this._keyUpHandlers.clear();
|
|
|
|
// 清理鼠标事件 | Clean up mouse events
|
|
this._mouseMoveHandlers.forEach((handler) => {
|
|
window.removeEventListener('mousemove', handler);
|
|
});
|
|
this._mouseMoveHandlers.clear();
|
|
|
|
this._mouseDownHandlers.forEach((handler) => {
|
|
window.removeEventListener('mousedown', handler);
|
|
});
|
|
this._mouseDownHandlers.clear();
|
|
|
|
this._mouseUpHandlers.forEach((handler) => {
|
|
window.removeEventListener('mouseup', handler);
|
|
});
|
|
this._mouseUpHandlers.clear();
|
|
|
|
this._wheelHandlers.forEach((handler) => {
|
|
window.removeEventListener('wheel', handler);
|
|
});
|
|
this._wheelHandlers.clear();
|
|
}
|
|
}
|